diff --git a/crates/workshop-rs-cli/src/census.rs b/crates/workshop-rs-cli/src/census.rs index aa69a38..a48eb7c 100644 --- a/crates/workshop-rs-cli/src/census.rs +++ b/crates/workshop-rs-cli/src/census.rs @@ -816,6 +816,7 @@ fn settings_probe(definition: &SettingDefinition, catalog: &Catalog) -> String { lines.push(format!("{indent}{name} {{")); lines.push(format!("{indent}}}")); } + other => unreachable!("census has no probe for settings value domain {other:?}"), } while depth > 1 { depth -= 1; diff --git a/crates/workshop-rs-cli/src/lib.rs b/crates/workshop-rs-cli/src/lib.rs index 9c6a82e..045ce5d 100644 --- a/crates/workshop-rs-cli/src/lib.rs +++ b/crates/workshop-rs-cli/src/lib.rs @@ -229,9 +229,8 @@ fn emit_command(args: Vec) -> i32 { return 1; } }; - let options = EmitOptions { - fallback_locale: fallback, - }; + let mut options = EmitOptions::default(); + options.fallback_locale = fallback; match emitter::emit_with_options(&program, &catalog, &locale, &options) { Ok(output) => { report_fallbacks(&output.fallback_ids); @@ -283,9 +282,8 @@ fn convert_command(args: Vec) -> i32 { return 1; } }; - let options = ConvertOptions { - fallback_locale: fallback, - }; + let mut options = ConvertOptions::default(); + options.fallback_locale = fallback; match convert::convert(&input, &catalog, &from, &to, &options) { Ok(output) => { report_fallbacks(&output.fallback_ids); diff --git a/crates/workshop-rs/src/actions/layout.rs b/crates/workshop-rs/src/actions/layout.rs index 4581b15..9651e96 100644 --- a/crates/workshop-rs/src/actions/layout.rs +++ b/crates/workshop-rs/src/actions/layout.rs @@ -14,6 +14,7 @@ pub struct ActionLayout { /// Errors returned while querying public canonical native action layout. #[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum ActionLayoutError { /// The public program cannot be materialized as a valid Workshop program. InvalidProgram { message: String }, diff --git a/crates/workshop-rs/src/analysis/element_count.rs b/crates/workshop-rs/src/analysis/element_count.rs index 53cbc25..5cfa25b 100644 --- a/crates/workshop-rs/src/analysis/element_count.rs +++ b/crates/workshop-rs/src/analysis/element_count.rs @@ -25,6 +25,7 @@ pub enum ElementNodeKind { /// One node's contribution and its nested element-count analysis. #[derive(Debug, Clone)] +#[non_exhaustive] pub struct ElementCountNode { pub kind: ElementNodeKind, /// An opaque identity unique within this report. It is not a WIR or @@ -47,6 +48,7 @@ pub struct ElementCountNode { /// A structured element-count report for one canonical Workshop program. #[derive(Debug, Clone)] +#[non_exhaustive] pub struct ElementCountReport { /// The sum of all rule counts. pub total: usize, @@ -65,6 +67,7 @@ impl ElementCountReport { /// A construct for which an exact canonical element count cannot be produced. #[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum ElementCountError { /// The input cannot be materialized or structurally validated as a /// canonical Workshop program. diff --git a/crates/workshop-rs/src/analysis/semantic.rs b/crates/workshop-rs/src/analysis/semantic.rs index 1abff75..6e164b8 100644 --- a/crates/workshop-rs/src/analysis/semantic.rs +++ b/crates/workshop-rs/src/analysis/semantic.rs @@ -60,6 +60,7 @@ impl ResidualClassification { } #[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] pub struct SemanticIssue { pub kind: IncompletenessKind, pub name: String, diff --git a/crates/workshop-rs/src/catalog/detect.rs b/crates/workshop-rs/src/catalog/detect.rs index 14ec871..2ac835a 100644 --- a/crates/workshop-rs/src/catalog/detect.rs +++ b/crates/workshop-rs/src/catalog/detect.rs @@ -3,6 +3,7 @@ use crate::core::error::{Result, WorkshopError}; /// A language-detection result with ranked evidence. #[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] pub struct Detection { /// The best-matching locale. pub locale: Locale, diff --git a/crates/workshop-rs/src/catalog/mod.rs b/crates/workshop-rs/src/catalog/mod.rs index 91e8bc7..8e172d1 100644 --- a/crates/workshop-rs/src/catalog/mod.rs +++ b/crates/workshop-rs/src/catalog/mod.rs @@ -111,6 +111,7 @@ impl Kind { /// vectors, or null. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)] #[serde(rename_all = "camelCase")] +#[non_exhaustive] pub struct ParamCoercions { /// Accept `False` as numeric zero. #[serde(default)] @@ -351,6 +352,7 @@ impl EnumDomain { /// Target-format metadata recorded in the catalog. #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +#[non_exhaustive] pub struct TargetMeta { pub game: String, pub format: String, @@ -360,6 +362,7 @@ pub struct TargetMeta { /// Provenance of the catalog data. #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] +#[non_exhaustive] pub struct Provenance { pub generator: String, pub generator_version: String, @@ -376,6 +379,7 @@ pub struct Provenance { /// localized preset identities, and enum members) carry a mapping for the /// locale out of the declared total. #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +#[non_exhaustive] pub struct LocaleCoverage { pub locale: Locale, /// Canonical entries with a declared mapping in this locale. @@ -391,6 +395,7 @@ pub struct LocaleCoverage { /// identity names. #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] +#[non_exhaustive] pub struct CatalogIdentity { /// The `workshop-rs` package version (semver); bumped by code changes. pub implementation_version: String, diff --git a/crates/workshop-rs/src/core/error.rs b/crates/workshop-rs/src/core/error.rs index cf95189..59444e6 100644 --- a/crates/workshop-rs/src/core/error.rs +++ b/crates/workshop-rs/src/core/error.rs @@ -5,6 +5,7 @@ use crate::core::source::Span; /// A structured Workshop-language error. #[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum WorkshopError { /// Catalog data is malformed or fails validation. Catalog(CatalogError), @@ -33,6 +34,7 @@ pub enum WorkshopError { /// Catalog-specific error. #[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] pub struct CatalogError { pub code: &'static str, pub message: String, diff --git a/crates/workshop-rs/src/core/source.rs b/crates/workshop-rs/src/core/source.rs index 2d3c38e..60d37a0 100644 --- a/crates/workshop-rs/src/core/source.rs +++ b/crates/workshop-rs/src/core/source.rs @@ -231,6 +231,7 @@ impl SourceEdit { /// Failure while creating or applying source edits. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] pub enum SourceEditError { InvalidRange, SourceMismatch, diff --git a/crates/workshop-rs/src/gameplay/mod.rs b/crates/workshop-rs/src/gameplay/mod.rs index b1f5e2e..fffba88 100644 --- a/crates/workshop-rs/src/gameplay/mod.rs +++ b/crates/workshop-rs/src/gameplay/mod.rs @@ -277,6 +277,7 @@ impl LocalizedText { /// A machine-identifiable source reference for a gameplay fact. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] +#[non_exhaustive] pub struct SourceReference { pub source: String, pub locator: String, @@ -284,10 +285,25 @@ pub struct SourceReference { pub note: Option, } +impl SourceReference { + pub fn new(source: impl Into, locator: impl Into) -> Self { + Self { + source: source.into(), + locator: locator.into(), + note: None, + } + } + pub fn with_note(mut self, note: impl Into) -> Self { + self.note = Some(note.into()); + self + } +} + /// 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")] +#[non_exhaustive] pub struct GameplayDatasetIdentity { pub dataset_id: String, pub version: String, @@ -298,6 +314,28 @@ pub struct GameplayDatasetIdentity { pub reviewed: bool, } +impl GameplayDatasetIdentity { + pub fn new( + dataset_id: impl Into, + version: impl Into, + digest: impl Into, + source: impl Into, + license: impl Into, + target: impl Into, + reviewed: bool, + ) -> Self { + Self { + dataset_id: dataset_id.into(), + version: version.into(), + digest: digest.into(), + source: source.into(), + license: license.into(), + target: target.into(), + reviewed, + } + } +} + /// A gameplay fact tied to source references in the dataset version being consumed. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Fact { @@ -537,6 +575,7 @@ impl Hero { /// Explicit failure for a logical-slot lookup. #[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum AbilityLookupError { Missing { hero: HeroId, @@ -583,6 +622,7 @@ impl std::error::Error for AbilityLookupError {} /// Validation and construction errors for gameplay data. #[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] pub enum GameplayDataError { EmptyIdentity(&'static str), DuplicateHero(HeroId), diff --git a/crates/workshop-rs/src/gameplay/query.rs b/crates/workshop-rs/src/gameplay/query.rs index e83b0a5..de92317 100644 --- a/crates/workshop-rs/src/gameplay/query.rs +++ b/crates/workshop-rs/src/gameplay/query.rs @@ -22,6 +22,7 @@ pub enum StatOwner { } #[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum GameplayQueryError { MissingHero { hero: HeroId, @@ -93,6 +94,7 @@ impl std::error::Error for GameplayQueryError {} /// Explicit failures from locale-aware display-name resolution. #[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum AbilityNameResolutionError { MissingHero { hero: HeroId, @@ -203,6 +205,7 @@ impl TryFrom for CooldownPercentage { } #[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] pub enum CooldownPercentageError { NotFinite { value: f64 }, OutOfRange { value: f64 }, @@ -228,6 +231,7 @@ pub enum CooldownNonApplicability { } #[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] pub enum CooldownError { Missing { ability: AbilityRef, diff --git a/crates/workshop-rs/src/output/convert.rs b/crates/workshop-rs/src/output/convert.rs index 93f0f46..295b28c 100644 --- a/crates/workshop-rs/src/output/convert.rs +++ b/crates/workshop-rs/src/output/convert.rs @@ -16,6 +16,7 @@ use crate::output::emitter::{self, EmitOptions}; /// Conversion options: opt-in fallback for missing target-locale mappings. #[derive(Debug, Clone, Default, PartialEq, Eq)] +#[non_exhaustive] pub struct ConvertOptions { /// When a canonical identity has no spelling for the target locale, its /// spelling in this declared locale is used instead. `None` (the @@ -26,6 +27,7 @@ pub struct ConvertOptions { /// The result of a raw Workshop locale conversion. #[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] pub struct Conversion { /// The converted localized Workshop text. pub text: String, diff --git a/crates/workshop-rs/src/output/emitter.rs b/crates/workshop-rs/src/output/emitter.rs index 51736bb..c79d6ce 100644 --- a/crates/workshop-rs/src/output/emitter.rs +++ b/crates/workshop-rs/src/output/emitter.rs @@ -19,6 +19,7 @@ pub(crate) use crate::wir; /// Emission options: opt-in fallback for missing target-locale mappings. #[derive(Debug, Clone, Default, PartialEq, Eq)] +#[non_exhaustive] pub struct EmitOptions { /// When a canonical identity has no spelling for the target locale, its /// spelling in this declared locale is used instead. `None` (the default) @@ -29,6 +30,7 @@ pub struct EmitOptions { /// The result of a localized emission. #[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] pub struct EmitOutput { /// The emitted localized Workshop text. pub text: String, diff --git a/crates/workshop-rs/src/output/roundtrip.rs b/crates/workshop-rs/src/output/roundtrip.rs index 5862c13..c8ec278 100644 --- a/crates/workshop-rs/src/output/roundtrip.rs +++ b/crates/workshop-rs/src/output/roundtrip.rs @@ -14,6 +14,7 @@ use crate::output::emitter; /// A recorded round-trip result for regression and contract checks. #[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] pub struct RoundTripRecord { /// SHA-256 of the input Workshop text. pub input_identity: String, diff --git a/crates/workshop-rs/src/program.rs b/crates/workshop-rs/src/program.rs index 88284ed..6b38fe0 100644 --- a/crates/workshop-rs/src/program.rs +++ b/crates/workshop-rs/src/program.rs @@ -51,6 +51,7 @@ struct ActionProvenance { /// A failure while attaching source mappings to a public [`Program`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] pub enum SourceMappingError { UnknownFile(FileId), InvalidSpan(Span), diff --git a/crates/workshop-rs/src/program/source_map.rs b/crates/workshop-rs/src/program/source_map.rs index c873711..dd82e5b 100644 --- a/crates/workshop-rs/src/program/source_map.rs +++ b/crates/workshop-rs/src/program/source_map.rs @@ -34,6 +34,7 @@ pub struct SourceMap { /// Workshop text together with the [`SourceMap`] of its authored origin: the /// `workshop-rs/mapped-text-v1` artifact. #[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] pub struct MappedText { /// The Workshop text, itself a `workshop-rs/text-v1` artifact. pub text: String, @@ -411,6 +412,13 @@ impl SourceMap { } impl MappedText { + pub fn new(text: impl Into, map: SourceMap) -> Self { + Self { + text: text.into(), + map, + } + } + /// Serialize as a `workshop-rs/mapped-text-v1` JSON document. pub fn to_json(&self) -> String { let artifact = Artifact { diff --git a/crates/workshop-rs/src/settings/schema.rs b/crates/workshop-rs/src/settings/schema.rs index 05a4ac0..fccf033 100644 --- a/crates/workshop-rs/src/settings/schema.rs +++ b/crates/workshop-rs/src/settings/schema.rs @@ -48,6 +48,7 @@ pub enum SettingIdentity { /// The Workshop-native section that owns a setting. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] pub enum SettingScope { Main, Lobby, @@ -123,6 +124,7 @@ pub enum Applicability { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] pub enum NumericBoundsError { NonFinite, Reversed, @@ -196,6 +198,7 @@ pub struct EffectiveNumber { /// The machine-readable value domain of a setting. #[derive(Debug, Clone, PartialEq, PartialOrd)] +#[non_exhaustive] pub enum SettingValueDomain { Boolean, Number(NumericBounds), @@ -288,6 +291,7 @@ impl SettingSourceEdit { /// Failure from a typed settings query or source-preserving edit. #[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] pub enum SettingOperationError { NotApplicable { setting: SettingId, @@ -389,6 +393,7 @@ impl SettingPresentation { /// Source metadata shared by the reviewed table projection. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] pub struct SettingSource { pub kind: SettingSourceKind, pub source: &'static str, diff --git a/crates/workshop-rs/tests/gameplay.rs b/crates/workshop-rs/tests/gameplay.rs index 169cb9e..fb9766b 100644 --- a/crates/workshop-rs/tests/gameplay.rs +++ b/crates/workshop-rs/tests/gameplay.rs @@ -5,11 +5,7 @@ use workshop_rs::gameplay::{ }; fn source_reference(locator: &str) -> SourceReference { - SourceReference { - source: "workshop-data".to_string(), - locator: locator.to_string(), - note: None, - } + SourceReference::new("workshop-data", locator) } fn names(name: &str, locator: &str) -> Fact { @@ -38,15 +34,15 @@ fn hero(id: &str, abilities: Vec) -> Hero { } fn identity() -> GameplayDatasetIdentity { - GameplayDatasetIdentity { - dataset_id: "overwatch-workshop-hero-gameplay".to_string(), - version: "2026-08-12".to_string(), - digest: "sha256:test".to_string(), - source: "workshop-data@d854bf01fc7bbf3b2169f67408c07a8da8989ad6".to_string(), - license: "MIT-compatible user-provided export".to_string(), - target: "Overwatch Workshop hero identity and gameplay facts".to_string(), - reviewed: true, - } + GameplayDatasetIdentity::new( + "overwatch-workshop-hero-gameplay", + "2026-08-12", + "sha256:test", + "workshop-data@d854bf01fc7bbf3b2169f67408c07a8da8989ad6", + "MIT-compatible user-provided export", + "Overwatch Workshop hero identity and gameplay facts", + true, + ) } #[test] @@ -238,11 +234,7 @@ fn empty_id_and_empty_sources_are_rejected() { HeroId::new("ana"), Fact::new( LocalizedText::new([("en-US".to_string(), "Ana".to_string())]), - vec![SourceReference { - source: String::new(), - locator: "heroes.ana".to_string(), - note: None, - }], + vec![SourceReference::new("", "heroes.ana")], ), vec![], vec![source_reference("heroes.ana")], diff --git a/crates/workshop-rs/tests/gameplay_data.rs b/crates/workshop-rs/tests/gameplay_data.rs index 5437185..15b2215 100644 --- a/crates/workshop-rs/tests/gameplay_data.rs +++ b/crates/workshop-rs/tests/gameplay_data.rs @@ -10,11 +10,7 @@ const SOURCE: &str = "workshop-data/workshop-data.json@d854bf01fc7bbf3b2169f6740 const OFFICIAL_HERO_SOURCE: &str = "Blizzard Entertainment official Overwatch hero detail"; fn source_reference(locator: &str) -> SourceReference { - SourceReference { - source: SOURCE.to_string(), - locator: locator.to_string(), - note: Some("commitDate=2026-08-12".to_string()), - } + SourceReference::new(SOURCE, locator).with_note("commitDate=2026-08-12") } fn names(name: &str, locator: &str) -> Fact { @@ -25,15 +21,15 @@ fn names(name: &str, locator: &str) -> Fact { } fn identity() -> GameplayDatasetIdentity { - GameplayDatasetIdentity { - dataset_id: "test-gameplay".to_string(), - version: "test".to_string(), - digest: "sha256:test".to_string(), - source: SOURCE.to_string(), - license: "MIT-compatible test data".to_string(), - target: "test".to_string(), - reviewed: true, - } + GameplayDatasetIdentity::new( + "test-gameplay", + "test", + "sha256:test", + SOURCE, + "MIT-compatible test data", + "test", + true, + ) } fn assert_sources(sources: &[SourceReference], source: &str, locator: &str) { diff --git a/crates/workshop-rs/tests/gameplay_query.rs b/crates/workshop-rs/tests/gameplay_query.rs index 4ace709..20a0c21 100644 --- a/crates/workshop-rs/tests/gameplay_query.rs +++ b/crates/workshop-rs/tests/gameplay_query.rs @@ -10,11 +10,7 @@ use workshop_rs::gameplay::{ }; fn source_reference(locator: &str) -> SourceReference { - SourceReference { - source: "test-fixture".to_string(), - locator: locator.to_string(), - note: None, - } + SourceReference::new("test-fixture", locator) } fn names(en: &str, zh: &str, locator: &str) -> Fact { @@ -41,15 +37,15 @@ fn seconds(value: f64) -> Quantity { } fn identity() -> GameplayDatasetIdentity { - GameplayDatasetIdentity { - dataset_id: "gameplay-test".to_string(), - version: "2026-08-18".to_string(), - digest: "sha256:test".to_string(), - source: "test-fixture".to_string(), - license: "MIT".to_string(), - target: "gameplay query tests".to_string(), - reviewed: true, - } + GameplayDatasetIdentity::new( + "gameplay-test", + "2026-08-18", + "sha256:test", + "test-fixture", + "MIT", + "gameplay query tests", + true, + ) } fn catalog() -> GameplayCatalog { diff --git a/crates/workshop-rs/tests/public_api.rs b/crates/workshop-rs/tests/public_api.rs index ff31b2a..089e685 100644 --- a/crates/workshop-rs/tests/public_api.rs +++ b/crates/workshop-rs/tests/public_api.rs @@ -1,6 +1,8 @@ use workshop_rs::catalog::{Catalog, Locale}; use workshop_rs::settings::{PathPart, schema}; -use workshop_rs::{Action, Event, Program, Rule, Value, emitter, parser, roundtrip, rules}; +use workshop_rs::{ + Action, Event, MappedText, Program, Rule, SourceMap, Value, emitter, parser, roundtrip, rules, +}; #[test] fn canonical_program_operations_cover_parse_validate_inspect_emit_and_roundtrip() { @@ -122,3 +124,24 @@ fn is_call(value: &Value, id: &str, numbers: &[f64]) -> bool { fn is_number(value: &Value, expected: f64) -> bool { matches!(value, Value::Number(actual) if *actual == expected) } + +#[test] +fn mapped_text_is_constructible_outside_the_crate() { + let catalog = Catalog::builtin().expect("built-in catalog"); + let locale = Locale::new("en-US"); + let program = parser::parse( + r#"rule ("mapped") { + event { Ongoing - Global; } + actions { Wait(1, Ignore Condition); } + }"#, + &catalog, + &locale, + ) + .expect("Workshop parses"); + let text = emitter::emit(&program, &catalog, &locale).expect("canonical emission"); + + let artifact = MappedText::new(text.clone(), SourceMap::extract(&program)); + assert_eq!(artifact.text, text); + let decoded = MappedText::from_json(&artifact.to_json()).expect("artifact decodes"); + assert_eq!(decoded, artifact); +} diff --git a/docs/README.md b/docs/README.md index eeb3d8f..62ad11a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -30,7 +30,8 @@ relevant code/tests/data and current Issue contract. shared implementation boundaries, CLI verification tooling, and test-owned fixtures. - [Public API and compatibility contract](compatibility-facades.md): crate-root - module exports, root re-exports, and retired compatibility paths. + module exports, root re-exports, retired compatibility paths, and which + public types may grow without a breaking change. - [Repository agent guidance](../AGENTS.md): implementation routing, verification, source attribution, and delivery rules. diff --git a/docs/compatibility-facades.md b/docs/compatibility-facades.md index feb032d..236ab9d 100644 --- a/docs/compatibility-facades.md +++ b/docs/compatibility-facades.md @@ -63,3 +63,20 @@ public Rust item private or remove its compatibility consequences. Before 1.0, implementation-only public exports may be removed as intentional breaking changes. Consumer migrations are owned by their respective repositories and do not need to precede an owner-side breaking release. + +## Growth without breaking changes + +Public types expected to grow in 1.x are `#[non_exhaustive]`: error enums and +error records, options, operation outputs and reports, catalog and gameplay +identity/metadata records, settings source metadata, and the settings scope and +value-domain enums. `Rule`, `Condition`, `PlayerEventKind`, and `ModifyOp` are +also `#[non_exhaustive]`. Consumers match them with a wildcard arm and read +their fields, but do not build them with struct literals. Options are built +from `Default` and then assigned field by field; records that consumers need to +build, such as `MappedText`, `gameplay::SourceReference`, and +`gameplay::GameplayDatasetIdentity`, have a `new` constructor. + +The canonical program model (`Action`, `Value`, `Event`, `EventTeam`, +`EventTarget`), `catalog::Kind`, source positions and spans, and values that +consumers construct stay exhaustive. New Workshop content reaches them through +catalog ids and `Call`, not new variants.