From e2a67c76db896b560d397fba2dc85c6663feb75d Mon Sep 17 00:00:00 2001 From: Teakowa <27560638+Teakowa@users.noreply.github.com> Date: Mon, 24 Aug 2026 02:42:55 +0800 Subject: [PATCH 1/4] fix(workshop): converge on released workshop-rs surface Fixes #191 --- .github/workflows/ci.yml | 48 ++++ Cargo.lock | 5 +- Cargo.toml | 2 +- crates/wright-analyzer/src/analysis.rs | 1 + crates/wright-analyzer/src/symbols.rs | 1 + crates/wright-cli/Cargo.toml | 1 + crates/wright-cli/tests/workshop_p0.rs | 231 ++++++++++++++++++ crates/wright-driver/src/session.rs | 26 +- crates/wright-driver/src/workshop_provider.rs | 27 +- .../wright-driver/tests/manifest_context.rs | 89 +++---- crates/wright-opy/src/reconstruct.rs | 7 + crates/wright-ostw/src/reconstruct.rs | 9 + crates/wright-ostw/tests/differential.rs | 1 + crates/wright-ostw/tests/reconstruct.rs | 1 + 14 files changed, 349 insertions(+), 100 deletions(-) create mode 100644 crates/wright-cli/tests/workshop_p0.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a74b7c5..91d06a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -134,6 +134,54 @@ jobs: - name: Run tests run: cargo test --locked --workspace --all-targets --all-features + # ------------------------------------------------------------------------- + # [1a] RAW WORKSHOP PRODUCT DOGFOOD (Wright's released Workshop consumer) + # Runs the owner-pinned real-project inputs through the actual `wright` + # check/lint commands. The source snapshots and residual expectations remain + # owned by the released workshop-rs corpus. + # ------------------------------------------------------------------------- + workshop-integration: + name: Raw Workshop product dogfood + needs: [paths, rust-quality] + if: needs.paths.outputs.rust_core == 'true' + runs-on: ubuntu-latest + steps: + - name: Check out Wright + uses: actions/checkout@v7 + + - name: Check out released workshop-rs P0 corpus + uses: actions/checkout@v7 + with: + repository: wrightkit/workshop-rs + ref: f2834ce09b83144070a5c8670695663be6f02daf # v0.1.8 + path: workshop-rs-pinned + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.85.0 + + - name: Restore Rust cache + uses: Swatinem/rust-cache@v2 + with: + shared-key: linux-quality + cache-targets: true + cache-all-crates: false + cache-workspace-crates: false + cache-bin: false + save-if: false + cache-on-failure: false + + - name: Run owner-contract cross-validation + env: + WRIGHTKIT_P0_ARTIFACT_DIR: ${{ github.workspace }}/workshop-rs-pinned/crates/workshop-rs/tests/fixtures/real-projects + run: cargo test --locked -p wright-driver --test p0_cross_validate -- --ignored + + - name: Run Wright check and lint dogfood + env: + WRIGHTKIT_P0_ARTIFACT_DIR: ${{ github.workspace }}/workshop-rs-pinned/crates/workshop-rs/tests/fixtures/real-projects + run: cargo test --locked -p wright-cli --test workshop_p0 -- --ignored --nocapture + # ------------------------------------------------------------------------- # [2] OPY INTEGRATION (Wright's consumer contract with OverPy/OPY) # Covers: diff --git a/Cargo.lock b/Cargo.lock index 9c6eb31..2c8ea06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1779,9 +1779,9 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "workshop-rs" -version = "0.1.5" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e741d6faadf9c0df949d76dbf4faa2af6799fae56ffbc780be975ed10855066a" +checksum = "fb8b6c25a292fbb06742c158966a628de20aecc543e218ddfef03219c431aad4" dependencies = [ "serde", "serde_json", @@ -1824,6 +1824,7 @@ dependencies = [ "sha2", "tar", "ureq", + "workshop-rs", "wright-driver", "wright-transform", ] diff --git a/Cargo.toml b/Cargo.toml index f57553a..2a4cf24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ repository = "https://github.com/wrightkit/wright" # parser, emitter, detection, validation, and Workshop IR. This is the single # released reference for the cutover — workspace crates consume it via # `workshop-rs.workspace = true`. -workshop-rs = "=0.1.5" +workshop-rs = "=0.1.8" libc = "0.2" serde = "1" serde_json = "1" diff --git a/crates/wright-analyzer/src/analysis.rs b/crates/wright-analyzer/src/analysis.rs index f75f5c6..c9318e1 100644 --- a/crates/wright-analyzer/src/analysis.rs +++ b/crates/wright-analyzer/src/analysis.rs @@ -643,6 +643,7 @@ fn visit_value_children(value: &Value, f: &mut impl FnMut(ValueId)) { } Value::Number { .. } | Value::String(_) + | Value::LocalizedString(_) | Value::Bool(_) | Value::Null | Value::Enum { .. } diff --git a/crates/wright-analyzer/src/symbols.rs b/crates/wright-analyzer/src/symbols.rs index a0ea771..618218a 100644 --- a/crates/wright-analyzer/src/symbols.rs +++ b/crates/wright-analyzer/src/symbols.rs @@ -537,6 +537,7 @@ impl<'a> Builder<'a> { } Value::Number { .. } | Value::String(_) + | Value::LocalizedString(_) | Value::Bool(_) | Value::Null | Value::Enum { .. } diff --git a/crates/wright-cli/Cargo.toml b/crates/wright-cli/Cargo.toml index 5d3f780..ed18bf8 100644 --- a/crates/wright-cli/Cargo.toml +++ b/crates/wright-cli/Cargo.toml @@ -28,3 +28,4 @@ wright-transform.workspace = true [dev-dependencies] insta = { version = "1", features = ["json"] } jsonschema = "0.18" +workshop-rs.workspace = true diff --git a/crates/wright-cli/tests/workshop_p0.rs b/crates/wright-cli/tests/workshop_p0.rs new file mode 100644 index 0000000..302b4d4 --- /dev/null +++ b/crates/wright-cli/tests/workshop_p0.rs @@ -0,0 +1,231 @@ +//! Product dogfood for the pinned raw Workshop corpus (#191). +//! +//! The source artifacts are owned by `workshop-rs`; this test consumes the +//! owner-provided expectation contract and invokes the actual `wright` binary +//! for both public product commands. + +use std::collections::BTreeSet; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use workshop_rs::p0::{P0_EXPECTATION, P0ResidualExpectation}; +use wright_driver::workshop_provider::{diagnostic_code, status_for_classification}; + +fn wright() -> &'static str { + env!("CARGO_BIN_EXE_wright") +} + +#[test] +#[ignore = "requires the released workshop-rs P0 fixture checkout"] +fn pinned_real_projects_run_check_and_lint_through_wright() { + let artifact_root = std::env::var_os("WRIGHTKIT_P0_ARTIFACT_DIR") + .map(PathBuf::from) + .unwrap_or_else(|| panic!("WRIGHTKIT_P0_ARTIFACT_DIR must be set for this test")); + assert!( + artifact_root.is_absolute() && artifact_root.is_dir(), + "WRIGHTKIT_P0_ARTIFACT_DIR must be an absolute directory: {}", + artifact_root.display() + ); + + for case in P0_EXPECTATION.cases { + let path = find_artifact_by_hash(&artifact_root, case.source_sha256, case.id); + let (check, check_success) = run_json("check", &path); + let (lint, lint_success) = run_json("lint", &path); + + let expected_diagnostics = case + .residuals + .iter() + .map(expected_diagnostic) + .collect::>(); + let actual_diagnostics = check["diagnostics"] + .as_array() + .expect("check diagnostics array") + .iter() + .map(|diagnostic| { + ( + diagnostic["code"] + .as_str() + .expect("diagnostic code") + .to_string(), + diagnostic["status"] + .as_str() + .expect("diagnostic status") + .to_string(), + ) + }) + .collect::>(); + assert_eq!( + actual_diagnostics, expected_diagnostics, + "{} check diagnostics", + case.id + ); + assert_eq!( + check["ok"], + serde_json::Value::Bool(expected_diagnostics.is_empty()), + "{} check status", + case.id + ); + assert_eq!( + check_success, + expected_diagnostics.is_empty(), + "{} check exit status", + case.id + ); + assert_eq!(check["command"], "check"); + + for diagnostic in check["diagnostics"].as_array().unwrap() { + assert_eq!( + diagnostic["span"]["path"], + path.to_string_lossy().as_ref(), + "{} diagnostics retain source attribution", + case.id + ); + } + + assert_eq!(lint["ok"], check["ok"], "{} lint status", case.id); + assert_eq!(lint_success, check_success, "{} lint exit status", case.id); + assert_eq!(lint["command"], "lint"); + assert_eq!(lint["result"]["program"]["origin"]["kind"], "workshop"); + assert_eq!( + lint["result"]["program"]["origin"]["locale"], + case.locale.to_ascii_lowercase(), + "{} locale detection", + case.id + ); + assert!( + lint["result"]["program"]["rules"] + .as_u64() + .is_some_and(|rules| rules > 0), + "{} lint must reach the canonical semantic program", + case.id + ); + assert_eq!( + lint["result"]["rules"].as_array().map(Vec::len), + Some(5), + "{} lint must execute every default rule", + case.id + ); + + let file_name = path + .file_name() + .expect("fixture filename") + .to_string_lossy(); + let findings = lint["result"]["findings"] + .as_array() + .expect("lint findings array"); + let review = findings + .iter() + .map(|finding| { + let code = finding["code"].as_str().expect("finding code"); + let evidence = finding["evidence"].as_str().expect("finding evidence"); + assert!( + matches!(evidence, "exact" | "heuristic" | "static-indicator"), + "{} finding {code} has unknown evidence class {evidence}", + case.id + ); + assert_eq!( + finding["span"]["path"], + file_name.as_ref(), + "{} finding {code} retains source attribution", + case.id + ); + serde_json::json!({ + "code": code, + "severity": finding["severity"], + "evidence": evidence, + "span": finding["span"], + }) + }) + .collect::>(); + println!( + "WORKSHOP_P0 {}", + serde_json::json!({ + "corpus": workshop_rs::p0::P0_CORPUS_ID, + "case": case.id, + "artifact": path, + "sourceSha256": case.source_sha256, + "check": { + "ok": check["ok"], + "diagnostics": check["diagnostics"], + }, + "lint": { + "ok": lint["ok"], + "rules": lint["result"]["rules"].as_array().map(Vec::len), + "findings": review, + }, + }) + ); + } +} + +fn run_json(command: &str, path: &Path) -> (serde_json::Value, bool) { + let output = Command::new(wright()) + .args([command, "--kind", "workshop"]) + .arg(path) + .args(["-f", "json"]) + .output() + .unwrap_or_else(|error| panic!("wright {command} failed to start: {error}")); + match serde_json::from_slice(&output.stdout) { + Ok(value) => (value, output.status.success()), + Err(error) => panic!( + "wright {command} returned invalid JSON (exit {}): {error}; stderr: {}", + output.status, + String::from_utf8_lossy(&output.stderr) + ), + } +} + +fn expected_diagnostic(residual: &P0ResidualExpectation) -> (String, String) { + ( + diagnostic_code(residual_kind_code(residual.kind), residual.identity), + serde_json::to_value(status_for_classification(residual.classification)) + .expect("provider status serializes") + .as_str() + .expect("provider status is a string") + .to_string(), + ) +} + +fn residual_kind_code(kind: workshop_rs::semantic::IncompletenessKind) -> &'static str { + match kind { + workshop_rs::semantic::IncompletenessKind::RawSetting => "raw-setting", + workshop_rs::semantic::IncompletenessKind::UnknownAction => "unknown-action", + workshop_rs::semantic::IncompletenessKind::UnknownValue => "unknown-value", + workshop_rs::semantic::IncompletenessKind::OpaqueAction => "opaque-action", + } +} + +fn find_artifact_by_hash(root: &Path, expected_hash: &str, case_id: &str) -> PathBuf { + let mut matches = Vec::new(); + collect_artifact_matches(root, expected_hash, &mut matches); + assert_eq!( + matches.len(), + 1, + "{case_id} must resolve exactly one artifact by owner-provided SHA-256, found {}", + matches.len() + ); + matches.pop().expect("one artifact match") +} + +fn collect_artifact_matches(root: &Path, expected_hash: &str, matches: &mut Vec) { + for entry in std::fs::read_dir(root).unwrap_or_else(|error| { + panic!( + "cannot read P0 artifact directory {}: {error}", + root.display() + ) + }) { + let entry = entry.expect("read P0 artifact directory entry"); + let path = entry.path(); + let file_type = entry.file_type().expect("inspect P0 artifact entry"); + if file_type.is_dir() { + collect_artifact_matches(&path, expected_hash, matches); + } else if file_type.is_file() { + let bytes = std::fs::read(&path).unwrap_or_else(|error| { + panic!("cannot read P0 artifact {}: {error}", path.display()) + }); + if wright_driver::sha256_hex(&bytes) == expected_hash { + matches.push(path); + } + } + } +} diff --git a/crates/wright-driver/src/session.rs b/crates/wright-driver/src/session.rs index 3fbbf38..d7d74f1 100644 --- a/crates/wright-driver/src/session.rs +++ b/crates/wright-driver/src/session.rs @@ -279,26 +279,11 @@ impl CompilerSession { override_locale.as_ref(), ) .map_err(|error| workshop_diag(error, resolved))?; - // Context-sensitive bare-enum resolution (#111): the #109 canonical - // signature metadata pins the expected domain for call arguments, so - // emitter-produced text like `Chase Global Variable Over Time(..., - // None)` reparses instead of failing on the ambiguous `None`. The - // canonical Workshop catalog supplies the remaining domains the - // manifest does not document (e.g. Create HUD Text's `HudReeval` - // reevaluation argument, #118). - let manifest = wright_opy::manifest::Manifest::builtin().map_err(|error| { - Diagnostic::error( - "manifest-error", - Stage::Frontend, - format!("cannot load the OPY semantic compatibility manifest: {error}"), - ) - })?; - let context = wright_core::signatures::ChainedExpectedDomain::new(manifest, &self.catalog); let program = workshop_rs::parser::parse_with_context( &resolved.text, &self.catalog, &locale, - &context, + &self.catalog, ) .map_err(|error| workshop_diag(error, resolved))?; self.progress(ProgressEvent::new(ProgressPhase::Validation)); @@ -542,10 +527,10 @@ impl CompilerSession { /// `lint`: load and produce the source identity, program summary, rule /// metadata, effective configuration, and findings (#98). /// - /// Lint findings are reported in `result.findings`, not in the envelope - /// diagnostics (like `analyze`). Rule enable/disable and severity come - /// from `self.config.lint`, the same configuration the CLI flags and - /// programmatic consumers set. + /// Lint rule findings are reported in `result.findings`; frontend and + /// Workshop semantic-completeness diagnostics remain in the envelope. + /// Rule enable/disable and severity come from `self.config.lint`, the same + /// configuration the CLI flags and programmatic consumers set. pub fn lint(&mut self) -> Envelope { let command = "lint"; let loaded = match self.load() { @@ -560,6 +545,7 @@ impl CompilerSession { // then run the shared semantic service over the lowered program. self.push_ostw_diagnostics(&loaded); } + self.attach_workshop_completeness(&loaded); let service = match self.service_with(&loaded, self.config.lint.clone()) { Ok(service) => service, Err(diagnostic) => { diff --git a/crates/wright-driver/src/workshop_provider.rs b/crates/wright-driver/src/workshop_provider.rs index 47d2a10..745ff0a 100644 --- a/crates/wright-driver/src/workshop_provider.rs +++ b/crates/wright-driver/src/workshop_provider.rs @@ -6,8 +6,6 @@ use wright_core::provider::{ Diagnostic as ProviderDiagnostic, LanguageProvider, ProviderError, Result as ProviderResult, Severity as ProviderSeverity, SourceSpan as ProviderSourceSpan, Status, }; -use wright_core::signatures::ExpectedDomain as WrightExpectedDomain; - /// Wright's in-process provider for localized raw Workshop source. pub struct WorkshopProvider { catalog: workshop_rs::catalog::Catalog, @@ -26,14 +24,8 @@ impl LanguageProvider for WorkshopProvider { fn check(&self, source: &str, path: &Path) -> ProviderResult> { let locale = workshop_rs::detect::resolve_locale(source, &self.catalog, None) .map_err(|error| ProviderError::new("workshop.locale", error.to_string()))?; - let manifest = wright_opy::manifest::Manifest::builtin() - .map_err(|error| ProviderError::new("workshop.manifest", error.to_string()))?; - let context = ProviderExpectedDomain { - manifest, - catalog: &self.catalog, - }; let program = - workshop_rs::parser::parse_with_context(source, &self.catalog, &locale, &context) + workshop_rs::parser::parse_with_context(source, &self.catalog, &locale, &self.catalog) .map_err(|error| ProviderError::new("workshop.parse", error.to_string()))?; program .validate() @@ -47,23 +39,6 @@ impl LanguageProvider for WorkshopProvider { } } -struct ProviderExpectedDomain<'a> { - manifest: &'a wright_opy::manifest::Manifest, - catalog: &'a workshop_rs::catalog::Catalog, -} - -impl workshop_rs::signatures::ExpectedDomain for ProviderExpectedDomain<'_> { - fn expected_domain(&self, catalog_id: &str, arg_index: usize) -> Option<&str> { - WrightExpectedDomain::expected_domain(self.manifest, catalog_id, arg_index).or_else(|| { - workshop_rs::signatures::ExpectedDomain::expected_domain( - self.catalog, - catalog_id, - arg_index, - ) - }) - } -} - fn map_issue(issue: workshop_rs::semantic::SemanticIssue, path: &Path) -> ProviderDiagnostic { let (kind_code, severity) = match issue.kind { workshop_rs::semantic::IncompletenessKind::RawSetting => { diff --git a/crates/wright-driver/tests/manifest_context.rs b/crates/wright-driver/tests/manifest_context.rs index b0edfc0..f977765 100644 --- a/crates/wright-driver/tests/manifest_context.rs +++ b/crates/wright-driver/tests/manifest_context.rs @@ -1,19 +1,16 @@ -//! Manifest-wired Workshop parse context (#109/#111). +//! Canonical Workshop signature context (#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`. +//! These regressions protect the consumer path against ambiguous bare `None` +//! members. The expected-domain context is supplied by the canonical +//! `workshop-rs` catalog; the catalog data and context-free parser behavior +//! remain 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::signatures::ExpectedDomain; use workshop_rs::wir; fn catalog() -> Catalog { @@ -24,10 +21,14 @@ 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") +fn parse_with_catalog(text: &str) -> wir::Program { + let catalog = catalog(); + parser::parse_with_context(text, &catalog, &en(), &catalog).expect("catalog context parses") +} + +fn round_trip_with_catalog(text: &str) -> roundtrip::RoundTripRecord { + let catalog = catalog(); + roundtrip::round_trip_with_context(text, &catalog, &en(), &catalog) } /// The last argument value of the first call action of a parsed program. @@ -47,9 +48,7 @@ fn context_pinned_ambiguous_none_resolves_via_canonical_signature() { // 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 program = parse_with_catalog(text); let value = enum_value_of_first_action(&program, 0); assert!( matches!(value, wir::Value::Enum { value_type, value } @@ -61,13 +60,11 @@ fn context_pinned_ambiguous_none_resolves_via_canonical_signature() { #[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 catalog'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 program = parse_with_catalog(text); let value = enum_value_of_first_action(&program, 0); assert!( matches!(value, wir::Value::Enum { value_type, value } @@ -83,9 +80,9 @@ fn wrong_domain_context_keeps_the_ambiguity_rejected() { // 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"); + let catalog = catalog(); + let error = parser::parse_with_context(text, &catalog, &en(), &catalog) + .expect_err("a non-matching expected domain must keep the ambiguity"); assert!( matches!(error, workshop_rs::WorkshopError::Unsupported { .. }), "expected a structured ambiguity: {error}" @@ -94,28 +91,20 @@ fn wrong_domain_context_keeps_the_ambiguity_rejected() { } #[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; +fn expected_domain_resolution_comes_from_the_canonical_catalog() { + let catalog = catalog(); // chaseOverTime: params [variable, destination, duration, reevaluation]. assert_eq!( - manifest.expected_domain("chaseOverTime", 3), + catalog.expected_domain("chaseOverTime", 3), Some("ChaseTimeReeval") ); - assert_eq!(manifest.expected_domain("chaseOverTime", 2), None); + assert_eq!(catalog.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") - ); + assert_eq!(catalog.expected_domain("setInvisibility", 0), None); + assert_eq!(catalog.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); + assert_eq!(catalog.expected_domain("noSuchAction", 0), None); + assert_eq!(catalog.expected_domain("chaseOverTime", 4), None); } #[test] @@ -125,7 +114,7 @@ fn emitter_chase_none_round_trips_through_the_shipped_path() { // 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()); + let record = round_trip_with_catalog(text); assert!( record.error.is_none(), "the pinned Chase None must round-trip: {:?}", @@ -139,7 +128,7 @@ 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()); + let record = round_trip_with_catalog(text); assert!( record.error.is_none(), "the pinned Invis None must round-trip: {:?}", @@ -152,10 +141,10 @@ fn emitter_set_invisible_none_round_trips_through_the_shipped_path() { 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 + // domain through the catalog'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()); + let record = round_trip_with_catalog(text); assert!( record.error.is_none(), "the pinned ChaseRateReeval None must round-trip: {:?}", @@ -164,7 +153,7 @@ fn emitter_chase_at_rate_none_round_trips_through_the_shipped_path() { 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()); + let record = round_trip_with_catalog(text); assert!( record.error.is_none(), "the pinned player ChaseRateReeval None must round-trip: {:?}", @@ -178,7 +167,7 @@ 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 + // frontend, emits through the catalog, reparses with the catalog // 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` @@ -195,11 +184,9 @@ fn chase_keyword_fixture_round_trips_through_the_shipped_path() { 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 + // and chase `None` members, so the catalog supplies // 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); + let record = round_trip_with_catalog(&emitted); assert!( record.error.is_none(), "the chase-keywords emission must round-trip: {:?}", @@ -226,14 +213,14 @@ fn context_chase_none_emission_is_a_fixed_point() { // 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()) + let first = parser::parse_with_context(text, &catalog, &en(), &catalog) .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()) + let reparsed = parser::parse_with_context(&emitted, &catalog, &en(), &catalog) .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/src/reconstruct.rs b/crates/wright-opy/src/reconstruct.rs index d9ff070..d37b7f6 100644 --- a/crates/wright-opy/src/reconstruct.rs +++ b/crates/wright-opy/src/reconstruct.rs @@ -1464,6 +1464,13 @@ impl<'a> Emitter<'a> { } } Value::String(value) => self.emit_string_literal(value), + Value::LocalizedString(value) => { + self.issue( + "unsupported-localized-string", + format!("localized Workshop preset string '{value}' has no OPY source representation"), + node.span, + ); + } Value::Bool(value) => { self.out.push_str(if *value { "true" } else { "false" }); } diff --git a/crates/wright-ostw/src/reconstruct.rs b/crates/wright-ostw/src/reconstruct.rs index 56e4c4e..3070bf4 100644 --- a/crates/wright-ostw/src/reconstruct.rs +++ b/crates/wright-ostw/src/reconstruct.rs @@ -675,6 +675,14 @@ impl<'a> Classifier<'a> { } } Value::String(_) | Value::Bool(_) | Value::Null | Value::EventPlayer => {} + Value::LocalizedString(value) => { + self.error(ReconstructError::at( + "reconstruct-unsupported-localized-string", + format!("localized-string:{value}"), + "localized Workshop preset strings have no OSTW source representation", + node.span, + )); + } Value::Array(elements) => { for element in elements { self.check_value(*element); @@ -1075,6 +1083,7 @@ impl<'a> Emitter<'a> { match &node.value { Value::Number { text, .. } => text.clone(), Value::String(value) => format!("\"{}\"", escape_string(value)), + Value::LocalizedString(_) => unreachable!("classified"), Value::Bool(true) => "true".to_string(), Value::Bool(false) => "false".to_string(), Value::Null => "null".to_string(), diff --git a/crates/wright-ostw/tests/differential.rs b/crates/wright-ostw/tests/differential.rs index 9d37322..15352dc 100644 --- a/crates/wright-ostw/tests/differential.rs +++ b/crates/wright-ostw/tests/differential.rs @@ -1183,6 +1183,7 @@ fn value_kind(value: &Value) -> &'static str { match value { Value::Number { .. } => "number", Value::String(_) => "string", + Value::LocalizedString(_) => "localizedString", Value::Bool(_) => "bool", Value::Null => "null", Value::Array(_) => "array", diff --git a/crates/wright-ostw/tests/reconstruct.rs b/crates/wright-ostw/tests/reconstruct.rs index caee758..af90a64 100644 --- a/crates/wright-ostw/tests/reconstruct.rs +++ b/crates/wright-ostw/tests/reconstruct.rs @@ -997,6 +997,7 @@ fn value_kind(value: &Value) -> &'static str { match value { Value::Number { .. } => "number", Value::String(_) => "string", + Value::LocalizedString(_) => "localizedString", Value::Bool(_) => "bool", Value::Null => "null", Value::Array(_) => "array", From 22dc4241f35e639fdf3f6481c2c40e584bb9aa4c Mon Sep 17 00:00:00 2001 From: Teakowa <27560638+Teakowa@users.noreply.github.com> Date: Mon, 24 Aug 2026 12:32:13 +0800 Subject: [PATCH 2/4] build(deps): consume released workshop-rs 0.1.9 --- .github/workflows/ci.yml | 2 +- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 91d06a9..eb96071 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -153,7 +153,7 @@ jobs: uses: actions/checkout@v7 with: repository: wrightkit/workshop-rs - ref: f2834ce09b83144070a5c8670695663be6f02daf # v0.1.8 + ref: 3d61bd4423924ca005d67f7c141a1866580f76fa # v0.1.9 path: workshop-rs-pinned - name: Install Rust toolchain diff --git a/Cargo.lock b/Cargo.lock index 2c8ea06..8bc6d60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1779,9 +1779,9 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "workshop-rs" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb8b6c25a292fbb06742c158966a628de20aecc543e218ddfef03219c431aad4" +checksum = "b566f0887d7ba1c9aafe12bcb51575bec59a2f37985c6a1103dbdd86028a23c4" dependencies = [ "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 2a4cf24..0d34219 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ repository = "https://github.com/wrightkit/wright" # parser, emitter, detection, validation, and Workshop IR. This is the single # released reference for the cutover — workspace crates consume it via # `workshop-rs.workspace = true`. -workshop-rs = "=0.1.8" +workshop-rs = "=0.1.9" libc = "0.2" serde = "1" serde_json = "1" From f1b4efd78ddf2961a47d9218a693ca53d9774f90 Mon Sep 17 00:00:00 2001 From: Teakowa <27560638+Teakowa@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:02:12 +0800 Subject: [PATCH 3/4] test(workshop): name integration coverage by capability --- .github/workflows/ci.yml | 16 ++++----- ...rkshop_p0.rs => workshop_real_projects.rs} | 33 +++++++++++-------- ...cross_validate.rs => workshop_contract.rs} | 30 +++++++++-------- 3 files changed, 44 insertions(+), 35 deletions(-) rename crates/wright-cli/tests/{workshop_p0.rs => workshop_real_projects.rs} (87%) rename crates/wright-driver/tests/{p0_cross_validate.rs => workshop_contract.rs} (77%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb96071..21c443c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -135,13 +135,13 @@ jobs: run: cargo test --locked --workspace --all-targets --all-features # ------------------------------------------------------------------------- - # [1a] RAW WORKSHOP PRODUCT DOGFOOD (Wright's released Workshop consumer) + # [1a] WORKSHOP REAL-PROJECT INTEGRATION (Wright's released Workshop consumer) # Runs the owner-pinned real-project inputs through the actual `wright` # check/lint commands. The source snapshots and residual expectations remain # owned by the released workshop-rs corpus. # ------------------------------------------------------------------------- workshop-integration: - name: Raw Workshop product dogfood + name: Workshop real-project integration needs: [paths, rust-quality] if: needs.paths.outputs.rust_core == 'true' runs-on: ubuntu-latest @@ -149,7 +149,7 @@ jobs: - name: Check out Wright uses: actions/checkout@v7 - - name: Check out released workshop-rs P0 corpus + - name: Check out released workshop-rs corpus uses: actions/checkout@v7 with: repository: wrightkit/workshop-rs @@ -174,13 +174,13 @@ jobs: - name: Run owner-contract cross-validation env: - WRIGHTKIT_P0_ARTIFACT_DIR: ${{ github.workspace }}/workshop-rs-pinned/crates/workshop-rs/tests/fixtures/real-projects - run: cargo test --locked -p wright-driver --test p0_cross_validate -- --ignored + WRIGHTKIT_WORKSHOP_CORPUS_DIR: ${{ github.workspace }}/workshop-rs-pinned/crates/workshop-rs/tests/fixtures/real-projects + run: cargo test --locked -p wright-driver --test workshop_contract -- --ignored - - name: Run Wright check and lint dogfood + - name: Run Wright check and lint on real projects env: - WRIGHTKIT_P0_ARTIFACT_DIR: ${{ github.workspace }}/workshop-rs-pinned/crates/workshop-rs/tests/fixtures/real-projects - run: cargo test --locked -p wright-cli --test workshop_p0 -- --ignored --nocapture + WRIGHTKIT_WORKSHOP_CORPUS_DIR: ${{ github.workspace }}/workshop-rs-pinned/crates/workshop-rs/tests/fixtures/real-projects + run: cargo test --locked -p wright-cli --test workshop_real_projects -- --ignored --nocapture # ------------------------------------------------------------------------- # [2] OPY INTEGRATION (Wright's consumer contract with OverPy/OPY) diff --git a/crates/wright-cli/tests/workshop_p0.rs b/crates/wright-cli/tests/workshop_real_projects.rs similarity index 87% rename from crates/wright-cli/tests/workshop_p0.rs rename to crates/wright-cli/tests/workshop_real_projects.rs index 302b4d4..bb95b98 100644 --- a/crates/wright-cli/tests/workshop_p0.rs +++ b/crates/wright-cli/tests/workshop_real_projects.rs @@ -1,4 +1,4 @@ -//! Product dogfood for the pinned raw Workshop corpus (#191). +//! Real-project integration coverage for the pinned Workshop corpus. //! //! The source artifacts are owned by `workshop-rs`; this test consumes the //! owner-provided expectation contract and invokes the actual `wright` binary @@ -8,7 +8,9 @@ use std::collections::BTreeSet; use std::path::{Path, PathBuf}; use std::process::Command; -use workshop_rs::p0::{P0_EXPECTATION, P0ResidualExpectation}; +use workshop_rs::p0::{ + P0_EXPECTATION as WORKSHOP_EXPECTATION, P0ResidualExpectation as WorkshopResidualExpectation, +}; use wright_driver::workshop_provider::{diagnostic_code, status_for_classification}; fn wright() -> &'static str { @@ -16,18 +18,18 @@ fn wright() -> &'static str { } #[test] -#[ignore = "requires the released workshop-rs P0 fixture checkout"] -fn pinned_real_projects_run_check_and_lint_through_wright() { - let artifact_root = std::env::var_os("WRIGHTKIT_P0_ARTIFACT_DIR") +#[ignore = "requires the released workshop-rs Workshop corpus checkout"] +fn real_projects_run_check_and_lint_through_wright() { + let artifact_root = std::env::var_os("WRIGHTKIT_WORKSHOP_CORPUS_DIR") .map(PathBuf::from) - .unwrap_or_else(|| panic!("WRIGHTKIT_P0_ARTIFACT_DIR must be set for this test")); + .unwrap_or_else(|| panic!("WRIGHTKIT_WORKSHOP_CORPUS_DIR must be set for this test")); assert!( artifact_root.is_absolute() && artifact_root.is_dir(), - "WRIGHTKIT_P0_ARTIFACT_DIR must be an absolute directory: {}", + "WRIGHTKIT_WORKSHOP_CORPUS_DIR must be an absolute directory: {}", artifact_root.display() ); - for case in P0_EXPECTATION.cases { + for case in WORKSHOP_EXPECTATION.cases { let path = find_artifact_by_hash(&artifact_root, case.source_sha256, case.id); let (check, check_success) = run_json("check", &path); let (lint, lint_success) = run_json("lint", &path); @@ -138,7 +140,7 @@ fn pinned_real_projects_run_check_and_lint_through_wright() { }) .collect::>(); println!( - "WORKSHOP_P0 {}", + "WORKSHOP_CORPUS {}", serde_json::json!({ "corpus": workshop_rs::p0::P0_CORPUS_ID, "case": case.id, @@ -175,7 +177,7 @@ fn run_json(command: &str, path: &Path) -> (serde_json::Value, bool) { } } -fn expected_diagnostic(residual: &P0ResidualExpectation) -> (String, String) { +fn expected_diagnostic(residual: &WorkshopResidualExpectation) -> (String, String) { ( diagnostic_code(residual_kind_code(residual.kind), residual.identity), serde_json::to_value(status_for_classification(residual.classification)) @@ -210,18 +212,21 @@ fn find_artifact_by_hash(root: &Path, expected_hash: &str, case_id: &str) -> Pat fn collect_artifact_matches(root: &Path, expected_hash: &str, matches: &mut Vec) { for entry in std::fs::read_dir(root).unwrap_or_else(|error| { panic!( - "cannot read P0 artifact directory {}: {error}", + "cannot read Workshop corpus directory {}: {error}", root.display() ) }) { - let entry = entry.expect("read P0 artifact directory entry"); + let entry = entry.expect("read Workshop corpus directory entry"); let path = entry.path(); - let file_type = entry.file_type().expect("inspect P0 artifact entry"); + let file_type = entry.file_type().expect("inspect Workshop corpus entry"); if file_type.is_dir() { collect_artifact_matches(&path, expected_hash, matches); } else if file_type.is_file() { let bytes = std::fs::read(&path).unwrap_or_else(|error| { - panic!("cannot read P0 artifact {}: {error}", path.display()) + panic!( + "cannot read Workshop corpus artifact {}: {error}", + path.display() + ) }); if wright_driver::sha256_hex(&bytes) == expected_hash { matches.push(path); diff --git a/crates/wright-driver/tests/p0_cross_validate.rs b/crates/wright-driver/tests/workshop_contract.rs similarity index 77% rename from crates/wright-driver/tests/p0_cross_validate.rs rename to crates/wright-driver/tests/workshop_contract.rs index d9fa609..20ec427 100644 --- a/crates/wright-driver/tests/p0_cross_validate.rs +++ b/crates/wright-driver/tests/workshop_contract.rs @@ -2,25 +2,25 @@ use std::collections::BTreeSet; use std::path::{Path, PathBuf}; use sha2::{Digest, Sha256}; -use workshop_rs::p0::P0_EXPECTATION; +use workshop_rs::p0::P0_EXPECTATION as WORKSHOP_EXPECTATION; use workshop_rs::semantic::{IncompletenessKind, ResidualClassification}; use wright_core::provider::{LanguageProvider, Status}; use wright_driver::{WorkshopProvider, workshop_provider}; #[test] -#[ignore = "requires the pinned external P0 artifact directory"] -fn provider_matches_workshop_rs_p0_expectations() { - let artifact_root = std::env::var_os("WRIGHTKIT_P0_ARTIFACT_DIR") +#[ignore = "requires the pinned external Workshop corpus directory"] +fn provider_matches_workshop_contract() { + let artifact_root = std::env::var_os("WRIGHTKIT_WORKSHOP_CORPUS_DIR") .map(PathBuf::from) - .unwrap_or_else(|| panic!("WRIGHTKIT_P0_ARTIFACT_DIR must be set for this test")); + .unwrap_or_else(|| panic!("WRIGHTKIT_WORKSHOP_CORPUS_DIR must be set for this test")); assert!( artifact_root.is_absolute() && artifact_root.is_dir(), - "WRIGHTKIT_P0_ARTIFACT_DIR must be an absolute directory: {}", + "WRIGHTKIT_WORKSHOP_CORPUS_DIR must be an absolute directory: {}", artifact_root.display() ); let provider = WorkshopProvider::new().expect("provider initializes"); - for case in P0_EXPECTATION.cases { + for case in WORKSHOP_EXPECTATION.cases { let path = find_artifact_by_hash(&artifact_root, case.source_sha256, case.id); let bytes = std::fs::read(&path).unwrap_or_else(|error| panic!("{}: {error}", path.display())); @@ -30,7 +30,7 @@ fn provider_matches_workshop_rs_p0_expectations() { "{} source hash", case.id ); - let source = String::from_utf8(bytes).expect("P0 source is UTF-8"); + let source = String::from_utf8(bytes).expect("Workshop source is UTF-8"); let diagnostics = provider .check(&source, &path) .unwrap_or_else(|error| panic!("{} provider failure: {error}", case.id)); @@ -73,14 +73,14 @@ fn find_artifact_by_hash(root: &Path, expected_hash: &str, case_id: &str) -> Pat fn collect_artifact_matches(root: &Path, expected_hash: &str, matches: &mut Vec) { let entries = std::fs::read_dir(root).unwrap_or_else(|error| { panic!( - "cannot read P0 artifact directory {}: {error}", + "cannot read Workshop corpus directory {}: {error}", root.display() ) }); for entry in entries { - let entry = entry.expect("read P0 artifact directory entry"); + let entry = entry.expect("read Workshop corpus directory entry"); let path = entry.path(); - let file_type = entry.file_type().expect("inspect P0 artifact entry"); + let file_type = entry.file_type().expect("inspect Workshop corpus entry"); if file_type.is_dir() { collect_artifact_matches(&path, expected_hash, matches); continue; @@ -88,8 +88,12 @@ fn collect_artifact_matches(root: &Path, expected_hash: &str, matches: &mut Vec< if !file_type.is_file() { continue; } - let bytes = std::fs::read(&path) - .unwrap_or_else(|error| panic!("cannot read P0 artifact {}: {error}", path.display())); + let bytes = std::fs::read(&path).unwrap_or_else(|error| { + panic!( + "cannot read Workshop corpus artifact {}: {error}", + path.display() + ) + }); if format!("{:x}", Sha256::digest(&bytes)) == expected_hash { matches.push(path); } From 1ef4526791ab2eea3f04236e352481363c976b86 Mon Sep 17 00:00:00 2001 From: Teakowa <27560638+Teakowa@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:10:32 +0800 Subject: [PATCH 4/4] test(workshop): remove brittle integration assumptions --- Cargo.lock | 1 + crates/wright-cli/Cargo.toml | 1 + .../tests/workshop_real_projects.rs | 24 ++++++++++++++++--- ...ntext.rs => workshop_signature_context.rs} | 10 ++++---- 4 files changed, 28 insertions(+), 8 deletions(-) rename crates/wright-driver/tests/{manifest_context.rs => workshop_signature_context.rs} (97%) diff --git a/Cargo.lock b/Cargo.lock index 8bc6d60..1e3d221 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1825,6 +1825,7 @@ dependencies = [ "tar", "ureq", "workshop-rs", + "wright-analyzer", "wright-driver", "wright-transform", ] diff --git a/crates/wright-cli/Cargo.toml b/crates/wright-cli/Cargo.toml index ed18bf8..ffeb10f 100644 --- a/crates/wright-cli/Cargo.toml +++ b/crates/wright-cli/Cargo.toml @@ -28,4 +28,5 @@ wright-transform.workspace = true [dev-dependencies] insta = { version = "1", features = ["json"] } jsonschema = "0.18" +wright-analyzer.workspace = true workshop-rs.workspace = true diff --git a/crates/wright-cli/tests/workshop_real_projects.rs b/crates/wright-cli/tests/workshop_real_projects.rs index bb95b98..80d62d9 100644 --- a/crates/wright-cli/tests/workshop_real_projects.rs +++ b/crates/wright-cli/tests/workshop_real_projects.rs @@ -11,6 +11,7 @@ use std::process::Command; use workshop_rs::p0::{ P0_EXPECTATION as WORKSHOP_EXPECTATION, P0ResidualExpectation as WorkshopResidualExpectation, }; +use wright_analyzer::registry::LintRegistry; use wright_driver::workshop_provider::{diagnostic_code, status_for_classification}; fn wright() -> &'static str { @@ -101,10 +102,27 @@ fn real_projects_run_check_and_lint_through_wright() { "{} lint must reach the canonical semantic program", case.id ); + let lint_rules = lint["result"]["rules"] + .as_array() + .expect("lint rules array"); + let expected_rule_ids = LintRegistry::default() + .rules() + .map(|rule| rule.id) + .collect::>(); + let actual_rule_ids = lint_rules + .iter() + .map(|rule| rule["id"].as_str().expect("lint rule id")) + .collect::>(); assert_eq!( - lint["result"]["rules"].as_array().map(Vec::len), - Some(5), - "{} lint must execute every default rule", + actual_rule_ids, expected_rule_ids, + "{} lint must report the authoritative default rule registry", + case.id + ); + assert!( + lint_rules + .iter() + .all(|rule| rule["enabled"].as_bool() == Some(true)), + "{} lint must enable every default rule", case.id ); diff --git a/crates/wright-driver/tests/manifest_context.rs b/crates/wright-driver/tests/workshop_signature_context.rs similarity index 97% rename from crates/wright-driver/tests/manifest_context.rs rename to crates/wright-driver/tests/workshop_signature_context.rs index f977765..6780add 100644 --- a/crates/wright-driver/tests/manifest_context.rs +++ b/crates/wright-driver/tests/workshop_signature_context.rs @@ -43,7 +43,7 @@ fn enum_value_of_first_action(program: &wir::Program, action_index: usize) -> &w } #[test] -fn context_pinned_ambiguous_none_resolves_via_canonical_signature() { +fn catalog_signature_resolves_ambiguous_none_for_chase() { // #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. @@ -58,7 +58,7 @@ fn context_pinned_ambiguous_none_resolves_via_canonical_signature() { } #[test] -fn context_pinned_ambiguous_none_resolves_for_set_invisible() { +fn catalog_signature_resolves_ambiguous_none_for_set_invisible() { // #111: `Set Invisible(Event Player, None)` reparses to Invis.NONE. The // the catalog's setInvisibility is a member action, so Workshop text places // the receiver as argument 0 and the signature-pinned parameter at @@ -74,7 +74,7 @@ fn context_pinned_ambiguous_none_resolves_for_set_invisible() { } #[test] -fn wrong_domain_context_keeps_the_ambiguity_rejected() { +fn mismatched_catalog_signature_keeps_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, @@ -196,7 +196,7 @@ fn chase_keyword_fixture_round_trips_through_the_shipped_path() { } #[test] -fn context_free_chase_none_stays_a_documented_exception() { +fn context_free_parser_preserves_ambiguous_none_boundary() { // 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. @@ -208,7 +208,7 @@ fn context_free_chase_none_stays_a_documented_exception() { } #[test] -fn context_chase_none_emission_is_a_fixed_point() { +fn catalog_signature_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); } }";