diff --git a/crates/workshop-rs/src/gameplay/data.rs b/crates/workshop-rs/src/gameplay/data.rs index b1440061..6251c158 100644 --- a/crates/workshop-rs/src/gameplay/data.rs +++ b/crates/workshop-rs/src/gameplay/data.rs @@ -1,4 +1,4 @@ -//! Embedded, provenance-pinned hero gameplay data. +//! Embedded gameplay data with a pinned source identity. //! //! The data file is a checked-in projection of the user-provided //! `workshop-data` export. This module owns parsing, schema validation, and @@ -12,7 +12,13 @@ use crate::gameplay::{GameplayCatalog, GameplayDataError, GameplayDatasetIdentit /// The canonical gameplay dataset embedded in the crate. pub const GAMEPLAY_DATA: &str = include_str!("gameplay.json"); -const SCHEMA_VERSION: u32 = 1; +const SCHEMA_VERSION: u32 = 2; + +#[derive(serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct GameplaySchema { + schema_version: u32, +} #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] @@ -24,9 +30,15 @@ struct GameplayFile { /// Load and validate a gameplay dataset from its JSON representation. pub fn load(json: &str) -> Result { + let schema: GameplaySchema = serde_json::from_str(json) + .map_err(|error| GameplayDataError::Malformed(error.to_string()))?; + if schema.schema_version != SCHEMA_VERSION { + return Err(GameplayDataError::UnsupportedSchema(schema.schema_version)); + } + let file: GameplayFile = serde_json::from_str(json) .map_err(|error| GameplayDataError::Malformed(error.to_string()))?; - if file.schema_version != SCHEMA_VERSION { + if file.schema_version != schema.schema_version { return Err(GameplayDataError::UnsupportedSchema(file.schema_version)); } diff --git a/crates/workshop-rs/src/gameplay/gameplay.json b/crates/workshop-rs/src/gameplay/gameplay.json index 454345d4..419d0b34 100644 --- a/crates/workshop-rs/src/gameplay/gameplay.json +++ b/crates/workshop-rs/src/gameplay/gameplay.json @@ -3,7 +3,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.ana.ability1", "note": "commitDate=2026-08-12", @@ -19,7 +19,7 @@ "crowdControl" ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.ana.ability1", "note": "commitDate=2026-08-12", @@ -52,7 +52,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.ana.ability2", "note": "commitDate=2026-08-12", @@ -70,7 +70,7 @@ "buff" ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.ana.ability2", "note": "commitDate=2026-08-12", @@ -103,7 +103,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.ana.ultimate", "note": "commitDate=2026-08-12", @@ -119,7 +119,7 @@ "buff" ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.ana.ultimate", "note": "commitDate=2026-08-12", @@ -152,7 +152,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.ana", "note": "commitDate=2026-08-12", @@ -161,7 +161,7 @@ ], "id": "ana", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.ana", "note": "commitDate=2026-08-12", @@ -187,7 +187,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/ana/", "note": "role metadata; accessed 2026-08-18", @@ -200,7 +200,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.anran.ability1", "note": "commitDate=2026-08-12", @@ -208,7 +208,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.anran.ability1", "note": "commitDate=2026-08-12", @@ -223,7 +223,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.anran.ability2", "note": "commitDate=2026-08-12", @@ -231,7 +231,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.anran.ability2", "note": "commitDate=2026-08-12", @@ -246,7 +246,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.anran.ultimate", "note": "commitDate=2026-08-12", @@ -254,7 +254,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.anran.ultimate", "note": "commitDate=2026-08-12", @@ -269,7 +269,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.anran", "note": "commitDate=2026-08-12", @@ -278,7 +278,7 @@ ], "id": "anran", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.anran", "note": "commitDate=2026-08-12", @@ -291,7 +291,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/anran/", "note": "role metadata; accessed 2026-08-18", @@ -304,7 +304,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.ashe.ability1", "note": "commitDate=2026-08-12", @@ -312,7 +312,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.ashe.ability1", "note": "commitDate=2026-08-12", @@ -340,7 +340,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.ashe.ability2", "note": "commitDate=2026-08-12", @@ -348,7 +348,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.ashe.ability2", "note": "commitDate=2026-08-12", @@ -376,7 +376,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.ashe.ultimate", "note": "commitDate=2026-08-12", @@ -384,7 +384,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.ashe.ultimate", "note": "commitDate=2026-08-12", @@ -412,7 +412,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.ashe", "note": "commitDate=2026-08-12", @@ -421,7 +421,7 @@ ], "id": "ashe", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.ashe", "note": "commitDate=2026-08-12", @@ -447,7 +447,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/ashe/", "note": "role metadata; accessed 2026-08-18", @@ -460,7 +460,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.baptiste.ability1", "note": "commitDate=2026-08-12", @@ -468,7 +468,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.baptiste.ability1", "note": "commitDate=2026-08-12", @@ -496,7 +496,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.baptiste.ability2", "note": "commitDate=2026-08-12", @@ -504,7 +504,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.baptiste.ability2", "note": "commitDate=2026-08-12", @@ -532,7 +532,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.baptiste.ultimate", "note": "commitDate=2026-08-12", @@ -540,7 +540,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.baptiste.ultimate", "note": "commitDate=2026-08-12", @@ -568,7 +568,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.baptiste", "note": "commitDate=2026-08-12", @@ -577,7 +577,7 @@ ], "id": "baptiste", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.baptiste", "note": "commitDate=2026-08-12", @@ -603,7 +603,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/baptiste/", "note": "role metadata; accessed 2026-08-18", @@ -616,7 +616,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.bastion.secondaryFire", "note": "commitDate=2026-08-12", @@ -624,7 +624,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.bastion.secondaryFire", "note": "commitDate=2026-08-12", @@ -652,7 +652,7 @@ "slot": "secondaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.bastion.ability1", "note": "commitDate=2026-08-12", @@ -668,7 +668,7 @@ "form" ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.bastion.ability1", "note": "commitDate=2026-08-12", @@ -701,7 +701,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.bastion.ultimate", "note": "commitDate=2026-08-12", @@ -709,7 +709,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.bastion.ultimate", "note": "commitDate=2026-08-12", @@ -737,7 +737,7 @@ "slot": "ultimate" }, { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/bastion/", "note": "official ability description; accessed 2026-08-18", @@ -745,7 +745,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/bastion/", "note": "official ability description; accessed 2026-08-18", @@ -760,7 +760,7 @@ "variant": "assault" }, { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/bastion/", "note": "official ability description; accessed 2026-08-18", @@ -768,7 +768,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/bastion/", "note": "official ability description; accessed 2026-08-18", @@ -783,7 +783,7 @@ "variant": "recon" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.bastion", "note": "commitDate=2026-08-12", @@ -792,7 +792,7 @@ ], "id": "bastion", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.bastion", "note": "commitDate=2026-08-12", @@ -818,7 +818,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/bastion/", "note": "role metadata; accessed 2026-08-18", @@ -831,7 +831,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.brigitte.secondaryFire", "note": "commitDate=2026-08-12", @@ -839,7 +839,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.brigitte.secondaryFire", "note": "commitDate=2026-08-12", @@ -867,7 +867,7 @@ "slot": "secondaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.brigitte.ability1", "note": "commitDate=2026-08-12", @@ -884,7 +884,7 @@ "knockback" ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.brigitte.ability1", "note": "commitDate=2026-08-12", @@ -917,7 +917,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.brigitte.ability2", "note": "commitDate=2026-08-12", @@ -925,7 +925,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.brigitte.ability2", "note": "commitDate=2026-08-12", @@ -953,7 +953,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.brigitte.ability3", "note": "commitDate=2026-08-12", @@ -971,7 +971,7 @@ "knockback" ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.brigitte.ability3", "note": "commitDate=2026-08-12", @@ -1004,7 +1004,7 @@ "slot": "ability3" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.brigitte.ultimate", "note": "commitDate=2026-08-12", @@ -1012,7 +1012,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.brigitte.ultimate", "note": "commitDate=2026-08-12", @@ -1040,7 +1040,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.brigitte", "note": "commitDate=2026-08-12", @@ -1049,7 +1049,7 @@ ], "id": "brigitte", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.brigitte", "note": "commitDate=2026-08-12", @@ -1075,7 +1075,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/brigitte/", "note": "role metadata; accessed 2026-08-18", @@ -1088,7 +1088,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.cassidy.ability1", "note": "commitDate=2026-08-12", @@ -1096,7 +1096,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.cassidy.ability1", "note": "commitDate=2026-08-12", @@ -1124,7 +1124,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.cassidy.ability2", "note": "commitDate=2026-08-12", @@ -1132,7 +1132,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.cassidy.ability2", "note": "commitDate=2026-08-12", @@ -1160,7 +1160,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.cassidy.ultimate", "note": "commitDate=2026-08-12", @@ -1168,7 +1168,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.cassidy.ultimate", "note": "commitDate=2026-08-12", @@ -1196,7 +1196,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.cassidy", "note": "commitDate=2026-08-12", @@ -1205,7 +1205,7 @@ ], "id": "cassidy", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.cassidy", "note": "commitDate=2026-08-12", @@ -1231,7 +1231,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/cassidy/", "note": "role metadata; accessed 2026-08-18", @@ -1244,7 +1244,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.dmon.secondaryFire", "note": "commitDate=2026-08-12", @@ -1252,7 +1252,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.dmon.secondaryFire", "note": "commitDate=2026-08-12", @@ -1266,7 +1266,7 @@ "slot": "secondaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.dmon.ability1", "note": "commitDate=2026-08-12", @@ -1274,7 +1274,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.dmon.ability1", "note": "commitDate=2026-08-12", @@ -1288,7 +1288,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.dmon.ability2", "note": "commitDate=2026-08-12", @@ -1296,7 +1296,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.dmon.ability2", "note": "commitDate=2026-08-12", @@ -1310,7 +1310,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.dmon.ultimate", "note": "commitDate=2026-08-12", @@ -1318,7 +1318,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.dmon.ultimate", "note": "commitDate=2026-08-12", @@ -1332,7 +1332,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.dmon", "note": "commitDate=2026-08-12", @@ -1341,7 +1341,7 @@ ], "id": "dmon", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.dmon", "note": "commitDate=2026-08-12", @@ -1353,7 +1353,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/dva/", "note": "role metadata; accessed 2026-08-18", @@ -1366,7 +1366,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.domina.primaryFire", "note": "commitDate=2026-08-12", @@ -1374,7 +1374,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.domina.primaryFire", "note": "commitDate=2026-08-12", @@ -1390,7 +1390,7 @@ "slot": "primaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.domina.secondaryFire", "note": "commitDate=2026-08-12", @@ -1398,7 +1398,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.domina.secondaryFire", "note": "commitDate=2026-08-12", @@ -1414,7 +1414,7 @@ "slot": "secondaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.domina.ability1", "note": "commitDate=2026-08-12", @@ -1422,7 +1422,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.domina.ability1", "note": "commitDate=2026-08-12", @@ -1438,7 +1438,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.domina.ability2", "note": "commitDate=2026-08-12", @@ -1446,7 +1446,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.domina.ability2", "note": "commitDate=2026-08-12", @@ -1462,7 +1462,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.domina.ultimate", "note": "commitDate=2026-08-12", @@ -1470,7 +1470,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.domina.ultimate", "note": "commitDate=2026-08-12", @@ -1486,7 +1486,7 @@ "slot": "ultimate" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.domina.passive", "note": "commitDate=2026-08-12", @@ -1494,7 +1494,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.domina.passive", "note": "commitDate=2026-08-12", @@ -1510,7 +1510,7 @@ "slot": "passive" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.domina", "note": "commitDate=2026-08-12", @@ -1519,7 +1519,7 @@ ], "id": "domina", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.domina", "note": "commitDate=2026-08-12", @@ -1533,7 +1533,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/domina/", "note": "role metadata; accessed 2026-08-18", @@ -1546,7 +1546,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.doomfist.secondaryFire", "note": "commitDate=2026-08-12", @@ -1554,7 +1554,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.doomfist.secondaryFire", "note": "commitDate=2026-08-12", @@ -1582,7 +1582,7 @@ "slot": "secondaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.doomfist.ability1", "note": "commitDate=2026-08-12", @@ -1590,7 +1590,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.doomfist.ability1", "note": "commitDate=2026-08-12", @@ -1618,7 +1618,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.doomfist.ability2", "note": "commitDate=2026-08-12", @@ -1626,7 +1626,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.doomfist.ability2", "note": "commitDate=2026-08-12", @@ -1654,7 +1654,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.doomfist.ultimate", "note": "commitDate=2026-08-12", @@ -1662,7 +1662,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.doomfist.ultimate", "note": "commitDate=2026-08-12", @@ -1690,7 +1690,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.doomfist", "note": "commitDate=2026-08-12", @@ -1699,7 +1699,7 @@ ], "id": "doomfist", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.doomfist", "note": "commitDate=2026-08-12", @@ -1725,7 +1725,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/doomfist/", "note": "role metadata; accessed 2026-08-18", @@ -1738,7 +1738,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.dva.secondaryFire", "note": "commitDate=2026-08-12", @@ -1755,7 +1755,7 @@ "resource" ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.dva.secondaryFire", "note": "commitDate=2026-08-12", @@ -1788,7 +1788,7 @@ "slot": "secondaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.dva.ability1", "note": "commitDate=2026-08-12", @@ -1796,7 +1796,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.dva.ability1", "note": "commitDate=2026-08-12", @@ -1824,7 +1824,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.dva.ability2", "note": "commitDate=2026-08-12", @@ -1832,7 +1832,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.dva.ability2", "note": "commitDate=2026-08-12", @@ -1860,7 +1860,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.dva.ultimate", "note": "commitDate=2026-08-12", @@ -1868,7 +1868,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.dva.ultimate", "note": "commitDate=2026-08-12", @@ -1896,7 +1896,7 @@ "slot": "ultimate" }, { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/dva/", "note": "official ability description; accessed 2026-08-18", @@ -1904,7 +1904,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/dva/", "note": "official ability description; accessed 2026-08-18", @@ -1919,7 +1919,7 @@ "variant": "mech" }, { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/dva/", "note": "official ability description; accessed 2026-08-18", @@ -1927,7 +1927,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/dva/", "note": "official ability description; accessed 2026-08-18", @@ -1942,7 +1942,7 @@ "variant": "pilot" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.dva", "note": "commitDate=2026-08-12", @@ -1951,7 +1951,7 @@ ], "id": "dva", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.dva", "note": "commitDate=2026-08-12", @@ -1977,7 +1977,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/dva/", "note": "role metadata; accessed 2026-08-18", @@ -1990,7 +1990,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.echo.secondaryFire", "note": "commitDate=2026-08-12", @@ -1998,7 +1998,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.echo.secondaryFire", "note": "commitDate=2026-08-12", @@ -2026,7 +2026,7 @@ "slot": "secondaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.echo.ability1", "note": "commitDate=2026-08-12", @@ -2034,7 +2034,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.echo.ability1", "note": "commitDate=2026-08-12", @@ -2062,7 +2062,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.echo.ability2", "note": "commitDate=2026-08-12", @@ -2070,7 +2070,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.echo.ability2", "note": "commitDate=2026-08-12", @@ -2098,7 +2098,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.echo.ultimate", "note": "commitDate=2026-08-12", @@ -2106,7 +2106,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.echo.ultimate", "note": "commitDate=2026-08-12", @@ -2134,7 +2134,7 @@ "slot": "ultimate" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.echo.passive", "note": "commitDate=2026-08-12", @@ -2142,7 +2142,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.echo.passive", "note": "commitDate=2026-08-12", @@ -2170,7 +2170,7 @@ "slot": "passive" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.echo", "note": "commitDate=2026-08-12", @@ -2179,7 +2179,7 @@ ], "id": "echo", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.echo", "note": "commitDate=2026-08-12", @@ -2205,7 +2205,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/echo/", "note": "role metadata; accessed 2026-08-18", @@ -2218,7 +2218,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.emre.ability1", "note": "commitDate=2026-08-12", @@ -2226,7 +2226,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.emre.ability1", "note": "commitDate=2026-08-12", @@ -2242,7 +2242,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.emre.ability2", "note": "commitDate=2026-08-12", @@ -2250,7 +2250,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.emre.ability2", "note": "commitDate=2026-08-12", @@ -2266,7 +2266,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.emre.ultimate", "note": "commitDate=2026-08-12", @@ -2274,7 +2274,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.emre.ultimate", "note": "commitDate=2026-08-12", @@ -2290,7 +2290,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.emre", "note": "commitDate=2026-08-12", @@ -2299,7 +2299,7 @@ ], "id": "emre", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.emre", "note": "commitDate=2026-08-12", @@ -2313,7 +2313,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/emre/", "note": "role metadata; accessed 2026-08-18", @@ -2326,7 +2326,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.freja.secondaryFire", "note": "commitDate=2026-08-12", @@ -2334,7 +2334,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.freja.secondaryFire", "note": "commitDate=2026-08-12", @@ -2350,7 +2350,7 @@ "slot": "secondaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.freja.ability1", "note": "commitDate=2026-08-12", @@ -2358,7 +2358,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.freja.ability1", "note": "commitDate=2026-08-12", @@ -2374,7 +2374,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.freja.ability2", "note": "commitDate=2026-08-12", @@ -2382,7 +2382,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.freja.ability2", "note": "commitDate=2026-08-12", @@ -2398,7 +2398,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.freja.ultimate", "note": "commitDate=2026-08-12", @@ -2406,7 +2406,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.freja.ultimate", "note": "commitDate=2026-08-12", @@ -2422,7 +2422,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.freja", "note": "commitDate=2026-08-12", @@ -2431,7 +2431,7 @@ ], "id": "freja", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.freja", "note": "commitDate=2026-08-12", @@ -2445,7 +2445,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/freja/", "note": "role metadata; accessed 2026-08-18", @@ -2458,7 +2458,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.genji.ability1", "note": "commitDate=2026-08-12", @@ -2466,7 +2466,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.genji.ability1", "note": "commitDate=2026-08-12", @@ -2494,7 +2494,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.genji.ability2", "note": "commitDate=2026-08-12", @@ -2502,7 +2502,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.genji.ability2", "note": "commitDate=2026-08-12", @@ -2530,7 +2530,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.genji.ultimate", "note": "commitDate=2026-08-12", @@ -2538,7 +2538,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.genji.ultimate", "note": "commitDate=2026-08-12", @@ -2566,7 +2566,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.genji", "note": "commitDate=2026-08-12", @@ -2575,7 +2575,7 @@ ], "id": "genji", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.genji", "note": "commitDate=2026-08-12", @@ -2601,7 +2601,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/genji/", "note": "role metadata; accessed 2026-08-18", @@ -2614,7 +2614,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.hanzo.ability1", "note": "commitDate=2026-08-12", @@ -2622,7 +2622,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.hanzo.ability1", "note": "commitDate=2026-08-12", @@ -2650,7 +2650,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.hanzo.ability2", "note": "commitDate=2026-08-12", @@ -2658,7 +2658,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.hanzo.ability2", "note": "commitDate=2026-08-12", @@ -2686,7 +2686,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.hanzo.ability3", "note": "commitDate=2026-08-12", @@ -2694,7 +2694,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.hanzo.ability3", "note": "commitDate=2026-08-12", @@ -2722,7 +2722,7 @@ "slot": "ability3" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.hanzo.ultimate", "note": "commitDate=2026-08-12", @@ -2730,7 +2730,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.hanzo.ultimate", "note": "commitDate=2026-08-12", @@ -2758,7 +2758,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.hanzo", "note": "commitDate=2026-08-12", @@ -2767,7 +2767,7 @@ ], "id": "hanzo", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.hanzo", "note": "commitDate=2026-08-12", @@ -2793,7 +2793,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/hanzo/", "note": "role metadata; accessed 2026-08-18", @@ -2806,7 +2806,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.hazard.primaryFire", "note": "commitDate=2026-08-12", @@ -2814,7 +2814,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.hazard.primaryFire", "note": "commitDate=2026-08-12", @@ -2842,7 +2842,7 @@ "slot": "primaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.hazard.secondaryFire", "note": "commitDate=2026-08-12", @@ -2850,7 +2850,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.hazard.secondaryFire", "note": "commitDate=2026-08-12", @@ -2878,7 +2878,7 @@ "slot": "secondaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.hazard.ability1", "note": "commitDate=2026-08-12", @@ -2886,7 +2886,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.hazard.ability1", "note": "commitDate=2026-08-12", @@ -2914,7 +2914,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.hazard.ability2", "note": "commitDate=2026-08-12", @@ -2922,7 +2922,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.hazard.ability2", "note": "commitDate=2026-08-12", @@ -2950,7 +2950,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.hazard.ultimate", "note": "commitDate=2026-08-12", @@ -2958,7 +2958,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.hazard.ultimate", "note": "commitDate=2026-08-12", @@ -2986,7 +2986,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.hazard", "note": "commitDate=2026-08-12", @@ -2995,7 +2995,7 @@ ], "id": "hazard", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.hazard", "note": "commitDate=2026-08-12", @@ -3021,7 +3021,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/hazard/", "note": "role metadata; accessed 2026-08-18", @@ -3034,7 +3034,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.illari.ability1", "note": "commitDate=2026-08-12", @@ -3042,7 +3042,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.illari.ability1", "note": "commitDate=2026-08-12", @@ -3070,7 +3070,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.illari.ability2", "note": "commitDate=2026-08-12", @@ -3078,7 +3078,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.illari.ability2", "note": "commitDate=2026-08-12", @@ -3106,7 +3106,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.illari.ultimate", "note": "commitDate=2026-08-12", @@ -3114,7 +3114,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.illari.ultimate", "note": "commitDate=2026-08-12", @@ -3142,7 +3142,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.illari", "note": "commitDate=2026-08-12", @@ -3151,7 +3151,7 @@ ], "id": "illari", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.illari", "note": "commitDate=2026-08-12", @@ -3177,7 +3177,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/illari/", "note": "role metadata; accessed 2026-08-18", @@ -3190,7 +3190,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.jetpackCat.secondaryFire", "note": "commitDate=2026-08-12", @@ -3198,7 +3198,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.jetpackCat.secondaryFire", "note": "commitDate=2026-08-12", @@ -3214,7 +3214,7 @@ "slot": "secondaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.jetpackCat.ability1", "note": "commitDate=2026-08-12", @@ -3222,7 +3222,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.jetpackCat.ability1", "note": "commitDate=2026-08-12", @@ -3238,7 +3238,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.jetpackCat.ability2", "note": "commitDate=2026-08-12", @@ -3246,7 +3246,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.jetpackCat.ability2", "note": "commitDate=2026-08-12", @@ -3262,7 +3262,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.jetpackCat.ultimate", "note": "commitDate=2026-08-12", @@ -3270,7 +3270,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.jetpackCat.ultimate", "note": "commitDate=2026-08-12", @@ -3286,7 +3286,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.jetpackCat", "note": "commitDate=2026-08-12", @@ -3295,7 +3295,7 @@ ], "id": "jetpackCat", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.jetpackCat", "note": "commitDate=2026-08-12", @@ -3309,7 +3309,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/jetpack-cat/", "note": "role metadata; accessed 2026-08-18", @@ -3322,7 +3322,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.junkerQueen.secondaryFire", "note": "commitDate=2026-08-12", @@ -3330,7 +3330,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.junkerQueen.secondaryFire", "note": "commitDate=2026-08-12", @@ -3358,7 +3358,7 @@ "slot": "secondaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.junkerQueen.ability1", "note": "commitDate=2026-08-12", @@ -3366,7 +3366,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.junkerQueen.ability1", "note": "commitDate=2026-08-12", @@ -3394,7 +3394,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.junkerQueen.ability2", "note": "commitDate=2026-08-12", @@ -3402,7 +3402,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.junkerQueen.ability2", "note": "commitDate=2026-08-12", @@ -3430,7 +3430,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.junkerQueen.ultimate", "note": "commitDate=2026-08-12", @@ -3438,7 +3438,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.junkerQueen.ultimate", "note": "commitDate=2026-08-12", @@ -3466,7 +3466,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.junkerQueen", "note": "commitDate=2026-08-12", @@ -3475,7 +3475,7 @@ ], "id": "junkerQueen", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.junkerQueen", "note": "commitDate=2026-08-12", @@ -3501,7 +3501,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/junker-queen/", "note": "role metadata; accessed 2026-08-18", @@ -3514,7 +3514,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.junkrat.ability1", "note": "commitDate=2026-08-12", @@ -3522,7 +3522,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.junkrat.ability1", "note": "commitDate=2026-08-12", @@ -3550,7 +3550,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.junkrat.ability2", "note": "commitDate=2026-08-12", @@ -3558,7 +3558,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.junkrat.ability2", "note": "commitDate=2026-08-12", @@ -3586,7 +3586,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.junkrat.ultimate", "note": "commitDate=2026-08-12", @@ -3594,7 +3594,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.junkrat.ultimate", "note": "commitDate=2026-08-12", @@ -3622,7 +3622,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.junkrat", "note": "commitDate=2026-08-12", @@ -3631,7 +3631,7 @@ ], "id": "junkrat", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.junkrat", "note": "commitDate=2026-08-12", @@ -3657,7 +3657,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/junkrat/", "note": "role metadata; accessed 2026-08-18", @@ -3670,7 +3670,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.juno.primaryFire", "note": "commitDate=2026-08-12", @@ -3678,7 +3678,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.juno.primaryFire", "note": "commitDate=2026-08-12", @@ -3706,7 +3706,7 @@ "slot": "primaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.juno.secondaryFire", "note": "commitDate=2026-08-12", @@ -3714,7 +3714,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.juno.secondaryFire", "note": "commitDate=2026-08-12", @@ -3742,7 +3742,7 @@ "slot": "secondaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.juno.ability1", "note": "commitDate=2026-08-12", @@ -3750,7 +3750,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.juno.ability1", "note": "commitDate=2026-08-12", @@ -3778,7 +3778,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.juno.ability2", "note": "commitDate=2026-08-12", @@ -3786,7 +3786,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.juno.ability2", "note": "commitDate=2026-08-12", @@ -3814,7 +3814,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.juno.ultimate", "note": "commitDate=2026-08-12", @@ -3822,7 +3822,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.juno.ultimate", "note": "commitDate=2026-08-12", @@ -3850,7 +3850,7 @@ "slot": "ultimate" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.juno.passive", "note": "commitDate=2026-08-12", @@ -3858,7 +3858,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.juno.passive", "note": "commitDate=2026-08-12", @@ -3886,7 +3886,7 @@ "slot": "passive" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.juno", "note": "commitDate=2026-08-12", @@ -3895,7 +3895,7 @@ ], "id": "juno", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.juno", "note": "commitDate=2026-08-12", @@ -3921,7 +3921,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/juno/", "note": "role metadata; accessed 2026-08-18", @@ -3934,7 +3934,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.kiriko.ability1", "note": "commitDate=2026-08-12", @@ -3942,7 +3942,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.kiriko.ability1", "note": "commitDate=2026-08-12", @@ -3970,7 +3970,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.kiriko.ability2", "note": "commitDate=2026-08-12", @@ -3978,7 +3978,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.kiriko.ability2", "note": "commitDate=2026-08-12", @@ -4006,7 +4006,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.kiriko.ultimate", "note": "commitDate=2026-08-12", @@ -4014,7 +4014,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.kiriko.ultimate", "note": "commitDate=2026-08-12", @@ -4042,7 +4042,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.kiriko", "note": "commitDate=2026-08-12", @@ -4051,7 +4051,7 @@ ], "id": "kiriko", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.kiriko", "note": "commitDate=2026-08-12", @@ -4077,7 +4077,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/kiriko/", "note": "role metadata; accessed 2026-08-18", @@ -4090,7 +4090,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.lifeweaver.secondaryFire", "note": "commitDate=2026-08-12", @@ -4098,7 +4098,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.lifeweaver.secondaryFire", "note": "commitDate=2026-08-12", @@ -4126,7 +4126,7 @@ "slot": "secondaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.lifeweaver.ability1", "note": "commitDate=2026-08-12", @@ -4134,7 +4134,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.lifeweaver.ability1", "note": "commitDate=2026-08-12", @@ -4162,7 +4162,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.lifeweaver.ability2", "note": "commitDate=2026-08-12", @@ -4170,7 +4170,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.lifeweaver.ability2", "note": "commitDate=2026-08-12", @@ -4198,7 +4198,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.lifeweaver.ultimate", "note": "commitDate=2026-08-12", @@ -4206,7 +4206,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.lifeweaver.ultimate", "note": "commitDate=2026-08-12", @@ -4234,7 +4234,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.lifeweaver", "note": "commitDate=2026-08-12", @@ -4243,7 +4243,7 @@ ], "id": "lifeweaver", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.lifeweaver", "note": "commitDate=2026-08-12", @@ -4269,7 +4269,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/lifeweaver/", "note": "role metadata; accessed 2026-08-18", @@ -4282,7 +4282,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.lucio.secondaryFire", "note": "commitDate=2026-08-12", @@ -4290,7 +4290,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.lucio.secondaryFire", "note": "commitDate=2026-08-12", @@ -4318,7 +4318,7 @@ "slot": "secondaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.lucio.ability1", "note": "commitDate=2026-08-12", @@ -4326,7 +4326,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.lucio.ability1", "note": "commitDate=2026-08-12", @@ -4354,7 +4354,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.lucio.ability2", "note": "commitDate=2026-08-12", @@ -4362,7 +4362,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.lucio.ability2", "note": "commitDate=2026-08-12", @@ -4390,7 +4390,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.lucio.ultimate", "note": "commitDate=2026-08-12", @@ -4398,7 +4398,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.lucio.ultimate", "note": "commitDate=2026-08-12", @@ -4426,7 +4426,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.lucio", "note": "commitDate=2026-08-12", @@ -4435,7 +4435,7 @@ ], "id": "lucio", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.lucio", "note": "commitDate=2026-08-12", @@ -4461,7 +4461,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/lucio/", "note": "role metadata; accessed 2026-08-18", @@ -4474,7 +4474,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.mauga.primaryFire", "note": "commitDate=2026-08-12", @@ -4482,7 +4482,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.mauga.primaryFire", "note": "commitDate=2026-08-12", @@ -4510,7 +4510,7 @@ "slot": "primaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.mauga.secondaryFire", "note": "commitDate=2026-08-12", @@ -4518,7 +4518,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.mauga.secondaryFire", "note": "commitDate=2026-08-12", @@ -4546,7 +4546,7 @@ "slot": "secondaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.mauga.ability1", "note": "commitDate=2026-08-12", @@ -4554,7 +4554,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.mauga.ability1", "note": "commitDate=2026-08-12", @@ -4582,7 +4582,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.mauga.ability2", "note": "commitDate=2026-08-12", @@ -4590,7 +4590,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.mauga.ability2", "note": "commitDate=2026-08-12", @@ -4618,7 +4618,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.mauga.ultimate", "note": "commitDate=2026-08-12", @@ -4626,7 +4626,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.mauga.ultimate", "note": "commitDate=2026-08-12", @@ -4654,7 +4654,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.mauga", "note": "commitDate=2026-08-12", @@ -4663,7 +4663,7 @@ ], "id": "mauga", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.mauga", "note": "commitDate=2026-08-12", @@ -4689,7 +4689,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/mauga/", "note": "role metadata; accessed 2026-08-18", @@ -4702,7 +4702,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.mei.ability1", "note": "commitDate=2026-08-12", @@ -4710,7 +4710,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.mei.ability1", "note": "commitDate=2026-08-12", @@ -4738,7 +4738,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.mei.ability2", "note": "commitDate=2026-08-12", @@ -4746,7 +4746,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.mei.ability2", "note": "commitDate=2026-08-12", @@ -4774,7 +4774,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.mei.ultimate", "note": "commitDate=2026-08-12", @@ -4782,7 +4782,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.mei.ultimate", "note": "commitDate=2026-08-12", @@ -4810,7 +4810,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.mei", "note": "commitDate=2026-08-12", @@ -4819,7 +4819,7 @@ ], "id": "mei", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.mei", "note": "commitDate=2026-08-12", @@ -4845,7 +4845,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/mei/", "note": "role metadata; accessed 2026-08-18", @@ -4858,7 +4858,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.mercy.ability1", "note": "commitDate=2026-08-12", @@ -4866,7 +4866,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.mercy.ability1", "note": "commitDate=2026-08-12", @@ -4894,7 +4894,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.mercy.ability2", "note": "commitDate=2026-08-12", @@ -4902,7 +4902,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.mercy.ability2", "note": "commitDate=2026-08-12", @@ -4930,7 +4930,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.mercy.ultimate", "note": "commitDate=2026-08-12", @@ -4938,7 +4938,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.mercy.ultimate", "note": "commitDate=2026-08-12", @@ -4966,7 +4966,7 @@ "slot": "ultimate" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.mercy.passive", "note": "commitDate=2026-08-12", @@ -4974,7 +4974,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.mercy.passive", "note": "commitDate=2026-08-12", @@ -5002,7 +5002,7 @@ "slot": "passive" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.mercy", "note": "commitDate=2026-08-12", @@ -5011,7 +5011,7 @@ ], "id": "mercy", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.mercy", "note": "commitDate=2026-08-12", @@ -5037,7 +5037,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/mercy/", "note": "role metadata; accessed 2026-08-18", @@ -5050,7 +5050,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.mizuki.secondaryFire", "note": "commitDate=2026-08-12", @@ -5058,7 +5058,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.mizuki.secondaryFire", "note": "commitDate=2026-08-12", @@ -5074,7 +5074,7 @@ "slot": "secondaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.mizuki.ability1", "note": "commitDate=2026-08-12", @@ -5082,7 +5082,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.mizuki.ability1", "note": "commitDate=2026-08-12", @@ -5098,7 +5098,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.mizuki.ability2", "note": "commitDate=2026-08-12", @@ -5106,7 +5106,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.mizuki.ability2", "note": "commitDate=2026-08-12", @@ -5122,7 +5122,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.mizuki.ultimate", "note": "commitDate=2026-08-12", @@ -5130,7 +5130,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.mizuki.ultimate", "note": "commitDate=2026-08-12", @@ -5146,7 +5146,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.mizuki", "note": "commitDate=2026-08-12", @@ -5155,7 +5155,7 @@ ], "id": "mizuki", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.mizuki", "note": "commitDate=2026-08-12", @@ -5169,7 +5169,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/mizuki/", "note": "role metadata; accessed 2026-08-18", @@ -5182,7 +5182,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.moira.ability1", "note": "commitDate=2026-08-12", @@ -5190,7 +5190,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.moira.ability1", "note": "commitDate=2026-08-12", @@ -5218,7 +5218,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.moira.ability2", "note": "commitDate=2026-08-12", @@ -5226,7 +5226,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.moira.ability2", "note": "commitDate=2026-08-12", @@ -5254,7 +5254,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.moira.ultimate", "note": "commitDate=2026-08-12", @@ -5262,7 +5262,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.moira.ultimate", "note": "commitDate=2026-08-12", @@ -5290,7 +5290,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.moira", "note": "commitDate=2026-08-12", @@ -5299,7 +5299,7 @@ ], "id": "moira", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.moira", "note": "commitDate=2026-08-12", @@ -5325,7 +5325,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/moira/", "note": "role metadata; accessed 2026-08-18", @@ -5338,7 +5338,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.orisa.secondaryFire", "note": "commitDate=2026-08-12", @@ -5346,7 +5346,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.orisa.secondaryFire", "note": "commitDate=2026-08-12", @@ -5374,7 +5374,7 @@ "slot": "secondaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.orisa.ability1", "note": "commitDate=2026-08-12", @@ -5382,7 +5382,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.orisa.ability1", "note": "commitDate=2026-08-12", @@ -5410,7 +5410,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.orisa.ability2", "note": "commitDate=2026-08-12", @@ -5418,7 +5418,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.orisa.ability2", "note": "commitDate=2026-08-12", @@ -5446,7 +5446,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.orisa.ultimate", "note": "commitDate=2026-08-12", @@ -5454,7 +5454,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.orisa.ultimate", "note": "commitDate=2026-08-12", @@ -5482,7 +5482,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.orisa", "note": "commitDate=2026-08-12", @@ -5491,7 +5491,7 @@ ], "id": "orisa", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.orisa", "note": "commitDate=2026-08-12", @@ -5517,7 +5517,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/orisa/", "note": "role metadata; accessed 2026-08-18", @@ -5530,7 +5530,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.pharah.secondaryFire", "note": "commitDate=2026-08-12", @@ -5538,7 +5538,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.pharah.secondaryFire", "note": "commitDate=2026-08-12", @@ -5566,7 +5566,7 @@ "slot": "secondaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.pharah.ability1", "note": "commitDate=2026-08-12", @@ -5574,7 +5574,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.pharah.ability1", "note": "commitDate=2026-08-12", @@ -5602,7 +5602,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.pharah.ability2", "note": "commitDate=2026-08-12", @@ -5610,7 +5610,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.pharah.ability2", "note": "commitDate=2026-08-12", @@ -5638,7 +5638,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.pharah.ultimate", "note": "commitDate=2026-08-12", @@ -5646,7 +5646,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.pharah.ultimate", "note": "commitDate=2026-08-12", @@ -5674,7 +5674,7 @@ "slot": "ultimate" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.pharah.passive", "note": "commitDate=2026-08-12", @@ -5682,7 +5682,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.pharah.passive", "note": "commitDate=2026-08-12", @@ -5710,7 +5710,7 @@ "slot": "passive" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.pharah", "note": "commitDate=2026-08-12", @@ -5719,7 +5719,7 @@ ], "id": "pharah", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.pharah", "note": "commitDate=2026-08-12", @@ -5745,7 +5745,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/pharah/", "note": "role metadata; accessed 2026-08-18", @@ -5758,7 +5758,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.ramattra.secondaryFire", "note": "commitDate=2026-08-12", @@ -5766,7 +5766,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.ramattra.secondaryFire", "note": "commitDate=2026-08-12", @@ -5794,7 +5794,7 @@ "slot": "secondaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.ramattra.ability1", "note": "commitDate=2026-08-12", @@ -5802,7 +5802,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.ramattra.ability1", "note": "commitDate=2026-08-12", @@ -5830,7 +5830,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.ramattra.ability2", "note": "commitDate=2026-08-12", @@ -5847,7 +5847,7 @@ "crowdControl" ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.ramattra.ability2", "note": "commitDate=2026-08-12", @@ -5880,7 +5880,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.ramattra.ultimate", "note": "commitDate=2026-08-12", @@ -5888,7 +5888,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.ramattra.ultimate", "note": "commitDate=2026-08-12", @@ -5916,7 +5916,7 @@ "slot": "ultimate" }, { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/ramattra/", "note": "official ability description; accessed 2026-08-18", @@ -5924,7 +5924,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/ramattra/", "note": "official ability description; accessed 2026-08-18", @@ -5939,7 +5939,7 @@ "variant": "omnic" }, { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/ramattra/", "note": "official ability description; accessed 2026-08-18", @@ -5947,7 +5947,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/ramattra/", "note": "official ability description; accessed 2026-08-18", @@ -5962,7 +5962,7 @@ "variant": "nemesis" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.ramattra", "note": "commitDate=2026-08-12", @@ -5971,7 +5971,7 @@ ], "id": "ramattra", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.ramattra", "note": "commitDate=2026-08-12", @@ -5997,7 +5997,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/ramattra/", "note": "role metadata; accessed 2026-08-18", @@ -6010,7 +6010,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.reaper.ability1", "note": "commitDate=2026-08-12", @@ -6018,7 +6018,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.reaper.ability1", "note": "commitDate=2026-08-12", @@ -6046,7 +6046,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.reaper.ability2", "note": "commitDate=2026-08-12", @@ -6054,7 +6054,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.reaper.ability2", "note": "commitDate=2026-08-12", @@ -6082,7 +6082,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.reaper.ultimate", "note": "commitDate=2026-08-12", @@ -6090,7 +6090,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.reaper.ultimate", "note": "commitDate=2026-08-12", @@ -6118,7 +6118,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.reaper", "note": "commitDate=2026-08-12", @@ -6127,7 +6127,7 @@ ], "id": "reaper", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.reaper", "note": "commitDate=2026-08-12", @@ -6153,7 +6153,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/reaper/", "note": "role metadata; accessed 2026-08-18", @@ -6166,7 +6166,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.reinhardt.secondaryFire", "note": "commitDate=2026-08-12", @@ -6174,7 +6174,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.reinhardt.secondaryFire", "note": "commitDate=2026-08-12", @@ -6202,7 +6202,7 @@ "slot": "secondaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.reinhardt.ability1", "note": "commitDate=2026-08-12", @@ -6210,7 +6210,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.reinhardt.ability1", "note": "commitDate=2026-08-12", @@ -6238,7 +6238,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.reinhardt.ability2", "note": "commitDate=2026-08-12", @@ -6246,7 +6246,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.reinhardt.ability2", "note": "commitDate=2026-08-12", @@ -6274,7 +6274,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.reinhardt.ultimate", "note": "commitDate=2026-08-12", @@ -6282,7 +6282,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.reinhardt.ultimate", "note": "commitDate=2026-08-12", @@ -6310,7 +6310,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.reinhardt", "note": "commitDate=2026-08-12", @@ -6319,7 +6319,7 @@ ], "id": "reinhardt", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.reinhardt", "note": "commitDate=2026-08-12", @@ -6345,7 +6345,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/reinhardt/", "note": "role metadata; accessed 2026-08-18", @@ -6358,7 +6358,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.roadhog.ability1", "note": "commitDate=2026-08-12", @@ -6366,7 +6366,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.roadhog.ability1", "note": "commitDate=2026-08-12", @@ -6394,7 +6394,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.roadhog.ability2", "note": "commitDate=2026-08-12", @@ -6402,7 +6402,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.roadhog.ability2", "note": "commitDate=2026-08-12", @@ -6430,7 +6430,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.roadhog.ultimate", "note": "commitDate=2026-08-12", @@ -6438,7 +6438,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.roadhog.ultimate", "note": "commitDate=2026-08-12", @@ -6466,7 +6466,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.roadhog", "note": "commitDate=2026-08-12", @@ -6475,7 +6475,7 @@ ], "id": "roadhog", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.roadhog", "note": "commitDate=2026-08-12", @@ -6501,7 +6501,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/roadhog/", "note": "role metadata; accessed 2026-08-18", @@ -6514,7 +6514,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.shion.secondaryFire", "note": "commitDate=2026-08-12", @@ -6522,7 +6522,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.shion.secondaryFire", "note": "commitDate=2026-08-12", @@ -6537,7 +6537,7 @@ "slot": "secondaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.shion.ability1", "note": "commitDate=2026-08-12", @@ -6545,7 +6545,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.shion.ability1", "note": "commitDate=2026-08-12", @@ -6560,7 +6560,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.shion.ability2", "note": "commitDate=2026-08-12", @@ -6568,7 +6568,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.shion.ability2", "note": "commitDate=2026-08-12", @@ -6583,7 +6583,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.shion.ultimate", "note": "commitDate=2026-08-12", @@ -6591,7 +6591,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.shion.ultimate", "note": "commitDate=2026-08-12", @@ -6606,7 +6606,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.shion", "note": "commitDate=2026-08-12", @@ -6615,7 +6615,7 @@ ], "id": "shion", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.shion", "note": "commitDate=2026-08-12", @@ -6628,7 +6628,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/shion/", "note": "role metadata; accessed 2026-08-18", @@ -6641,7 +6641,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.sierra.secondaryFire", "note": "commitDate=2026-08-12", @@ -6649,7 +6649,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.sierra.secondaryFire", "note": "commitDate=2026-08-12", @@ -6664,7 +6664,7 @@ "slot": "secondaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.sierra.ability1", "note": "commitDate=2026-08-12", @@ -6672,7 +6672,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.sierra.ability1", "note": "commitDate=2026-08-12", @@ -6687,7 +6687,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.sierra.ability2", "note": "commitDate=2026-08-12", @@ -6695,7 +6695,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.sierra.ability2", "note": "commitDate=2026-08-12", @@ -6710,7 +6710,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.sierra.ultimate", "note": "commitDate=2026-08-12", @@ -6718,7 +6718,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.sierra.ultimate", "note": "commitDate=2026-08-12", @@ -6733,7 +6733,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.sierra", "note": "commitDate=2026-08-12", @@ -6742,7 +6742,7 @@ ], "id": "sierra", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.sierra", "note": "commitDate=2026-08-12", @@ -6755,7 +6755,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/sierra/", "note": "role metadata; accessed 2026-08-18", @@ -6768,7 +6768,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.sigma.secondaryFire", "note": "commitDate=2026-08-12", @@ -6776,7 +6776,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.sigma.secondaryFire", "note": "commitDate=2026-08-12", @@ -6804,7 +6804,7 @@ "slot": "secondaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.sigma.ability1", "note": "commitDate=2026-08-12", @@ -6812,7 +6812,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.sigma.ability1", "note": "commitDate=2026-08-12", @@ -6840,7 +6840,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.sigma.ability2", "note": "commitDate=2026-08-12", @@ -6848,7 +6848,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.sigma.ability2", "note": "commitDate=2026-08-12", @@ -6876,7 +6876,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.sigma.ultimate", "note": "commitDate=2026-08-12", @@ -6884,7 +6884,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.sigma.ultimate", "note": "commitDate=2026-08-12", @@ -6912,7 +6912,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.sigma", "note": "commitDate=2026-08-12", @@ -6921,7 +6921,7 @@ ], "id": "sigma", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.sigma", "note": "commitDate=2026-08-12", @@ -6947,7 +6947,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/sigma/", "note": "role metadata; accessed 2026-08-18", @@ -6960,7 +6960,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.sojourn.secondaryFire", "note": "commitDate=2026-08-12", @@ -6968,7 +6968,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.sojourn.secondaryFire", "note": "commitDate=2026-08-12", @@ -6982,7 +6982,7 @@ "slot": "secondaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.sojourn.ability1", "note": "commitDate=2026-08-12", @@ -6990,7 +6990,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.sojourn.ability1", "note": "commitDate=2026-08-12", @@ -7018,7 +7018,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.sojourn.ability2", "note": "commitDate=2026-08-12", @@ -7026,7 +7026,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.sojourn.ability2", "note": "commitDate=2026-08-12", @@ -7054,7 +7054,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.sojourn.ultimate", "note": "commitDate=2026-08-12", @@ -7062,7 +7062,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.sojourn.ultimate", "note": "commitDate=2026-08-12", @@ -7090,7 +7090,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.sojourn", "note": "commitDate=2026-08-12", @@ -7099,7 +7099,7 @@ ], "id": "sojourn", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.sojourn", "note": "commitDate=2026-08-12", @@ -7125,7 +7125,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/sojourn/", "note": "role metadata; accessed 2026-08-18", @@ -7138,7 +7138,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.soldier.secondaryFire", "note": "commitDate=2026-08-12", @@ -7146,7 +7146,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.soldier.secondaryFire", "note": "commitDate=2026-08-12", @@ -7174,7 +7174,7 @@ "slot": "secondaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.soldier.ability1", "note": "commitDate=2026-08-12", @@ -7182,7 +7182,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.soldier.ability1", "note": "commitDate=2026-08-12", @@ -7210,7 +7210,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.soldier.ability2", "note": "commitDate=2026-08-12", @@ -7218,7 +7218,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.soldier.ability2", "note": "commitDate=2026-08-12", @@ -7246,7 +7246,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.soldier.ultimate", "note": "commitDate=2026-08-12", @@ -7254,7 +7254,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.soldier.ultimate", "note": "commitDate=2026-08-12", @@ -7282,7 +7282,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.soldier", "note": "commitDate=2026-08-12", @@ -7291,7 +7291,7 @@ ], "id": "soldier", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.soldier", "note": "commitDate=2026-08-12", @@ -7317,7 +7317,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/soldier-76/", "note": "role metadata; accessed 2026-08-18", @@ -7330,7 +7330,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.sombra.secondaryFire", "note": "commitDate=2026-08-12", @@ -7338,7 +7338,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.sombra.secondaryFire", "note": "commitDate=2026-08-12", @@ -7366,7 +7366,7 @@ "slot": "secondaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.sombra.ability1", "note": "commitDate=2026-08-12", @@ -7374,7 +7374,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.sombra.ability1", "note": "commitDate=2026-08-12", @@ -7402,7 +7402,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.sombra.ability2", "note": "commitDate=2026-08-12", @@ -7410,7 +7410,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.sombra.ability2", "note": "commitDate=2026-08-12", @@ -7438,7 +7438,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.sombra.ultimate", "note": "commitDate=2026-08-12", @@ -7446,7 +7446,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.sombra.ultimate", "note": "commitDate=2026-08-12", @@ -7474,7 +7474,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.sombra", "note": "commitDate=2026-08-12", @@ -7483,7 +7483,7 @@ ], "id": "sombra", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.sombra", "note": "commitDate=2026-08-12", @@ -7509,7 +7509,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/sombra/", "note": "role metadata; accessed 2026-08-18", @@ -7522,7 +7522,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.symmetra.ability1", "note": "commitDate=2026-08-12", @@ -7530,7 +7530,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.symmetra.ability1", "note": "commitDate=2026-08-12", @@ -7558,7 +7558,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.symmetra.ability2", "note": "commitDate=2026-08-12", @@ -7566,7 +7566,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.symmetra.ability2", "note": "commitDate=2026-08-12", @@ -7594,7 +7594,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.symmetra.ultimate", "note": "commitDate=2026-08-12", @@ -7602,7 +7602,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.symmetra.ultimate", "note": "commitDate=2026-08-12", @@ -7630,7 +7630,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.symmetra", "note": "commitDate=2026-08-12", @@ -7639,7 +7639,7 @@ ], "id": "symmetra", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.symmetra", "note": "commitDate=2026-08-12", @@ -7665,7 +7665,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/symmetra/", "note": "role metadata; accessed 2026-08-18", @@ -7678,7 +7678,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.torbjorn.ability1", "note": "commitDate=2026-08-12", @@ -7686,7 +7686,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.torbjorn.ability1", "note": "commitDate=2026-08-12", @@ -7714,7 +7714,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.torbjorn.ability2", "note": "commitDate=2026-08-12", @@ -7722,7 +7722,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.torbjorn.ability2", "note": "commitDate=2026-08-12", @@ -7750,7 +7750,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.torbjorn.ultimate", "note": "commitDate=2026-08-12", @@ -7758,7 +7758,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.torbjorn.ultimate", "note": "commitDate=2026-08-12", @@ -7786,7 +7786,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.torbjorn", "note": "commitDate=2026-08-12", @@ -7795,7 +7795,7 @@ ], "id": "torbjorn", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.torbjorn", "note": "commitDate=2026-08-12", @@ -7821,7 +7821,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/torbjorn/", "note": "role metadata; accessed 2026-08-18", @@ -7834,7 +7834,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.tracer.ability1", "note": "commitDate=2026-08-12", @@ -7842,7 +7842,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.tracer.ability1", "note": "commitDate=2026-08-12", @@ -7870,7 +7870,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.tracer.ability2", "note": "commitDate=2026-08-12", @@ -7878,7 +7878,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.tracer.ability2", "note": "commitDate=2026-08-12", @@ -7906,7 +7906,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.tracer.ultimate", "note": "commitDate=2026-08-12", @@ -7914,7 +7914,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.tracer.ultimate", "note": "commitDate=2026-08-12", @@ -7942,7 +7942,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.tracer", "note": "commitDate=2026-08-12", @@ -7951,7 +7951,7 @@ ], "id": "tracer", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.tracer", "note": "commitDate=2026-08-12", @@ -7977,7 +7977,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/tracer/", "note": "role metadata; accessed 2026-08-18", @@ -7990,7 +7990,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.vendetta.primaryFire", "note": "commitDate=2026-08-12", @@ -7998,7 +7998,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.vendetta.primaryFire", "note": "commitDate=2026-08-12", @@ -8014,7 +8014,7 @@ "slot": "primaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.vendetta.secondaryFire", "note": "commitDate=2026-08-12", @@ -8022,7 +8022,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.vendetta.secondaryFire", "note": "commitDate=2026-08-12", @@ -8038,7 +8038,7 @@ "slot": "secondaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.vendetta.ability1", "note": "commitDate=2026-08-12", @@ -8046,7 +8046,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.vendetta.ability1", "note": "commitDate=2026-08-12", @@ -8062,7 +8062,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.vendetta.ability2", "note": "commitDate=2026-08-12", @@ -8070,7 +8070,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.vendetta.ability2", "note": "commitDate=2026-08-12", @@ -8086,7 +8086,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.vendetta.ultimate", "note": "commitDate=2026-08-12", @@ -8094,7 +8094,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.vendetta.ultimate", "note": "commitDate=2026-08-12", @@ -8110,7 +8110,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.vendetta", "note": "commitDate=2026-08-12", @@ -8119,7 +8119,7 @@ ], "id": "vendetta", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.vendetta", "note": "commitDate=2026-08-12", @@ -8133,7 +8133,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/vendetta/", "note": "role metadata; accessed 2026-08-18", @@ -8146,7 +8146,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.venture.primaryFire", "note": "commitDate=2026-08-12", @@ -8154,7 +8154,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.venture.primaryFire", "note": "commitDate=2026-08-12", @@ -8182,7 +8182,7 @@ "slot": "primaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.venture.secondaryFire", "note": "commitDate=2026-08-12", @@ -8200,7 +8200,7 @@ "knockback" ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.venture.secondaryFire", "note": "commitDate=2026-08-12", @@ -8233,7 +8233,7 @@ "slot": "secondaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.venture.ability1", "note": "commitDate=2026-08-12", @@ -8241,7 +8241,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.venture.ability1", "note": "commitDate=2026-08-12", @@ -8269,7 +8269,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.venture.ultimate", "note": "commitDate=2026-08-12", @@ -8277,7 +8277,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.venture.ultimate", "note": "commitDate=2026-08-12", @@ -8305,7 +8305,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.venture", "note": "commitDate=2026-08-12", @@ -8314,7 +8314,7 @@ ], "id": "venture", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.venture", "note": "commitDate=2026-08-12", @@ -8340,7 +8340,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/venture/", "note": "role metadata; accessed 2026-08-18", @@ -8353,7 +8353,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.widowmaker.ability1", "note": "commitDate=2026-08-12", @@ -8361,7 +8361,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.widowmaker.ability1", "note": "commitDate=2026-08-12", @@ -8389,7 +8389,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.widowmaker.ability2", "note": "commitDate=2026-08-12", @@ -8397,7 +8397,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.widowmaker.ability2", "note": "commitDate=2026-08-12", @@ -8425,7 +8425,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.widowmaker.ultimate", "note": "commitDate=2026-08-12", @@ -8433,7 +8433,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.widowmaker.ultimate", "note": "commitDate=2026-08-12", @@ -8461,7 +8461,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.widowmaker", "note": "commitDate=2026-08-12", @@ -8470,7 +8470,7 @@ ], "id": "widowmaker", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.widowmaker", "note": "commitDate=2026-08-12", @@ -8496,7 +8496,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/widowmaker/", "note": "role metadata; accessed 2026-08-18", @@ -8509,7 +8509,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.winston.ability1", "note": "commitDate=2026-08-12", @@ -8517,7 +8517,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.winston.ability1", "note": "commitDate=2026-08-12", @@ -8545,7 +8545,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.winston.ability2", "note": "commitDate=2026-08-12", @@ -8553,7 +8553,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.winston.ability2", "note": "commitDate=2026-08-12", @@ -8581,7 +8581,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.winston.ultimate", "note": "commitDate=2026-08-12", @@ -8589,7 +8589,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.winston.ultimate", "note": "commitDate=2026-08-12", @@ -8617,7 +8617,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.winston", "note": "commitDate=2026-08-12", @@ -8626,7 +8626,7 @@ ], "id": "winston", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.winston", "note": "commitDate=2026-08-12", @@ -8652,7 +8652,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/winston/", "note": "role metadata; accessed 2026-08-18", @@ -8665,7 +8665,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.wreckingBall.secondaryFire", "note": "commitDate=2026-08-12", @@ -8673,7 +8673,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.wreckingBall.secondaryFire", "note": "commitDate=2026-08-12", @@ -8701,7 +8701,7 @@ "slot": "secondaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.wreckingBall.ability1", "note": "commitDate=2026-08-12", @@ -8709,7 +8709,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.wreckingBall.ability1", "note": "commitDate=2026-08-12", @@ -8737,7 +8737,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.wreckingBall.ability2", "note": "commitDate=2026-08-12", @@ -8745,7 +8745,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.wreckingBall.ability2", "note": "commitDate=2026-08-12", @@ -8773,7 +8773,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.wreckingBall.ability3", "note": "commitDate=2026-08-12", @@ -8781,7 +8781,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.wreckingBall.ability3", "note": "commitDate=2026-08-12", @@ -8809,7 +8809,7 @@ "slot": "ability3" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.wreckingBall.ultimate", "note": "commitDate=2026-08-12", @@ -8817,7 +8817,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.wreckingBall.ultimate", "note": "commitDate=2026-08-12", @@ -8845,7 +8845,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.wreckingBall", "note": "commitDate=2026-08-12", @@ -8854,7 +8854,7 @@ ], "id": "wreckingBall", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.wreckingBall", "note": "commitDate=2026-08-12", @@ -8880,7 +8880,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/wrecking-ball/", "note": "role metadata; accessed 2026-08-18", @@ -8893,7 +8893,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.wuyang.secondaryFire", "note": "commitDate=2026-08-12", @@ -8901,7 +8901,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.wuyang.secondaryFire", "note": "commitDate=2026-08-12", @@ -8917,7 +8917,7 @@ "slot": "secondaryFire" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.wuyang.ability1", "note": "commitDate=2026-08-12", @@ -8925,7 +8925,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.wuyang.ability1", "note": "commitDate=2026-08-12", @@ -8941,7 +8941,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.wuyang.ability2", "note": "commitDate=2026-08-12", @@ -8949,7 +8949,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.wuyang.ability2", "note": "commitDate=2026-08-12", @@ -8965,7 +8965,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.wuyang.ultimate", "note": "commitDate=2026-08-12", @@ -8973,7 +8973,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.wuyang.ultimate", "note": "commitDate=2026-08-12", @@ -8989,7 +8989,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.wuyang", "note": "commitDate=2026-08-12", @@ -8998,7 +8998,7 @@ ], "id": "wuyang", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.wuyang", "note": "commitDate=2026-08-12", @@ -9012,7 +9012,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/wuyang/", "note": "role metadata; accessed 2026-08-18", @@ -9025,7 +9025,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.zarya.ability1", "note": "commitDate=2026-08-12", @@ -9033,7 +9033,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.zarya.ability1", "note": "commitDate=2026-08-12", @@ -9061,7 +9061,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.zarya.ability2", "note": "commitDate=2026-08-12", @@ -9069,7 +9069,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.zarya.ability2", "note": "commitDate=2026-08-12", @@ -9097,7 +9097,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.zarya.ultimate", "note": "commitDate=2026-08-12", @@ -9105,7 +9105,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.zarya.ultimate", "note": "commitDate=2026-08-12", @@ -9133,7 +9133,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.zarya", "note": "commitDate=2026-08-12", @@ -9142,7 +9142,7 @@ ], "id": "zarya", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.zarya", "note": "commitDate=2026-08-12", @@ -9168,7 +9168,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/zarya/", "note": "role metadata; accessed 2026-08-18", @@ -9181,7 +9181,7 @@ { "abilities": [ { - "evidence": [ + "sources": [ { "locator": "data.heroes.zenyatta.ability1", "note": "commitDate=2026-08-12", @@ -9189,7 +9189,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.zenyatta.ability1", "note": "commitDate=2026-08-12", @@ -9217,7 +9217,7 @@ "slot": "ability1" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.zenyatta.ability2", "note": "commitDate=2026-08-12", @@ -9225,7 +9225,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.zenyatta.ability2", "note": "commitDate=2026-08-12", @@ -9253,7 +9253,7 @@ "slot": "ability2" }, { - "evidence": [ + "sources": [ { "locator": "data.heroes.zenyatta.ultimate", "note": "commitDate=2026-08-12", @@ -9261,7 +9261,7 @@ } ], "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.zenyatta.ultimate", "note": "commitDate=2026-08-12", @@ -9289,7 +9289,7 @@ "slot": "ultimate" } ], - "evidence": [ + "sources": [ { "locator": "data.heroes.zenyatta", "note": "commitDate=2026-08-12", @@ -9298,7 +9298,7 @@ ], "id": "zenyatta", "name": { - "evidence": [ + "sources": [ { "locator": "data.heroes.zenyatta", "note": "commitDate=2026-08-12", @@ -9324,7 +9324,7 @@ } }, "role": { - "evidence": [ + "sources": [ { "locator": "https://overwatch.blizzard.com/en-us/heroes/zenyatta/", "note": "role metadata; accessed 2026-08-18", @@ -9337,12 +9337,12 @@ ], "identity": { "datasetId": "overwatch-workshop-hero-gameplay", - "digest": "sha256:5c01599839834f3599a524c7307d3ceaa493e6a1e845d9884dc9617f2af4068a", + "digest": "sha256:0902a247fb709bf5e326bbdb5475b41d4062b991ba3e0aea9350e5ecd404bc3c", "license": "MIT-compatible user-provided export", "reviewed": true, "source": "workshop-data/workshop-data.json@d854bf01fc7bbf3b2169f67408c07a8da8989ad6", "target": "Overwatch Workshop hero identities and named ability slots", "version": "2026-08-12" }, - "schemaVersion": 1 + "schemaVersion": 2 } diff --git a/crates/workshop-rs/src/gameplay/mod.rs b/crates/workshop-rs/src/gameplay/mod.rs index 5b67c3db..b1f5e2ef 100644 --- a/crates/workshop-rs/src/gameplay/mod.rs +++ b/crates/workshop-rs/src/gameplay/mod.rs @@ -4,7 +4,7 @@ //! deliberately independent from the Workshop [`crate::catalog`] identity //! and from any source-language provider. Ability identity is the open //! `hero + logical slot + optional hero-local variant` tuple; display names -//! are localized, source-backed metadata and are not semantic identity. +//! are localized source metadata and are not semantic identity. pub mod data; pub mod query; @@ -274,17 +274,18 @@ impl LocalizedText { } } -/// A machine-identifiable evidence reference for a gameplay fact. +/// A machine-identifiable source reference for a gameplay fact. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct EvidenceRef { +pub struct SourceReference { pub source: String, pub locator: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub note: Option, } -/// Identity and provenance of a gameplay dataset. This is distinct from the Workshop parser/catalog dataset identity. +/// Identity and source metadata of a gameplay dataset. This is distinct from +/// the Workshop parser/catalog dataset identity. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GameplayDatasetIdentity { @@ -297,22 +298,22 @@ pub struct GameplayDatasetIdentity { pub reviewed: bool, } -/// A gameplay fact tied to evidence in the dataset version being consumed. +/// A gameplay fact tied to source references in the dataset version being consumed. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Fact { pub value: T, - pub evidence: Vec, + pub sources: Vec, } impl Fact { - pub fn new(value: T, evidence: Vec) -> Self { - Self { value, evidence } + pub fn new(value: T, sources: Vec) -> Self { + Self { value, sources } } pub fn value(&self) -> &T { &self.value } - pub fn evidence(&self) -> &[EvidenceRef] { - &self.evidence + pub fn sources(&self) -> &[SourceReference] { + &self.sources } } @@ -369,7 +370,7 @@ pub struct Ability { keywords: BTreeSet, #[serde(default)] stats: BTreeMap>, - evidence: Vec, + sources: Vec, } impl Ability { @@ -377,7 +378,7 @@ impl Ability { slot: LogicalSlot, variant: Option, name: Fact, - evidence: Vec, + sources: Vec, ) -> Self { Self { slot, @@ -385,7 +386,7 @@ impl Ability { name, keywords: BTreeSet::new(), stats: BTreeMap::new(), - evidence, + sources, } } pub fn with_keyword(mut self, keyword: impl Into) -> Self { @@ -420,8 +421,8 @@ impl Ability { pub fn stats(&self) -> impl Iterator)> { self.stats.iter() } - pub fn evidence(&self) -> &[EvidenceRef] { - &self.evidence + pub fn sources(&self) -> &[SourceReference] { + &self.sources } } @@ -436,7 +437,7 @@ pub struct Hero { #[serde(default)] stats: BTreeMap>, abilities: Vec, - evidence: Vec, + sources: Vec, } impl Hero { @@ -444,7 +445,7 @@ impl Hero { id: HeroId, name: Fact, abilities: Vec, - evidence: Vec, + sources: Vec, ) -> Self { Self { id, @@ -452,7 +453,7 @@ impl Hero { role: None, stats: BTreeMap::new(), abilities, - evidence, + sources, } } pub fn with_role(mut self, role: Fact) -> Self { @@ -529,8 +530,8 @@ impl Hero { variant: variant.clone(), }) } - pub fn evidence(&self) -> &[EvidenceRef] { - &self.evidence + pub fn sources(&self) -> &[SourceReference] { + &self.sources } } @@ -594,7 +595,7 @@ pub enum GameplayDataError { hero: HeroId, slot: LogicalSlot, }, - MissingEvidence(String), + MissingSource(String), EmptyId(&'static str), InvalidQuantity { value: f64, @@ -626,7 +627,9 @@ impl std::fmt::Display for GameplayDataError { f, "hero '{hero}' has multiple abilities in slot '{slot}' but not every record has a variant" ), - Self::MissingEvidence(path) => write!(f, "gameplay fact '{path}' has no evidence"), + Self::MissingSource(path) => { + write!(f, "gameplay fact '{path}' has no source reference") + } Self::EmptyId(field) => write!(f, "gameplay identity '{field}' is empty"), Self::InvalidQuantity { value } => write!(f, "quantity value '{value}' is not finite"), Self::Malformed(message) => write!(f, "malformed gameplay data: {message}"), @@ -723,20 +726,20 @@ fn validate_hero(hero: &Hero) -> Result<(), GameplayDataError> { if hero.id.as_str().is_empty() { return Err(GameplayDataError::EmptyId("hero")); } - if hero.evidence.is_empty() { - return Err(GameplayDataError::MissingEvidence(format!( + if hero.sources.is_empty() { + return Err(GameplayDataError::MissingSource(format!( "hero {}", hero.id ))); } - if hero.name.evidence.is_empty() { - return Err(GameplayDataError::MissingEvidence(format!( + if hero.name.sources.is_empty() { + return Err(GameplayDataError::MissingSource(format!( "hero {} name", hero.id ))); } - validate_evidence(&format!("hero {}", hero.id), &hero.evidence)?; - validate_evidence(&format!("hero {} name", hero.id), &hero.name.evidence)?; + validate_sources(&format!("hero {}", hero.id), &hero.sources)?; + validate_sources(&format!("hero {} name", hero.id), &hero.name.sources)?; if let Some(role) = &hero.role { if role.value.as_str().is_empty() { return Err(GameplayDataError::EmptyId("hero role")); @@ -763,25 +766,25 @@ fn validate_hero(hero: &Hero) -> Result<(), GameplayDataError> { { return Err(GameplayDataError::EmptyId("ability variant")); } - if ability.evidence.is_empty() { - return Err(GameplayDataError::MissingEvidence(format!( + if ability.sources.is_empty() { + return Err(GameplayDataError::MissingSource(format!( "hero {} ability {}", hero.id, ability.slot ))); } - validate_evidence( + validate_sources( &format!("hero {} ability {}", hero.id, ability.slot), - &ability.evidence, + &ability.sources, )?; - if ability.name.evidence.is_empty() { - return Err(GameplayDataError::MissingEvidence(format!( + if ability.name.sources.is_empty() { + return Err(GameplayDataError::MissingSource(format!( "hero {} ability {} name", hero.id, ability.slot ))); } - validate_evidence( + validate_sources( &format!("hero {} ability {} name", hero.id, ability.slot), - &ability.name.evidence, + &ability.name.sources, )?; let slot_variant = (ability.slot.clone(), ability.variant.clone()); if !slot_variants.insert(slot_variant) { @@ -838,16 +841,16 @@ fn validate_stat_value(_path: &str, value: &StatValue) -> Result<(), GameplayDat } fn validate_fact(path: &str, fact: &Fact) -> Result<(), GameplayDataError> { - if fact.evidence.is_empty() { - return Err(GameplayDataError::MissingEvidence(path.to_string())); + if fact.sources.is_empty() { + return Err(GameplayDataError::MissingSource(path.to_string())); } - validate_evidence(path, &fact.evidence) + validate_sources(path, &fact.sources) } -fn validate_evidence(path: &str, evidence: &[EvidenceRef]) -> Result<(), GameplayDataError> { - for item in evidence { +fn validate_sources(path: &str, sources: &[SourceReference]) -> Result<(), GameplayDataError> { + for item in sources { if item.source.is_empty() || item.locator.is_empty() { - return Err(GameplayDataError::MissingEvidence(path.to_string())); + return Err(GameplayDataError::MissingSource(path.to_string())); } } Ok(()) diff --git a/crates/workshop-rs/src/gameplay/query.rs b/crates/workshop-rs/src/gameplay/query.rs index 456736ff..e83b0a51 100644 --- a/crates/workshop-rs/src/gameplay/query.rs +++ b/crates/workshop-rs/src/gameplay/query.rs @@ -153,7 +153,7 @@ impl std::fmt::Display for AbilityNameResolutionError { ), Self::MissingName { reference, locale } => write!( f, - "ability {reference:?} has no evidenced name for locale '{locale}'" + "ability {reference:?} has no source-backed name for locale '{locale}'" ), Self::MissingDisplayName { hero, locale, name } => write!( f, diff --git a/crates/workshop-rs/src/settings/data/generated_hero_settings.rs b/crates/workshop-rs/src/settings/data/generated_hero_settings.rs index 89fd024a..f64ce09c 100644 --- a/crates/workshop-rs/src/settings/data/generated_hero_settings.rs +++ b/crates/workshop-rs/src/settings/data/generated_hero_settings.rs @@ -1,6 +1,6 @@ // Generated from the reviewed workshop-data hero settings export. This // projection is locale presentation data only; absence or emptiness is not an -// applicability fact. Applicability requires reviewed semantic evidence. +// applicability fact. Applicability requires a reviewed semantic source. pub struct GeneratedHeroSettingName { pub hero: &'static str, pub key: &'static str, pub locales: &'static [(&'static str, &'static str)] } impl GeneratedHeroSettingName { pub fn localized(&self, locale: &str) -> Option<&'static str> { diff --git a/crates/workshop-rs/src/settings/emitter.rs b/crates/workshop-rs/src/settings/emitter.rs index 4f4c0830..89e33d4c 100644 --- a/crates/workshop-rs/src/settings/emitter.rs +++ b/crates/workshop-rs/src/settings/emitter.rs @@ -2,7 +2,7 @@ use crate::output::emitter::*; impl EmitContext<'_> { /// Emit the `settings { ... }` section from the validated settings - /// carrier, table-driven (fixture-evidenced names). Only runs on + /// carrier, table-driven (fixture-backed names). Only runs on /// validated programs, so unknown keys cannot reach this point. pub(crate) fn emit_settings(&mut self, settings: &SettingsTree) -> Result<()> { let settings_keyword = self.structural("settings")?; @@ -92,7 +92,7 @@ impl EmitContext<'_> { None => name.clone(), }; // `enabled: false` prefixes the mode header; true renders with no - // prefix (only false is evidenced in the corpus, #86). + // prefix (only false is source-backed in the corpus, #86). let disabled = children.iter().any(|member| { matches!( member, diff --git a/crates/workshop-rs/src/settings/mod.rs b/crates/workshop-rs/src/settings/mod.rs index 31d8b4f2..dacc16cc 100644 --- a/crates/workshop-rs/src/settings/mod.rs +++ b/crates/workshop-rs/src/settings/mod.rs @@ -2,7 +2,7 @@ //! //! A typed, non-serde tree for custom-game-settings blocks shared by //! validation and emission. The tree is a carrier: settings are carried and -//! emitted, never interpreted by lowering/analysis. The fixture-evidenced +//! emitted, never interpreted by lowering/analysis. The source-backed //! emission table lives in [`table`]. //! //! Extracted from the Wright-authored `wright-ir` crate; see @@ -21,8 +21,8 @@ pub use table::{KeyKind, PathPart, TableEntry, entries, enum_name, mode_name, pa pub use schema::{ Applicability, EffectiveNumber, NumericBounds, NumericBoundsError, SettingDefinition, - SettingEvidenceKind, SettingId, SettingIdentity, SettingOccurrence, SettingOperationError, - SettingPresentation, SettingProvenance, SettingScope, SettingSourceEdit, SettingTarget, + SettingId, SettingIdentity, SettingOccurrence, SettingOperationError, SettingPresentation, + SettingScope, SettingSource, SettingSourceEdit, SettingSourceKind, SettingTarget, SettingTargetKind, SettingValue, SettingValueDomain, TeamId, definition, definitions, definitions_by_id, }; diff --git a/crates/workshop-rs/src/settings/schema.rs b/crates/workshop-rs/src/settings/schema.rs index 30531c24..a3b46281 100644 --- a/crates/workshop-rs/src/settings/schema.rs +++ b/crates/workshop-rs/src/settings/schema.rs @@ -128,8 +128,8 @@ pub enum NumericBoundsError { Reversed, } -/// Evidence-backed effective numeric bounds. `None` means the current -/// reviewed evidence does not establish that bound. +/// Source-backed effective numeric bounds. `None` means the current reviewed +/// source does not establish that bound. #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] pub struct NumericBounds { min: Option, @@ -220,7 +220,7 @@ pub enum SettingValue { PresenceOnly, } -/// A typed occurrence together with an evidenced effective numeric value. +/// A typed occurrence together with a source-backed effective numeric value. #[derive(Debug, Clone, PartialEq)] pub struct SettingOccurrence { pub authored: SettingValue, @@ -290,7 +290,7 @@ pub enum SettingOperationError { message: String, span: Option, }, - SourceProvenanceUnavailable { + SourceUnavailable { setting: SettingId, }, SourceMismatch, @@ -327,11 +327,8 @@ impl fmt::Display for SettingOperationError { Self::InvalidValue { setting, message, .. } => write!(formatter, "invalid value for setting {setting}: {message}"), - Self::SourceProvenanceUnavailable { setting } => { - write!( - formatter, - "setting {setting} has no editable source provenance" - ) + Self::SourceUnavailable { setting } => { + write!(formatter, "setting {setting} has no editable source") } Self::SourceMismatch => formatter.write_str("source no longer matches the edit"), } @@ -341,7 +338,7 @@ impl fmt::Display for SettingOperationError { impl std::error::Error for SettingOperationError {} impl SettingValueDomain { - /// Apply evidenced effective clamping without changing the authored + /// Apply source-backed effective clamping without changing the authored /// value held by [`super::SettingsNode`]. pub fn effective_number(&self, authored: f64) -> Option { match self { @@ -368,16 +365,16 @@ impl SettingPresentation { } } -/// Provenance shared by the reviewed table projection. +/// Source metadata shared by the reviewed table projection. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct SettingProvenance { - pub kind: SettingEvidenceKind, +pub struct SettingSource { + pub kind: SettingSourceKind, pub source: &'static str, pub reviewed: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SettingEvidenceKind { +pub enum SettingSourceKind { RawWorkshopFixture, WorkshopDataExport, } @@ -393,7 +390,7 @@ pub struct SettingDefinition { target: TargetPattern, domain: SettingValueDomain, presentation: SettingPresentation, - provenance: SettingProvenance, + source: SettingSource, } impl SettingDefinition { @@ -460,8 +457,8 @@ impl SettingDefinition { } } - pub fn provenance(&self) -> SettingProvenance { - self.provenance + pub fn source(&self) -> SettingSource { + self.source } /// Query effective applicability without exposing table deduplication. @@ -607,7 +604,7 @@ impl SettingDefinition { } /// Read an existing source-preserving occurrence with its authored value - /// and, when evidenced, its effective numeric value. + /// and, when source-backed, its effective numeric value. pub fn read( &self, settings: &Settings, @@ -681,11 +678,11 @@ impl SettingDefinition { } })?; validate_value(&self.domain, &id, &value, node.span())?; - let span = - node.span() - .ok_or_else(|| SettingOperationError::SourceProvenanceUnavailable { - setting: id.clone(), - })?; + let span = node + .span() + .ok_or_else(|| SettingOperationError::SourceUnavailable { + setting: id.clone(), + })?; let range = source_value_range(source, span, &id)?; let kind = table::lookup(self.path_parts) .expect("settings definition must retain its table entry") @@ -789,22 +786,22 @@ fn source_value_range( setting: &SettingId, ) -> Result, SettingOperationError> { let start = byte_offset(source, span.start).ok_or_else(|| { - SettingOperationError::SourceProvenanceUnavailable { - setting: setting.clone(), - } - })?; - let end = byte_offset(source, span.end).ok_or_else(|| { - SettingOperationError::SourceProvenanceUnavailable { + SettingOperationError::SourceUnavailable { setting: setting.clone(), } })?; - let member = source.get(start..end).ok_or_else(|| { - SettingOperationError::SourceProvenanceUnavailable { + let end = + byte_offset(source, span.end).ok_or_else(|| SettingOperationError::SourceUnavailable { setting: setting.clone(), - } - })?; + })?; + let member = + source + .get(start..end) + .ok_or_else(|| SettingOperationError::SourceUnavailable { + setting: setting.clone(), + })?; let Some(colon) = member.find(':') else { - return Err(SettingOperationError::SourceProvenanceUnavailable { + return Err(SettingOperationError::SourceUnavailable { setting: setting.clone(), }); }; @@ -815,7 +812,7 @@ fn source_value_range( .len(); let range = value_start + leading..end; if range.is_empty() || source.get(range.clone()).is_none() { - return Err(SettingOperationError::SourceProvenanceUnavailable { + return Err(SettingOperationError::SourceUnavailable { setting: setting.clone(), }); } @@ -903,7 +900,7 @@ fn source_value_spelling( })?; localized("enums", english) } - _ => Err(SettingOperationError::SourceProvenanceUnavailable { + _ => Err(SettingOperationError::SourceUnavailable { setting: setting.clone(), }), } @@ -1208,11 +1205,11 @@ impl SettingDefinition { english_name: entry.workshop_name, locale_section: "labels", }, - provenance: SettingProvenance { + source: SettingSource { kind: if table::is_generated_entry(entry) { - SettingEvidenceKind::WorkshopDataExport + SettingSourceKind::WorkshopDataExport } else { - SettingEvidenceKind::RawWorkshopFixture + SettingSourceKind::RawWorkshopFixture }, source: if table::is_generated_entry(entry) { "workshop-data/workshop-data.json" @@ -1401,7 +1398,7 @@ pub fn validate_catalog() -> Result<(), Vec> { )); continue; }; - if !definition.provenance.reviewed { + if !definition.source.reviewed { errors.push(format!( "unreviewed settings definition: {}", definition.path @@ -1692,8 +1689,8 @@ mod tests { english_name: "Value", locale_section: "labels", }, - provenance: SettingProvenance { - kind: SettingEvidenceKind::RawWorkshopFixture, + source: SettingSource { + kind: SettingSourceKind::RawWorkshopFixture, source: "test", reviewed: true, }, diff --git a/crates/workshop-rs/src/settings/table.rs b/crates/workshop-rs/src/settings/table.rs index 65548056..874ace90 100644 --- a/crates/workshop-rs/src/settings/table.rs +++ b/crates/workshop-rs/src/settings/table.rs @@ -1,6 +1,6 @@ -//! Fixture-evidenced settings emission table (#86). +//! Fixture-backed settings emission table (#86). //! -//! PROVENANCE: observed from the pinned oracle 9.7.10 en-US output of the +//! SOURCE: observed from the pinned oracle 9.7.10 en-US output of the //! oracle-success settings programs (`compile.workshop` settings section of //! the committed snapshots pixelart/santa/broken-weapons/client-to-server, //! plus the parabola/crosshair/inputhud oracle runs) at OverPy commit @@ -110,16 +110,16 @@ macro_rules! entry { }; } -/// The fixture-evidenced settings surface. +/// The fixture-backed settings surface. /// -/// Slot sets (evidenced): teams {allTeams}, heroes {mei} config groups + -/// the 10 ListHero names. `enabled: true` is not evidenced; it renders with +/// Slot sets (source-backed): teams {allTeams}, heroes {mei} config groups + +/// the 10 ListHero names. `enabled: true` is not source-backed; it renders with /// no prefix. Keys outside this table (e.g. team1Slots, scoreToWin, /// gamemodeStartTrigger, spawnHealthPacks, healthPackRespawnTime%, /// abilityCooldown%, healingReceived%, primaryFireKb%, enableSpawningWithUlt, /// resetPlayersAfterGoalScored, scoreLeadToWin, gameLengthInSec, /// heroes..general, roleLimit under general, heroLimit under a named -/// mode) are `settings-unknown-key` at validation (only evidenced in +/// mode) are `settings-unknown-key` at validation (only source-backed in /// oracle-failing programs; corpus-bounded). pub static ENTRIES: &[TableEntry] = &[ // main @@ -591,7 +591,7 @@ pub struct NameMap { pub name: &'static str, } -/// Game-mode names (evidenced: assault, control, escort, hybrid, skirmish, +/// Game-mode names (source-backed: assault, control, escort, hybrid, skirmish, /// ffa, tdm, general). pub static MODE_NAMES: &[NameMap] = &[ NameMap { @@ -860,7 +860,7 @@ include!("data/generated_map_entries.rs"); include!("data/generated_hero_entries.rs"); include!("data/generated_mode_entries.rs"); -/// Team names inside `heroes` (evidenced: allTeams). +/// Team names inside `heroes` (source-backed: allTeams). pub static TEAM_NAMES: &[NameMap] = &[ NameMap { key: "allTeams", diff --git a/crates/workshop-rs/tests/gameplay.rs b/crates/workshop-rs/tests/gameplay.rs index 9e50ceed..169cb9e8 100644 --- a/crates/workshop-rs/tests/gameplay.rs +++ b/crates/workshop-rs/tests/gameplay.rs @@ -1,11 +1,11 @@ use workshop_rs::gameplay::{ - Ability, AbilityLookupError, AbilityRef, AbilityVariant, EvidenceRef, Fact, GameplayCatalog, + Ability, AbilityLookupError, AbilityRef, AbilityVariant, Fact, GameplayCatalog, GameplayDataError, GameplayDatasetIdentity, Hero, HeroId, LocalizedText, LogicalSlot, Quantity, - StatKey, StatValue, Unit, hero_ids, slots, + SourceReference, StatKey, StatValue, Unit, hero_ids, slots, }; -fn evidence(locator: &str) -> EvidenceRef { - EvidenceRef { +fn source_reference(locator: &str) -> SourceReference { + SourceReference { source: "workshop-data".to_string(), locator: locator.to_string(), note: None, @@ -15,7 +15,7 @@ fn evidence(locator: &str) -> EvidenceRef { fn names(name: &str, locator: &str) -> Fact { Fact::new( LocalizedText::new([("en-US".to_string(), name.to_string())]), - vec![evidence(locator)], + vec![source_reference(locator)], ) } @@ -24,7 +24,7 @@ fn ability(slot: &str, variant: Option<&str>, name: &str) -> Ability { LogicalSlot::new(slot), variant.map(AbilityVariant::new), names(name, &format!("heroes.ana.{slot}")), - vec![evidence(&format!("heroes.ana.{slot}"))], + vec![source_reference(&format!("heroes.ana.{slot}"))], ) } @@ -33,7 +33,7 @@ fn hero(id: &str, abilities: Vec) -> Hero { HeroId::new(id), names(id, &format!("heroes.{id}")), abilities, - vec![evidence(&format!("heroes.{id}"))], + vec![source_reference(&format!("heroes.{id}"))], ) } @@ -131,10 +131,10 @@ fn missing_and_unknown_data_are_explicit() { } #[test] -fn facts_carry_evidence_and_quantities_reject_non_finite_values() { +fn facts_carry_sources_and_quantities_reject_non_finite_values() { let stat = Fact::new( StatValue::Quantity(Quantity::new(12.0, Unit::new("seconds")).unwrap()), - vec![evidence("heroes.ana.ability1.cooldown")], + vec![source_reference("heroes.ana.ability1.cooldown")], ); let sleep = ability("ability1", None, "Sleep Dart") .with_keyword("crowd-control") @@ -148,14 +148,14 @@ fn facts_carry_evidence_and_quantities_reject_non_finite_values() { .stat(&StatKey::new("cooldown")) .unwrap(); assert_eq!( - cooldown.evidence()[0].locator, + cooldown.sources()[0].locator, "heroes.ana.ability1.cooldown" ); assert!(Quantity::new(f64::NAN, Unit::new("seconds")).is_err()); } #[test] -fn gameplay_serialization_preserves_dataset_identity_and_open_data() { +fn gameplay_serialization_preserves_dataset_identity_and_source_records() { let catalog = GameplayCatalog::new(identity(), vec![hero("dva", vec![])]).unwrap(); let encoded = serde_json::to_string(catalog.identity()).unwrap(); assert!(encoded.contains("datasetId")); @@ -164,6 +164,9 @@ fn gameplay_serialization_preserves_dataset_identity_and_open_data() { catalog.identity().dataset_id, "overwatch-workshop-hero-gameplay" ); + let fact = serde_json::to_value(names("D.Va", "heroes.dva")).unwrap(); + assert!(fact.get("sources").is_some()); + assert!(fact.get("evidence").is_none()); } #[test] @@ -223,26 +226,29 @@ fn multiple_entries_require_variants_and_display_names_are_not_identity() { } #[test] -fn empty_id_and_empty_evidence_are_rejected() { +fn empty_id_and_empty_sources_are_rejected() { let empty_id = Hero::new( HeroId::new(""), names("Unnamed", "heroes.unnamed"), vec![], - vec![evidence("heroes.unnamed")], + vec![source_reference("heroes.unnamed")], ); assert!(GameplayCatalog::new(identity(), vec![empty_id]).is_err()); - let empty_evidence = Hero::new( + let empty_sources = Hero::new( HeroId::new("ana"), Fact::new( LocalizedText::new([("en-US".to_string(), "Ana".to_string())]), - vec![EvidenceRef { + vec![SourceReference { source: String::new(), locator: "heroes.ana".to_string(), note: None, }], ), vec![], - vec![evidence("heroes.ana")], + vec![source_reference("heroes.ana")], ); - assert!(GameplayCatalog::new(identity(), vec![empty_evidence]).is_err()); + assert!(matches!( + GameplayCatalog::new(identity(), vec![empty_sources]), + Err(GameplayDataError::MissingSource(_)) + )); } diff --git a/crates/workshop-rs/tests/gameplay_data.rs b/crates/workshop-rs/tests/gameplay_data.rs index ebd866de..d40b68a8 100644 --- a/crates/workshop-rs/tests/gameplay_data.rs +++ b/crates/workshop-rs/tests/gameplay_data.rs @@ -1,17 +1,16 @@ use std::collections::BTreeSet; use workshop_rs::gameplay::{ - Ability, AbilityVariant, EvidenceRef, Fact, GameplayCatalog, GameplayDataError, - GameplayDatasetIdentity, Hero, HeroId, LocalizedText, LogicalSlot, Quantity, StatKey, - StatValue, Unit, + Ability, AbilityVariant, Fact, GameplayCatalog, GameplayDataError, GameplayDatasetIdentity, + Hero, HeroId, LocalizedText, LogicalSlot, Quantity, SourceReference, StatKey, StatValue, Unit, }; use workshop_rs::gameplay_data::{GAMEPLAY_DATA, builtin, content_digest, load}; const SOURCE: &str = "workshop-data/workshop-data.json@d854bf01fc7bbf3b2169f67408c07a8da8989ad6"; const OFFICIAL_HERO_SOURCE: &str = "Blizzard Entertainment official Overwatch hero detail"; -fn evidence(locator: &str) -> EvidenceRef { - EvidenceRef { +fn source_reference(locator: &str) -> SourceReference { + SourceReference { source: SOURCE.to_string(), locator: locator.to_string(), note: Some("commitDate=2026-08-12".to_string()), @@ -21,7 +20,7 @@ fn evidence(locator: &str) -> EvidenceRef { fn names(name: &str, locator: &str) -> Fact { Fact::new( LocalizedText::new([("en-US".to_string(), name.to_string())]), - vec![evidence(locator)], + vec![source_reference(locator)], ) } @@ -37,12 +36,12 @@ fn identity() -> GameplayDatasetIdentity { } } -fn assert_evidence(evidence: &[EvidenceRef], source: &str, locator: &str) { +fn assert_sources(sources: &[SourceReference], source: &str, locator: &str) { assert!( - evidence + sources .iter() .any(|item| item.source == source && item.locator == locator), - "missing evidence {source} at {locator}" + "missing source {source} at {locator}" ); } @@ -51,7 +50,7 @@ fn ability(name: &str, slot: &str, variant: Option<&str>) -> Ability { LogicalSlot::new(slot), variant.map(AbilityVariant::new), names(name, "data.heroes.test.ability1"), - vec![evidence("data.heroes.test.ability1")], + vec![source_reference("data.heroes.test.ability1")], ) } @@ -317,12 +316,12 @@ fn embedded_catalog_covers_the_pinned_roster_and_named_slots() { .collect(); let expected_slots: BTreeSet<_> = expected_slots.iter().copied().collect(); assert_eq!(actual_slots, expected_slots, "slot topology for {hero_id}"); - let role = hero.role().expect("official role evidence for every hero"); + let role = hero.role().expect("official role sources for every hero"); assert!(matches!( role.value().as_str(), "tank" | "damage" | "support" )); - assert_eq!(role.evidence().len(), 1); + assert_eq!(role.sources().len(), 1); for ability in hero.abilities() { assert!(ability.name().value().get("en-US").is_some()); } @@ -409,7 +408,7 @@ fn embedded_catalog_is_deterministic_and_preserves_representative_ids() { } #[test] -fn embedded_facts_have_representative_names_values_and_official_provenance() { +fn embedded_facts_have_representative_names_values_and_official_sources() { let catalog = builtin().unwrap(); let ana = catalog.hero_by_id("ana").unwrap(); @@ -422,8 +421,8 @@ fn embedded_facts_have_representative_names_values_and_official_provenance() { let ability = ana.ability(&LogicalSlot::new(slot)).unwrap(); assert_eq!(ability.name().value().get("en-US"), Some(name)); assert!(ability.has_keyword(keyword)); - assert_evidence( - ability.evidence(), + assert_sources( + ability.sources(), OFFICIAL_HERO_SOURCE, "https://overwatch.blizzard.com/en-us/heroes/ana/", ); @@ -438,8 +437,8 @@ fn embedded_facts_have_representative_names_values_and_official_provenance() { let ability = brigitte.ability(&LogicalSlot::new(slot)).unwrap(); assert_eq!(ability.name().value().get("en-US"), Some(name)); assert!(ability.has_keyword(keyword)); - assert_evidence( - ability.evidence(), + assert_sources( + ability.sources(), OFFICIAL_HERO_SOURCE, "https://overwatch.blizzard.com/en-us/heroes/brigitte/", ); @@ -450,8 +449,8 @@ fn embedded_facts_have_representative_names_values_and_official_provenance() { let vortex = ramattra.ability(&LogicalSlot::new("ability2")).unwrap(); assert_eq!(vortex.name().value().get("en-US"), Some("Ravenous Vortex")); assert!(vortex.has_keyword("crowdControl")); - assert_evidence( - vortex.evidence(), + assert_sources( + vortex.sources(), OFFICIAL_HERO_SOURCE, "https://overwatch.blizzard.com/en-us/heroes/ramattra/", ); @@ -464,8 +463,8 @@ fn embedded_facts_have_representative_names_values_and_official_provenance() { .unwrap(); assert_eq!(ability.variant().unwrap().as_str(), variant); assert_eq!(ability.name().value().get("en-US"), Some(name)); - assert_evidence( - ability.evidence(), + assert_sources( + ability.sources(), OFFICIAL_HERO_SOURCE, "https://overwatch.blizzard.com/en-us/heroes/ramattra/", ); @@ -476,8 +475,8 @@ fn embedded_facts_have_representative_names_values_and_official_provenance() { let matrix = dva.ability(&LogicalSlot::new("secondaryFire")).unwrap(); assert!(matrix.has_keyword("barrier")); assert!(matrix.has_keyword("resource")); - assert_evidence( - matrix.evidence(), + assert_sources( + matrix.sources(), OFFICIAL_HERO_SOURCE, "https://overwatch.blizzard.com/en-us/heroes/dva/", ); @@ -490,8 +489,8 @@ fn embedded_facts_have_representative_names_values_and_official_provenance() { .unwrap(); assert_eq!(ability.variant().unwrap().as_str(), variant); assert_eq!(ability.name().value().get("en-US"), Some(name)); - assert_evidence( - ability.evidence(), + assert_sources( + ability.sources(), OFFICIAL_HERO_SOURCE, "https://overwatch.blizzard.com/en-us/heroes/dva/", ); @@ -501,8 +500,8 @@ fn embedded_facts_have_representative_names_values_and_official_provenance() { assert_eq!(bastion.role().unwrap().value().as_str(), "damage"); let reconfigure = bastion.ability(&LogicalSlot::new("ability1")).unwrap(); assert!(reconfigure.has_keyword("form")); - assert_evidence( - reconfigure.evidence(), + assert_sources( + reconfigure.sources(), OFFICIAL_HERO_SOURCE, "https://overwatch.blizzard.com/en-us/heroes/bastion/", ); @@ -518,8 +517,8 @@ fn embedded_facts_have_representative_names_values_and_official_provenance() { .unwrap(); assert_eq!(ability.variant().unwrap().as_str(), variant); assert_eq!(ability.name().value().get("en-US"), Some(name)); - assert_evidence( - ability.evidence(), + assert_sources( + ability.sources(), OFFICIAL_HERO_SOURCE, "https://overwatch.blizzard.com/en-us/heroes/bastion/", ); @@ -530,8 +529,8 @@ fn embedded_facts_have_representative_names_values_and_official_provenance() { assert!(venture.stat(&StatKey::new("health")).is_none()); let drill_dash = venture.ability(&LogicalSlot::new("secondaryFire")).unwrap(); assert!(drill_dash.has_keyword("mobility")); - assert_evidence( - drill_dash.evidence(), + assert_sources( + drill_dash.sources(), OFFICIAL_HERO_SOURCE, "https://overwatch.blizzard.com/en-us/heroes/venture/", ); @@ -540,45 +539,48 @@ fn embedded_facts_have_representative_names_values_and_official_provenance() { } #[test] -fn embedded_records_keep_pinned_provenance_on_every_fact() { +fn embedded_records_keep_pinned_source_metadata_on_every_fact() { let catalog = builtin().unwrap(); for hero in catalog.heroes() { - for evidence in hero.evidence().iter().chain(hero.name().evidence()) { - assert_eq!(evidence.source, SOURCE); - assert_eq!(evidence.note.as_deref(), Some("commitDate=2026-08-12")); + for source_record in hero.sources().iter().chain(hero.name().sources()) { + assert_eq!(source_record.source, SOURCE); + assert_eq!(source_record.note.as_deref(), Some("commitDate=2026-08-12")); } - let role = hero.role().expect("official role evidence for every hero"); - let role_evidence = &role.evidence()[0]; + let role = hero.role().expect("official role sources for every hero"); + let role_source = &role.sources()[0]; assert_eq!( - role_evidence.source, + role_source.source, "Blizzard Entertainment official Overwatch hero detail" ); assert!( - role_evidence + role_source .locator .starts_with("https://overwatch.blizzard.com/en-us/heroes/") ); assert_eq!( - role_evidence.note.as_deref(), + role_source.note.as_deref(), Some("role metadata; accessed 2026-08-18") ); for ability in hero.abilities() { - for evidence in ability.evidence().iter().chain(ability.name().evidence()) { - if evidence.source == SOURCE { - assert!(evidence.locator.starts_with("data.heroes.")); - assert_eq!(evidence.note.as_deref(), Some("commitDate=2026-08-12")); - } else if evidence.source == OFFICIAL_HERO_SOURCE { + for source_record in ability.sources().iter().chain(ability.name().sources()) { + if source_record.source == SOURCE { + assert!(source_record.locator.starts_with("data.heroes.")); + assert_eq!(source_record.note.as_deref(), Some("commitDate=2026-08-12")); + } else if source_record.source == OFFICIAL_HERO_SOURCE { assert!( - evidence + source_record .locator .starts_with("https://overwatch.blizzard.com/en-us/heroes/") ); assert_eq!( - evidence.note.as_deref(), + source_record.note.as_deref(), Some("official ability description; accessed 2026-08-18") ); } else { - panic!("unexpected ability evidence source: {}", evidence.source); + panic!( + "unexpected ability source reference: {}", + source_record.source + ); } } } @@ -610,7 +612,7 @@ fn representative_hero_and_ability_records_round_trip_through_json() { #[test] fn loader_rejects_stale_digest_and_unsupported_schema() { let stale = GAMEPLAY_DATA.replacen( - "5c01599839834f3599a524c7307d3ceaa493e6a1e845d9884dc9617f2af4068a", + "0902a247fb709bf5e326bbdb5475b41d4062b991ba3e0aea9350e5ecd404bc3c", "e15bf17d413e7057bc7ef25e90a6e33df1a79e279a9dbff41e643a30fb9f7635", 1, ); @@ -619,10 +621,18 @@ fn loader_rejects_stale_digest_and_unsupported_schema() { Err(GameplayDataError::DigestMismatch { .. }) )); - let unsupported = GAMEPLAY_DATA.replacen("\"schemaVersion\": 1", "\"schemaVersion\": 2", 1); + let unsupported = GAMEPLAY_DATA.replacen("\"schemaVersion\": 2", "\"schemaVersion\": 3", 1); assert!(matches!( load(&unsupported), - Err(GameplayDataError::UnsupportedSchema(2)) + Err(GameplayDataError::UnsupportedSchema(3)) + )); + + let legacy = GAMEPLAY_DATA + .replace("\"sources\"", "\"evidence\"") + .replacen("\"schemaVersion\": 2", "\"schemaVersion\": 1", 1); + assert!(matches!( + load(&legacy), + Err(GameplayDataError::UnsupportedSchema(1)) )); } @@ -632,7 +642,7 @@ fn catalog_rejects_invalid_slots_unqualified_variants_and_non_finite_values() { HeroId::new("test"), names("Test", "data.heroes.test"), vec![ability("testAbility", " ", None)], - vec![evidence("data.heroes.test")], + vec![source_reference("data.heroes.test")], ); assert!(matches!( GameplayCatalog::new(identity(), vec![empty_slot]), @@ -643,7 +653,7 @@ fn catalog_rejects_invalid_slots_unqualified_variants_and_non_finite_values() { HeroId::new("test"), names("Test", "data.heroes.test"), vec![ability("testAbility", "futureSlot", None)], - vec![evidence("data.heroes.test")], + vec![source_reference("data.heroes.test")], ); assert!(GameplayCatalog::new(identity(), vec![future_slot]).is_ok()); @@ -654,7 +664,7 @@ fn catalog_rejects_invalid_slots_unqualified_variants_and_non_finite_values() { ability("one", "ability1", None), ability("two", "ability1", Some("alternate")), ], - vec![evidence("data.heroes.test")], + vec![source_reference("data.heroes.test")], ); assert!(matches!( GameplayCatalog::new(identity(), vec![unqualified]), @@ -671,10 +681,10 @@ fn catalog_rejects_invalid_slots_unqualified_variants_and_non_finite_values() { value: f64::INFINITY, unit: Unit::new("damage"), }), - vec![evidence("data.heroes.test.ability1.damage")], + vec![source_reference("data.heroes.test.ability1.damage")], ), )], - vec![evidence("data.heroes.test")], + vec![source_reference("data.heroes.test")], ); assert!(matches!( GameplayCatalog::new(identity(), vec![non_finite]), diff --git a/crates/workshop-rs/tests/gameplay_query.rs b/crates/workshop-rs/tests/gameplay_query.rs index 1cc27da3..ecc28eda 100644 --- a/crates/workshop-rs/tests/gameplay_query.rs +++ b/crates/workshop-rs/tests/gameplay_query.rs @@ -1,7 +1,7 @@ use workshop_rs::gameplay::{ - Ability, AbilityRef, AbilityVariant, EvidenceRef, Fact, GameplayCatalog, - GameplayDatasetIdentity, Hero, HeroId, LocalizedText, LogicalSlot, Quantity, StatKey, - StatValue, Unit, hero_ids, slots, units, + Ability, AbilityRef, AbilityVariant, Fact, GameplayCatalog, GameplayDatasetIdentity, Hero, + HeroId, LocalizedText, LogicalSlot, Quantity, SourceReference, StatKey, StatValue, Unit, + hero_ids, slots, units, }; use workshop_rs::gameplay_data::builtin; use workshop_rs::gameplay_query::{ @@ -9,8 +9,8 @@ use workshop_rs::gameplay_query::{ CooldownPercentageError, GameplayQueryError, StatOwner, }; -fn evidence(locator: &str) -> EvidenceRef { - EvidenceRef { +fn source_reference(locator: &str) -> SourceReference { + SourceReference { source: "test-fixture".to_string(), locator: locator.to_string(), note: None, @@ -23,7 +23,7 @@ fn names(en: &str, zh: &str, locator: &str) -> Fact { ("en-US".to_string(), en.to_string()), ("zh-CN".to_string(), zh.to_string()), ]), - vec![evidence(locator)], + vec![source_reference(locator)], ) } @@ -32,7 +32,7 @@ fn ability(en: &str, zh: &str, slot: &str, variant: Option<&str>) -> Ability { LogicalSlot::new(slot), variant.map(AbilityVariant::new), names(en, zh, &format!("ability.{slot}")), - vec![evidence(&format!("ability.{slot}"))], + vec![source_reference(&format!("ability.{slot}"))], ) } @@ -59,7 +59,7 @@ fn catalog() -> GameplayCatalog { StatKey::new("cooldown"), Fact::new( StatValue::Quantity(seconds(12.0)), - vec![evidence("ana.sleep")], + vec![source_reference("ana.sleep")], ), ); let nano = ability("Nano Boost", "纳米激素", "ultimate", None) @@ -68,14 +68,14 @@ fn catalog() -> GameplayCatalog { StatKey::new("description"), Fact::new( StatValue::Text("ultimate".to_string()), - vec![evidence("ana.nano.description")], + vec![source_reference("ana.nano.description")], ), ); let ana = Hero::new( HeroId::new("ana"), names("Ana", "安娜", "heroes.ana"), vec![sleep, nano], - vec![evidence("heroes.ana")], + vec![source_reference("heroes.ana")], ); let ramattra = Hero::new( HeroId::new("ramattra"), @@ -86,7 +86,7 @@ fn catalog() -> GameplayCatalog { ability("Void Barrier", "虚空屏障", "secondaryFire", Some("omnic")) .with_keyword("barrier"), ], - vec![evidence("heroes.ramattra")], + vec![source_reference("heroes.ramattra")], ); GameplayCatalog::new(identity(), vec![ramattra, ana]).unwrap() } @@ -279,28 +279,34 @@ fn cooldown_percentage_and_data_errors_never_default() { HeroId::new("missing"), names("Missing", "缺失", "heroes.missing"), vec![ability("No Cooldown", "无冷却", "ability1", None)], - vec![evidence("heroes.missing")], + vec![source_reference("heroes.missing")], ); let wrong_type = ability("Text Cooldown", "文本冷却", "ability1", None).with_stat( StatKey::new("cooldown"), - Fact::new(StatValue::Text("12".to_string()), vec![evidence("text")]), + Fact::new( + StatValue::Text("12".to_string()), + vec![source_reference("text")], + ), ); let wrong_unit = ability("Meter Cooldown", "米冷却", "ability2", None).with_stat( StatKey::new("cooldown"), Fact::new( StatValue::Quantity(Quantity::new(12.0, Unit::new("meters")).unwrap()), - vec![evidence("meters")], + vec![source_reference("meters")], ), ); let zero = ability("Zero Cooldown", "零冷却", "ability3", None).with_stat( StatKey::new("cooldown"), - Fact::new(StatValue::Quantity(seconds(0.0)), vec![evidence("zero")]), + Fact::new( + StatValue::Quantity(seconds(0.0)), + vec![source_reference("zero")], + ), ); let edge = Hero::new( HeroId::new("edge"), names("Edge", "边界", "heroes.edge"), vec![wrong_type, wrong_unit, zero], - vec![evidence("heroes.edge")], + vec![source_reference("heroes.edge")], ); let catalog = GameplayCatalog::new(identity(), vec![missing, edge]).unwrap(); let query = catalog.query(); diff --git a/crates/workshop-rs/tests/settings_pipeline.rs b/crates/workshop-rs/tests/settings_pipeline.rs index 38b3e2ea..1a174008 100644 --- a/crates/workshop-rs/tests/settings_pipeline.rs +++ b/crates/workshop-rs/tests/settings_pipeline.rs @@ -6,9 +6,9 @@ use std::path::{Path, PathBuf}; use workshop_rs::catalog::{Catalog, Locale}; use workshop_rs::gameplay::{AbilityVariant, HeroId, LogicalSlot, hero_ids, slots}; use workshop_rs::settings::{ - Applicability, NumericBounds, SettingEvidenceKind, SettingId, SettingIdentity, - SettingOperationError, SettingScope, SettingTarget, SettingTargetKind, SettingValue, - SettingValueDomain, TeamId, definitions, definitions_by_id, + Applicability, NumericBounds, SettingId, SettingIdentity, SettingOperationError, SettingScope, + SettingSourceKind, SettingTarget, SettingTargetKind, SettingValue, SettingValueDomain, TeamId, + definitions, definitions_by_id, }; use workshop_rs::{convert, emitter, parser, roundtrip, semantic}; @@ -93,7 +93,7 @@ fn match_voice_chat_rejects_unreviewed_disabled_token() { let en = Locale::new("en-US"); let source = "settings { lobby { Match Voice Chat: Disabled } }"; let error = - parser::parse_wir(source, &catalog, &en).expect_err("disabled state is unevidenced"); + parser::parse_wir(source, &catalog, &en).expect_err("disabled state is source-unbacked"); assert!(format!("{error:?}").contains("settings enum")); } @@ -366,7 +366,7 @@ fn settings_schema_projects_workshop_facts_without_display_names_in_ids() { ), Ok(Some("燃火链式机枪")) ); - assert!(hero_ability.provenance().reviewed); + assert!(hero_ability.source().reviewed); assert_eq!( hero_ability.id().map(SettingId::as_str), Some("setting.hero.ability.enabled") @@ -422,7 +422,7 @@ fn settings_schema_exposes_normal_enum_and_list_domains() { } #[test] -fn settings_schema_distinguishes_applicability_and_unknown_hero_evidence() { +fn settings_schema_distinguishes_applicability_and_unknown_hero_source_status() { let definitions: Vec<_> = definitions().collect(); let ability3 = definitions .iter() @@ -594,7 +594,7 @@ fn settings_schema_normalizes_concept_ids_and_group_targets() { .find(|definition| definition.path().ends_with(suffix)) .expect("projected ability setting"); assert!(definition.id().is_some()); - assert!(definition.provenance().reviewed); + assert!(definition.source().reviewed); } let automatic_fire = definitions .iter() @@ -705,7 +705,7 @@ fn settings_schema_normalizes_concept_ids_and_group_targets() { } #[test] -fn settings_schema_preserves_locale_and_evidence_provenance() { +fn settings_schema_preserves_locale_and_source_metadata() { let definitions: Vec<_> = definitions().collect(); let main = definitions .iter() @@ -715,18 +715,15 @@ fn settings_schema_preserves_locale_and_evidence_provenance() { main.presentation().localized_name("en-US"), Some("Description") ); - assert_eq!( - main.provenance().kind, - SettingEvidenceKind::RawWorkshopFixture - ); + assert_eq!(main.source().kind, SettingSourceKind::RawWorkshopFixture); let generated = definitions .iter() .find(|definition| definition.path() == "extensions.beamEffects") .expect("generated definition"); assert_eq!( - generated.provenance().kind, - SettingEvidenceKind::WorkshopDataExport + generated.source().kind, + SettingSourceKind::WorkshopDataExport ); } @@ -896,6 +893,39 @@ fn typed_settings_source_edit_replaces_only_the_existing_value_bytes() { )); } +#[test] +fn typed_settings_source_edit_rejects_missing_source_location() { + let definition = definitions_by_id(&SettingId::from("setting.lobby.spectatorSlots")) + .next() + .expect("spectator-slot definition"); + let settings = workshop_rs::settings::Settings { + span: None, + children: vec![workshop_rs::settings::SettingsNode::Group { + name: "lobby".to_string(), + children: vec![workshop_rs::settings::SettingsNode::Number { + name: "spectatorSlots".to_string(), + value: 2.0, + span: None, + }], + span: None, + }], + }; + + let error = definition + .source_edit( + "settings { lobby { Max Spectators: 2 } }", + &settings, + "en-US", + &SettingTarget::Global, + SettingValue::Number(4.0), + ) + .expect_err("source edits require a source location"); + assert!(matches!( + error, + SettingOperationError::SourceUnavailable { .. } + )); +} + #[test] fn typed_settings_source_edit_uses_settings_string_escaping() { let catalog = Catalog::builtin().expect("catalog"); @@ -1052,7 +1082,7 @@ fn typed_settings_writes_fail_closed_for_unknown_applicability() { }, SettingValue::Boolean(false), ) - .expect_err("team-to-hero applicability without evidence must refuse writes"); + .expect_err("team-to-hero applicability without sources must refuse writes"); assert!(matches!( widening_error, SettingOperationError::ApplicabilityUnknown { .. } diff --git a/docs/README.md b/docs/README.md index 30e52fc4..2f36b66b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,7 +9,7 @@ This directory is the canonical documentation index for `workshop-rs`. The root architecture/README.md current architecture routing └─ current domain contracts language-support.md current declared support, backed by executable tests -domain/provenance docs durable public contracts and fixture provenance +domain/source docs durable public contracts and fixture attribution adr/ point-in-time decisions and rationale Issues / PRs / releases mutable execution state source / tests / data current implementation reality @@ -30,7 +30,7 @@ relevant code/tests/data and current Issue contract. shared implementation boundaries, CLI verification tooling, and test-owned fixtures. - [Repository agent guidance](../AGENTS.md): implementation routing, - verification, provenance, and delivery rules. + verification, source attribution, and delivery rules. [`implementation-role.md`](implementation-role.md) is retained only as a compatibility pointer for older links. @@ -52,17 +52,17 @@ compatibility pointer for older links. Support prose is not a substitute for current executable tests. -## Domain contracts and fixture provenance +## Domain contracts and fixture attribution -- [Provenance record](provenance.md): catalog, locale, gameplay, fixture, and - dataset provenance. +- [Source attribution record](provenance.md): catalog, locale, gameplay, + fixture, and dataset source attribution. - [Source preservation contract](source-preservation.md): optional authored source, comment attachment, checked edits, and mixed-source boundaries. - [Hero gameplay dataset](gameplay-data.md): embedded data model and validation boundaries. - [Gameplay query API](gameplay-query.md): read-only semantic queries and locale-aware ability resolution. -- [Hero gameplay topology survey](gameplay-roster-survey.md): evidence used by +- [Hero gameplay topology survey](gameplay-roster-survey.md): source material used by the gameplay domain. - [Canonical action layout](action-layout.md): validated WIR action-width and structured action expansion behavior. diff --git a/docs/adr/0006-settings-semantic-schema.md b/docs/adr/0006-settings-semantic-schema.md index 6bde047f..7ccd14fd 100644 --- a/docs/adr/0006-settings-semantic-schema.md +++ b/docs/adr/0006-settings-semantic-schema.md @@ -46,9 +46,10 @@ into a guessed definition. `SettingIdentity::Known(SettingId)` means that a reviewed canonical concept identity has been resolved; unresolved projected hero-ability concepts use `SettingIdentity::Unknown` and `id() == None`. This is independent from -`SettingProvenance`: the latter reports whether the underlying table or export -source was reviewed, so reviewed source data may still carry an unknown -semantic identity when no canonical typed identity has been established. +`SettingSource`: the latter reports the source kind, source identity, and +review status of the underlying table or export, so reviewed source data may +still carry an unknown semantic identity when no canonical typed identity has +been established. ## Consequences @@ -62,7 +63,7 @@ concept identities normalize reusable hero/ability settings without embedding localized ability display names; mode-specific enum concepts remain distinct when their reviewed domains differ. Query/edit operations can use definitions and source-preserving occurrences without inventing another -identity, scope, target, domain, or provenance model. +identity, scope, target, domain, or generic source-trust model. No UI step metadata, source-language carrier parsing, per-hero Rust structs, consumer-side applicability hacks, or query/edit API ergonomics are part of this diff --git a/docs/adr/0007-gameplay-domain-api.md b/docs/adr/0007-gameplay-domain-api.md index 5cb0f555..b405b192 100644 --- a/docs/adr/0007-gameplay-domain-api.md +++ b/docs/adr/0007-gameplay-domain-api.md @@ -37,7 +37,7 @@ ambiguous. 3. Raw gameplay records use `Hero`, `Ability`, `AbilityRef`, `Fact`, `StatValue`, and `Quantity`. Ability localized/display names are metadata on the hero/slot/variant record and can change without changing its - `AbilityRef`. Facts carry concrete source references with a source, + `AbilityRef`. Facts carry concrete `SourceReference` values with a source, locator, and optional note; the enclosing `GameplayDatasetIdentity` carries dataset id, version, digest, source, license, target, and review status. Missing facts stay absent. diff --git a/docs/architecture/README.md b/docs/architecture/README.md index ec0dec47..0ec2f4d2 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -16,7 +16,7 @@ For substantive implementation work, resolve the smallest relevant current contr | --- | --- | | Repository ownership, dependency direction, WIR boundary, semantic-code/data boundary | [`core-boundaries.md`](core-boundaries.md) | | Declared Workshop language support and current capability state | [`../language-support.md`](../language-support.md) and executable tests | -| Catalog/localization provenance | [`../provenance.md`](../provenance.md) | +| Catalog/localization source attribution | [`../provenance.md`](../provenance.md) | | Hero/gameplay dataset model | [`../gameplay-data.md`](../gameplay-data.md) | | Gameplay semantic queries | [`../gameplay-query.md`](../gameplay-query.md) | | Action layout contract | [`../action-layout.md`](../action-layout.md) | diff --git a/docs/architecture/core-boundaries.md b/docs/architecture/core-boundaries.md index 1d803f4e..86b301aa 100644 --- a/docs/architecture/core-boundaries.md +++ b/docs/architecture/core-boundaries.md @@ -12,7 +12,7 @@ It owns: - the public `Program` model and locale-independent Workshop identities; - Workshop catalog, settings, localization, and Workshop-owned gameplay facts; - Workshop semantic/query contracts used by downstream tooling; -- Workshop contract tests, provenance-linked fixtures, and the semantic +- Workshop contract tests, source-attributed fixtures, and the semantic contracts consumed by the CLI verification tooling. It does not own OverPy or DEL/OSTW syntax, preprocessing, project models, source-language semantics, runtime/compiler lowering policy, reconstruction policy, or Wright tooling behavior. @@ -54,7 +54,7 @@ Use data for large declarative fact sets whose meaning is already defined by the - signatures and declared parameter domains when they are factual catalog properties; - locale spellings and aliases; - roster/catalog/settings inventories; -- provenance and evidence references. +- source attribution and source references. Use typed Rust code for behavior and invariants, including: @@ -74,7 +74,7 @@ Canonical identities are locale-independent and independent from OPY/DEL naming. Likewise, OverPy or DEL/OSTW names, defaults, helpers, or compiler quirks do not become Workshop identities unless an independent Workshop source establishes the same concept. -## Support and provenance +## Support and source attribution Architecture does not prove feature support. Current support claims come from the language-support surface and executable checks: code, tests, corpus/fixtures, catalog checks, real consumer workflows, and live/runtime records where applicable. diff --git a/docs/architecture/source-layout.md b/docs/architecture/source-layout.md index cb629860..d8cd8fdb 100644 --- a/docs/architecture/source-layout.md +++ b/docs/architecture/source-layout.md @@ -40,7 +40,7 @@ shared home: - `workshop-rs-cli/src/` owns census execution, conformance report schemas, capture comparison, and corpus tooling; - `tests/` owns executable Workshop contract and regression checks, with - provenance-linked fixtures beside the tests that exercise them. + source-attributed fixtures beside the tests that exercise them. The root operation modules (`parser`, `emitter`, `validate`, `convert`, and `roundtrip`) are the public Workshop operations over `Program`. Storage, diff --git a/docs/compatibility-facades.md b/docs/compatibility-facades.md index 95d15dae..e923ac5b 100644 --- a/docs/compatibility-facades.md +++ b/docs/compatibility-facades.md @@ -28,7 +28,7 @@ They include source, tests, and checked-in documentation where applicable. | `format` | yes | no replacement public path; shared formatter | 0 | 2 | 0 | 1 | retain: external consumers | | `ids` | yes | no replacement public path; typed WIR identities | 3 | 0 | 0 | 6 | retain: external consumers | | `signatures` | no | `signatures` remains the public signature-context surface | 1 | 1 | 0 | 0 | retain: external consumer | -| `source` | no | `source` remains the public provenance surface | 8 | 4 | 3 | 12 | retain: external consumers | +| `source` | no | `source` remains the public source-metadata surface | 8 | 4 | 3 | 12 | retain: external consumers | | `element_count` | yes | `Program::element_count`, `actions` re-exports | 1 | 0 | 0 | 0 | retain: in-repo compatibility use | | `semantic` | yes | `Program::semantic_issues`, `rules` re-exports | 3 | 0 | 0 | 3 | retain: external consumers | | `gameplay_data` | yes | `gameplay::data` | 3 | 0 | 0 | 0 | retain: in-repo/docs use | diff --git a/docs/gameplay-data.md b/docs/gameplay-data.md index 93575083..f1c9060d 100644 --- a/docs/gameplay-data.md +++ b/docs/gameplay-data.md @@ -7,13 +7,15 @@ content digest before constructing a `GameplayCatalog`. ## Schema and identity -The file has `schemaVersion: 1`, a `GameplayDatasetIdentity`, and a sorted +The file has `schemaVersion: 2`, a `GameplayDatasetIdentity`, and a sorted hero list. Heroes and abilities carry source references. Hero, slot, and variant identities are open strings; an ability is canonically referenced by its hero, logical slot, and optional hero-local variant. Display names are metadata, not identity. The current dataset uses the seven canonical slots from ADR-0002; the open string-backed API also accepts future non-empty slot identities. Multiple abilities in one slot must have distinct, non-empty variants. +Version 1 payloads used the incompatible `evidence` record key and are rejected +before version-specific records are deserialized. The dataset digest is `sha256:` followed by the SHA-256 of the canonical JSON content with `identity.digest` removed. Object keys are sorted before hashing, @@ -21,7 +23,7 @@ so formatting and input key order do not change the identity. The loader also sorts heroes and abilities before building lookup indexes. The committed dataset identity is version `2026-08-12` with digest -`sha256:5c01599839834f3599a524c7307d3ceaa493e6a1e845d9884dc9617f2af4068a`. +`sha256:0902a247fb709bf5e326bbdb5475b41d4062b991ba3e0aea9350e5ecd404bc3c`. ## Source records and known gaps diff --git a/docs/gameplay-query.md b/docs/gameplay-query.md index 6e89d28c..21e0029f 100644 --- a/docs/gameplay-query.md +++ b/docs/gameplay-query.md @@ -35,7 +35,7 @@ query with no matches returns an empty collection because it is a collection query rather than a single-result lookup. `stat()` and `quantity_stat()` take hero + logical slot + optional explicit -hero-local variant. They return the extensible `Fact` or a +hero-local variant. They return the extensible source-backed `Fact` or a `Quantity`, and report `WrongStatType` when the stat is not numeric. Hero-level stats are available through `hero_stat()`. @@ -67,7 +67,7 @@ clamped or silently approximated. A target with another unit is rejected. ## Locale-aware ability names -`ability_name(hero, slot, variant, locale)` resolves an evidenced client name, +`ability_name(hero, slot, variant, locale)` resolves a source-backed client name, and `resolve_ability_name(hero, locale, display_name)` performs exact, hero-context inverse lookup. Unsupported locales, missing mappings, and ambiguous names are explicit errors; fuzzy aliases are not admitted. Raw diff --git a/docs/language-support/settings.md b/docs/language-support/settings.md index ee85aed6..b4b13b07 100644 --- a/docs/language-support/settings.md +++ b/docs/language-support/settings.md @@ -18,7 +18,7 @@ The `workshop_rs::settings` module exposes the reviewed settings catalog through `definitions()`. Each `SettingDefinition` carries a locale-independent `SettingId`, Workshop `SettingScope`, target shape, typed -`SettingValueDomain`, locale presentation metadata, and source provenance. +`SettingValueDomain`, locale presentation metadata, and setting source metadata. The source-preserving `Settings` / `SettingsNode` tree remains the authored value carrier; the catalog does not regenerate or discard unknown settings. @@ -79,8 +79,8 @@ assert!(health Hero and ability display names are presentation data only. Consumers use the canonical concept and `SettingTarget`; localized aliases remain parser/emitter resolution details. Numeric bounds remain unknown until reviewed Workshop -evidence establishes them; `SettingValueDomain::effective_number` exposes a -clamped effective value only for a definition carrying such evidence. +source records establish them; `SettingValueDomain::effective_number` exposes a +clamped effective value only for a definition carrying such source records. `SettingDefinition::read` and `write` operate on existing occurrences. A write changes only the typed leaf value, preserving its span and all unrelated diff --git a/docs/language-support/strings.md b/docs/language-support/strings.md index b0456f49..0082ae80 100644 --- a/docs/language-support/strings.md +++ b/docs/language-support/strings.md @@ -13,5 +13,5 @@ | Feature | Status | Notes | | --- | --- | --- | -| `en-US`, `de-DE`, `es-ES`, `es-MX`, `fr-FR`, `it-IT`, `ja-JP`, `ko-KR`, `pl-PL`, `pt-BR`, `ru-RU`, `th-TH`, `tr-TR`, `zh-CN`, `zh-TW` | ✅ Supported | Declared from the pinned OverPy 9.7.10 Workshop language set. Locale spellings are provenance-backed catalog/settings data; uncovered identities remain explicit missing mappings. | +| `en-US`, `de-DE`, `es-ES`, `es-MX`, `fr-FR`, `it-IT`, `ja-JP`, `ko-KR`, `pl-PL`, `pt-BR`, `ru-RU`, `th-TH`, `tr-TR`, `zh-CN`, `zh-TW` | ✅ Supported | Declared from the pinned OverPy 9.7.10 Workshop language set. Locale spellings are source-attributed catalog/settings data; uncovered identities remain explicit missing mappings. | | Bidirectional conversion across the declared locales | ✅ Supported | Conversion resolves locale-independent canonical identities and fails explicitly on missing mappings; opt-in fallback is recorded in the conversion result. | diff --git a/docs/provenance.md b/docs/provenance.md index 861185b5..64485afe 100644 --- a/docs/provenance.md +++ b/docs/provenance.md @@ -1,10 +1,10 @@ -# Provenance record +# Source attribution record -This document records the source provenance of committed datasets and fixtures +This document records the source attribution of committed datasets and fixtures in `workshop-rs`, as required for attribution, licensing, and reproducible regeneration. -Hero/gameplay data follows the separate identity and evidence contract in +Hero/gameplay data follows the separate identity and source-record contract in [`docs/adr/0007-gameplay-domain-api.md`](adr/0007-gameplay-domain-api.md). The Workshop catalog identity below does not identify a gameplay dataset. @@ -23,18 +23,19 @@ declared named ability-slot topology; no OverPy or OSTW data is copied. The committed projection contains hero identities, role facts, and export-declared named ability slots, plus official-detail variant records for Bastion, D.Va, and Ramattra. Each hero/ability name fact and export record -carries the export path as an `EvidenceRef`. Each role fact carries its -official Blizzard hero-detail URL and access date 2026-08-18 as separate -evidence. +carries the export path as a `SourceReference`. Each role fact carries its +official Blizzard hero-detail URL and access date 2026-08-18 as a separate +source reference. The current identity digest is -`sha256:5c01599839834f3599a524c7307d3ceaa493e6a1e845d9884dc9617f2af4068a`. +`sha256:0902a247fb709bf5e326bbdb5475b41d4062b991ba3e0aea9350e5ecd404bc3c`. Representative ability keywords are semantic labels, not Blizzard or Workshop -enum values. Their labels and the six variant names/shapes are evidenced by +enum values. Their labels and the six variant names/shapes are source-linked to the official Blizzard hero-detail URLs for Ana, Brigitte, Ramattra, D.Va, Bastion, and Venture, accessed 2026-08-18; -the variants intentionally carry no fabricated Workshop-export provenance. +the variants intentionally carry no fabricated Workshop-export source +attribution. Venture base health and Drill Dash cooldown/damage are intentionally absent because the cited June 30, 2026 Community Crafted patch source is scoped to a limited mode and that scope is not modeled. Other base stats, armor/shields, cooldowns, damage, diff --git a/docs/source-preservation.md b/docs/source-preservation.md index 764dc8a0..5a1ae0e9 100644 --- a/docs/source-preservation.md +++ b/docs/source-preservation.md @@ -53,7 +53,7 @@ whole-file formatting or comment preservation. The raw Workshop parser accepts a complete supported Workshop document. It does not interpret DEL/OSTW, OverPy, or other embedded source-language syntax. A consumer embedding Workshop regions owns extraction and source-language -provenance; it can attach the extracted Workshop source to a canonical program +source attribution; it can attach the extracted Workshop source to a canonical program file when available. Malformed or unsupported mixed content must be rejected or reported incomplete by that consumer rather than being assigned guessed Workshop semantics.