From 1098bc17b4569513e764ff83d5afa46e5cf040a8 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Wed, 23 Sep 2026 02:06:22 +0800 Subject: [PATCH 1/2] refactor(api): align settings and gameplay source metadata Fixes #254 --- crates/workshop-rs/src/gameplay/data.rs | 2 +- crates/workshop-rs/src/gameplay/gameplay.json | 1148 ++++++++--------- crates/workshop-rs/src/gameplay/mod.rs | 87 +- crates/workshop-rs/src/gameplay/query.rs | 2 +- .../settings/data/generated_hero_settings.rs | 2 +- crates/workshop-rs/src/settings/emitter.rs | 4 +- crates/workshop-rs/src/settings/mod.rs | 6 +- crates/workshop-rs/src/settings/schema.rs | 81 +- crates/workshop-rs/src/settings/table.rs | 16 +- crates/workshop-rs/tests/gameplay.rs | 40 +- crates/workshop-rs/tests/gameplay_data.rs | 110 +- crates/workshop-rs/tests/gameplay_query.rs | 38 +- crates/workshop-rs/tests/settings_pipeline.rs | 60 +- docs/README.md | 12 +- docs/adr/0006-settings-semantic-schema.md | 9 +- docs/adr/0007-gameplay-domain-api.md | 2 +- docs/architecture/README.md | 2 +- docs/architecture/core-boundaries.md | 6 +- docs/architecture/source-layout.md | 2 +- docs/compatibility-facades.md | 2 +- docs/gameplay-data.md | 2 +- docs/gameplay-query.md | 4 +- docs/language-support/settings.md | 6 +- docs/language-support/strings.md | 2 +- docs/provenance.md | 19 +- docs/source-preservation.md | 2 +- 26 files changed, 856 insertions(+), 810 deletions(-) diff --git a/crates/workshop-rs/src/gameplay/data.rs b/crates/workshop-rs/src/gameplay/data.rs index b144006..c6e4363 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 diff --git a/crates/workshop-rs/src/gameplay/gameplay.json b/crates/workshop-rs/src/gameplay/gameplay.json index 454345d..2a23df3 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,7 +9337,7 @@ ], "identity": { "datasetId": "overwatch-workshop-hero-gameplay", - "digest": "sha256:5c01599839834f3599a524c7307d3ceaa493e6a1e845d9884dc9617f2af4068a", + "digest": "sha256:388a2964056c1cb515ad43fa85050f608e583c1ce076a7dcc846de09e458e734", "license": "MIT-compatible user-provided export", "reviewed": true, "source": "workshop-data/workshop-data.json@d854bf01fc7bbf3b2169f67408c07a8da8989ad6", diff --git a/crates/workshop-rs/src/gameplay/mod.rs b/crates/workshop-rs/src/gameplay/mod.rs index 5b67c3d..b1f5e2e 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 456736f..e83b0a5 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 89fd024..f64ce09 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 4f4c083..89e33d4 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 31d8b4f..dacc16c 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 30531c2..a3b4628 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 6554805..874ace9 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 9e50cee..169cb9e 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 ebd866d..d578874 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", + "388a2964056c1cb515ad43fa85050f608e583c1ce076a7dcc846de09e458e734", "e15bf17d413e7057bc7ef25e90a6e33df1a79e279a9dbff41e643a30fb9f7635", 1, ); @@ -632,7 +634,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 +645,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 +656,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 +673,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 1cc27da..ecc28ed 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 38b3e2e..1a17400 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 30e52fc..2f36b66 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 6bde047..7ccd14f 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 5cb0f55..b405b19 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 ec0dec4..0ec2f4d 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 1d803f4..86b301a 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 cb62986..d8cd8fd 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 95d15da..e923ac5 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 9357508..ef457f0 100644 --- a/docs/gameplay-data.md +++ b/docs/gameplay-data.md @@ -21,7 +21,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:388a2964056c1cb515ad43fa85050f608e583c1ce076a7dcc846de09e458e734`. ## Source records and known gaps diff --git a/docs/gameplay-query.md b/docs/gameplay-query.md index 6e89d28..21e0029 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 ee85aed..b4b13b0 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 b0456f4..0082ae8 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 861185b..af6191f 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:388a2964056c1cb515ad43fa85050f608e583c1ce076a7dcc846de09e458e734`. 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 764dc8a..5a1ae0e 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. From 4927f68280cd1cf3485e7258ebf7f4aa7fb1e124 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Wed, 23 Sep 2026 02:26:11 +0800 Subject: [PATCH 2/2] fix(gameplay): version source metadata wire schema Refs #254 --- crates/workshop-rs/src/gameplay/data.rs | 16 ++++++++++++++-- crates/workshop-rs/src/gameplay/gameplay.json | 4 ++-- crates/workshop-rs/tests/gameplay_data.rs | 14 +++++++++++--- docs/gameplay-data.md | 6 ++++-- docs/provenance.md | 2 +- 5 files changed, 32 insertions(+), 10 deletions(-) diff --git a/crates/workshop-rs/src/gameplay/data.rs b/crates/workshop-rs/src/gameplay/data.rs index c6e4363..6251c15 100644 --- a/crates/workshop-rs/src/gameplay/data.rs +++ b/crates/workshop-rs/src/gameplay/data.rs @@ -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 2a23df3..419d0b3 100644 --- a/crates/workshop-rs/src/gameplay/gameplay.json +++ b/crates/workshop-rs/src/gameplay/gameplay.json @@ -9337,12 +9337,12 @@ ], "identity": { "datasetId": "overwatch-workshop-hero-gameplay", - "digest": "sha256:388a2964056c1cb515ad43fa85050f608e583c1ce076a7dcc846de09e458e734", + "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/tests/gameplay_data.rs b/crates/workshop-rs/tests/gameplay_data.rs index d578874..d40b68a 100644 --- a/crates/workshop-rs/tests/gameplay_data.rs +++ b/crates/workshop-rs/tests/gameplay_data.rs @@ -612,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( - "388a2964056c1cb515ad43fa85050f608e583c1ce076a7dcc846de09e458e734", + "0902a247fb709bf5e326bbdb5475b41d4062b991ba3e0aea9350e5ecd404bc3c", "e15bf17d413e7057bc7ef25e90a6e33df1a79e279a9dbff41e643a30fb9f7635", 1, ); @@ -621,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)) )); } diff --git a/docs/gameplay-data.md b/docs/gameplay-data.md index ef457f0..f1c9060 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:388a2964056c1cb515ad43fa85050f608e583c1ce076a7dcc846de09e458e734`. +`sha256:0902a247fb709bf5e326bbdb5475b41d4062b991ba3e0aea9350e5ecd404bc3c`. ## Source records and known gaps diff --git a/docs/provenance.md b/docs/provenance.md index af6191f..64485af 100644 --- a/docs/provenance.md +++ b/docs/provenance.md @@ -28,7 +28,7 @@ official Blizzard hero-detail URL and access date 2026-08-18 as a separate source reference. The current identity digest is -`sha256:388a2964056c1cb515ad43fa85050f608e583c1ce076a7dcc846de09e458e734`. +`sha256:0902a247fb709bf5e326bbdb5475b41d4062b991ba3e0aea9350e5ecd404bc3c`. Representative ability keywords are semantic labels, not Blizzard or Workshop enum values. Their labels and the six variant names/shapes are source-linked to