From 2cf27a72fb3629706072a214d7371f8918f99eac Mon Sep 17 00:00:00 2001 From: Teakowa <27560638+Teakowa@users.noreply.github.com> Date: Sat, 22 Aug 2026 22:10:00 +0800 Subject: [PATCH] refactor(workshop): remove the wright-workshop cutover adapter wright#143 made workshop-rs the canonical Workshop core and left wright-workshop as a re-export-only adapter with a documented removal path: migrate call sites to workshop_rs:: and delete the crate. That cutover is now complete. - migrate all 111 wright_workshop:: call sites in wright-analyzer, wright-opy, wright-driver, and wright-ostw (src and tests) to the workshop_rs:: paths they already re-exported; all four crates already depended on workshop-rs directly - preserve the ten #109/#111 manifest-wiring regressions (shipped driver parse context, chase/Invis None fixed points, chase-keywords end-to-end round-trip) as wright-driver/tests/manifest_context.rs, next to the session wiring they protect - delete the adapter crate and its 2568-line test suite: the remaining coverage tested workshop-rs's own catalog/parser/emitter/round-trip behavior through the re-export and is owned and tested by workshop-rs (crates/workshop-rs/tests, 18 files incl. all 8 counterpart suites) - update the 17 documents and 4 code comments that described consumption 'via the wright-workshop adapter'; ADR-0009's migration-state list now records the adapter as removed Validation: cargo fmt --check clean; cargo clippy --workspace --all-targets --all-features -D warnings clean; cargo test --workspace 591 passed / 0 failed; no wright-workshop references remain outside intentional historical notes. --- CONTRIBUTING.md | 2 +- Cargo.lock | 16 - Cargo.toml | 2 +- crates/wright-analyzer/Cargo.toml | 1 - crates/wright-analyzer/src/bin/wright-tool.rs | 4 +- crates/wright-analyzer/src/service.rs | 2 +- .../tests/workshop_integration.rs | 4 +- crates/wright-driver/Cargo.toml | 1 - crates/wright-driver/src/service.rs | 20 +- crates/wright-driver/src/session.rs | 28 +- crates/wright-driver/tests/convert.rs | 26 +- .../wright-driver/tests/manifest_context.rs | 240 ++++ crates/wright-opy/Cargo.toml | 1 - crates/wright-opy/src/manifest/mod.rs | 18 +- crates/wright-opy/src/reconstruct.rs | 10 +- crates/wright-opy/tests/reconstruct.rs | 10 +- crates/wright-ostw/Cargo.toml | 1 - crates/wright-ostw/src/lib.rs | 2 +- crates/wright-ostw/src/reconstruct.rs | 2 +- crates/wright-ostw/src/semantic.rs | 2 +- crates/wright-ostw/src/signature.rs | 4 +- crates/wright-ostw/tests/differential.rs | 20 +- crates/wright-ostw/tests/reconstruct.rs | 80 +- crates/wright-ostw/tests/semantic.rs | 4 +- crates/wright-workshop/Cargo.toml | 21 - crates/wright-workshop/src/lib.rs | 17 - crates/wright-workshop/tests/catalog.rs | 277 ----- crates/wright-workshop/tests/closure.rs | 76 -- crates/wright-workshop/tests/detect.rs | 98 -- crates/wright-workshop/tests/emitter.rs | 1006 ----------------- crates/wright-workshop/tests/parser.rs | 407 ------- crates/wright-workshop/tests/pipeline.rs | 76 -- crates/wright-workshop/tests/roundtrip.rs | 358 ------ crates/wright-workshop/tests/wir_expansion.rs | 270 ----- docs/README.md | 2 +- ...language-ownership-licensing-boundaries.md | 3 +- docs/compatibility.md | 5 +- docs/compatibility/upstream-references.md | 4 +- docs/licensing.md | 3 +- docs/opy/compat-manifest-spec.md | 4 +- docs/opy/support-matrix.md | 2 +- docs/ostw/compatibility-baseline.md | 6 +- docs/ostw/support-matrix.md | 4 +- docs/v1-matrix.md | 2 +- docs/workshop/catalog-pipeline.md | 8 +- docs/workshop/support-matrix.md | 3 +- 46 files changed, 383 insertions(+), 2769 deletions(-) create mode 100644 crates/wright-driver/tests/manifest_context.rs delete mode 100644 crates/wright-workshop/Cargo.toml delete mode 100644 crates/wright-workshop/src/lib.rs delete mode 100644 crates/wright-workshop/tests/catalog.rs delete mode 100644 crates/wright-workshop/tests/closure.rs delete mode 100644 crates/wright-workshop/tests/detect.rs delete mode 100644 crates/wright-workshop/tests/emitter.rs delete mode 100644 crates/wright-workshop/tests/parser.rs delete mode 100644 crates/wright-workshop/tests/pipeline.rs delete mode 100644 crates/wright-workshop/tests/roundtrip.rs delete mode 100644 crates/wright-workshop/tests/wir_expansion.rs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9869ac4..6f2fc1f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,7 +47,7 @@ not enable Clippy's `pedantic`, `nursery`, or `restriction` groups. This policy was audited against the workspace manifests, CI, and representative compiler/tooling crates (`wright-ir`, `wright-core`, `wright-driver`, -`wright-opy`, `wright-workshop`, `wright-language`, and `wright-lsp`): +`wright-opy`, `wright-language`, and `wright-lsp`): * Semantic identities use typed IR IDs and arena lookups; raw strings remain at protocol, source-name, and presentation boundaries. New public contracts diff --git a/Cargo.lock b/Cargo.lock index 96dacb5..593ef69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1797,7 +1797,6 @@ dependencies = [ "workshop-rs", "wright-core", "wright-ir", - "wright-workshop", ] [[package]] @@ -1862,7 +1861,6 @@ dependencies = [ "wright-opy", "wright-ostw", "wright-transform", - "wright-workshop", ] [[package]] @@ -1917,7 +1915,6 @@ dependencies = [ "workshop-rs", "wright-core", "wright-ir", - "wright-workshop", ] [[package]] @@ -1931,7 +1928,6 @@ dependencies = [ "wright-ir", "wright-opy", "wright-transform", - "wright-workshop", ] [[package]] @@ -1943,18 +1939,6 @@ dependencies = [ "wright-ir", ] -[[package]] -name = "wright-workshop" -version = "0.2.11" -dependencies = [ - "regex", - "serde_json", - "workshop-rs", - "wright-core", - "wright-ir", - "wright-opy", -] - [[package]] name = "writeable" version = "0.6.4" diff --git a/Cargo.toml b/Cargo.toml index 3b15b43..737882b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["crates/wright-core", "crates/wright-ir", "crates/wright-analyzer", "crates/wright-workshop", "crates/wright-driver", "crates/wright-cli", "crates/wright-opy", "crates/wright-ostw", "crates/wright-transform", "crates/wright-bench", "crates/wright-consumer", "crates/wright-language", "crates/wright-lsp", "crates/wright-lpp"] +members = ["crates/wright-core", "crates/wright-ir", "crates/wright-analyzer", "crates/wright-driver", "crates/wright-cli", "crates/wright-opy", "crates/wright-ostw", "crates/wright-transform", "crates/wright-bench", "crates/wright-consumer", "crates/wright-language", "crates/wright-lsp", "crates/wright-lpp"] resolver = "3" [workspace.package] diff --git a/crates/wright-analyzer/Cargo.toml b/crates/wright-analyzer/Cargo.toml index 8fc2258..eb741f9 100644 --- a/crates/wright-analyzer/Cargo.toml +++ b/crates/wright-analyzer/Cargo.toml @@ -14,6 +14,5 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" wright-core = { path = "../wright-core" } wright-ir = { path = "../wright-ir" } -wright-workshop = { path = "../wright-workshop" } # Single pinned reference: `[workspace.dependencies]` in the root Cargo.toml. workshop-rs.workspace = true diff --git a/crates/wright-analyzer/src/bin/wright-tool.rs b/crates/wright-analyzer/src/bin/wright-tool.rs index a68312c..64a09b8 100644 --- a/crates/wright-analyzer/src/bin/wright-tool.rs +++ b/crates/wright-analyzer/src/bin/wright-tool.rs @@ -19,10 +19,10 @@ use std::io::{BufRead, Write}; use std::process::ExitCode; +use workshop_rs::catalog::{Catalog, Locale}; +use workshop_rs::{detect, parser}; use wright_analyzer::service::SemanticService; use wright_ir::lower; -use wright_workshop::catalog::{Catalog, Locale}; -use wright_workshop::{detect, parser}; enum Input { Protocol(wright_core::hir::Program), diff --git a/crates/wright-analyzer/src/service.rs b/crates/wright-analyzer/src/service.rs index cbaf01e..8346678 100644 --- a/crates/wright-analyzer/src/service.rs +++ b/crates/wright-analyzer/src/service.rs @@ -115,7 +115,7 @@ impl<'a> SemanticService<'a> { program, Origin { kind: "workshop".to_string(), - locale: Some(wright_workshop::catalog::Locale::new(locale).to_string()), + locale: Some(workshop_rs::catalog::Locale::new(locale).to_string()), }, ) } diff --git a/crates/wright-analyzer/tests/workshop_integration.rs b/crates/wright-analyzer/tests/workshop_integration.rs index 8a3e61f..8d3815d 100644 --- a/crates/wright-analyzer/tests/workshop_integration.rs +++ b/crates/wright-analyzer/tests/workshop_integration.rs @@ -8,9 +8,9 @@ use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use serde_json::Value; +use workshop_rs::catalog::{Catalog, Locale}; +use workshop_rs::parser; use wright_analyzer::service::SemanticService; -use wright_workshop::catalog::{Catalog, Locale}; -use wright_workshop::parser; fn oracle_path(fixture_id: &str) -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")) diff --git a/crates/wright-driver/Cargo.toml b/crates/wright-driver/Cargo.toml index 9796a0a..4ae02fb 100644 --- a/crates/wright-driver/Cargo.toml +++ b/crates/wright-driver/Cargo.toml @@ -20,6 +20,5 @@ wright-lpp = { path = "../wright-lpp" } wright-opy = { path = "../wright-opy" } wright-ostw = { path = "../wright-ostw" } wright-transform = { path = "../wright-transform" } -wright-workshop = { path = "../wright-workshop" } # Single released reference: `[workspace.dependencies]` in the root Cargo.toml. workshop-rs.workspace = true diff --git a/crates/wright-driver/src/service.rs b/crates/wright-driver/src/service.rs index 1f9cb77..f2938a9 100644 --- a/crates/wright-driver/src/service.rs +++ b/crates/wright-driver/src/service.rs @@ -532,16 +532,16 @@ impl<'a> ToolService<'a> { /// Compiler-host performance is measured by the `wright-bench` harness, not /// in-process. fn cost_estimate(&self) -> serde_json::Value { - let catalog = wright_workshop::catalog::Catalog::builtin().expect("built-in catalog loads"); + let catalog = workshop_rs::catalog::Catalog::builtin().expect("built-in catalog loads"); let locale = self .loaded .origin .locale .clone() - .map(|locale| wright_workshop::catalog::Locale::new(&locale)) - .unwrap_or_else(|| wright_workshop::catalog::Locale::new("en-US")); - let text = wright_workshop::emitter::emit(&self.loaded.program, &catalog, &locale) - .unwrap_or_default(); + .map(|locale| workshop_rs::catalog::Locale::new(&locale)) + .unwrap_or_else(|| workshop_rs::catalog::Locale::new("en-US")); + let text = + workshop_rs::emitter::emit(&self.loaded.program, &catalog, &locale).unwrap_or_default(); let waits = self .loaded .program @@ -575,7 +575,7 @@ impl<'a> ToolService<'a> { /// Target/catalog metadata for reasoning about Workshop operations. fn target_metadata(&self) -> serde_json::Value { - let catalog = match wright_workshop::catalog::Catalog::builtin() { + let catalog = match workshop_rs::catalog::Catalog::builtin() { Ok(catalog) => catalog, Err(error) => { return json!({ "error": error.to_string() }); @@ -584,10 +584,10 @@ impl<'a> ToolService<'a> { json!({ "catalogVersion": catalog.schema_version, "locales": catalog.locales().iter().map(|l| l.to_string()).collect::>(), - "actions": catalog.entries_of(wright_workshop::catalog::Kind::Action).count(), - "values": catalog.entries_of(wright_workshop::catalog::Kind::Value).count(), - "events": catalog.entries_of(wright_workshop::catalog::Kind::Event).count(), - "operators": catalog.entries_of(wright_workshop::catalog::Kind::Operator).count(), + "actions": catalog.entries_of(workshop_rs::catalog::Kind::Action).count(), + "values": catalog.entries_of(workshop_rs::catalog::Kind::Value).count(), + "events": catalog.entries_of(workshop_rs::catalog::Kind::Event).count(), + "operators": catalog.entries_of(workshop_rs::catalog::Kind::Operator).count(), "enumDomains": catalog.enum_domains().map(|domain| json!({ "domain": domain.domain, "members": domain.members.iter().map(|m| m.member.clone()).collect::>(), diff --git a/crates/wright-driver/src/session.rs b/crates/wright-driver/src/session.rs index 25e9a99..4e621a6 100644 --- a/crates/wright-driver/src/session.rs +++ b/crates/wright-driver/src/session.rs @@ -49,7 +49,7 @@ pub struct Loaded { pub struct CompilerSession { /// The session configuration (input, frontend, overrides, format). pub config: SessionConfig, - catalog: wright_workshop::catalog::Catalog, + catalog: workshop_rs::catalog::Catalog, loaded: Option, diagnostics: Vec, } @@ -57,7 +57,7 @@ pub struct CompilerSession { impl CompilerSession { /// Build a session from a configuration. pub fn new(config: SessionConfig) -> Result { - let catalog = wright_workshop::catalog::Catalog::builtin().map_err(|error| { + let catalog = workshop_rs::catalog::Catalog::builtin().map_err(|error| { Diagnostic::error( "catalog-error", Stage::Internal, @@ -244,8 +244,8 @@ impl CompilerSession { .config .locale .as_deref() - .map(wright_workshop::catalog::Locale::new); - let locale = wright_workshop::detect::resolve_locale( + .map(workshop_rs::catalog::Locale::new); + let locale = workshop_rs::detect::resolve_locale( &resolved.text, &self.catalog, override_locale.as_ref(), @@ -266,7 +266,7 @@ impl CompilerSession { ) })?; let context = wright_core::signatures::ChainedExpectedDomain::new(manifest, &self.catalog); - let program = wright_workshop::parser::parse_with_context( + let program = workshop_rs::parser::parse_with_context( &resolved.text, &self.catalog, &locale, @@ -376,9 +376,9 @@ impl CompilerSession { .origin .locale .clone() - .map(|locale| wright_workshop::catalog::Locale::new(&locale)) - .unwrap_or_else(|| wright_workshop::catalog::Locale::new("en-US")); - let text = wright_workshop::emitter::emit(&loaded.program, &self.catalog, &locale) + .map(|locale| workshop_rs::catalog::Locale::new(&locale)) + .unwrap_or_else(|| workshop_rs::catalog::Locale::new("en-US")); + let text = workshop_rs::emitter::emit(&loaded.program, &self.catalog, &locale) .map_err(|error| workshop_diag(error, &loaded.input))?; let sha256 = input_identity(&text); Ok(CompiledOutput { @@ -946,16 +946,16 @@ fn root_relative(path: Option<&Path>, root: &Path) -> Option { } /// Map a Workshop-language error to a driver diagnostic. -fn workshop_diag(error: wright_workshop::WorkshopError, resolved: &ResolvedInput) -> Diagnostic { +fn workshop_diag(error: workshop_rs::WorkshopError, resolved: &ResolvedInput) -> Diagnostic { let (code, stage, span) = match &error { - wright_workshop::WorkshopError::Catalog(catalog) => { + workshop_rs::WorkshopError::Catalog(catalog) => { return Diagnostic::error( "catalog-error", Stage::Internal, format!("{}: {}", catalog.code, catalog.message), ); } - wright_workshop::WorkshopError::Unknown { kind, span, .. } => ( + workshop_rs::WorkshopError::Unknown { kind, span, .. } => ( format!("unknown-{kind}"), Stage::Frontend, span.map(|span| SourceSpan { @@ -971,7 +971,7 @@ fn workshop_diag(error: wright_workshop::WorkshopError, resolved: &ResolvedInput }, }), ), - wright_workshop::WorkshopError::Malformed { span, .. } => ( + workshop_rs::WorkshopError::Malformed { span, .. } => ( "parse-error".to_string(), Stage::Frontend, span.map(|span| SourceSpan { @@ -987,7 +987,7 @@ fn workshop_diag(error: wright_workshop::WorkshopError, resolved: &ResolvedInput }, }), ), - wright_workshop::WorkshopError::Unsupported { span, .. } => ( + workshop_rs::WorkshopError::Unsupported { span, .. } => ( "unsupported-construct".to_string(), Stage::Frontend, span.map(|span| SourceSpan { @@ -1007,7 +1007,7 @@ fn workshop_diag(error: wright_workshop::WorkshopError, resolved: &ResolvedInput // a first-class error (ADR-0001 Decision 7; wright#143): conversion // or emission into a locale without a mapping is a diagnostic, never // a guess or a silent passthrough. - wright_workshop::WorkshopError::MissingMapping { kind, id, locale } => ( + workshop_rs::WorkshopError::MissingMapping { kind, id, locale } => ( "missing-mapping".to_string(), Stage::Frontend, Diagnostic::error( diff --git a/crates/wright-driver/tests/convert.rs b/crates/wright-driver/tests/convert.rs index 6d2ab19..72bc41a 100644 --- a/crates/wright-driver/tests/convert.rs +++ b/crates/wright-driver/tests/convert.rs @@ -5,7 +5,7 @@ //! proves both reverse loops through the real native frontends: //! //! * `Workshop → convert(opy) → native wright-opy frontend → HIR → WIR → -//! Workshop` — equivalence under `wright_workshop::roundtrip::equivalent` +//! Workshop` — equivalence under `workshop_rs::roundtrip::equivalent` //! (the #124 contract, no normalization); //! * `Workshop → convert(ostw) → native wright-ostw frontend (generated //! `ds.toml` project root) → HIR → WIR → Workshop` — equivalence under the @@ -58,14 +58,14 @@ fn sha256(input: &str) -> String { /// Parse Workshop text through the shared parser with the canonical /// signature context (the same path the driver uses). -fn parse(catalog: &wright_workshop::catalog::Catalog, text: &str) -> wir::Program { +fn parse(catalog: &workshop_rs::catalog::Catalog, text: &str) -> wir::Program { let manifest = wright_opy::manifest::Manifest::builtin().expect("the OPY manifest is embedded and valid"); let context = wright_core::signatures::ChainedExpectedDomain::new(manifest, catalog); - let program = wright_workshop::parser::parse_with_context( + let program = workshop_rs::parser::parse_with_context( text, catalog, - &wright_workshop::catalog::Locale::new("en-US"), + &workshop_rs::catalog::Locale::new("en-US"), &context, ) .unwrap_or_else(|error| panic!("fixture Workshop text must parse: {error}")); @@ -129,13 +129,13 @@ fn compile_reconstructed_ostw(ostw_text: &str, test_name: &str) -> wright_ir::hi /// Emit Workshop text for a WIR program through the shared emitter. fn emit_workshop( - catalog: &wright_workshop::catalog::Catalog, + catalog: &workshop_rs::catalog::Catalog, program: &wir::Program, ) -> Result { - wright_workshop::emitter::emit( + workshop_rs::emitter::emit( program, catalog, - &wright_workshop::catalog::Locale::new("en-US"), + &workshop_rs::catalog::Locale::new("en-US"), ) .map_err(|error| error.to_string()) } @@ -538,7 +538,7 @@ fn normalize(program: &mut wir::Program) { /// The full Workshop → OPY loop through the shared driver path. fn opy_round_trip( - catalog: &wright_workshop::catalog::Catalog, + catalog: &workshop_rs::catalog::Catalog, fixture: &str, failures: &mut Vec, ) -> serde_json::Value { @@ -579,7 +579,7 @@ fn opy_round_trip( return serde_json::json!({ "status": "lower-failed" }); } }; - let equivalent = wright_workshop::roundtrip::equivalent(&original, &recompiled); + let equivalent = workshop_rs::roundtrip::equivalent(&original, &recompiled); if !equivalent { failures.push(format!("{fixture}: recompiled WIR is not equivalent")); } @@ -606,7 +606,7 @@ fn opy_round_trip( /// The full Workshop → OSTW loop through the shared driver path. fn ostw_round_trip( - catalog: &wright_workshop::catalog::Catalog, + catalog: &workshop_rs::catalog::Catalog, fixture: &str, failures: &mut Vec, ) -> serde_json::Value { @@ -669,7 +669,7 @@ fn ostw_round_trip( let mut reference = original; normalize(&mut actual); normalize(&mut reference); - let equivalent = wright_workshop::roundtrip::equivalent(&actual, &reference); + let equivalent = workshop_rs::roundtrip::equivalent(&actual, &reference); if !equivalent { failures.push(format!( "{fixture}: normalized recompiled WIR is not equivalent" @@ -690,7 +690,7 @@ fn ostw_round_trip( #[test] fn cross_format_conversion_round_trips_and_reports() { - let catalog = wright_workshop::catalog::Catalog::builtin().expect("catalog loads"); + let catalog = workshop_rs::catalog::Catalog::builtin().expect("catalog loads"); let mut failures = Vec::new(); let mut report = serde_json::Map::new(); for fixture in OPY_FIXTURES { @@ -784,7 +784,7 @@ fn rejection_entry( #[test] fn conversion_is_byte_deterministic_across_runs() { - let catalog = wright_workshop::catalog::Catalog::builtin().expect("catalog loads"); + let catalog = workshop_rs::catalog::Catalog::builtin().expect("catalog loads"); for (target, fixture) in [ ( ConvertTarget::Opy, diff --git a/crates/wright-driver/tests/manifest_context.rs b/crates/wright-driver/tests/manifest_context.rs new file mode 100644 index 0000000..b0edfc0 --- /dev/null +++ b/crates/wright-driver/tests/manifest_context.rs @@ -0,0 +1,240 @@ +//! Manifest-wired Workshop parse context (#109/#111). +//! +//! These regressions protect the shipped driver wiring: the session parses +//! Workshop text with the OPY compatibility manifest chained onto the catalog +//! as the expected-domain context (`CompilerSession`'s parse path, #111), so +//! emitter-produced ambiguous bare `None` members reparse to their pinned +//! enum domains and the emission is a fixed point. Preserved from the +//! removed `wright-workshop` cutover adapter's test suite; the context-free +//! parser/emitter/round-trip behavior itself is owned and tested by +//! `workshop-rs`. + +use std::path::Path; + +use workshop_rs::catalog::{Catalog, Locale}; +use workshop_rs::parser; +use workshop_rs::roundtrip; +use workshop_rs::wir; + +fn catalog() -> Catalog { + Catalog::builtin().unwrap() +} + +fn en() -> Locale { + Locale::new("en-US") +} + +/// The canonical signature context from the #109 manifest, as the shipped +/// driver wires it into the Workshop parse path (#111). +fn manifest_context() -> &'static dyn wright_core::signatures::ExpectedDomain { + wright_opy::manifest::Manifest::builtin().expect("builtin manifest") +} + +/// The last argument value of the first call action of a parsed program. +fn enum_value_of_first_action(program: &wir::Program, action_index: usize) -> &wir::Value { + let action = program.actions.iter().nth(action_index).expect("action"); + let wir::Action::Call { args, .. } = action else { + panic!("expected a call action, got {action:?}"); + }; + let last = args.last().expect("call has an argument"); + let wir::ValueNode { value, .. } = program.values.get(*last).expect("value"); + value +} + +#[test] +fn context_pinned_ambiguous_none_resolves_via_canonical_signature() { + // #111: emitter-produced `Chase Global Variable Over Time(..., None)` + // reparses to ChaseTimeReeval.NONE because the canonical chaseOverTime + // signature pins argument 3 to the ChaseTimeReeval domain. + let text = "variables { global: 0: g }\nrule (\"x\") { event { Ongoing - Global; } actions { Chase Global Variable Over Time(Global.g, 0, 30, None); } }"; + let program = + parser::parse_with_context(text, &catalog(), &Locale::new("en-US"), manifest_context()) + .expect("the pinned Chase None must resolve"); + let value = enum_value_of_first_action(&program, 0); + assert!( + matches!(value, wir::Value::Enum { value_type, value } + if value_type == "ChaseTimeReeval" && value == "NONE"), + "the bare None resolves to ChaseTimeReeval.NONE, got {value:?}" + ); +} + +#[test] +fn context_pinned_ambiguous_none_resolves_for_set_invisible() { + // #111: `Set Invisible(Event Player, None)` reparses to Invis.NONE. The + // manifest's setInvisibility is a member action, so Workshop text places + // the receiver as argument 0 and the signature-pinned parameter at + // argument 1. + let text = "rule (\"x\") { event { Ongoing - Each Player; } actions { Set Invisible(Event Player, None); } }"; + let program = + parser::parse_with_context(text, &catalog(), &Locale::new("en-US"), manifest_context()) + .expect("the pinned Invis None must resolve"); + let value = enum_value_of_first_action(&program, 0); + assert!( + matches!(value, wir::Value::Enum { value_type, value } + if value_type == "Invis" && value == "NONE"), + "the bare None resolves to Invis.NONE, got {value:?}" + ); +} + +#[test] +fn wrong_domain_context_keeps_the_ambiguity_rejected() { + // A signature pinning a *different* domain than the ambiguous member's + // candidates must not resolve it: `Wait(...)` expects `Wait` (which has + // no `None` member), so the bare `None` stays ambiguous — no guessing, + // no arbitrary precedence. + let text = "rule (\"x\") { event { Ongoing - Global; } actions { Wait(0.016, None); } }"; + let error = + parser::parse_with_context(text, &catalog(), &Locale::new("en-US"), manifest_context()) + .expect_err("a non-matching expected domain must keep the ambiguity"); + assert!( + matches!(error, workshop_rs::WorkshopError::Unsupported { .. }), + "expected a structured ambiguity: {error}" + ); + assert!(error.to_string().contains("ambiguous enum member 'None'")); +} + +#[test] +fn expected_domain_resolution_tracks_the_manifest_declared_domains() { + // Behavioral check that resolution consumes the #109 manifest as the + // single source of expected domains: the adapter answers exactly the + // manifest's declared parameter domains, including the receiver-offset + // rule for member-kind functions. + let manifest = wright_opy::manifest::Manifest::builtin().expect("builtin manifest"); + use wright_core::signatures::ExpectedDomain; + // chaseOverTime: params [variable, destination, duration, reevaluation]. + assert_eq!( + manifest.expected_domain("chaseOverTime", 3), + Some("ChaseTimeReeval") + ); + assert_eq!(manifest.expected_domain("chaseOverTime", 2), None); + // setInvisibility: member action; Workshop arg 1 is the pinned param. + assert_eq!(manifest.expected_domain("setInvisibility", 0), None); + assert_eq!( + manifest.expected_domain("setInvisibility", 1), + Some("Invis") + ); + // Unknown catalog ids and out-of-range indexes answer None. + assert_eq!(manifest.expected_domain("noSuchAction", 0), None); + assert_eq!(manifest.expected_domain("chaseOverTime", 4), None); +} + +#[test] +fn emitter_chase_none_round_trips_through_the_shipped_path() { + // #111: emitter-produced `Chase Global Variable Over Time(..., None)` + // (bare `None` shared by ChaseTimeReeval/ChaseRateReeval/Invis) reparses + // to ChaseTimeReeval.NONE through the shipped parse+emit path, and the + // round-tripped WIR is equivalent to the input WIR. + let text = "variables { global: 0: g }\nrule (\"chase\") { event { Ongoing - Global; } actions { Chase Global Variable Over Time(Global.g, 0, 30, None); } }"; + let record = roundtrip::round_trip_with_context(text, &catalog(), &en(), manifest_context()); + assert!( + record.error.is_none(), + "the pinned Chase None must round-trip: {:?}", + record.error + ); + assert!(record.parse_ok && record.emit_ok && record.reparse_ok && record.equivalent); +} + +#[test] +fn emitter_set_invisible_none_round_trips_through_the_shipped_path() { + // #111: `Set Invisible(Event Player, None)` reparses to Invis.NONE via + // the member-function receiver offset and round-trips to equivalent WIR. + let text = "rule (\"inv\") { event { Ongoing - Each Player; } actions { Set Invisible(Event Player, None); } }"; + let record = roundtrip::round_trip_with_context(text, &catalog(), &en(), manifest_context()); + assert!( + record.error.is_none(), + "the pinned Invis None must round-trip: {:?}", + record.error + ); + assert!(record.parse_ok && record.emit_ok && record.reparse_ok && record.equivalent); +} + +#[test] +fn emitter_chase_at_rate_none_round_trips_through_the_shipped_path() { + // #110: the chase rate form emits `Chase Global Variable At Rate(..., + // None)`; the catalog id `chaseAtRate` selects the `ChaseRateReeval` + // domain through the manifest's contextual-dispatch data, so the bare + // `None` reparses and round-trips to equivalent WIR. + let text = "variables { global: 0: g }\nrule (\"chase\") { event { Ongoing - Global; } actions { Chase Global Variable At Rate(Global.g, 10, 2, None); } }"; + let record = roundtrip::round_trip_with_context(text, &catalog(), &en(), manifest_context()); + assert!( + record.error.is_none(), + "the pinned ChaseRateReeval None must round-trip: {:?}", + record.error + ); + assert!(record.parse_ok && record.emit_ok && record.reparse_ok && record.equivalent); + // The player form follows the same path through its own catalog id. + let text = "variables { player: 0: P }\nrule (\"chase\") { event { Ongoing - Each Player; } actions { Chase Player Variable At Rate(Event Player, P, 0, 1, None); } }"; + let record = roundtrip::round_trip_with_context(text, &catalog(), &en(), manifest_context()); + assert!( + record.error.is_none(), + "the pinned player ChaseRateReeval None must round-trip: {:?}", + record.error + ); + assert!(record.parse_ok && record.emit_ok && record.reparse_ok && record.equivalent); +} + +#[test] +fn chase_keyword_fixture_round_trips_through_the_shipped_path() { + // The `synthetic/chase-keywords` surface (rate/duration forms, global + // and player variables, keyword-bound wait/vect/len/print/ + // getPlayersInRadius/setStatusEffect) compiles through the native OPY + // frontend, emits through the catalog, reparses with the manifest + // signature context, and re-emits to a fixed point (#110). The oracle + // text itself is not the input: the reference emits bare variable names + // where the native Workshop parser's canonical spelling is `Global.g` + // (documented N-level presentation difference), so the round-trip uses + // the native emission. + let source = std::fs::read_to_string( + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../compatibility/fixtures/synthetic/chase-keywords/source.opy"), + ) + .unwrap(); + let hir = wright_opy::compile(&source, "source.opy", Path::new("")) + .expect("the fixture compiles natively"); + let model = wright_core::hir::convert::convert(&hir).expect("the HIR converts"); + let wir = wright_ir::lower::lower(&model).expect("the fixture lowers to WIR"); + let emitted = workshop_rs::emitter::emit(&wir, &catalog(), &en()).expect("the fixture emits"); + // The emission includes Debug/Print HUD text (canonical catalog layout) + // and chase `None` members, so both the manifest and the catalog supply + // the expected enum domains. + let catalog = catalog(); + let context = wright_core::signatures::ChainedExpectedDomain::new(manifest_context(), &catalog); + let record = roundtrip::round_trip_with_context(&emitted, &catalog, &en(), &context); + assert!( + record.error.is_none(), + "the chase-keywords emission must round-trip: {:?}", + record.error + ); + assert!(record.parse_ok && record.emit_ok && record.reparse_ok && record.equivalent); +} + +#[test] +fn context_free_chase_none_stays_a_documented_exception() { + // Without a signature pin the ambiguity stays rejected: the same input + // through the plain (context-free) round-trip fails at parse, keeping the + // pre-#111 boundary deterministic. + let text = "variables { global: 0: g }\nrule (\"chase\") { event { Ongoing - Global; } actions { Chase Global Variable Over Time(Global.g, 0, 30, None); } }"; + let record = roundtrip::round_trip(text, &catalog(), &en()); + assert!(!record.parse_ok, "context-free None must stay rejected"); + let error = record.error.expect("a parse failure is recorded"); + assert!(error.contains("ambiguous enum member 'None'"), "{error}"); +} + +#[test] +fn context_chase_none_emission_is_a_fixed_point() { + // Parse the emitted form with context, emit, reparse with context, and + // emit again: the text is a fixed point. + let text = "variables { global: 0: g }\nrule (\"chase\") { event { Ongoing - Global; } actions { Chase Global Variable Over Time(Global.g, 0, 30, None); } }"; + let catalog = catalog(); + let first = parser::parse_with_context(text, &catalog, &en(), manifest_context()) + .expect("pinned Chase None parses"); + let emitted = workshop_rs::emitter::emit(&first, &catalog, &en()).expect("emits"); + assert!( + emitted.contains("Chase Global Variable Over Time(Global.g, 0, 30, None)"), + "emission preserves the bare None spelling:\n{emitted}" + ); + let reparsed = parser::parse_with_context(&emitted, &catalog, &en(), manifest_context()) + .expect("emitted text reparses with context"); + let reemitted = workshop_rs::emitter::emit(&reparsed, &catalog, &en()).expect("re-emits"); + assert_eq!(emitted, reemitted, "emission must be a fixed point"); +} diff --git a/crates/wright-opy/Cargo.toml b/crates/wright-opy/Cargo.toml index b997c84..2c63d91 100644 --- a/crates/wright-opy/Cargo.toml +++ b/crates/wright-opy/Cargo.toml @@ -14,7 +14,6 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" wright-core = { path = "../wright-core" } wright-ir = { path = "../wright-ir" } -wright-workshop = { path = "../wright-workshop" } # Single pinned reference: `[workspace.dependencies]` in the root Cargo.toml. workshop-rs.workspace = true diff --git a/crates/wright-opy/src/manifest/mod.rs b/crates/wright-opy/src/manifest/mod.rs index fab087e..15bb1b1 100644 --- a/crates/wright-opy/src/manifest/mod.rs +++ b/crates/wright-opy/src/manifest/mod.rs @@ -15,7 +15,7 @@ //! pinned OverPy 9.7.10 oracle and verifies accept/reject, emission hash, //! and diagnostic category deterministically. //! * `catalogId` links each entry to the canonical Workshop emission -//! catalog (workshop-rs, consumed through the `wright-workshop` adapter) +//! catalog (workshop-rs, consumed directly) //! by canonical identity without duplicating localization/output spelling //! data; a cross-check test verifies every declared id exists with the //! matching kind. @@ -887,15 +887,15 @@ mod tests { #[test] fn catalog_ids_link_to_the_workshop_emission_catalog() { // Every declared `catalogId` must exist in the canonical Workshop - // emission catalog (workshop-rs, consumed through the wright-workshop - // adapter) under the matching kind, so manifest entries never - // surface as accidental emitter catalog misses. Entries without a + // emission catalog (workshop-rs, consumed directly) under the + // matching kind, so manifest entries never surface as accidental + // emitter catalog misses. Entries without a // `catalogId` (special emission forms like `debug`/`print`, or // emission surfaces not yet catalog-covered like the alias targets) // are exempt by design. let manifest = Manifest::builtin().expect("builtin"); - let catalog = wright_workshop::catalog::Catalog::builtin().expect("catalog loads"); - let has_id = |kind: wright_workshop::catalog::Kind, id: &str| { + let catalog = workshop_rs::catalog::Catalog::builtin().expect("catalog loads"); + let has_id = |kind: workshop_rs::catalog::Kind, id: &str| { catalog.entries_of(kind).any(|entry| entry.id == id) }; for function in &manifest.functions { @@ -904,10 +904,10 @@ mod tests { }; let kind = match function.kind { FunctionKind::Action | FunctionKind::MemberAction => { - wright_workshop::catalog::Kind::Action + workshop_rs::catalog::Kind::Action } FunctionKind::Value | FunctionKind::MemberValue => { - wright_workshop::catalog::Kind::Value + workshop_rs::catalog::Kind::Value } }; assert!( @@ -922,7 +922,7 @@ mod tests { if let Some(contextual) = &function.contextual_domain { for (keyword, option) in &contextual.options { assert!( - has_id(wright_workshop::catalog::Kind::Action, &option.target), + has_id(workshop_rs::catalog::Kind::Action, &option.target), "contextual target '{}' (keyword '{keyword}') of '{}' is missing \ from the Workshop emission catalog", option.target, diff --git a/crates/wright-opy/src/reconstruct.rs b/crates/wright-opy/src/reconstruct.rs index 966e216..d9ff070 100644 --- a/crates/wright-opy/src/reconstruct.rs +++ b/crates/wright-opy/src/reconstruct.rs @@ -3,7 +3,7 @@ //! Consumes a validated [`workshop_rs::wir::Program`] and emits deterministic, //! byte-stable canonical OPY source that the native [`crate::compile`] //! frontend accepts and that re-lowers to a structurally equivalent WIR -//! program under `wright_workshop::roundtrip::equivalent`. +//! program under `workshop_rs::roundtrip::equivalent`. //! //! Scope and ownership: //! @@ -42,9 +42,9 @@ use std::fmt; +use workshop_rs::catalog::{Catalog, Locale}; use workshop_rs::source::Span; use workshop_rs::wir::{self, Action, Event, ModifyOp, Value}; -use wright_workshop::catalog::{Catalog, Locale}; use crate::manifest::{Function, FunctionKind, Manifest}; @@ -1306,10 +1306,10 @@ impl<'a> Emitter<'a> { if let Some(catalog_id) = &entry.catalog_id { let expected_kind = match entry.kind { FunctionKind::Action | FunctionKind::MemberAction => { - wright_workshop::catalog::Kind::Action + workshop_rs::catalog::Kind::Action } FunctionKind::Value | FunctionKind::MemberValue => { - wright_workshop::catalog::Kind::Value + workshop_rs::catalog::Kind::Value } }; if self @@ -1809,7 +1809,7 @@ mod tests { let source = emit(program).expect("reconstruction succeeds"); let recompiled = recompile(&source); assert!( - wright_workshop::roundtrip::equivalent(program, &recompiled), + workshop_rs::roundtrip::equivalent(program, &recompiled), "recompiled WIR must be equivalent to the input:\n{source}" ); } diff --git a/crates/wright-opy/tests/reconstruct.rs b/crates/wright-opy/tests/reconstruct.rs index 3ec4772..bb5c117 100644 --- a/crates/wright-opy/tests/reconstruct.rs +++ b/crates/wright-opy/tests/reconstruct.rs @@ -4,7 +4,7 @@ //! semantic equivalence for every deterministic reconstruction fixture: //! the native OPY frontend accepts the reconstructed source, the recompiled //! WIR is structurally equivalent to the parsed Workshop program under -//! `wright_workshop::roundtrip::equivalent`, and the recompiled WIR still +//! `workshop_rs::roundtrip::equivalent`, and the recompiled WIR still //! emits to Workshop text through the shipped emitter (the trailing //! `→ Workshop` hop). Determinism, the machine-readable support boundary, //! and the explicit rejection surface are all asserted here through the @@ -13,10 +13,10 @@ use std::collections::BTreeMap; use std::path::{Path, PathBuf}; +use workshop_rs::catalog::{Catalog, Locale}; +use workshop_rs::parser; use workshop_rs::source::{Position, Span}; use workshop_rs::wir::{self, Action, Event, ModifyOp, Program, Value, ValueNode}; -use wright_workshop::catalog::{Catalog, Locale}; -use wright_workshop::parser; /// (fixture id, constructs covered) — the machine-readable coverage map that /// the support boundary consistency test cross-checks. @@ -403,10 +403,10 @@ fn every_fixture_round_trips_through_the_shipped_path() { continue; } }; - let equivalent = wright_workshop::roundtrip::equivalent(&parsed, &recompiled); + let equivalent = workshop_rs::roundtrip::equivalent(&parsed, &recompiled); // The trailing `→ Workshop` hop: the recompiled WIR still emits to // Workshop text through the shipped emitter. - let workshop_emit = wright_workshop::emitter::emit(&recompiled, &catalog, &locale) + let workshop_emit = workshop_rs::emitter::emit(&recompiled, &catalog, &locale) .map(|_| ()) .map_err(|error| error.to_string()); if !equivalent { diff --git a/crates/wright-ostw/Cargo.toml b/crates/wright-ostw/Cargo.toml index 9a14591..40fed47 100644 --- a/crates/wright-ostw/Cargo.toml +++ b/crates/wright-ostw/Cargo.toml @@ -11,7 +11,6 @@ workspace = true [dependencies] wright-ir = { path = "../wright-ir" } -wright-workshop = { path = "../wright-workshop" } # Single pinned reference: `[workspace.dependencies]` in the root Cargo.toml. workshop-rs.workspace = true diff --git a/crates/wright-ostw/src/lib.rs b/crates/wright-ostw/src/lib.rs index bb586e7..f10cd6e 100644 --- a/crates/wright-ostw/src/lib.rs +++ b/crates/wright-ostw/src/lib.rs @@ -6,7 +6,7 @@ //! semantic phase that resolves the entry-point reachable graph into //! frontend-neutral Wright HIR (#118). Workshop actions/values/enums resolve //! through the canonical Wright-owned Workshop catalog -//! (`wright-workshop`'s `catalog`), with only OSTW source-name bindings kept +//! (`workshop-rs`'s `catalog`), with only OSTW source-name bindings kept //! here; no OSTW game-derived table is imported. Upstream .NET/OSTW remains a //! reference-only oracle and never enters the production dependency graph. //! diff --git a/crates/wright-ostw/src/reconstruct.rs b/crates/wright-ostw/src/reconstruct.rs index 7835efe..56e4c4e 100644 --- a/crates/wright-ostw/src/reconstruct.rs +++ b/crates/wright-ostw/src/reconstruct.rs @@ -39,9 +39,9 @@ use std::collections::HashSet; use std::fmt::Write; +use workshop_rs::catalog::{Catalog, Kind}; use workshop_rs::source::Span; use workshop_rs::wir::{self, Action, Event, ModifyOp, Value, ValueId}; -use wright_workshop::catalog::{Catalog, Kind}; use crate::signature; diff --git a/crates/wright-ostw/src/semantic.rs b/crates/wright-ostw/src/semantic.rs index ec0b740..a03fdcf 100644 --- a/crates/wright-ostw/src/semantic.rs +++ b/crates/wright-ostw/src/semantic.rs @@ -20,7 +20,7 @@ use std::collections::HashMap; use wright_ir::hir::{self, ExprId, FunctionId, GlobalVarId, PlayerVarId, StmtId, SubroutineId}; -use wright_workshop::catalog::{Catalog, Kind}; +use workshop_rs::catalog::{Catalog, Kind}; use crate::cst; use crate::diag::FrontendError; diff --git a/crates/wright-ostw/src/signature.rs b/crates/wright-ostw/src/signature.rs index 2ad923c..7747486 100644 --- a/crates/wright-ostw/src/signature.rs +++ b/crates/wright-ostw/src/signature.rs @@ -5,14 +5,14 @@ //! the exercised builtin surface, and the OSTW source member name -> canonical //! catalog member id mapping per exercised enum domain. All canonical //! Workshop parameter/spelling and enum domain/member data lives in the -//! canonical catalog (`workshop-rs`, consumed via `wright-workshop`); the +//! canonical catalog (`workshop-rs`); the //! semantic phase //! resolves builtins and enum domains through that catalog at the consume //! sites. No OSTW `Elements.json` or upstream compiler table is copied; //! every binding is exercised by the protect-ban reachable closure or a //! committed pinned-reference probe under `compatibility/ostw/probes/`. -use wright_workshop::catalog::Kind; +use workshop_rs::catalog::Kind; /// One exercised builtin binding: OSTW source name -> (kind, canonical catalog id). pub const BUILTIN_BINDINGS: &[(&str, (Kind, &str))] = &[ diff --git a/crates/wright-ostw/tests/differential.rs b/crates/wright-ostw/tests/differential.rs index de91226..9d37322 100644 --- a/crates/wright-ostw/tests/differential.rs +++ b/crates/wright-ostw/tests/differential.rs @@ -68,14 +68,14 @@ fn compile_semantic(root: &Path, main_rel: &str) -> wright_ostw::SemanticOutcome /// Parse Workshop text through the shared parser with the canonical /// signature context (the same path the driver uses). -fn parse(catalog: &wright_workshop::catalog::Catalog, text: &str) -> wir::Program { +fn parse(catalog: &workshop_rs::catalog::Catalog, text: &str) -> wir::Program { let manifest = wright_opy::manifest::Manifest::builtin().expect("the OPY manifest is embedded and valid"); let context = wright_core::signatures::ChainedExpectedDomain::new(manifest, catalog); - let program = wright_workshop::parser::parse_with_context( + let program = workshop_rs::parser::parse_with_context( text, catalog, - &wright_workshop::catalog::Locale::new("en-US"), + &workshop_rs::catalog::Locale::new("en-US"), &context, ) .unwrap_or_else(|error| panic!("reference/Wright text must parse: {error}")); @@ -98,7 +98,7 @@ fn fold(program: &mut wir::Program) { /// Team/Color collision needs it: the other shared spellings (e.g. /// `Visible To And String`) resolve through the catalog's expected-domain /// pins at their call positions. -fn qualify_ambiguous_members(catalog: &wright_workshop::catalog::Catalog, text: &str) -> String { +fn qualify_ambiguous_members(catalog: &workshop_rs::catalog::Catalog, text: &str) -> String { let mut out = text.to_string(); // workshop-rs 0.1.5 exposes Vector as a catalog enum domain. The pinned // reference's zero-vector spelling is otherwise ambiguous with @@ -117,7 +117,7 @@ fn qualify_ambiguous_members(catalog: &wright_workshop::catalog::Catalog, text: "Start Camera(Event Player, Vector(0, 0, 0), Left, 0)", "Start Camera(Event Player, Vector(0, 0, 0), Vector.Left, 0)", ); - let locale = wright_workshop::catalog::Locale::new("en-US"); + let locale = workshop_rs::catalog::Locale::new("en-US"); for domain in catalog.enum_domains() { if domain.domain != "Team" { continue; @@ -1198,7 +1198,7 @@ fn value_kind(value: &Value) -> &'static str { #[test] fn accepted_targets_compile_and_match_pinned_reference_semantics() { - let catalog = wright_workshop::catalog::Catalog::builtin().expect("catalog loads"); + let catalog = workshop_rs::catalog::Catalog::builtin().expect("catalog loads"); let mut report = serde_json::Map::new(); for target in TARGETS { let dir = workspace_root() @@ -1214,20 +1214,20 @@ fn accepted_targets_compile_and_match_pinned_reference_semantics() { let program = wright_ir::lower::lower(hir).expect("lowering succeeds"); program.validate().expect("lowered program validates"); - let emitted = wright_workshop::emitter::emit( + let emitted = workshop_rs::emitter::emit( &program, &catalog, - &wright_workshop::catalog::Locale::new("en-US"), + &workshop_rs::catalog::Locale::new("en-US"), ) .expect("emission succeeds"); // Declared round-trip contract: Wright-emitted Workshop reparses and // re-emits byte-identically (semantic fixed point). let reparsed = parse(&catalog, &emitted); - let reemitted = wright_workshop::emitter::emit( + let reemitted = workshop_rs::emitter::emit( &reparsed, &catalog, - &wright_workshop::catalog::Locale::new("en-US"), + &workshop_rs::catalog::Locale::new("en-US"), ) .expect("re-emission succeeds"); assert_eq!( diff --git a/crates/wright-ostw/tests/reconstruct.rs b/crates/wright-ostw/tests/reconstruct.rs index eaf610d..caee758 100644 --- a/crates/wright-ostw/tests/reconstruct.rs +++ b/crates/wright-ostw/tests/reconstruct.rs @@ -37,14 +37,14 @@ fn read(path: &Path) -> String { /// Parse Workshop text through the shared parser with the canonical /// signature context (the same path the driver uses). -fn parse(catalog: &wright_workshop::catalog::Catalog, text: &str) -> wir::Program { +fn parse(catalog: &workshop_rs::catalog::Catalog, text: &str) -> wir::Program { let manifest = wright_opy::manifest::Manifest::builtin().expect("the OPY manifest is embedded and valid"); let context = wright_core::signatures::ChainedExpectedDomain::new(manifest, catalog); - let program = wright_workshop::parser::parse_with_context( + let program = workshop_rs::parser::parse_with_context( text, catalog, - &wright_workshop::catalog::Locale::new("en-US"), + &workshop_rs::catalog::Locale::new("en-US"), &context, ) .unwrap_or_else(|error| panic!("fixture Workshop text must parse: {error}")); @@ -1043,7 +1043,7 @@ fn fixture_dir(name: &str) -> PathBuf { } /// The full loop for one positive fixture. -fn run_full_loop(catalog: &wright_workshop::catalog::Catalog, name: &str) -> serde_json::Value { +fn run_full_loop(catalog: &workshop_rs::catalog::Catalog, name: &str) -> serde_json::Value { let dir = fixture_dir(name); let fixture_text = read(&dir.join("workshop.txt")); @@ -1068,20 +1068,20 @@ fn run_full_loop(catalog: &wright_workshop::catalog::Catalog, name: &str) -> ser reconstructed.validate().expect("lowered program validates"); // WIR → Workshop text (the shared emitter). - let emitted = wright_workshop::emitter::emit( + let emitted = workshop_rs::emitter::emit( &reconstructed, catalog, - &wright_workshop::catalog::Locale::new("en-US"), + &workshop_rs::catalog::Locale::new("en-US"), ) .expect("reconstructed Workshop emission succeeds"); // The reconstructed Workshop text reparses and re-emits byte-identically // (round-trip fixed point). let reparsed = parse(catalog, &emitted); - let reemitted = wright_workshop::emitter::emit( + let reemitted = workshop_rs::emitter::emit( &reparsed, catalog, - &wright_workshop::catalog::Locale::new("en-US"), + &workshop_rs::catalog::Locale::new("en-US"), ) .expect("re-emission succeeds"); assert_eq!( @@ -1115,7 +1115,7 @@ fn run_full_loop(catalog: &wright_workshop::catalog::Catalog, name: &str) -> ser #[test] fn reconstruction_full_loop_holds_for_positive_fixtures() { - let catalog = wright_workshop::catalog::Catalog::builtin().expect("catalog loads"); + let catalog = workshop_rs::catalog::Catalog::builtin().expect("catalog loads"); let mut report = serde_json::Map::new(); for name in POSITIVE_FIXTURES { report.insert(name.to_string(), run_full_loop(&catalog, name)); @@ -1142,7 +1142,7 @@ fn reconstruction_full_loop_holds_for_positive_fixtures() { /// Build a minimal WIR program exercising every declared-surface construct, /// used by the per-construct emission assertions. -fn surface_program(catalog: &wright_workshop::catalog::Catalog) -> wir::Program { +fn surface_program(catalog: &workshop_rs::catalog::Catalog) -> wir::Program { let mut program = wir::Program::default(); let g = program.global_variables.push(wir::WorkshopVariable { name: "g".to_string(), @@ -1424,7 +1424,7 @@ fn surface_program(catalog: &wright_workshop::catalog::Catalog) -> wir::Program #[test] fn emission_covers_every_declared_construct() { - let catalog = wright_workshop::catalog::Catalog::builtin().expect("catalog loads"); + let catalog = workshop_rs::catalog::Catalog::builtin().expect("catalog loads"); let program = surface_program(&catalog); program.validate().expect("synthetic program validates"); let text = wright_ostw::reconstruct::reconstruct(&program, &catalog) @@ -1463,7 +1463,7 @@ fn emission_covers_every_declared_construct() { #[test] fn emission_is_byte_identical_across_runs() { - let catalog = wright_workshop::catalog::Catalog::builtin().expect("catalog loads"); + let catalog = workshop_rs::catalog::Catalog::builtin().expect("catalog loads"); let program = surface_program(&catalog); program.validate().expect("synthetic program validates"); let first = wright_ostw::reconstruct::reconstruct(&program, &catalog).expect("reconstructs"); @@ -1482,9 +1482,9 @@ fn emission_is_byte_identical_across_runs() { // -- structured rejections (synthetic WIR per declared boundary) ------------ -type ProgramBuilder = fn(&wright_workshop::catalog::Catalog) -> wir::Program; +type ProgramBuilder = fn(&workshop_rs::catalog::Catalog) -> wir::Program; -fn program_with_for_player_variable(_: &wright_workshop::catalog::Catalog) -> wir::Program { +fn program_with_for_player_variable(_: &workshop_rs::catalog::Catalog) -> wir::Program { let mut program = wir::Program::default(); let p = program.player_variables.push(wir::WorkshopVariable { name: "p".to_string(), @@ -1537,7 +1537,7 @@ fn program_with_for_player_variable(_: &wright_workshop::catalog::Catalog) -> wi program } -fn program_with_debug(_: &wright_workshop::catalog::Catalog) -> wir::Program { +fn program_with_debug(_: &workshop_rs::catalog::Catalog) -> wir::Program { let mut program = wir::Program::default(); let value = program.values.push(wir::ValueNode::new( wir::Value::Number { @@ -1561,7 +1561,7 @@ fn program_with_debug(_: &wright_workshop::catalog::Catalog) -> wir::Program { program } -fn program_with_print(_: &wright_workshop::catalog::Catalog) -> wir::Program { +fn program_with_print(_: &workshop_rs::catalog::Catalog) -> wir::Program { let mut program = wir::Program::default(); let message = program.values.push(wir::ValueNode::new( wir::Value::String("x".to_string()), @@ -1583,7 +1583,7 @@ fn program_with_print(_: &wright_workshop::catalog::Catalog) -> wir::Program { program } -fn program_with_settings(_: &wright_workshop::catalog::Catalog) -> wir::Program { +fn program_with_settings(_: &workshop_rs::catalog::Catalog) -> wir::Program { wir::Program { settings: Some(workshop_rs::settings::Settings { span: None, @@ -1593,7 +1593,7 @@ fn program_with_settings(_: &wright_workshop::catalog::Catalog) -> wir::Program } } -fn program_with_unbound_action(_: &wright_workshop::catalog::Catalog) -> wir::Program { +fn program_with_unbound_action(_: &workshop_rs::catalog::Catalog) -> wir::Program { let mut program = wir::Program::default(); let action = program.actions.push(wir::Action::Call { name: "createBeamEffect".to_string(), @@ -1612,7 +1612,7 @@ fn program_with_unbound_action(_: &wright_workshop::catalog::Catalog) -> wir::Pr program } -fn program_with_unbound_value(_: &wright_workshop::catalog::Catalog) -> wir::Program { +fn program_with_unbound_value(_: &workshop_rs::catalog::Catalog) -> wir::Program { let mut program = wir::Program::default(); let g = program.global_variables.push(wir::WorkshopVariable { name: "g".to_string(), @@ -1645,7 +1645,7 @@ fn program_with_unbound_value(_: &wright_workshop::catalog::Catalog) -> wir::Pro program } -fn program_with_unbound_enum(_: &wright_workshop::catalog::Catalog) -> wir::Program { +fn program_with_unbound_enum(_: &workshop_rs::catalog::Catalog) -> wir::Program { let mut program = wir::Program::default(); let g = program.global_variables.push(wir::WorkshopVariable { name: "g".to_string(), @@ -1678,7 +1678,7 @@ fn program_with_unbound_enum(_: &wright_workshop::catalog::Catalog) -> wir::Prog program } -fn program_with_raise_to_power(_: &wright_workshop::catalog::Catalog) -> wir::Program { +fn program_with_raise_to_power(_: &workshop_rs::catalog::Catalog) -> wir::Program { let mut program = wir::Program::default(); let g = program.global_variables.push(wir::WorkshopVariable { name: "g".to_string(), @@ -1712,7 +1712,7 @@ fn program_with_raise_to_power(_: &wright_workshop::catalog::Catalog) -> wir::Pr program } -fn program_with_remove_from_array(_: &wright_workshop::catalog::Catalog) -> wir::Program { +fn program_with_remove_from_array(_: &workshop_rs::catalog::Catalog) -> wir::Program { let mut program = wir::Program::default(); let g = program.global_variables.push(wir::WorkshopVariable { name: "g".to_string(), @@ -1746,7 +1746,7 @@ fn program_with_remove_from_array(_: &wright_workshop::catalog::Catalog) -> wir: program } -fn program_with_non_comparison_condition(_: &wright_workshop::catalog::Catalog) -> wir::Program { +fn program_with_non_comparison_condition(_: &workshop_rs::catalog::Catalog) -> wir::Program { let mut program = wir::Program::default(); let condition = program .values @@ -1763,7 +1763,7 @@ fn program_with_non_comparison_condition(_: &wright_workshop::catalog::Catalog) program } -fn program_with_partial_arity(_: &wright_workshop::catalog::Catalog) -> wir::Program { +fn program_with_partial_arity(_: &workshop_rs::catalog::Catalog) -> wir::Program { let mut program = wir::Program::default(); let event_player = program .values @@ -1785,7 +1785,7 @@ fn program_with_partial_arity(_: &wright_workshop::catalog::Catalog) -> wir::Pro program } -fn program_with_name_collision(_: &wright_workshop::catalog::Catalog) -> wir::Program { +fn program_with_name_collision(_: &workshop_rs::catalog::Catalog) -> wir::Program { let mut program = wir::Program::default(); program.global_variables.push(wir::WorkshopVariable { name: "g".to_string(), @@ -1802,7 +1802,7 @@ fn program_with_name_collision(_: &wright_workshop::catalog::Catalog) -> wir::Pr program } -fn program_with_empty_name(_: &wright_workshop::catalog::Catalog) -> wir::Program { +fn program_with_empty_name(_: &workshop_rs::catalog::Catalog) -> wir::Program { let mut program = wir::Program::default(); program.global_variables.push(wir::WorkshopVariable { name: String::new(), @@ -1813,7 +1813,7 @@ fn program_with_empty_name(_: &wright_workshop::catalog::Catalog) -> wir::Progra program } -fn program_with_bodiless_subroutine(_: &wright_workshop::catalog::Catalog) -> wir::Program { +fn program_with_bodiless_subroutine(_: &workshop_rs::catalog::Catalog) -> wir::Program { let mut program = wir::Program::default(); program.subroutines.push(wir::WorkshopSubroutine { name: "sub".to_string(), @@ -1824,7 +1824,7 @@ fn program_with_bodiless_subroutine(_: &wright_workshop::catalog::Catalog) -> wi program } -fn program_with_player_modify_receiver(_: &wright_workshop::catalog::Catalog) -> wir::Program { +fn program_with_player_modify_receiver(_: &workshop_rs::catalog::Catalog) -> wir::Program { let mut program = wir::Program::default(); let p = program.player_variables.push(wir::WorkshopVariable { name: "p".to_string(), @@ -1873,7 +1873,7 @@ fn program_with_player_modify_receiver(_: &wright_workshop::catalog::Catalog) -> program } -fn program_with_non_literal_format_text(_: &wright_workshop::catalog::Catalog) -> wir::Program { +fn program_with_non_literal_format_text(_: &workshop_rs::catalog::Catalog) -> wir::Program { let mut program = wir::Program::default(); let g = program.global_variables.push(wir::WorkshopVariable { name: "g".to_string(), @@ -1913,7 +1913,7 @@ fn program_with_non_literal_format_text(_: &wright_workshop::catalog::Catalog) - program } -fn program_with_strict_greater_in_format(_: &wright_workshop::catalog::Catalog) -> wir::Program { +fn program_with_strict_greater_in_format(_: &workshop_rs::catalog::Catalog) -> wir::Program { let mut program = wir::Program::default(); let g = program.global_variables.push(wir::WorkshopVariable { name: "g".to_string(), @@ -1974,7 +1974,7 @@ fn program_with_strict_greater_in_format(_: &wright_workshop::catalog::Catalog) program } -fn program_with_invalid_number(_: &wright_workshop::catalog::Catalog) -> wir::Program { +fn program_with_invalid_number(_: &workshop_rs::catalog::Catalog) -> wir::Program { let mut program = wir::Program::default(); let g = program.global_variables.push(wir::WorkshopVariable { name: "g".to_string(), @@ -2103,7 +2103,7 @@ fn rejection_cases() -> Vec<(&'static str, &'static str, ProgramBuilder)> { #[test] fn every_declared_rejection_is_structured_and_total() { - let catalog = wright_workshop::catalog::Catalog::builtin().expect("catalog loads"); + let catalog = workshop_rs::catalog::Catalog::builtin().expect("catalog loads"); for (kind, code, builder) in rejection_cases() { let program = builder(&catalog); program @@ -2132,7 +2132,7 @@ fn every_declared_rejection_is_structured_and_total() { #[test] fn rejection_never_produces_partial_output() { - let catalog = wright_workshop::catalog::Catalog::builtin().expect("catalog loads"); + let catalog = workshop_rs::catalog::Catalog::builtin().expect("catalog loads"); // The committed for-player-variable rejection fixture. let dir = fixture_dir("reject/for-player-variable"); let fixture = parse(&catalog, &read(&dir.join("workshop.txt"))); @@ -2283,7 +2283,7 @@ fn manifest() -> serde_json::Value { #[test] fn boundary_manifest_matches_classification_and_fixture_coverage() { - let catalog = wright_workshop::catalog::Catalog::builtin().expect("catalog loads"); + let catalog = workshop_rs::catalog::Catalog::builtin().expect("catalog loads"); let manifest = manifest(); // The rejected set in the manifest is exactly the tested rejection table. @@ -2464,32 +2464,32 @@ fn boundary_manifest_matches_classification_and_fixture_coverage() { #[test] fn reconstruct_api_exposes_the_reverse_binding_tables() { - let catalog = wright_workshop::catalog::Catalog::builtin().expect("catalog loads"); + let catalog = workshop_rs::catalog::Catalog::builtin().expect("catalog loads"); // Every bound id maps to a catalog entry of the right kind, and the OSTW // name resolves back through signature::builtin to the same id. for (id, source) in wright_ostw::reconstruct::bound_action_ids() { assert!( catalog - .entry(wright_workshop::catalog::Kind::Action, id) + .entry(workshop_rs::catalog::Kind::Action, id) .is_some(), "bound action id '{id}' must exist in the canonical catalog" ); assert_eq!( crate_signature_builtin(source), - Some((wright_workshop::catalog::Kind::Action, id)), + Some((workshop_rs::catalog::Kind::Action, id)), "OSTW action name '{source}' must resolve back to catalog id '{id}'" ); } for (id, source) in wright_ostw::reconstruct::bound_value_ids() { assert!( catalog - .entry(wright_workshop::catalog::Kind::Value, id) + .entry(workshop_rs::catalog::Kind::Value, id) .is_some(), "bound value id '{id}' must exist in the canonical catalog" ); assert_eq!( crate_signature_builtin(source), - Some((wright_workshop::catalog::Kind::Value, id)), + Some((workshop_rs::catalog::Kind::Value, id)), "OSTW value name '{source}' must resolve back to catalog id '{id}'" ); } @@ -2514,6 +2514,6 @@ fn reconstruct_api_exposes_the_reverse_binding_tables() { /// The same lookup the OSTW semantic phase uses (signature::builtin), kept /// local so the test asserts the round trip through the shipped frontend /// binding table without importing the private module path. -fn crate_signature_builtin(name: &str) -> Option<(wright_workshop::catalog::Kind, &'static str)> { +fn crate_signature_builtin(name: &str) -> Option<(workshop_rs::catalog::Kind, &'static str)> { wright_ostw::signature::builtin(name) } diff --git a/crates/wright-ostw/tests/semantic.rs b/crates/wright-ostw/tests/semantic.rs index 2e21e24..f67c9ad 100644 --- a/crates/wright-ostw/tests/semantic.rs +++ b/crates/wright-ostw/tests/semantic.rs @@ -203,8 +203,8 @@ fn every_ostw_binding_resolves_through_the_canonical_catalog() { // Catalog-ownership invariant (#118 AC): wright-ostw ships only OSTW // source-name bindings, and every binding resolves to real canonical // catalog data (kind/id for builtins; domain + member ids for enums). - let catalog = wright_workshop::catalog::Catalog::builtin().expect("catalog loads"); - let en = wright_workshop::catalog::Locale::new("en-US"); + let catalog = workshop_rs::catalog::Catalog::builtin().expect("catalog loads"); + let en = workshop_rs::catalog::Locale::new("en-US"); for (source, (kind, id)) in wright_ostw::signature::BUILTIN_BINDINGS { let entry = catalog diff --git a/crates/wright-workshop/Cargo.toml b/crates/wright-workshop/Cargo.toml deleted file mode 100644 index 33d2c49..0000000 --- a/crates/wright-workshop/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "wright-workshop" -version.workspace = true -edition.workspace = true -rust-version.workspace = true -license.workspace = true -description = "Cutover adapter for the workshop-rs canonical Workshop core (wright#143); re-exports only, no implementation." - -[lints] -workspace = true - -[dependencies] -# Single pinned reference: `[workspace.dependencies]` in the root Cargo.toml. -workshop-rs.workspace = true - -[dev-dependencies] -regex = "1" -serde_json = "1" -wright-core = { path = "../wright-core" } -wright-ir = { path = "../wright-ir" } -wright-opy = { path = "../wright-opy" } diff --git a/crates/wright-workshop/src/lib.rs b/crates/wright-workshop/src/lib.rs deleted file mode 100644 index fc285d0..0000000 --- a/crates/wright-workshop/src/lib.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! Wright's Workshop-language adapter: re-exports the canonical `workshop-rs` -//! core. -//! -//! This crate is a **cutover adapter** (wright#143, ADR-0009): the canonical -//! Workshop catalog, parser, emitter, detection, round-trip, validation, and -//! Workshop IR are owned by `workshop-rs`; this crate only re-exports that -//! surface so existing `wright_workshop::…` call sites keep resolving during -//! the v0.2 cutover. It contains **no independent semantic implementation**: -//! every item below is the `workshop-rs` item. -//! -//! Removal path: migrate call sites to `workshop_rs::…` directly (the -//! re-exported paths are the `workshop-rs` paths), then delete this crate. -//! Wright tooling (CLI, services, analyzer, driver) consumes -//! `workshop-rs` for Workshop semantics; do not reintroduce Workshop -//! implementation here. - -pub use workshop_rs::*; diff --git a/crates/wright-workshop/tests/catalog.rs b/crates/wright-workshop/tests/catalog.rs deleted file mode 100644 index 4104b22..0000000 --- a/crates/wright-workshop/tests/catalog.rs +++ /dev/null @@ -1,277 +0,0 @@ -//! Catalog tests (#29): canonical identities resolve localized spellings to -//! locale-independent ids and back, and catalog validation rejects -//! malformed or colliding data. - -use wright_workshop::catalog::{Catalog, Kind, Locale}; - -fn builtin() -> Catalog { - Catalog::builtin().expect("built-in catalog validates") -} - -fn en() -> Locale { - Locale::new("en-US") -} - -#[test] -fn builtin_catalog_loads_and_declares_locales() { - let catalog = builtin(); - assert!(catalog.supports(&en())); - // The workshop-rs catalog declares the two v0.2 conversion locales and - // exposes partial coverage for the secondary locale. - assert_eq!(catalog.locales().len(), 2); - assert_eq!(catalog.locales()[0], en()); - let zh = Locale::new("zh-CN"); - assert!(catalog.supports(&zh)); - let coverage = catalog.locale_coverage(&zh); - assert!( - coverage.mapped > 0, - "the secondary locale has mapped entries" - ); - assert!(coverage.mapped <= coverage.total); - assert!(coverage.total > 0, "the declared surface is non-empty"); -} - -#[test] -fn localized_spelling_resolves_to_canonical_id_and_back() { - let catalog = builtin(); - - // Action: "Disable Inspector Recording" -> disableInspector -> spelling. - let entry = catalog - .resolve(Kind::Action, &en(), "Disable Inspector Recording") - .expect("spelling resolves"); - assert_eq!(entry.id, "disableInspector"); - assert_eq!( - catalog.spelling(Kind::Action, &en(), "disableInspector"), - Some("Disable Inspector Recording") - ); - - // Value: multi-word "Count Of" -> countOf. - let entry = catalog - .resolve(Kind::Value, &en(), "Count Of") - .expect("count of resolves"); - assert_eq!(entry.id, "countOf"); - assert_eq!( - catalog.spelling(Kind::Value, &en(), "countOf"), - Some("Count Of") - ); - - // Structural: "For Global Variable" -> forGlobalVariable. - let entry = catalog - .resolve(Kind::Structural, &en(), "For Global Variable") - .expect("structural resolves"); - assert_eq!(entry.id, "forGlobalVariable"); -} - -#[test] -fn enums_resolve_members_to_canonical_identity() { - let catalog = builtin(); - assert_eq!( - catalog.resolve_enum_member("Beam", &en(), "Grapple Beam"), - Some(("Beam".to_string(), "GRAPPLE".to_string())) - ); - assert_eq!( - catalog.enum_spelling("Beam", &en(), "GRAPPLE"), - Some("Grapple Beam") - ); - assert_eq!( - catalog.resolve_enum_member("Color", &en(), "Yellow"), - Some(("Color".to_string(), "YELLOW".to_string())) - ); - assert_eq!( - catalog.resolve_enum_member("Wait", &en(), "Ignore Condition"), - Some(("Wait".to_string(), "IGNORE_CONDITION".to_string())) - ); -} - -#[test] -fn unknown_spellings_and_ids_do_not_resolve() { - let catalog = builtin(); - assert!( - catalog - .resolve(Kind::Action, &en(), "Totally Unknown Thing") - .is_none() - ); - assert!(catalog.entry(Kind::Value, "noSuchId").is_none()); - assert!( - catalog - .resolve_enum_member("Beam", &en(), "Purple Beam") - .is_none() - ); -} - -#[test] -fn locale_normalization_is_case_insensitive() { - let catalog = builtin(); - let en_upper = Locale::new("EN-US"); - assert_eq!(en_upper, en()); - assert!(catalog.supports(&en_upper)); - assert_eq!( - catalog.spelling(Kind::Action, &en_upper, "disableInspector"), - Some("Disable Inspector Recording") - ); -} - -#[test] -fn duplicate_aliases_fail_validation() { - let bad = r#"{ - "schemaVersion": 1, - "locales": ["en-US"], - "target": { "game": "g", "format": "f", "surface": "s" }, - "provenance": { "generator": "g", "generatorVersion": "0", "source": "s", "license": "l", "reviewed": true }, - "structural": [ - { "id": "if", "aliases": { "en-US": "If" } }, - { "id": "elseIf", "aliases": { "en-US": "If" } } - ] - }"#; - let error = Catalog::load(bad).expect_err("colliding aliases must fail"); - assert!(error.to_string().contains("duplicate")); -} - -#[test] -fn missing_locale_alias_fails_validation() { - let bad = r#"{ - "schemaVersion": 1, - "locales": ["en-US"], - "target": { "game": "g", "format": "f", "surface": "s" }, - "provenance": { "generator": "g", "generatorVersion": "0", "source": "s", "license": "l", "reviewed": true }, - "structural": [ - { "id": "if", "aliases": { "en-US": "If" } } - ], - "actions": [ - { "id": "wait", "aliases": {} } - ] - }"#; - let error = Catalog::load(bad).expect_err("missing alias must fail"); - assert!(error.to_string().contains("missing")); -} - -#[test] -fn undeclared_locale_fails_validation() { - let bad = r#"{ - "schemaVersion": 1, - "locales": ["en-US"], - "target": { "game": "g", "format": "f", "surface": "s" }, - "provenance": { "generator": "g", "generatorVersion": "0", "source": "s", "license": "l", "reviewed": true }, - "structural": [ - { "id": "if", "aliases": { "en-US": "If", "zh-CN": "如果" } } - ] - }"#; - let error = Catalog::load(bad).expect_err("undeclared locale must fail"); - assert!(error.to_string().contains("undeclared locale")); -} - -#[test] -fn ostw_exercised_builtin_surface_resolves_with_canonical_params_and_spellings() { - // The #118 OSTW frontend resolves Workshop builtins through the catalog: - // canonical param order (named-arg binding, probes P6/P6b) and en-US - // spellings are catalog-owned. - let catalog = builtin(); - - // Action with a full canonical param list. - let effect = catalog - .entry(Kind::Action, "createEffect") - .expect("createEffect is in the catalog"); - assert_eq!( - effect.params, - vec![ - "VisibleTo", - "Type", - "Color", - "Position", - "Radius", - "Reevaluation" - ] - ); - assert_eq!( - catalog.spelling(Kind::Action, &en(), "createEffect"), - Some("Create Effect") - ); - - // Value with no params. - let event_player = catalog - .entry(Kind::Value, "eventPlayer") - .expect("eventPlayer is in the catalog"); - assert!(event_player.params.is_empty()); - assert_eq!( - catalog.spelling(Kind::Value, &en(), "eventPlayer"), - Some("Event Player") - ); - - // A shared canonical identity: OSTW `Wait`/`MinWait` both bind to `wait`. - assert_eq!( - catalog - .entry(Kind::Action, "wait") - .map(|e| e.params.clone()), - Some(vec!["Duration".to_string(), "WaitBehavior".to_string()]) - ); - - // The exercised param surface resolves by en-US spelling too. - assert!( - catalog - .resolve( - Kind::Action, - &en(), - "Disable Movement Collision With Environment" - ) - .is_some() - ); - assert!( - catalog - .resolve(Kind::Value, &en(), "Workshop Setting Combo") - .is_some() - ); -} - -#[test] -fn ostw_exercised_enum_domains_resolve_members_to_canonical_identity() { - let catalog = builtin(); - - // Hero members resolve with their canonical ids and en-US spellings. - assert_eq!( - catalog.resolve_enum_member("Hero", &en(), "D.Va"), - Some(("Hero".to_string(), "DVA".to_string())) - ); - assert_eq!( - catalog.enum_spelling("Hero", &en(), "WRECKING_BALL"), - Some("Wrecking Ball") - ); - - // Button, Team, Color, and the reevaluation domains exercised by the - // protect-ban closure. - assert_eq!( - catalog.resolve_enum_member("Button", &en(), "Ability 2"), - Some(("Button".to_string(), "ABILITY_2".to_string())) - ); - assert_eq!( - catalog.resolve_enum_member("Team", &en(), "Team 1"), - Some(("Team".to_string(), "TEAM_1".to_string())) - ); - assert_eq!( - catalog.resolve_enum_member("Color", &en(), "Sky Blue"), - Some(("Color".to_string(), "SKY_BLUE".to_string())) - ); - assert_eq!( - catalog.resolve_enum_member("EffectReeval", &en(), "Visible To Position and Radius"), - Some(( - "EffectReeval".to_string(), - "VISIBLE_TO_POSITION_AND_RADIUS".to_string() - )) - ); - assert_eq!( - catalog.resolve_enum_member( - "InworldTextReeval", - &en(), - "Visible To Position String and Color" - ), - Some(( - "InworldTextReeval".to_string(), - "VISIBLE_TO_POSITION_STRING_AND_COLOR".to_string() - )) - ); - - // Map members resolve (exercised by the protect-ban MapData surface). - assert_eq!( - catalog.resolve_enum_member("Map", &en(), "Watchpoint: Gibraltar"), - Some(("Map".to_string(), "WATCHPOINT_GIBRALTAR".to_string())) - ); -} diff --git a/crates/wright-workshop/tests/closure.rs b/crates/wright-workshop/tests/closure.rs deleted file mode 100644 index c6318a9..0000000 --- a/crates/wright-workshop/tests/closure.rs +++ /dev/null @@ -1,76 +0,0 @@ -//! Matrix-surface emission closure scan (#87 AC-17). -//! -//! For every construct family on the matrix-listed supported -//! surface, the native emitter's spelling must self-round-trip: -//! parse(emit(x)) reparses and re-emits byte-identically. Each case -//! below is a real `wright compile --profile compat` artifact of a -//! minimal supported-surface program; the scan asserts the emitted -//! spelling is a byte-identical fixed point of the ws parser/ -//! emitter. Oracle-canonical spellings pinned by repros are -//! asserted in the emitter tests (AC-15/16). - -use wright_workshop::catalog::{Catalog, Locale}; -use wright_workshop::emitter; -use wright_workshop::parser; - -fn catalog() -> Catalog { - Catalog::builtin().unwrap() -} - -fn en() -> Locale { - Locale::new("en-US") -} - -/// Parse an emitted artifact, re-emit, and assert byte-identity. -fn assert_closure(label: &str, artifact: &str) { - let program = parser::parse(artifact, &catalog(), &en()) - .unwrap_or_else(|error| panic!("{label} must reparse: {error}")); - let reemitted = emitter::emit(&program, &catalog(), &en()) - .unwrap_or_else(|error| panic!("{label} must re-emit: {error}")); - assert_eq!( - artifact, reemitted, - "{label} emitted spelling must be a byte-identical fixed point" - ); -} - -const ASSIGN_AUG: &str = "variables {\n global:\n 0: g\n}\n\nrule (\"r\") {\n event {\n Ongoing - Global;\n }\n actions {\n Set Global Variable(g, 1);\n Modify Global Variable(g, Add, 2);\n Modify Global Variable(g, Subtract, 1);\n Modify Global Variable(g, Multiply, 2);\n }\n}\n\n"; -const CALLS: &str = "variables {\n global:\n 0: g\n}\n\nrule (\"r\") {\n event {\n Ongoing - Global;\n }\n actions {\n Set Global Variable(g, Count Of(Array(1, 2)));\n Set Global Variable(g, 3);\n Set Global Variable(g, 2);\n Wait(1, Ignore Condition);\n }\n}\n\n"; -const CONDITIONS: &str = "variables {\n global:\n 0: x\n player:\n 0: p\n}\n\nrule (\"r\") {\n event {\n Ongoing - Each Player;\n All;\n All;\n }\n conditions {\n (Event Player).p > 1;\n Global.x == 0;\n }\n actions {\n Disable Inspector Recording;\n }\n}\n\n"; -const DECL_INDEX: &str = "variables {\n global:\n 3: x\n player:\n 1: y\n}\n\nrule (\"r\") {\n event {\n Ongoing - Global;\n }\n actions {\n Disable Inspector Recording;\n }\n}\n\n"; -const DECL_NUMS: &str = "variables {\n global:\n 0: j\n 1: h\n 2: k\n player:\n 0: p\n}\n\nrule (\"Initialize global variables\") {\n event {\n Ongoing - Global;\n }\n actions {\n Set Global Variable(j, 5);\n Set Global Variable(k, 0.0);\n }\n}\n\nrule (\"Initialize player variables\") {\n event {\n Ongoing - Each Player;\n All;\n All;\n }\n actions {\n Set Player Variable(Event Player, p, 7);\n }\n}\n\nrule (\"r\") {\n event {\n Ongoing - Global;\n }\n actions {\n Disable Inspector Recording;\n }\n}\n\n"; -const EXPR_ESCAPES: &str = "variables {\n global:\n 0: g\n}\n\nrule (\"r\") {\n event {\n Ongoing - Global;\n }\n actions {\n Set Global Variable(g, Custom String(\"a\\nb\tc\\\"d\"));\n }\n}\n\n"; -const EXPR_LITERALS: &str = "variables {\n global:\n 0: g\n}\n\nrule (\"r\") {\n event {\n Ongoing - Global;\n }\n actions {\n Set Global Variable(g, Array(1, 2, 3));\n Set Global Variable(g, Array(Custom String(\"a\"), Custom String(\"b\")));\n Set Global Variable(g, Vector(1.5, -2, 3));\n Set Global Variable(g, True);\n Set Global Variable(g, 0.5);\n }\n}\n\n"; -const EXPR_LONG: &str = "variables {\n global:\n 0: g\n}\n\nrule (\"r\") {\n event {\n Ongoing - Global;\n }\n actions {\n Set Global Variable(g, Custom String(\"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB{0}\", Custom String(\"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB{0}\", Custom String(\"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\"))));\n }\n}\n\n"; -const FORMAT_FOLD: &str = "variables {\n global:\n 0: z\n}\n\nrule (\"r\") {\n event {\n Ongoing - Global;\n }\n actions {\n Set Global Variable(z, Custom String(\"v: 3\"));\n }\n}\n\n"; -const FORMAT_PARTIAL: &str = "variables {\n global:\n 0: x\n 1: z\n}\n\nrule (\"r\") {\n event {\n Ongoing - Global;\n }\n actions {\n Set Global Variable(z, Custom String(\"3 {0}\", Global.x));\n }\n}\n\n"; -const FORMAT_VAR: &str = "variables {\n global:\n 0: x\n 1: z\n}\n\nrule (\"r\") {\n event {\n Ongoing - Global;\n }\n actions {\n Set Global Variable(z, Custom String(\"v: {0}\", Global.x));\n }\n}\n\n"; -const IF_ELSE: &str = "variables {\n global:\n 0: x\n}\n\nrule (\"r\") {\n event {\n Ongoing - Global;\n }\n actions {\n If(Compare(Global.x, ==, 1));\n Disable Inspector Recording;\n Else If(Compare(Global.x, ==, 2));\n Disable Inspector Recording;\n Else;\n Disable Inspector Recording;\n }\n}\n\n"; -const IF_FINAL: &str = "variables {\n global:\n 0: x\n}\n\nrule (\"r\") {\n event {\n Ongoing - Global;\n }\n actions {\n If(Compare(Global.x, ==, 1));\n Disable Inspector Recording;\n }\n}\n\n"; -const LOOPS: &str = "variables {\n global:\n 0: i\n 1: g\n}\n\nrule (\"r\") {\n event {\n Ongoing - Global;\n }\n actions {\n For Global Variable(i, 0, 3, 1);\n Set Global Variable(g, Global.i);\n End;\n While(Compare(Global.g, <, 3));\n Modify Global Variable(g, Add, 1);\n Wait(1, Ignore Condition);\n End;\n }\n}\n\n"; -const MACRO: &str = "variables {\n global:\n 0: g\n}\n\nrule (\"r\") {\n event {\n Ongoing - Global;\n }\n actions {\n Set Global Variable(g, 6);\n }\n}\n\n"; -const PLAYERVAR_READ: &str = "variables {\n global:\n 0: g\n player:\n 0: p\n 1: q\n}\n\nrule (\"r\") {\n event {\n Ongoing - Each Player;\n All;\n All;\n }\n actions {\n Set Global Variable(g, (Event Player).p);\n If(Compare(Add((Event Player).p, (Event Player).q), >, 1));\n Disable Inspector Recording;\n }\n}\n\n"; -const SUBROUTINE: &str = "subroutines {\n 0: foo\n 1: bar\n}\n\nrule (\"Subroutine bar\") {\n event {\n Subroutine;\n bar;\n }\n actions {\n Disable Inspector Recording;\n }\n}\n\nrule (\"r\") {\n event {\n Ongoing - Global;\n }\n actions {\n Call Subroutine(foo);\n }\n}\n\n"; - -const PVMOD: &str = "variables {\n player:\n 0: p\n}\n\nrule (\"r\") {\n event {\n Ongoing - Each Player;\n All;\n All;\n }\n actions {\n Modify Player Variable(Event Player, p, Add, 2);\n Modify Player Variable(Event Player, p, Subtract, 1);\n Modify Player Variable(Event Player, p, Multiply, 3);\n Modify Player Variable(Event Player, p, Divide, 2);\n Modify Player Variable(Event Player, p, Modulo, 5);\n }\n}\n\n"; - -#[test] -fn emitted_spellings_self_round_trip_across_the_matrix_surface() { - assert_closure("assign_aug", ASSIGN_AUG); - assert_closure("calls", CALLS); - assert_closure("conditions", CONDITIONS); - assert_closure("pvmod", PVMOD); - assert_closure("decl_index", DECL_INDEX); - assert_closure("decl_nums", DECL_NUMS); - assert_closure("expr_escapes", EXPR_ESCAPES); - assert_closure("expr_literals", EXPR_LITERALS); - assert_closure("expr_long", EXPR_LONG); - assert_closure("format_fold", FORMAT_FOLD); - assert_closure("format_partial", FORMAT_PARTIAL); - assert_closure("format_var", FORMAT_VAR); - assert_closure("if_else", IF_ELSE); - assert_closure("if_final", IF_FINAL); - assert_closure("loops", LOOPS); - assert_closure("macro", MACRO); - assert_closure("playervar_read", PLAYERVAR_READ); - assert_closure("subroutine", SUBROUTINE); -} diff --git a/crates/wright-workshop/tests/detect.rs b/crates/wright-workshop/tests/detect.rs deleted file mode 100644 index 6bde5ff..0000000 --- a/crates/wright-workshop/tests/detect.rs +++ /dev/null @@ -1,98 +0,0 @@ -//! Language detection and locale-override tests (#33): representative -//! supported-language fixtures are detected with confidence, ambiguous input -//! fails explicitly, and an explicit locale override always wins. - -use std::path::{Path, PathBuf}; - -use wright_workshop::catalog::{Catalog, Locale}; -use wright_workshop::detect::{self, MIN_MATCHES}; -use wright_workshop::parser; - -fn oracle_path(fixture_id: &str) -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../compatibility/fixtures") - .join(fixture_id) - .join("oracle.json") -} - -fn corpus_text(fixture_id: &str) -> String { - let oracle = serde_json::from_str::( - &std::fs::read_to_string(oracle_path(fixture_id)).unwrap(), - ) - .unwrap(); - oracle["compile"]["workshop"].as_str().unwrap().to_string() -} - -fn catalog() -> Catalog { - Catalog::builtin().unwrap() -} - -#[test] -fn supported_language_fixtures_are_detected_confidently() { - for fixture_id in [ - "synthetic/basic-rule", - "synthetic/control-flow", - "synthetic/declarations-rules", - "synthetic/expressions-values", - "synthetic/preprocessing", - "real-world/overpy-cake", - ] { - let text = corpus_text(fixture_id); - let detection = detect::detect(&text, &catalog()); - assert_eq!(detection.locale, Locale::new("en-US")); - assert!( - detection.matches >= MIN_MATCHES, - "{fixture_id} must have enough evidence: {detection:?}" - ); - assert!( - detection.confidence > 0.5, - "{fixture_id} confidence: {detection:?}" - ); - } -} - -#[test] -fn resolve_locale_auto_detects_supported_input() { - let text = corpus_text("synthetic/basic-rule"); - let locale = detect::resolve_locale(&text, &catalog(), None).expect("detected"); - assert_eq!(locale, Locale::new("en-US")); -} - -#[test] -fn explicit_locale_override_bypasses_detection() { - // The explicit locale wins even for input that would not auto-detect - // confidently (garbage), because override skips detection. - let garbage = "not workshop at all"; - let error = detect::resolve_locale(garbage, &catalog(), None).expect_err("no detection"); - assert!(error.to_string().contains("language"), "{error}"); - let locale = detect::resolve_locale(garbage, &catalog(), Some(&Locale::new("en-US"))) - .expect("override wins"); - assert_eq!(locale, Locale::new("en-US")); -} - -#[test] -fn insufficient_evidence_fails_explicitly() { - let garbage = "hello world this is not workshop syntax at all"; - let error = detect::resolve_locale(garbage, &catalog(), None).expect_err("ambiguous"); - assert!( - error.to_string().contains("language") || error.to_string().contains("insufficient"), - "{error}" - ); -} - -#[test] -fn detection_is_deterministic() { - let text = corpus_text("real-world/overpy-cake"); - let first = detect::detect(&text, &catalog()); - let second = detect::detect(&text, &catalog()); - assert_eq!(first, second); -} - -#[test] -fn detected_locale_parses_the_input() { - // The full loop: detect, then parse with the detected locale. - let text = corpus_text("synthetic/control-flow"); - let locale = detect::resolve_locale(&text, &catalog(), None).expect("detected"); - let program = parser::parse(&text, &catalog(), &locale).expect("parses with detected locale"); - assert!(!program.rules.is_empty()); -} diff --git a/crates/wright-workshop/tests/emitter.rs b/crates/wright-workshop/tests/emitter.rs deleted file mode 100644 index c766027..0000000 --- a/crates/wright-workshop/tests/emitter.rs +++ /dev/null @@ -1,1006 +0,0 @@ -//! Emitter tests (#34): byte-stable localized emission, round-trip -//! equivalence, and structured failure for unsupported/unknown output. - -use std::path::{Path, PathBuf}; - -use wright_workshop::catalog::{Catalog, Locale}; -use wright_workshop::emitter; -use wright_workshop::parser; -use wright_workshop::wir; - -fn oracle_path(fixture_id: &str) -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../compatibility/fixtures") - .join(fixture_id) - .join("oracle.json") -} - -fn corpus_text(fixture_id: &str) -> String { - let oracle = serde_json::from_str::( - &std::fs::read_to_string(oracle_path(fixture_id)).unwrap(), - ) - .unwrap(); - oracle["compile"]["workshop"].as_str().unwrap().to_string() -} - -fn catalog() -> Catalog { - Catalog::builtin().unwrap() -} - -fn en() -> Locale { - Locale::new("en-US") -} - -/// Remove span suffixes so dumps can be compared modulo source locations. -fn without_spans(dump: &str) -> String { - let re = regex::Regex::new(r" @\d+:\d+:\d+-\d+:\d+").unwrap(); - re.replace_all(dump, "").into_owned() -} - -#[test] -fn emission_is_byte_stable_and_a_fixed_point() { - let program = parser::parse(&corpus_text("synthetic/control-flow"), &catalog(), &en()).unwrap(); - let first = emitter::emit(&program, &catalog(), &en()).expect("emits"); - let second = emitter::emit(&program, &catalog(), &en()).expect("emits"); - assert_eq!(first, second, "emission must be byte-stable"); - - // The emitted text is a fixed point: re-emitting the reparsed program - // produces identical text. - let reparsed = parser::parse(&first, &catalog(), &en()).expect("emitted text reparses"); - let reemitted = emitter::emit(&reparsed, &catalog(), &en()).expect("re-emits"); - assert_eq!(first, reemitted, "emission must be a fixed point"); -} - -#[test] -fn every_corpus_program_round_trips_to_equivalent_wir() { - // Corpus text parses against the catalog context (expected enum domains - // come from the canonical catalog signatures). `real-world/overpy-cake` - // is the documented exception: its bare `Up` (OverPy folds the vector-up - // constant inside `Add(...)`) is genuinely ambiguous between the `Vector` - // and `Rounding` enum domains and no enclosing signature pins it, so the - // parser rejects it deterministically (#111). - let documented_ambiguities = [("real-world/overpy-cake", "ambiguous enum member 'Up'")]; - for fixture_id in [ - "synthetic/basic-rule", - "synthetic/control-flow", - "synthetic/declarations-rules", - "synthetic/expressions-values", - "synthetic/preprocessing", - "synthetic/receiver-calls", - "real-world/overpy-cake", - ] { - let catalog = catalog(); - let program = - match parser::parse_with_context(&corpus_text(fixture_id), &catalog, &en(), &catalog) { - Ok(program) => program, - Err(error) => { - let Some((_, message)) = documented_ambiguities - .iter() - .find(|(id, _)| **id == *fixture_id) - else { - panic!("{fixture_id} must parse: {error}"); - }; - assert!( - error.to_string().contains(message), - "{fixture_id} fails only with the documented ambiguity, got: {error}" - ); - continue; - } - }; - let emitted = emitter::emit(&program, &catalog, &en()) - .unwrap_or_else(|error| panic!("{fixture_id} must emit: {error}")); - let reparsed = parser::parse_with_context(&emitted, &catalog, &en(), &catalog) - .unwrap_or_else(|error| { - panic!("{fixture_id} emitted text must reparse:\n{error}\n{emitted}") - }); - let original = without_spans(&program.dump()); - let round_tripped = without_spans(&reparsed.dump()); - assert_eq!( - original, round_tripped, - "{fixture_id} round trip must preserve semantics" - ); - } -} - -#[test] -fn emitted_text_is_recognizably_workshop() { - let program = parser::parse(&corpus_text("synthetic/basic-rule"), &catalog(), &en()).unwrap(); - let emitted = emitter::emit(&program, &catalog(), &en()).unwrap(); - assert!(emitted.contains("rule (\"setup\") {")); - assert!(emitted.contains("event {")); - assert!(emitted.contains("Ongoing - Global;")); - assert!(emitted.contains("actions {")); - assert!(emitted.contains("Disable Inspector Recording;")); -} - -#[test] -fn emitted_condition_matches_reference_infix_form() { - let catalog = catalog(); - let program = parser::parse_with_context( - &corpus_text("synthetic/declarations-rules"), - &catalog, - &en(), - &catalog, - ) - .unwrap(); - let emitted = emitter::emit(&program, &catalog, &en()).unwrap(); - assert!( - emitted.contains("Has Spawned(Event Player) == True"), - "non-comparison conditions emit in reference infix form:\n{emitted}" - ); -} - -#[test] -fn debug_actions_emit_hud_text() { - // Since the transform pipeline landed (#51/#52), Debug/Print emit a - // semantically equivalent Create HUD Text effect (documented intentional - // difference from the reference's type-aware formatting). - let mut program = wir::Program::default(); - let file = program - .files - .push(wright_workshop::source::SourceFile::new("workshop.txt")); - let value = program.values.push(wright_workshop::wir::ValueNode::new( - wright_workshop::wir::Value::Number { - value: 1.0, - text: "1".to_string(), - }, - None, - )); - let debug = program - .actions - .push(wir::Action::Debug { value, span: None }); - program.rules.push(wir::Rule { - name: "x".into(), - span: None, - name_span: None, - disabled: false, - event: wir::Event::Global, - conditions: vec![], - actions: vec![debug], - }); - let _ = file; - let emitted = emitter::emit(&program, &catalog(), &en()).expect("Debug emits"); - assert!( - emitted.contains("Create HUD Text(All Players(All Teams), Null, Custom String(\"{0}\", 1)"), - "debug emits the value as HUD text:\n{emitted}" - ); - // The emitted text reparses to a createHudText action call. - let catalog = catalog(); - let reparsed = - wright_workshop::parser::parse_with_context(&emitted, &catalog, &en(), &catalog).unwrap(); - assert_eq!(reparsed.rules.len(), 1); -} - -#[test] -fn unknown_value_id_fails_explicitly() { - let mut program = wir::Program::default(); - program - .files - .push(wright_workshop::source::SourceFile::new("workshop.txt")); - let value = program.values.push(wright_workshop::wir::ValueNode::new( - wright_workshop::wir::Value::Call { - name: "notACatalogId".into(), - args: vec![], - }, - None, - )); - let call = program.actions.push(wir::Action::Call { - name: "wait".into(), - args: vec![value], - span: None, - }); - program.rules.push(wir::Rule { - name: "x".into(), - span: None, - name_span: None, - disabled: false, - event: wir::Event::Global, - conditions: vec![], - actions: vec![call], - }); - let error = emitter::emit(&program, &catalog(), &en()).expect_err("unknown id must fail"); - assert!(error.to_string().contains("notACatalogId"), "{error}"); -} - -use wright_workshop::settings::{Settings, SettingsListElement, SettingsNode}; - -fn settings(children: Vec) -> Settings { - Settings { - span: None, - children, - } -} - -fn group(name: &str, children: Vec) -> SettingsNode { - SettingsNode::Group { - name: name.to_string(), - children, - span: None, - } -} - -fn string(name: &str, value: &str) -> SettingsNode { - SettingsNode::String { - name: name.to_string(), - value: value.to_string(), - span: None, - } -} - -fn number(name: &str, value: f64) -> SettingsNode { - SettingsNode::Number { - name: name.to_string(), - value, - span: None, - } -} - -fn boolean(name: &str, value: bool) -> SettingsNode { - SettingsNode::Bool { - name: name.to_string(), - value, - span: None, - } -} - -fn list(name: &str, elements: &[&str]) -> SettingsNode { - SettingsNode::List { - name: name.to_string(), - elements: elements - .iter() - .map(|value| SettingsListElement { - value: value.to_string(), - span: None, - }) - .collect(), - span: None, - } -} - -/// The pixelart settings tree (source order: assault, control, escort, -/// hybrid, skirmish). -fn pixelart_settings() -> Settings { - let mode = |name: &str| { - group( - name, - vec![ - list("enabledMaps", &[]), - string("roleLimit", "2OfEachRolePerTeam"), - ], - ) - }; - settings(vec![group( - "gamemodes", - vec![ - mode("assault"), - mode("control"), - mode("escort"), - mode("hybrid"), - group("skirmish", vec![list("enabledMaps", &["workshopIsland"])]), - ], - )]) -} - -/// The santa settings tree (source order). -fn santa_settings() -> Settings { - settings(vec![ - group("lobby", vec![number("ffaSlots", 6.0)]), - group( - "gamemodes", - vec![ - group("ffa", vec![list("enabledMaps", &["kingsRowWinter"])]), - group( - "general", - vec![ - boolean("enableHeroSwitching", false), - string("heroLimit", "off"), - boolean("enableRandomHeroes", true), - number("respawnTime%", 30.0), - ], - ), - ], - ), - group( - "heroes", - vec![group( - "allTeams", - vec![ - group( - "mei", - vec![ - boolean("enablePrimaryFire", false), - boolean("enableSecondaryFire", false), - boolean("enableAbility1", false), - number("health%", 266.4), - boolean("enableAbility2", false), - number("passiveUltGen%", 0.0), - number("combatUltGen%", 0.0), - ], - ), - list("enabledHeroes", &["mei"]), - ], - )], - ), - ]) -} - -fn program_with_settings(settings: Settings) -> wir::Program { - wir::Program { - settings: Some(settings), - ..wir::Program::default() - } -} - -/// The `settings` section of a Workshop text (the text starts with it). -fn settings_section(text: &str) -> String { - let start = text.find("settings").expect("text has a settings section"); - let mut depth = 0usize; - let mut end = start; - for (index, ch) in text[start..].char_indices() { - match ch { - '{' => depth += 1, - '}' => { - depth -= 1; - if depth == 0 { - end = start + index + 1; - break; - } - } - _ => {} - } - } - text[start..end].to_string() -} - -/// Collapse all whitespace for structural equality (string contents are -/// compared collapsed too; both sides render the same values). -fn collapse(text: &str) -> String { - text.chars().filter(|c| !c.is_whitespace()).collect() -} - -#[test] -fn settings_emission_matches_oracle_for_pixelart() { - let program = program_with_settings(pixelart_settings()); - let emitted = emitter::emit(&program, &catalog(), &en()).expect("emits"); - let oracle = corpus_text("real-world/overpy-pixelart"); - assert_eq!( - collapse(&settings_section(&emitted)), - collapse(&settings_section(&oracle)), - "emitted settings section must match the oracle region (whitespace-collapsed)" - ); -} - -#[test] -fn settings_emission_matches_oracle_for_santa() { - let program = program_with_settings(santa_settings()); - let emitted = emitter::emit(&program, &catalog(), &en()).expect("emits"); - let oracle = corpus_text("real-world/overpy-santa"); - assert_eq!( - collapse(&settings_section(&emitted)), - collapse(&settings_section(&oracle)), - "emitted settings section must match the oracle region (whitespace-collapsed)" - ); -} - -#[test] -fn settings_emission_is_deterministic() { - let program = program_with_settings(santa_settings()); - let first = emitter::emit(&program, &catalog(), &en()).expect("emits"); - let second = emitter::emit(&program, &catalog(), &en()).expect("emits"); - assert_eq!(first, second); -} - -#[test] -fn settings_free_program_emits_no_settings_section() { - let mut program = wir::Program::default(); - let rule = program.rules.push(wir::Rule { - name: "x".into(), - span: None, - name_span: None, - disabled: false, - event: wir::Event::Global, - conditions: vec![], - actions: vec![], - }); - let _ = rule; - let emitted = emitter::emit(&program, &catalog(), &en()).expect("emits"); - assert!( - !emitted.contains("settings"), - "settings-free programs emit no settings section:\n{emitted}" - ); -} - -#[test] -fn settings_emission_reparses_with_the_workshop_parser() { - let program = program_with_settings(pixelart_settings()); - let emitted = emitter::emit(&program, &catalog(), &en()).expect("emits"); - assert!(emitted.starts_with("settings {")); - let reparsed = - parser::parse(&emitted, &catalog(), &en()).expect("settings-bearing text parses"); - assert!(reparsed.settings.is_some()); -} - -#[test] -fn enabled_false_prefixes_the_mode_header() { - let program = program_with_settings(settings(vec![group( - "gamemodes", - vec![group( - "assault", - vec![ - boolean("enabled", false), - boolean("enableCompetitiveRules", true), - ], - )], - )])); - let emitted = emitter::emit(&program, &catalog(), &en()).expect("emits"); - let section = settings_section(&emitted); - assert!(collapse(§ion).contains("disabledAssault{CompetitiveRules:On}")); -} - -#[test] -fn empty_list_emits_empty_braces_block() { - let program = program_with_settings(settings(vec![group( - "gamemodes", - vec![group("skirmish", vec![list("enabledMaps", &[])])], - )])); - let emitted = emitter::emit(&program, &catalog(), &en()).expect("emits"); - let section = settings_section(&emitted); - assert!( - collapse(§ion).contains("Skirmish{enabledmaps{}}"), - "empty lists emit an empty braces block:\n{section}" - ); -} - -#[test] -fn settings_section_precedes_variables() { - let mut program = program_with_settings(santa_settings()); - program.global_variables.push(wir::WorkshopVariable { - name: "x".into(), - index: 0, - span: None, - name_span: None, - }); - let emitted = emitter::emit(&program, &catalog(), &en()).expect("emits"); - let settings_pos = emitted.find("settings").unwrap(); - let variables_pos = emitted.find("variables {").unwrap(); - assert!(settings_pos < variables_pos, "settings precedes variables"); -} - -#[test] -fn percent_keys_append_the_suffix() { - let program = program_with_settings(settings(vec![group( - "gamemodes", - vec![group("general", vec![number("respawnTime%", 30.0)])], - )])); - let emitted = emitter::emit(&program, &catalog(), &en()).expect("emits"); - assert!(collapse(&settings_section(&emitted)).contains("RespawnTimeScalar:30%")); -} - -#[test] -fn string_values_are_escaped() { - let program = program_with_settings(settings(vec![group( - "main", - vec![string("description", "a \"quoted\" line")], - )])); - let emitted = emitter::emit(&program, &catalog(), &en()).expect("emits"); - assert!( - collapse(&settings_section(&emitted)).contains("Description:\"a\\\"quoted\\\"line\""), - "strings are escaped: {emitted}" - ); -} - -#[test] -fn settings_strings_re_escape_decoded_escapes() { - // A decoded `\n` (and other JSONC escapes) must round-trip to the - // oracle's literal two-character spelling, not a raw byte (evidence: the - // inputhud description, #86). - let program = program_with_settings(settings(vec![group( - "main", - vec![string("description", "line one\nline two\t\"quoted\"\\end")], - )])); - let emitted = emitter::emit(&program, &catalog(), &en()).expect("emits"); - let section = settings_section(&emitted); - assert!( - section.contains("Description: \"line one\\nline two\\t\\\"quoted\\\"\\\\end\""), - "decoded escapes re-escape to the oracle spelling: {section}" - ); - assert!( - !section.contains('\u{000A}') || !section.contains("Description: \"line one\n"), - "no raw newline byte inside the settings string: {section}" - ); -} - -#[test] -fn rule_final_if_omits_the_trailing_end_and_round_trips() { - // Amended AC-12: a rule-final if/if-else closes with the `}` (no - // trailing `End;`), byte-equal to the oracle's spelling; the ws parser - // accepts it and re-emission is a byte-identical fixed point. The - // middle-of-rule if keeps `End;`. - let oracle_spelling = r#"variables { - global: - 0: x -} - -rule ("r") { - event { - Ongoing - Global; - } - actions { - If(Compare(Global.x, ==, 1)); - Disable Inspector Recording; - Else; - Disable Inspector Recording; - } -} - -"#; - let program = parser::parse(oracle_spelling, &catalog(), &en()) - .expect("the oracle's rule-final if spelling must parse"); - let emitted = emitter::emit(&program, &catalog(), &en()).expect("emits"); - assert_eq!( - emitted, oracle_spelling, - "the rule-final if-else re-emits byte-identically" - ); - assert!( - !emitted.contains("End;"), - "no trailing End; in the rule-final if-else" - ); - // Middle-of-rule if keeps End;. - let middle = parser::parse( - r#"variables { - global: - 0: x -} - -rule ("r") { - event { - Ongoing - Global; - } - actions { - If(Compare(Global.x, ==, 1)); - Disable Inspector Recording; - End; - Set Global Variable(x, 2); - } -} - -"#, - &catalog(), - &en(), - ) - .expect("middle-of-rule if parses"); - let emitted = emitter::emit(&middle, &catalog(), &en()).expect("emits"); - assert!(emitted.contains("End;"), "middle-of-rule if keeps End;"); -} - -#[test] -fn constant_format_calls_fold_to_the_substituted_text() { - // Amended AC-13: all-constant `.format()` calls fold into the - // substituted Custom String text (oracle spelling), including the - // two-decimal float rendering; variable arguments stay as format nodes. - let mut program = wir::Program::default(); - let file = program - .files - .push(wright_workshop::source::SourceFile::new("workshop.txt")); - let text = program.values.push(wright_workshop::wir::ValueNode::new( - wright_workshop::wir::Value::String("value: {0}".to_string()), - None, - )); - let three = program.values.push(wright_workshop::wir::ValueNode::new( - wright_workshop::wir::Value::Number { - value: 3.0, - text: "3".to_string(), - }, - None, - )); - let folded = program.values.push(wright_workshop::wir::ValueNode::new( - wright_workshop::wir::Value::Call { - name: "format".to_string(), - args: vec![text, three], - }, - None, - )); - let action = program.actions.push(wir::Action::SetGlobalVariable { - variable: wright_ir::ids::Id::from_index(0), - value: folded, - span: None, - target_span: None, - }); - program.global_variables.push(wir::WorkshopVariable { - name: "y".into(), - index: 0, - span: None, - name_span: None, - }); - program.rules.push(wir::Rule { - name: "r".into(), - span: None, - name_span: None, - disabled: false, - event: wir::Event::Global, - conditions: vec![], - actions: vec![action], - }); - let _ = file; - let emitted = emitter::emit(&program, &catalog(), &en()).expect("emits"); - assert!( - emitted.contains("Set Global Variable(y, Custom String(\"value: 3\"));"), - "constant format folds to the substituted text: {emitted}" - ); - let reparsed = parser::parse(&emitted, &catalog(), &en()).expect("folded output reparses"); - let reemitted = emitter::emit(&reparsed, &catalog(), &en()).expect("re-emits"); - assert_eq!( - emitted, reemitted, - "folded format must be a byte-identical fixed point" - ); -} - -#[test] -fn constant_float_format_arguments_use_two_decimals() { - // The oracle folds 0.5 to `0.50` and 0.125 to `0.13` (JS toFixed(2)). - let mut program = wir::Program::default(); - let file = program - .files - .push(wright_workshop::source::SourceFile::new("workshop.txt")); - let text = program.values.push(wright_workshop::wir::ValueNode::new( - wright_workshop::wir::Value::String("v: {0}".to_string()), - None, - )); - let half = program.values.push(wright_workshop::wir::ValueNode::new( - wright_workshop::wir::Value::Number { - value: 0.5, - text: "0.5".to_string(), - }, - None, - )); - let folded = program.values.push(wright_workshop::wir::ValueNode::new( - wright_workshop::wir::Value::Call { - name: "format".to_string(), - args: vec![text, half], - }, - None, - )); - let action = program.actions.push(wir::Action::SetGlobalVariable { - variable: wright_ir::ids::Id::from_index(0), - value: folded, - span: None, - target_span: None, - }); - program.global_variables.push(wir::WorkshopVariable { - name: "y".into(), - index: 0, - span: None, - name_span: None, - }); - program.rules.push(wir::Rule { - name: "r".into(), - span: None, - name_span: None, - disabled: false, - event: wir::Event::Global, - conditions: vec![], - actions: vec![action], - }); - let _ = file; - let emitted = emitter::emit(&program, &catalog(), &en()).expect("emits"); - assert!( - emitted.contains("Set Global Variable(y, Custom String(\"v: 0.50\"));"), - "0.5 folds to 0.50 (toFixed(2)): {emitted}" - ); -} - -#[test] -fn split_and_reescaped_value_strings_round_trip_byte_identically() { - // Amended AC-6: a long string (split continuation chain) and an escaped - // string parse and re-emit byte-identically through the ws parser. - let mut program = wir::Program::default(); - let file = program - .files - .push(wright_workshop::source::SourceFile::new("workshop.txt")); - let long = program.values.push(wright_workshop::wir::ValueNode::new( - wright_workshop::wir::Value::String("B".repeat(300)), - None, - )); - let escaped = program.values.push(wright_workshop::wir::ValueNode::new( - wright_workshop::wir::Value::String("a\nb\"c\\d\te".to_string()), - None, - )); - let first = program.actions.push(wir::Action::SetGlobalVariable { - variable: wright_ir::ids::Id::from_index(0), - value: long, - span: None, - target_span: None, - }); - let second = program.actions.push(wir::Action::SetGlobalVariable { - variable: wright_ir::ids::Id::from_index(1), - value: escaped, - span: None, - target_span: None, - }); - program.global_variables.push(wir::WorkshopVariable { - name: "x".into(), - index: 0, - span: None, - name_span: None, - }); - program.global_variables.push(wir::WorkshopVariable { - name: "y".into(), - index: 1, - span: None, - name_span: None, - }); - program.rules.push(wir::Rule { - name: "r".into(), - span: None, - name_span: None, - disabled: false, - event: wir::Event::Global, - conditions: vec![], - actions: vec![first, second], - }); - let _ = file; - let emitted = emitter::emit(&program, &catalog(), &en()).expect("emits"); - assert!( - emitted.contains("Custom String(\"B",), - "the long string splits into a continuation chain: {emitted}" - ); - assert!( - emitted.contains("Custom String(\"a\\nb\\\"c\\\\d\te\")"), - "escapes re-emit in the oracle spelling: {emitted}" - ); - let reparsed = parser::parse(&emitted, &catalog(), &en()) - .expect("the split chain and escaped string must reparse"); - let reemitted = emitter::emit(&reparsed, &catalog(), &en()).expect("re-emits"); - assert_eq!( - emitted, reemitted, - "split and re-escaped spellings must be a byte-identical fixed point" - ); -} - -#[test] -fn implicit_format_placeholders_renumber_to_the_oracle_form() { - // Amended AC-15: implicit `{}` placeholders renumber positionally to the - // oracle's explicit form (`"v: {}".format(x)` -> `Custom String("v: - // {0}", Global.x)`), and constant arguments fold into the text with the - // remaining placeholders renumbered (`"{} {}".format(3, x)` -> - // `Custom String("3 {0}", Global.x)`). Both byte-quoted oracle pins. - let mut program = wir::Program::default(); - let file = program - .files - .push(wright_workshop::source::SourceFile::new("workshop.txt")); - let text = program.values.push(wright_workshop::wir::ValueNode::new( - wright_workshop::wir::Value::String("v: {}".to_string()), - None, - )); - let variable = program.values.push(wright_workshop::wir::ValueNode::new( - wright_workshop::wir::Value::GlobalVariable(wright_ir::ids::Id::from_index(0)), - None, - )); - let call = program.values.push(wright_workshop::wir::ValueNode::new( - wright_workshop::wir::Value::Call { - name: "format".to_string(), - args: vec![text, variable], - }, - None, - )); - let action = program.actions.push(wir::Action::SetGlobalVariable { - variable: wright_ir::ids::Id::from_index(1), - value: call, - span: None, - target_span: None, - }); - program.global_variables.push(wir::WorkshopVariable { - name: "x".into(), - index: 0, - span: None, - name_span: None, - }); - program.global_variables.push(wir::WorkshopVariable { - name: "z".into(), - index: 1, - span: None, - name_span: None, - }); - program.rules.push(wir::Rule { - name: "r".into(), - span: None, - name_span: None, - disabled: false, - event: wir::Event::Global, - conditions: vec![], - actions: vec![action], - }); - let _ = file; - let emitted = emitter::emit(&program, &catalog(), &en()).expect("emits"); - assert!( - emitted.contains("Set Global Variable(z, Custom String(\"v: {0}\", Global.x));"), - "implicit placeholders renumber to the oracle form: {emitted}" - ); - let reparsed = parser::parse(&emitted, &catalog(), &en()).expect("reparses"); - let reemitted = emitter::emit(&reparsed, &catalog(), &en()).expect("re-emits"); - assert_eq!( - emitted, reemitted, - "renumbered format must be a byte-identical fixed point" - ); -} - -#[test] -fn partial_constant_format_folds_and_renumbers() { - // The oracle folds the constant into the text and renumbers the - // remaining placeholder: `"{} {}".format(3, x)` -> - // `Custom String("3 {0}", Global.x)` (byte-quoted pin). - let mut program = wir::Program::default(); - let file = program - .files - .push(wright_workshop::source::SourceFile::new("workshop.txt")); - let text = program.values.push(wright_workshop::wir::ValueNode::new( - wright_workshop::wir::Value::String("{} {}".to_string()), - None, - )); - let three = program.values.push(wright_workshop::wir::ValueNode::new( - wright_workshop::wir::Value::Number { - value: 3.0, - text: "3".to_string(), - }, - None, - )); - let variable = program.values.push(wright_workshop::wir::ValueNode::new( - wright_workshop::wir::Value::GlobalVariable(wright_ir::ids::Id::from_index(0)), - None, - )); - let call = program.values.push(wright_workshop::wir::ValueNode::new( - wright_workshop::wir::Value::Call { - name: "format".to_string(), - args: vec![text, three, variable], - }, - None, - )); - let action = program.actions.push(wir::Action::SetGlobalVariable { - variable: wright_ir::ids::Id::from_index(1), - value: call, - span: None, - target_span: None, - }); - program.global_variables.push(wir::WorkshopVariable { - name: "x".into(), - index: 0, - span: None, - name_span: None, - }); - program.global_variables.push(wir::WorkshopVariable { - name: "z".into(), - index: 1, - span: None, - name_span: None, - }); - program.rules.push(wir::Rule { - name: "r".into(), - span: None, - name_span: None, - disabled: false, - event: wir::Event::Global, - conditions: vec![], - actions: vec![action], - }); - let _ = file; - let emitted = emitter::emit(&program, &catalog(), &en()).expect("emits"); - assert!( - emitted.contains("Set Global Variable(z, Custom String(\"3 {0}\", Global.x));"), - "the constant folds and the variable placeholder renumbers: {emitted}" - ); -} - -#[test] -fn playervar_reads_parenthesize_the_receiver() { - // Amended AC-16: `g = eventPlayer.p` emits `Set Global Variable(g, - // (Event Player).p)` (byte-quoted oracle pin) and the spelling - // round-trips through the ws parser. - let mut program = wir::Program::default(); - let file = program - .files - .push(wright_workshop::source::SourceFile::new("workshop.txt")); - let player = program.values.push(wright_workshop::wir::ValueNode::new( - wright_workshop::wir::Value::EventPlayer, - None, - )); - let read = program.values.push(wright_workshop::wir::ValueNode::new( - wright_workshop::wir::Value::PlayerVariable { - player, - variable: wright_ir::ids::Id::from_index(0), - }, - None, - )); - let action = program.actions.push(wir::Action::SetGlobalVariable { - variable: wright_ir::ids::Id::from_index(0), - value: read, - span: None, - target_span: None, - }); - program.global_variables.push(wir::WorkshopVariable { - name: "g".into(), - index: 0, - span: None, - name_span: None, - }); - program.player_variables.push(wir::WorkshopVariable { - name: "p".into(), - index: 0, - span: None, - name_span: None, - }); - program.rules.push(wir::Rule { - name: "r".into(), - span: None, - name_span: None, - disabled: false, - event: wir::Event::Global, - conditions: vec![], - actions: vec![action], - }); - let _ = file; - let emitted = emitter::emit(&program, &catalog(), &en()).expect("emits"); - assert!( - emitted.contains("Set Global Variable(g, (Event Player).p);"), - "playervar reads parenthesize the receiver: {emitted}" - ); - let reparsed = - parser::parse(&emitted, &catalog(), &en()).expect("the oracle spelling reparses"); - let reemitted = emitter::emit(&reparsed, &catalog(), &en()).expect("re-emits"); - assert_eq!( - emitted, reemitted, - "playervar reads must be a byte-identical fixed point" - ); -} - -#[test] -fn receiver_call_actions_and_values_emit_catalog_spellings() { - // Issue #104: `.opy` receiver calls lower to `Action::Call`/`Value::Call` - // whose `name` is the receiver method; emission resolves those names - // through the catalog (general path, no per-name special cases). - let mut program = wir::Program::default(); - program - .files - .push(wright_workshop::source::SourceFile::new("workshop.txt")); - let player = program.values.push(wright_workshop::wir::ValueNode::new( - wright_workshop::wir::Value::EventPlayer, - None, - )); - let percent = program.values.push(wright_workshop::wir::ValueNode::new( - wright_workshop::wir::Value::Number { - value: 100.0, - text: "100".to_string(), - }, - None, - )); - let alive = program.values.push(wright_workshop::wir::ValueNode::new( - wright_workshop::wir::Value::Call { - name: "isAlive".to_string(), - args: vec![player], - }, - None, - )); - let move_speed = program.actions.push(wir::Action::Call { - name: "setMoveSpeed".to_string(), - args: vec![player, percent], - span: None, - }); - program.rules.push(wir::Rule { - name: "r".into(), - span: None, - name_span: None, - disabled: false, - event: wir::Event::EachPlayer, - conditions: vec![alive], - actions: vec![move_speed], - }); - - let emitted = emitter::emit(&program, &catalog(), &en()).expect("emits"); - assert!( - emitted.contains("Is Alive(Event Player) == True;"), - "receiver value calls resolve through the catalog: {emitted}" - ); - assert!( - emitted.contains("Set Move Speed(Event Player, 100);"), - "receiver action calls resolve through the catalog: {emitted}" - ); -} diff --git a/crates/wright-workshop/tests/parser.rs b/crates/wright-workshop/tests/parser.rs deleted file mode 100644 index 9105615..0000000 --- a/crates/wright-workshop/tests/parser.rs +++ /dev/null @@ -1,407 +0,0 @@ -//! Native parser tests (#32): the en-US corpus Workshop text parses directly -//! into validated, locale-independent WIR, and diagnostics distinguish -//! malformed, unknown, and unsupported input. - -use std::path::{Path, PathBuf}; - -use wright_workshop::catalog::{Catalog, Locale}; -use wright_workshop::parser; -use wright_workshop::validate; -use wright_workshop::wir; - -fn fixture_oracle_path(fixture_id: &str) -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../compatibility/fixtures") - .join(fixture_id) - .join("oracle.json") -} - -fn corpus_workshop_text(fixture_id: &str) -> String { - let oracle = serde_json::from_str::( - &std::fs::read_to_string(fixture_oracle_path(fixture_id)) - .unwrap_or_else(|error| panic!("cannot read oracle for {fixture_id}: {error}")), - ) - .expect("oracle json"); - oracle["compile"]["workshop"] - .as_str() - .expect("workshop text") - .to_string() -} - -fn catalog() -> Catalog { - Catalog::builtin().expect("built-in catalog") -} - -const CORPUS_FIXTURES: &[&str] = &[ - "synthetic/basic-rule", - "synthetic/control-flow", - "synthetic/declarations-rules", - "synthetic/expressions-values", - "synthetic/preprocessing", - "real-world/overpy-cake", -]; - -#[test] -fn every_corpus_workshop_text_parses_to_valid_wir() { - // The corpus Workshop text parses directly against the catalog context, - // which pins the expected enum domains from the canonical catalog - // signatures (e.g. Create HUD Text's Reevaluation argument is `HudReeval`, - // Create Beam Effect's is `EffectReeval`), resolving bare members that - // are ambiguous across the catalog's enum domains (e.g. - // `Visible To and String`). - // - // One documented exception: `real-world/overpy-cake`'s bare `Up` (OverPy - // folds the vector-up constant into the bare member inside `Add(...)`) is - // genuinely ambiguous between the `Vector` and `Rounding` enum domains and - // no enclosing signature pins it, so the parser rejects it deterministically - // rather than guessing (#111). The assertion below pins that this is the - // ONLY way the fixture fails. - let documented_ambiguities = [("real-world/overpy-cake", "ambiguous enum member 'Up'")]; - for fixture_id in CORPUS_FIXTURES { - let text = corpus_workshop_text(fixture_id); - let catalog = catalog(); - match parser::parse_with_context(&text, &catalog, &Locale::new("en-US"), &catalog) { - Ok(program) => { - program - .validate() - .unwrap_or_else(|error| panic!("{fixture_id} WIR must validate: {error}")); - validate::validate_canonical_ids(&program, &catalog).unwrap_or_else(|error| { - panic!("{fixture_id} canonical ids must resolve: {error}") - }); - assert!(!program.rules.is_empty(), "{fixture_id} must produce rules"); - assert!( - !program.dump().is_empty(), - "{fixture_id} dump must not be empty" - ); - } - Err(error) => { - let Some((_, message)) = documented_ambiguities - .iter() - .find(|(id, _)| *id == *fixture_id) - else { - panic!("{fixture_id} must parse:\n{error}"); - }; - assert!( - error.to_string().contains(message), - "{fixture_id} fails only with the documented ambiguity, got: {error}" - ); - } - } - } -} - -#[test] -fn parsing_is_deterministic() { - let text = corpus_workshop_text("synthetic/control-flow"); - let catalog = catalog(); - let first = - parser::parse_with_context(&text, &catalog, &Locale::new("en-US"), &catalog).unwrap(); - let second = - parser::parse_with_context(&text, &catalog, &Locale::new("en-US"), &catalog).unwrap(); - assert_eq!(first.dump(), second.dump()); -} - -#[test] -fn parsed_variables_and_subroutines_carry_indexes() { - let catalog = catalog(); - let program = parser::parse_with_context( - &corpus_workshop_text("synthetic/declarations-rules"), - &catalog, - &Locale::new("en-US"), - &catalog, - ) - .unwrap(); - let globals: Vec<_> = program - .global_variables - .iter() - .map(|variable| (variable.name.as_str(), variable.index)) - .collect(); - assert_eq!(globals, vec![("score", 0)]); - let players: Vec<_> = program - .player_variables - .iter() - .map(|variable| (variable.name.as_str(), variable.index)) - .collect(); - assert_eq!(players, vec![("hasStarted", 0)]); - let subroutines: Vec<_> = program - .subroutines - .iter() - .map(|subroutine| (subroutine.name.as_str(), subroutine.index)) - .collect(); - assert_eq!(subroutines, vec![("showStatus", 0)]); -} - -#[test] -fn parsed_events_are_canonical() { - let catalog = catalog(); - let program = parser::parse_with_context( - &corpus_workshop_text("synthetic/declarations-rules"), - &catalog, - &Locale::new("en-US"), - &catalog, - ) - .unwrap(); - let events: Vec = program - .rules - .iter() - .map(|rule| match &rule.event { - wir::Event::Global => "global".to_string(), - wir::Event::EachPlayer | wir::Event::EachPlayerWithFilters { .. } => { - "eachPlayer".to_string() - } - wir::Event::Player { kind, .. } => kind.catalog_id().to_string(), - wir::Event::Subroutine(subroutine) => format!( - "subroutine:{}", - program.subroutines.get(*subroutine).unwrap().name - ), - }) - .collect(); - assert_eq!( - events, - vec![ - "subroutine:showStatus".to_string(), - "eachPlayer".to_string() - ] - ); -} - -#[test] -fn parsed_conditions_resolve_infix_operators() { - let catalog = catalog(); - let program = parser::parse_with_context( - &corpus_workshop_text("synthetic/declarations-rules"), - &catalog, - &Locale::new("en-US"), - &catalog, - ) - .unwrap(); - let rule = program - .rules - .iter() - .find(|rule| rule.name == "player starts") - .expect("rule"); - assert_eq!(rule.conditions.len(), 1, "one condition"); - // The condition is `==(hasSpawned(eventPlayer), true)`. - let condition = program.values.get(rule.conditions[0]).unwrap(); - match &condition.value { - wir::Value::Call { name, args } => { - assert_eq!(name, "=="); - assert_eq!(args.len(), 2); - } - other => panic!("expected a comparison call, got {other:?}"), - } -} - -#[test] -fn spans_are_preserved() { - let text = corpus_workshop_text("synthetic/basic-rule"); - let program = parser::parse(&text, &catalog(), &Locale::new("en-US")).unwrap(); - let rule = program.rules.iter().next().unwrap(); - let rule_span = rule.span.expect("rule span"); - assert_eq!(rule.name, "setup"); - assert!(rule_span.start.line >= 1); - // The disable-inspector action carries its own span. - let action = program.actions.get(rule.actions[0]).expect("action"); - assert!(action.span().is_some()); -} - -#[test] -fn malformed_input_is_reported_as_malformed() { - // A rule-final If without `End;` is the oracle's valid spelling (#87); - // an If whose body never closes at all stays malformed. - let text = "rule (\"broken\") { actions { If(True);"; - let error = parser::parse(text, &catalog(), &Locale::new("en-US")).unwrap_err(); - assert!( - matches!(error, wright_workshop::WorkshopError::Malformed { .. }), - "an unclosed If body is malformed: {error}" - ); - assert!(error.to_string().contains("malformed")); -} - -#[test] -fn rule_final_if_without_end_is_the_oracle_spelling() { - let text = "rule (\"ok\") { actions { If(True); } }"; - let program = parser::parse(text, &catalog(), &Locale::new("en-US")) - .expect("a rule-final If without End; is valid (oracle spelling, #87)"); - assert_eq!(program.rules.len(), 1); -} - -#[test] -fn unknown_spelling_is_reported_as_unknown() { - let text = "rule (\"x\") { event { Ongoing - Global; } actions { Totally Unknown Thing(1); } }"; - let error = parser::parse(text, &catalog(), &Locale::new("en-US")).unwrap_err(); - assert!( - matches!(error, wright_workshop::WorkshopError::Unknown { .. }), - "unknown action must be Unknown: {error}" - ); - assert!(error.to_string().contains("Totally Unknown Thing")); -} - -#[test] -fn unsupported_construct_is_distinct_from_malformed() { - // A non-default eachPlayer sub-parameter is recognized but unsupported. - let text = "rule (\"x\") { event { Ongoing - Each Player; Team 1; } actions { } }"; - let error = parser::parse(text, &catalog(), &Locale::new("en-US")).unwrap_err(); - assert!( - matches!(error, wright_workshop::WorkshopError::Unsupported { .. }), - "non-default event parameter must be Unsupported: {error}" - ); -} - -#[test] -fn bare_chase_reevaluation_none_is_ambiguous_across_domains() { - // #105: both reference reevaluation domains spell their NONE member - // "None". The flat Workshop parser cannot disambiguate the bare spelling, - // so it fails with a structured Unsupported diagnostic (documented - // round-trip boundary in docs/workshop/support-matrix.md; the emitted - // semantic value is reference-equivalent). - let text = "variables { global: 0: g }\nrule (\"x\") { event { Ongoing - Global; } actions { Set Global Variable(g, None); } }"; - let error = parser::parse(text, &catalog(), &Locale::new("en-US")).unwrap_err(); - assert!( - matches!(error, wright_workshop::WorkshopError::Unsupported { .. }), - "the shared None member spelling must be a structured ambiguity: {error}" - ); - assert!(error.to_string().contains("ambiguous enum member 'None'")); -} - -#[test] -fn explicit_locale_is_honored() { - // en-US parsing is deterministic; the parser never guesses a locale. - let text = corpus_workshop_text("synthetic/basic-rule"); - let program = parser::parse(&text, &catalog(), &Locale::new("en-US")).unwrap(); - let dump = program.dump(); - assert!(!dump.is_empty()); -} - -/// The canonical signature context from the #109 manifest, as the shipped -/// driver wires it into the Workshop parse path (#111). -fn manifest_context() -> &'static dyn wright_core::signatures::ExpectedDomain { - wright_opy::manifest::Manifest::builtin().expect("builtin manifest") -} - -/// The last argument value of the first call action of a parsed program. -fn enum_value_of_first_action(program: &wir::Program, action_index: usize) -> &wir::Value { - let action = program.actions.iter().nth(action_index).expect("action"); - let wir::Action::Call { args, .. } = action else { - panic!("expected a call action, got {action:?}"); - }; - let last = args.last().expect("call has an argument"); - let wir::ValueNode { value, .. } = program.values.get(*last).expect("value"); - value -} - -#[test] -fn context_pinned_ambiguous_none_resolves_via_canonical_signature() { - // #111: emitter-produced `Chase Global Variable Over Time(..., None)` - // reparses to ChaseTimeReeval.NONE because the canonical chaseOverTime - // signature pins argument 3 to the ChaseTimeReeval domain. - let text = "variables { global: 0: g }\nrule (\"x\") { event { Ongoing - Global; } actions { Chase Global Variable Over Time(Global.g, 0, 30, None); } }"; - let program = - parser::parse_with_context(text, &catalog(), &Locale::new("en-US"), manifest_context()) - .expect("the pinned Chase None must resolve"); - let value = enum_value_of_first_action(&program, 0); - assert!( - matches!(value, wir::Value::Enum { value_type, value } - if value_type == "ChaseTimeReeval" && value == "NONE"), - "the bare None resolves to ChaseTimeReeval.NONE, got {value:?}" - ); -} - -#[test] -fn context_pinned_ambiguous_none_resolves_for_set_invisible() { - // #111: `Set Invisible(Event Player, None)` reparses to Invis.NONE. The - // manifest's setInvisibility is a member action, so Workshop text places - // the receiver as argument 0 and the signature-pinned parameter at - // argument 1. - let text = "rule (\"x\") { event { Ongoing - Each Player; } actions { Set Invisible(Event Player, None); } }"; - let program = - parser::parse_with_context(text, &catalog(), &Locale::new("en-US"), manifest_context()) - .expect("the pinned Invis None must resolve"); - let value = enum_value_of_first_action(&program, 0); - assert!( - matches!(value, wir::Value::Enum { value_type, value } - if value_type == "Invis" && value == "NONE"), - "the bare None resolves to Invis.NONE, got {value:?}" - ); -} - -#[test] -fn wrong_domain_context_keeps_the_ambiguity_rejected() { - // A signature pinning a *different* domain than the ambiguous member's - // candidates must not resolve it: `Wait(...)` expects `Wait` (which has - // no `None` member), so the bare `None` stays ambiguous — no guessing, - // no arbitrary precedence. - let text = "rule (\"x\") { event { Ongoing - Global; } actions { Wait(0.016, None); } }"; - let error = - parser::parse_with_context(text, &catalog(), &Locale::new("en-US"), manifest_context()) - .expect_err("a non-matching expected domain must keep the ambiguity"); - assert!( - matches!(error, wright_workshop::WorkshopError::Unsupported { .. }), - "expected a structured ambiguity: {error}" - ); - assert!(error.to_string().contains("ambiguous enum member 'None'")); -} - -#[test] -fn expected_domain_resolution_tracks_the_manifest_declared_domains() { - // Behavioral check that resolution consumes the #109 manifest as the - // single source of expected domains: the adapter answers exactly the - // manifest's declared parameter domains, including the receiver-offset - // rule for member-kind functions. - let manifest = wright_opy::manifest::Manifest::builtin().expect("builtin manifest"); - use wright_core::signatures::ExpectedDomain; - // chaseOverTime: params [variable, destination, duration, reevaluation]. - assert_eq!( - manifest.expected_domain("chaseOverTime", 3), - Some("ChaseTimeReeval") - ); - assert_eq!(manifest.expected_domain("chaseOverTime", 2), None); - // setInvisibility: member action; Workshop arg 1 is the pinned param. - assert_eq!(manifest.expected_domain("setInvisibility", 0), None); - assert_eq!( - manifest.expected_domain("setInvisibility", 1), - Some("Invis") - ); - // Unknown catalog ids and out-of-range indexes answer None. - assert_eq!(manifest.expected_domain("noSuchAction", 0), None); - assert_eq!(manifest.expected_domain("chaseOverTime", 4), None); -} - -#[test] -fn cross_domain_member_spelling_collisions_are_the_documented_inventory() { - // Systematic collision check (#111): scan the declared catalog for member - // spellings shared by more than one enum domain (en-US). The released - // catalog may add collisions as its enum surface grows, so assert the - // ambiguity contract for the spellings used by parser resolution rather - // than freezing the entire upstream inventory. - use std::collections::BTreeMap; - let mut spelling_to_domains: BTreeMap> = BTreeMap::new(); - for domain in catalog().enum_domains() { - for member in &domain.members { - let spelling = member - .spelling(&Locale::new("en-US")) - .expect("en-US member spelling") - .to_string(); - spelling_to_domains - .entry(spelling) - .or_default() - .push(domain.domain.clone()); - } - } - let collisions: Vec<(String, Vec)> = spelling_to_domains - .into_iter() - .filter(|(_, domains)| domains.len() > 1) - .collect(); - assert!(!collisions.is_empty()); - for (_, domains) in &collisions { - assert!(domains.len() > 1); - } - for spelling in ["All", "None", "Team 1", "Team 2", "Up", "Visible To"] { - assert!( - collisions - .iter() - .any(|(candidate, _)| candidate == spelling), - "expected {spelling:?} to remain an ambiguous enum spelling" - ); - } -} diff --git a/crates/wright-workshop/tests/pipeline.rs b/crates/wright-workshop/tests/pipeline.rs deleted file mode 100644 index 7e066a7..0000000 --- a/crates/wright-workshop/tests/pipeline.rs +++ /dev/null @@ -1,76 +0,0 @@ -//! Pipeline tests (#30): the committed canonical catalog data is regenerable -//! deterministically and validation rejects collisions and missing aliases. -//! -//! Cutover note (wright#143): the catalog data, generator binary -//! (`workshop-catalog-gen`), and its pipeline tests are owned by -//! `workshop-rs`. The wright-owned `wright-catalog-gen` binary was removed -//! with the duplicate catalog implementation; the tests that spawned it are -//! superseded by the workshop-rs pipeline suite. These library-level tests -//! remain as adapter regression coverage, running against the re-exported -//! `workshop_rs::catalog` API and the workshop-rs catalog dataset. - -use wright_workshop::catalog::{CATALOG_DATA, Catalog, canonicalize}; - -fn read_data() -> String { - CATALOG_DATA.to_string() -} - -#[test] -fn canonicalize_is_deterministic_and_idempotent() { - let source = read_data(); - let first = canonicalize(&source).expect("source canonicalizes"); - let second = canonicalize(&first).expect("canonical form canonicalizes"); - assert_eq!(first, second, "canonicalize must be idempotent"); - // The canonical form still loads and validates. - Catalog::load(&first).expect("canonical form validates"); -} - -#[test] -fn committed_catalog_is_already_canonical() { - let source = read_data(); - let canonical = canonicalize(&source).expect("source canonicalizes"); - assert_eq!( - source, canonical, - "committed catalog.json must be canonical" - ); -} - -#[test] -fn catalog_rejects_colliding_aliases() { - let bad = r#"{ - "schemaVersion": 1, - "locales": ["en-US"], - "target": { "game": "g", "format": "f", "surface": "s" }, - "provenance": { "generator": "g", "generatorVersion": "0", "source": "s", "license": "l", "reviewed": true }, - "structural": [ - { "id": "if", "aliases": { "en-US": "If" } }, - { "id": "elseIf", "aliases": { "en-US": "If" } } - ] - }"#; - let error = Catalog::load(bad).expect_err("collision must fail validation"); - assert!( - error.to_string().contains("duplicate"), - "error names the problem: {error}" - ); -} - -#[test] -fn catalog_rejects_missing_locale_alias() { - let bad = r#"{ - "schemaVersion": 1, - "locales": ["en-US"], - "target": { "game": "g", "format": "f", "surface": "s" }, - "provenance": { "generator": "g", "generatorVersion": "0", "source": "s", "license": "l", "reviewed": true }, - "structural": [ - { "id": "if", "aliases": { "en-US": "If" } } - ], - "actions": [ - { "id": "wait", "aliases": {} } - ] - }"#; - let error = Catalog::load(bad).expect_err("missing alias must fail"); - assert!( - error.to_string().contains("missing"), - "error names the problem: {error}" - ); -} diff --git a/crates/wright-workshop/tests/roundtrip.rs b/crates/wright-workshop/tests/roundtrip.rs deleted file mode 100644 index b0fbc85..0000000 --- a/crates/wright-workshop/tests/roundtrip.rs +++ /dev/null @@ -1,358 +0,0 @@ -//! Cross-language round-trip suite (#35): every supported-locale fixture -//! round-trips with recorded evidence, equivalence ignores presentation-only -//! differences, and negative fixtures fail at the right stage. - -use std::path::{Path, PathBuf}; - -use wright_workshop::catalog::{Catalog, Locale}; -use wright_workshop::roundtrip::{self, RoundTripRecord}; - -fn oracle_path(fixture_id: &str) -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../compatibility/fixtures") - .join(fixture_id) - .join("oracle.json") -} - -fn corpus_text(fixture_id: &str) -> String { - let oracle = serde_json::from_str::( - &std::fs::read_to_string(oracle_path(fixture_id)).unwrap(), - ) - .unwrap(); - oracle["compile"]["workshop"].as_str().unwrap().to_string() -} - -fn catalog() -> Catalog { - Catalog::builtin().unwrap() -} - -fn en() -> Locale { - Locale::new("en-US") -} - -#[test] -fn every_corpus_fixture_round_trips_with_full_evidence() { - // Corpus text round-trips against the catalog context, which pins the - // expected enum domains from the canonical catalog signatures. - // `real-world/overpy-cake` is the documented exception: its bare `Up` - // (OverPy folds the vector-up constant inside `Add(...)`) is genuinely - // ambiguous between the `Vector` and `Rounding` enum domains and no - // enclosing signature pins it, so the parser rejects it (#111). - let documented_ambiguities = [("real-world/overpy-cake", "ambiguous enum member 'Up'")]; - for fixture_id in [ - "synthetic/basic-rule", - "synthetic/control-flow", - "synthetic/declarations-rules", - "synthetic/expressions-values", - "synthetic/preprocessing", - "synthetic/receiver-calls", - "real-world/overpy-cake", - ] { - let catalog = catalog(); - let record = - roundtrip::round_trip_with_context(&corpus_text(fixture_id), &catalog, &en(), &catalog); - if let Some(error) = &record.error { - let Some((_, message)) = documented_ambiguities - .iter() - .find(|(id, _)| **id == *fixture_id) - else { - panic!("{fixture_id} must round-trip cleanly: {error}"); - }; - assert!( - error.contains(message), - "{fixture_id} fails only with the documented ambiguity, got: {error}" - ); - continue; - } - assert!(record.parse_ok, "{fixture_id}"); - assert!(record.emit_ok, "{fixture_id}"); - assert!(record.reparse_ok, "{fixture_id}"); - assert!(record.equivalent, "{fixture_id} must be WIR-equivalent"); - assert_eq!(record.locale, en()); - assert_eq!(record.catalog_version, 1); - assert_eq!(record.input_identity.len(), 64, "identity is a sha256 hex"); - } -} - -#[test] -fn same_locale_round_trip_is_a_release_gate() { - // The suite fails closed: any fixture failing round-trip equivalence - // blocks the gate. The `real-world/overpy-cake` bare-`Up` ambiguity is - // the single documented exception (#118): the catalog now documents both - // the `Vector` and `Rounding` "Up" members, and no enclosing signature - // pins the fixture's folded vector-up constant. - let failures: Vec = [ - "synthetic/basic-rule", - "synthetic/control-flow", - "synthetic/declarations-rules", - "synthetic/expressions-values", - "synthetic/preprocessing", - "synthetic/receiver-calls", - "real-world/overpy-cake", - ] - .iter() - .filter(|fixture_id| **fixture_id != "real-world/overpy-cake") - .map(|fixture_id| { - let catalog = catalog(); - roundtrip::round_trip_with_context(&corpus_text(fixture_id), &catalog, &en(), &catalog) - }) - .filter(|record: &RoundTripRecord| !record.equivalent || record.error.is_some()) - .map(|record| record.locale.to_string()) - .collect(); - assert!( - failures.is_empty(), - "round-trip gate failures: {failures:?}" - ); -} - -#[test] -fn equivalence_ignores_presentation_but_preserves_semantics() { - let a = wright_workshop::wir::Program::default(); - let b = wright_workshop::wir::Program::default(); - // Two empty programs are equivalent. - assert!(roundtrip::equivalent(&a, &b)); - // Same semantics, different file ids in spans: still equivalent. - let mut c = wright_workshop::wir::Program::default(); - c.files - .push(wright_workshop::source::SourceFile::new("other.txt")); - assert!( - roundtrip::equivalent(&a, &c), - "file paths are presentation-only" - ); -} - -#[test] -fn equivalence_detects_semantic_differences() { - let mut a = wright_workshop::wir::Program::default(); - a.files - .push(wright_workshop::source::SourceFile::new("workshop.txt")); - let mut b = a.clone(); - b.files - .push(wright_workshop::source::SourceFile::new("workshop.txt")); - - let value_a = a.values.push(wright_workshop::wir::ValueNode::new( - wright_workshop::wir::Value::Number { - value: 1.0, - text: "1".to_string(), - }, - None, - )); - let value_b = b.values.push(wright_workshop::wir::ValueNode::new( - wright_workshop::wir::Value::Number { - value: 2.0, - text: "2".to_string(), - }, - None, - )); - a.actions.push(wright_workshop::wir::Action::Debug { - value: value_a, - span: None, - }); - b.actions.push(wright_workshop::wir::Action::Debug { - value: value_b, - span: None, - }); - a.rules.push(wright_workshop::wir::Rule { - name: "r".into(), - span: None, - name_span: None, - disabled: false, - event: wright_workshop::wir::Event::Global, - conditions: vec![], - actions: a - .actions - .iter() - .map(|_| wright_ir::ids::Id::from_index(0)) - .collect(), - }); - b.rules.push(wright_workshop::wir::Rule { - name: "r".into(), - span: None, - name_span: None, - disabled: false, - event: wright_workshop::wir::Event::Global, - conditions: vec![], - actions: b - .actions - .iter() - .map(|_| wright_ir::ids::Id::from_index(0)) - .collect(), - }); - assert!( - !roundtrip::equivalent(&a, &b), - "different values must not be equivalent" - ); -} - -#[test] -fn malformed_input_is_recorded_not_crashed() { - let record = - roundtrip::round_trip("rule (\"broken\") { actions { If(True);", &catalog(), &en()); - assert!(!record.parse_ok); - assert!(!record.equivalent); - assert!(record.error.is_some(), "failure is recorded"); -} - -#[test] -fn unknown_builtin_fails_at_emit_stage() { - // A program that parses but contains a non-catalog value fails emit. - let mut program = wright_workshop::wir::Program::default(); - program - .files - .push(wright_workshop::source::SourceFile::new("workshop.txt")); - let value = program.values.push(wright_workshop::wir::ValueNode::new( - wright_workshop::wir::Value::Call { - name: "notACatalogId".into(), - args: vec![], - }, - None, - )); - let call = program.actions.push(wright_workshop::wir::Action::Call { - name: "wait".into(), - args: vec![value], - span: None, - }); - program.rules.push(wright_workshop::wir::Rule { - name: "x".into(), - span: None, - name_span: None, - disabled: false, - event: wright_workshop::wir::Event::Global, - conditions: vec![], - actions: vec![call], - }); - // Equivalent to the round-trip emit stage: emission of unknown ids fails. - let error = - wright_workshop::emitter::emit(&program, &catalog(), &en()).expect_err("unknown id"); - assert!(error.to_string().contains("notACatalogId")); -} - -/// The canonical signature context from the #109 manifest, as the shipped -/// driver wires it into the Workshop parse path (#111). -fn manifest_context() -> &'static dyn wright_core::signatures::ExpectedDomain { - wright_opy::manifest::Manifest::builtin().expect("builtin manifest") -} - -#[test] -fn emitter_chase_none_round_trips_through_the_shipped_path() { - // #111: emitter-produced `Chase Global Variable Over Time(..., None)` - // (bare `None` shared by ChaseTimeReeval/ChaseRateReeval/Invis) reparses - // to ChaseTimeReeval.NONE through the shipped parse+emit path, and the - // round-tripped WIR is equivalent to the input WIR. - let text = "variables { global: 0: g }\nrule (\"chase\") { event { Ongoing - Global; } actions { Chase Global Variable Over Time(Global.g, 0, 30, None); } }"; - let record = roundtrip::round_trip_with_context(text, &catalog(), &en(), manifest_context()); - assert!( - record.error.is_none(), - "the pinned Chase None must round-trip: {:?}", - record.error - ); - assert!(record.parse_ok && record.emit_ok && record.reparse_ok && record.equivalent); -} - -#[test] -fn emitter_set_invisible_none_round_trips_through_the_shipped_path() { - // #111: `Set Invisible(Event Player, None)` reparses to Invis.NONE via - // the member-function receiver offset and round-trips to equivalent WIR. - let text = "rule (\"inv\") { event { Ongoing - Each Player; } actions { Set Invisible(Event Player, None); } }"; - let record = roundtrip::round_trip_with_context(text, &catalog(), &en(), manifest_context()); - assert!( - record.error.is_none(), - "the pinned Invis None must round-trip: {:?}", - record.error - ); - assert!(record.parse_ok && record.emit_ok && record.reparse_ok && record.equivalent); -} - -#[test] -fn emitter_chase_at_rate_none_round_trips_through_the_shipped_path() { - // #110: the chase rate form emits `Chase Global Variable At Rate(..., - // None)`; the catalog id `chaseAtRate` selects the `ChaseRateReeval` - // domain through the manifest's contextual-dispatch data, so the bare - // `None` reparses and round-trips to equivalent WIR. - let text = "variables { global: 0: g }\nrule (\"chase\") { event { Ongoing - Global; } actions { Chase Global Variable At Rate(Global.g, 10, 2, None); } }"; - let record = roundtrip::round_trip_with_context(text, &catalog(), &en(), manifest_context()); - assert!( - record.error.is_none(), - "the pinned ChaseRateReeval None must round-trip: {:?}", - record.error - ); - assert!(record.parse_ok && record.emit_ok && record.reparse_ok && record.equivalent); - // The player form follows the same path through its own catalog id. - let text = "variables { player: 0: P }\nrule (\"chase\") { event { Ongoing - Each Player; } actions { Chase Player Variable At Rate(Event Player, P, 0, 1, None); } }"; - let record = roundtrip::round_trip_with_context(text, &catalog(), &en(), manifest_context()); - assert!( - record.error.is_none(), - "the pinned player ChaseRateReeval None must round-trip: {:?}", - record.error - ); - assert!(record.parse_ok && record.emit_ok && record.reparse_ok && record.equivalent); -} - -#[test] -fn chase_keyword_fixture_round_trips_through_the_shipped_path() { - // The `synthetic/chase-keywords` surface (rate/duration forms, global - // and player variables, keyword-bound wait/vect/len/print/ - // getPlayersInRadius/setStatusEffect) compiles through the native OPY - // frontend, emits through the catalog, reparses with the manifest - // signature context, and re-emits to a fixed point (#110). The oracle - // text itself is not the input: the reference emits bare variable names - // where the native Workshop parser's canonical spelling is `Global.g` - // (documented N-level presentation difference), so the round-trip uses - // the native emission. - let source = std::fs::read_to_string( - Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../compatibility/fixtures/synthetic/chase-keywords/source.opy"), - ) - .unwrap(); - let hir = wright_opy::compile(&source, "source.opy", Path::new("")) - .expect("the fixture compiles natively"); - let model = wright_core::hir::convert::convert(&hir).expect("the HIR converts"); - let wir = wright_ir::lower::lower(&model).expect("the fixture lowers to WIR"); - let emitted = - wright_workshop::emitter::emit(&wir, &catalog(), &en()).expect("the fixture emits"); - // The emission includes Debug/Print HUD text (canonical catalog layout) - // and chase `None` members, so both the manifest and the catalog supply - // the expected enum domains. - let catalog = catalog(); - let context = wright_core::signatures::ChainedExpectedDomain::new(manifest_context(), &catalog); - let record = roundtrip::round_trip_with_context(&emitted, &catalog, &en(), &context); - assert!( - record.error.is_none(), - "the chase-keywords emission must round-trip: {:?}", - record.error - ); - assert!(record.parse_ok && record.emit_ok && record.reparse_ok && record.equivalent); -} - -#[test] -fn context_free_chase_none_stays_a_documented_exception() { - // Without a signature pin the ambiguity stays rejected: the same input - // through the plain (context-free) round-trip fails at parse, keeping the - // pre-#111 boundary deterministic. - let text = "variables { global: 0: g }\nrule (\"chase\") { event { Ongoing - Global; } actions { Chase Global Variable Over Time(Global.g, 0, 30, None); } }"; - let record = roundtrip::round_trip(text, &catalog(), &en()); - assert!(!record.parse_ok, "context-free None must stay rejected"); - let error = record.error.expect("a parse failure is recorded"); - assert!(error.contains("ambiguous enum member 'None'"), "{error}"); -} - -#[test] -fn context_chase_none_emission_is_a_fixed_point() { - // Parse the emitted form with context, emit, reparse with context, and - // emit again: the text is a fixed point. - let text = "variables { global: 0: g }\nrule (\"chase\") { event { Ongoing - Global; } actions { Chase Global Variable Over Time(Global.g, 0, 30, None); } }"; - let catalog = catalog(); - let first = - wright_workshop::parser::parse_with_context(text, &catalog, &en(), manifest_context()) - .expect("pinned Chase None parses"); - let emitted = wright_workshop::emitter::emit(&first, &catalog, &en()).expect("emits"); - assert!( - emitted.contains("Chase Global Variable Over Time(Global.g, 0, 30, None)"), - "emission preserves the bare None spelling:\n{emitted}" - ); - let reparsed = - wright_workshop::parser::parse_with_context(&emitted, &catalog, &en(), manifest_context()) - .expect("emitted text reparses with context"); - let reemitted = wright_workshop::emitter::emit(&reparsed, &catalog, &en()).expect("re-emits"); - assert_eq!(emitted, reemitted, "emission must be a fixed point"); -} diff --git a/crates/wright-workshop/tests/wir_expansion.rs b/crates/wright-workshop/tests/wir_expansion.rs deleted file mode 100644 index 462b123..0000000 --- a/crates/wright-workshop/tests/wir_expansion.rs +++ /dev/null @@ -1,270 +0,0 @@ -//! WIR expansion and canonical-identity tests (#31): the declared P0 surface -//! is representable in Workshop IR, and catalog-backed validation rejects -//! unknown or locale-tainted builtin references deterministically. - -use wright_workshop::catalog::{Catalog, Locale}; -use wright_workshop::source::{Position, SourceFile, Span}; -use wright_workshop::validate; -use wright_workshop::wir::{self, Action, Event, Value, ValueNode}; - -fn catalog() -> Catalog { - Catalog::builtin().expect("built-in catalog") -} - -fn span(file: wright_ir::ids::Id, line: u32, col: u32, end_col: u32) -> Span { - Span::new(file, Position::new(line, col), Position::new(line, end_col)) -} - -/// Build a WIR program representing the corpus Workshop surface: one global -/// variable, a rule with a condition, a For loop, an If, a Modify action, a -/// generic action, and the catalog-backed values. -fn build_surface_program() -> wir::Program { - let mut program = wir::Program::default(); - let file = program.files.push(SourceFile::new("workshop.txt")); - let s = |line, col, end| span(file, line, col, end); - - let index = program.global_variables.push(wir::WorkshopVariable { - name: "index".into(), - index: 0, - span: Some(s(1, 15, 20)), - name_span: Some(s(1, 15, 20)), - }); - - let zero = program.values.push(ValueNode::new( - Value::Number { - value: 0.0, - text: "0".to_string(), - }, - Some(s(3, 24, 25)), - )); - let stop = program.values.push(ValueNode::new( - Value::Number { - value: 3.0, - text: "3".to_string(), - }, - Some(s(3, 27, 28)), - )); - let one = program.values.push(ValueNode::new( - Value::Number { - value: 1.0, - text: "1".to_string(), - }, - Some(s(3, 30, 31)), - )); - let index_ref = program.values.push(ValueNode::new( - Value::GlobalVariable(index), - Some(s(4, 18, 23)), - )); - let compare = program.values.push(ValueNode::new( - Value::Call { - name: "==".into(), - args: vec![index_ref, zero], - }, - Some(s(4, 10, 24)), - )); - let modified = program.values.push(ValueNode::new( - Value::Call { - name: "add".into(), - args: vec![index_ref, one], - }, - Some(s(5, 26, 33)), - )); - let yellow = program.values.push(ValueNode::new( - Value::Enum { - value_type: "Color".into(), - value: "YELLOW".into(), - }, - Some(s(5, 41, 47)), - )); - let beam_type = program.values.push(ValueNode::new( - Value::Enum { - value_type: "Beam".into(), - value: "GOOD".into(), - }, - Some(s(5, 35, 39)), - )); - let start_position = program.values.push(ValueNode::new( - Value::Vector { - x: zero, - y: zero, - z: zero, - }, - Some(s(5, 49, 60)), - )); - let end_position = program.values.push(ValueNode::new( - Value::Vector { - x: one, - y: one, - z: one, - }, - Some(s(5, 62, 73)), - )); - let all_teams = program.values.push(ValueNode::new( - Value::Enum { - value_type: "Team".into(), - value: "ALL".into(), - }, - Some(s(6, 14, 23)), - )); - let players = program.values.push(ValueNode::new( - Value::Call { - name: "allPlayers".into(), - args: vec![all_teams], - }, - Some(s(6, 14, 24)), - )); - - let debug_value = program.values.push(ValueNode::new( - Value::Number { - value: 1.0, - text: "1".to_string(), - }, - Some(s(7, 11, 12)), - )); - let debug = program.actions.push(Action::Debug { - value: debug_value, - span: Some(s(7, 9, 13)), - }); - let if_body = vec![debug]; - let if_action = program.actions.push(Action::If { - branches: vec![wir::IfBranch { - condition: compare, - body: if_body, - }], - else_body: None, - span: Some(s(4, 5, 8)), - }); - let modify = program.actions.push(Action::ModifyGlobalVariable { - variable: index, - op: wir::ModifyOp::Add, - value: modified, - span: Some(s(5, 5, 34)), - target_span: Some(s(5, 5, 10)), - }); - let beam = program.actions.push(Action::Call { - name: "createBeamEffect".into(), - args: vec![players, beam_type, start_position, end_position, yellow], - span: Some(s(6, 5, 25)), - }); - let for_action = program.actions.push(Action::ForGlobalVariable { - variable: index, - start: zero, - stop, - step: one, - body: vec![if_action, modify, beam], - span: Some(s(3, 5, 31)), - target_span: Some(s(3, 5, 10)), - }); - - program.rules.push(wir::Rule { - name: "surface".into(), - span: Some(s(2, 1, 6)), - name_span: Some(s(2, 5, 6)), - disabled: false, - event: Event::Global, - conditions: vec![], - actions: vec![for_action], - }); - program -} - -#[test] -fn corpus_surface_is_representable_and_validates() { - let program = build_surface_program(); - program.validate().expect("WIR is structurally valid"); - validate::validate_canonical_ids(&program, &catalog()).expect("canonical ids resolve"); - - let dump = program.dump(); - assert!(dump.contains("forGlobalVariable index in 0, 3, 1")); - assert!(dump.contains("modifyGlobalVariable index Add")); - assert!(dump.contains("call createBeamEffect")); - assert!(dump.contains("Color.YELLOW")); - assert!(dump.contains("allPlayers(Team.ALL)")); -} - -#[test] -fn unknown_action_id_is_rejected_with_location() { - let mut program = build_surface_program(); - let file = program - .files - .iter() - .next() - .map(|_| wright_ir::ids::Id::from_index(0)) - .expect("one file"); - let action = program - .actions - .get_mut(wir::ActionId::from_index(3)) - .expect("beam action"); - if let Action::Call { name, span, .. } = action { - *name = "createLaserEffect".into(); - *span = Some(Span::new(file, Position::new(9, 5), Position::new(9, 20))); - } - let error = validate::validate_canonical_ids(&program, &catalog()).expect_err("unknown action"); - assert!(error.to_string().contains("createLaserEffect"), "{error}"); -} - -#[test] -fn unknown_enum_member_is_rejected() { - let mut program = build_surface_program(); - // The Color enum node is at a known value id; replace its member. - for node in program.values.iter() { - if let Value::Enum { - value_type, value, .. - } = &node.value - { - if value_type == "Color" { - let _ = value; - } - } - } - // Rebuild a bad enum node: find the Color enum and mutate it. - let color_id = program - .values - .iter() - .enumerate() - .find(|(_, node)| { - matches!(&node.value, Value::Enum { value_type, .. } if value_type == "Color") - }) - .map(|(index, _)| wright_workshop::wir::ValueId::from_index(index)) - .expect("color node"); - if let Value::Enum { value, .. } = &mut program.values.get_mut(color_id).unwrap().value { - *value = "NEON".into(); - } - let error = validate::validate_canonical_ids(&program, &catalog()).expect_err("unknown member"); - assert!(error.to_string().contains("NEON"), "{error}"); -} - -#[test] -fn unknown_value_id_is_rejected() { - let mut program = build_surface_program(); - for node in program.values.iter() { - if let Value::Call { name, .. } = &node.value { - if name == "add" { - let _ = name; - } - } - } - let add_id = program - .values - .iter() - .enumerate() - .find(|(_, node)| matches!(&node.value, Value::Call { name, .. } if name == "add")) - .map(|(index, _)| wright_workshop::wir::ValueId::from_index(index)) - .expect("add node"); - if let Value::Call { name, .. } = &mut program.values.get_mut(add_id).unwrap().value { - *name = "plus".into(); - } - let error = validate::validate_canonical_ids(&program, &catalog()).expect_err("unknown value"); - assert!(error.to_string().contains("plus"), "{error}"); -} - -#[test] -fn canonical_validation_is_locale_independent() { - // Resolution uses canonical ids; locale spelling never appears in WIR. - let program = build_surface_program(); - let _ = Locale::new("en-US"); - validate::validate_canonical_ids(&program, &catalog()).expect("valid"); - // No WIR dump contains a localized spelling. - let dump = program.dump(); - assert!(!dump.contains("Disable Inspector Recording")); -} diff --git a/docs/README.md b/docs/README.md index f4c7f44..66dda88 100644 --- a/docs/README.md +++ b/docs/README.md @@ -152,7 +152,7 @@ JSON document asset, and historical artifact across the repository. | Asset Path | Document Class | Action | Description & Rationale | | --- | --- | --- | --- | -| `workshop-rs` catalog data (`crates/workshop-rs/src/catalog/data/catalog.json`, external repo, pinned rev in root `Cargo.toml`) | Catalog Data Asset (external owner) | `keep` | Canonical localized Workshop catalog data (actions, values, enums, events, keywords) with recorded provenance; owned by `workshop-rs` per ADR-0009/wright#143, consumed by Wright through the `wright-workshop` re-export adapter. | +| `workshop-rs` catalog data (`crates/workshop-rs/src/catalog/data/catalog.json`, external repo, pinned rev in root `Cargo.toml`) | Catalog Data Asset (external owner) | `keep` | Canonical localized Workshop catalog data (actions, values, enums, events, keywords) with recorded provenance; owned by `workshop-rs` per ADR-0009/wright#143, consumed by Wright directly. | | `compatibility/oracle/oracle-metadata.json` | Reference Metadata Asset | `keep` | Records pinned `overpy@9.7.10` tarball integrity, gitHead commit, registry URL, and license assumption. | | `compatibility/oracle/package.json` | Tooling Package Manifest | `keep` | Pinned npm package manifest for installing external OverPy oracle during compatibility evaluation. | | `adapter/package.json` | Tooling Package Manifest | `keep` | Pinned npm package manifest for executing the isolated OverPy adapter bridge. | diff --git a/docs/adr/0009-language-ownership-licensing-boundaries.md b/docs/adr/0009-language-ownership-licensing-boundaries.md index eee2dbc..9832a19 100644 --- a/docs/adr/0009-language-ownership-licensing-boundaries.md +++ b/docs/adr/0009-language-ownership-licensing-boundaries.md @@ -28,7 +28,8 @@ and LPP-conformant providers, with OPY and DEL provider cutover following in v0.3 and v0.4. The current in-repo crates (`crates/wright-opy`, `crates/wright-ostw`, -`crates/wright-workshop`, `crates/wright-ir`) are a migration state: they +`crates/wright-ir`; the `crates/wright-workshop` cutover adapter was removed +after its call-site migration completed) are a migration state: they coexist inside this repository until extraction completes. They do not define the target ownership. diff --git a/docs/compatibility.md b/docs/compatibility.md index 68cc731..9b134b7 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -14,8 +14,9 @@ Wright owns tooling and orchestration, not the durable source-language implementations. `opy-rs` owns OPY language semantics, `del-rs` owns the DEL/OSTW-compatible implementation, and `workshop-rs` owns canonical Workshop semantics and WIR. During the migration described by ADR-0009, Wright still -contains the in-repo `wright-opy` / `wright-ostw` migration frontends and the -`wright-workshop` re-export adapter until their planned cutovers complete. +contains the in-repo `wright-opy` / `wright-ostw` migration frontends until +their planned cutovers complete (the `wright-workshop` re-export adapter was +removed once call sites consumed `workshop-rs` directly). Wright therefore keeps regression coverage for those current integration paths, but upstream OverPy and OSTW compilers/language services remain compatibility oracles and behavior references rather than production or default-CI runtime diff --git a/docs/compatibility/upstream-references.md b/docs/compatibility/upstream-references.md index 7458c38..248c66a 100644 --- a/docs/compatibility/upstream-references.md +++ b/docs/compatibility/upstream-references.md @@ -55,7 +55,7 @@ artifacts. Concretely, it serves as: | Wright surface | Use of the reference | | --- | --- | | `wright-opy` native frontend | Differential HIR parity, accept/reject agreement, structured diagnostics | -| `workshop-rs` catalog/emission (via `wright-workshop` adapter) | Canonical en-US spelling validation against oracle-emitted Workshop text; receiver-method and enum emission evidence | +| `workshop-rs` catalog/emission | Canonical en-US spelling validation against oracle-emitted Workshop text; receiver-method and enum emission evidence | | `compatibility/` harness | Fixture snapshots, oracle identity blocks, S/D/N gate evidence | | Systematic baseline | Reference-validated probes for builtin action/value/member/enum/signature metadata — implemented as the OPY semantic compatibility manifest (`crates/wright-opy/src/manifest/`): every entry records the probe that validates it, and `probes/validate.py` runs the full probe set against the pinned oracle (accept/reject, normalized emission hash, diagnostic category; wired into `compatibility/tests`) | @@ -141,7 +141,7 @@ release artifacts. Concretely it serves as: | Wright surface | Use of the reference | | --- | --- | | `wright-ostw` native frontend | Accept/reject agreement, structured diagnostics, HIR semantic identity for the declared OSTW surface | -| `workshop-rs` emitter/catalog (via `wright-workshop` adapter) | Canonical en-US emission cross-check against the oracle's `workshopCode` output for shared Workshop surfaces | +| `workshop-rs` emitter/catalog | Canonical en-US emission cross-check against the oracle's `workshopCode` output for shared Workshop surfaces | | Workshop → OSTW reconstruction (future) | Reference decompiler output for the declared reconstruction surface and quality criteria | | `compatibility/` harness | OSTW fixture snapshots, oracle identity blocks, S/D/N gate evidence | diff --git a/docs/licensing.md b/docs/licensing.md index 3123e4e..9d4daad 100644 --- a/docs/licensing.md +++ b/docs/licensing.md @@ -9,8 +9,7 @@ qualified lawyer. Wright is independently implemented Rust software: a tooling-first semantic platform for the Overwatch Workshop ecosystem, with the semantic frontends currently co-located as in-repo crates (`wright-opy`, `wright-ostw`); -canonical Workshop semantics are consumed from `workshop-rs` through the -`wright-workshop` re-export adapter (wright#143). Per +canonical Workshop semantics are consumed from `workshop-rs` (wright#143). Per [ADR-0009](adr/0009-language-ownership-licensing-boundaries.md), durable ownership of source-language frontends moves to provider repositories (`opy-rs`, `del-rs`) and canonical Workshop semantics to `workshop-rs`; the diff --git a/docs/opy/compat-manifest-spec.md b/docs/opy/compat-manifest-spec.md index 1091c39..8268f38 100644 --- a/docs/opy/compat-manifest-spec.md +++ b/docs/opy/compat-manifest-spec.md @@ -29,7 +29,7 @@ reference-validated source for: It is **language-compatibility metadata**, distinct from: * the `workshop-rs` catalog (`crates/workshop-rs/src/catalog/data/catalog.json`, - consumed via the `wright-workshop` adapter) — the Workshop + consumed from `workshop-rs`) — the Workshop emission/localization layer (en-US spellings, emitter output); the manifest links to it by canonical id (`catalogId`) rather than duplicating spellings; and @@ -204,7 +204,7 @@ the probe source hash, expected oracle status, normalized emission hash, and * `wright-opy` — name/member/enum resolution, arity and signature checks, `KNOWN_ENUMS` absorption, earlier resolution of unknown-action/value errors (addressing the diagnostic-provenance limitation); -* `wright-workshop` (adapter over `workshop-rs`) — canonical-id linkage to +* `workshop-rs` — canonical-id linkage to the emission catalog (validated by the cross-check test); * differential and systematic reference tests (the probe validator); * documentation, agents, and future release metadata can consume the same diff --git a/docs/opy/support-matrix.md b/docs/opy/support-matrix.md index 53685fe..1a37923 100644 --- a/docs/opy/support-matrix.md +++ b/docs/opy/support-matrix.md @@ -271,7 +271,7 @@ resolve/lower → Opy HIR` (see [`docs/architecture.md`](../architecture.md) and `wright_opy::reconstruct` consumes a validated Workshop IR program and emits deterministic, byte-stable canonical OPY that the native frontend accepts and that re-lowers to a structurally equivalent WIR program under -`wright_workshop::roundtrip::equivalent` (re-exported from `workshop-rs`). The machine-readable support +`workshop_rs::roundtrip::equivalent`. The machine-readable support boundary (supported vs explicitly rejected constructs, with a consistency test) lives in `crates/wright-opy/tests/fixtures/reconstruct/boundary.json`; the round-trip diff --git a/docs/ostw/compatibility-baseline.md b/docs/ostw/compatibility-baseline.md index 2d346ac..580a1f9 100644 --- a/docs/ostw/compatibility-baseline.md +++ b/docs/ostw/compatibility-baseline.md @@ -269,9 +269,9 @@ semantics/WIR" requirement): * **HIR/WIR/lowering** (`wright-core`, `wright-ir`): the OSTW frontend lowers to Wright HIR; workshop-function calls resolve through the existing catalog/WIR path exactly like the OPY receiver-call surface (the canonical - `workshop-rs` catalog, consumed via `wright-workshop`). -* **Workshop parser/emitter/round-trip** (`workshop-rs` via the - `wright-workshop` adapter): Workshop stays the interoperability hub; + `workshop-rs` catalog). +* **Workshop parser/emitter/round-trip** (`workshop-rs`): Workshop stays + the interoperability hub; OSTW → Workshop emission reuses the canonical emitter, and Workshop → OSTW reconstruction reuses the canonical Workshop parser and WIR, adding only a Wright-owned OSTW emitter. diff --git a/docs/ostw/support-matrix.md b/docs/ostw/support-matrix.md index 7e9dc1e..c3f843d 100644 --- a/docs/ostw/support-matrix.md +++ b/docs/ostw/support-matrix.md @@ -15,7 +15,7 @@ the corrected explicit-root oracle evidence model is documented there too The pipeline is `wright-ostw` (project + syntax + #118 semantics) → frontend-neutral HIR → shared `wright-ir` lowering → canonical -`workshop-rs` emitter (en-US, via the `wright-workshop` adapter), identical +`workshop-rs` emitter (en-US), identical to the OPY/Workshop paths — no OSTW-specific backend exists. ## Accepted differential targets @@ -209,6 +209,6 @@ writes `target/wright-convert-report.json`. and the boundary-conformance test. - `workshop-rs` catalog data (`crates/workshop-rs/src/catalog/data/catalog.json`) — canonical catalog with `paramDefaults` (probe-evidenced) and the `abort` - action, consumed via the `wright-workshop` adapter. + action, consumed from `workshop-rs`. - `docs/ostw/compatibility-baseline.md` — the explicit-root evidence model and #122 correction. diff --git a/docs/v1-matrix.md b/docs/v1-matrix.md index 780683c..0afd57b 100644 --- a/docs/v1-matrix.md +++ b/docs/v1-matrix.md @@ -10,7 +10,7 @@ thresholds, unsupported constructs, and intentional differences | --- | --- | --- | | Native `.opy` (lexer/preprocess/parser/resolve/lower/settings) | `wright-opy` | [`opy/support-matrix.md`](opy/support-matrix.md) | | Native OSTW (lexer/parser/CST/project/lower) | `wright-ostw` | [`ostw/support-matrix.md`](ostw/support-matrix.md), [`ostw/compatibility-baseline.md`](ostw/compatibility-baseline.md) | -| Localized Workshop text (catalog/lexer/parser/emitter) | `workshop-rs` (via `wright-workshop` adapter) | [`workshop/support-matrix.md`](workshop/support-matrix.md) | +| Localized Workshop text (catalog/lexer/parser/emitter) | `workshop-rs` | [`workshop/support-matrix.md`](workshop/support-matrix.md) | | Driver/CLI result contract | `wright-driver`/`wright-cli` | [`cli.md`](cli.md) | Supported Workshop target: the Overwatch Workshop surface evidenced by the diff --git a/docs/workshop/catalog-pipeline.md b/docs/workshop/catalog-pipeline.md index d031c9f..fbf1477 100644 --- a/docs/workshop/catalog-pipeline.md +++ b/docs/workshop/catalog-pipeline.md @@ -12,10 +12,10 @@ dataset and surfaced by `workshop-rs-cli version --json`. It is the locale-identity layer for the canonical Workshop parser and emitter; parser and emitter code never contain locale-specific branches. -Wright no longer authors or generates catalog data. The in-repo -`wright-workshop` crate is a re-export-only adapter over `workshop-rs` -(wright#143): it contains no catalog implementation and no `wright-catalog-gen` -binary. The consumed `workshop-rs` revision is pinned in one place, +Wright no longer authors or generates catalog data. The `wright-workshop` +re-export-only adapter from the wright#143 cutover has been removed: Wright +consumes `workshop-rs` directly and contains no catalog implementation and no +`wright-catalog-gen` binary. The consumed `workshop-rs` revision is pinned in one place, `[workspace.dependencies]` in the root `Cargo.toml`; catalog or locale changes route to `workshop-rs` and are picked up here by updating that pin. diff --git a/docs/workshop/support-matrix.md b/docs/workshop/support-matrix.md index 340db88..6c701a7 100644 --- a/docs/workshop/support-matrix.md +++ b/docs/workshop/support-matrix.md @@ -6,8 +6,7 @@ supports for native localized Workshop input/output This document inventories the Workshop surface evidenced by the compatibility corpus, records the localization matrix, and specifies the supported feature -set of the canonical `workshop-rs` core as consumed through the -`wright-workshop` adapter. +set of the canonical `workshop-rs` core as consumed directly by Wright. ## Evidence sources