From f5d216c4dfbf9ad4f658c22405a2c0654e578d58 Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 22 May 2026 13:32:17 -0700 Subject: [PATCH 1/3] fix(cli): reject module target alias order collisions --- CHANGELOG.md | 4 ++ docs/BEARING.md | 14 +++--- packages/wesley-cli/src/commands/compile.mjs | 8 ++++ .../wesley-cli/test/module-loading.test.mjs | 47 +++++++++++++++++++ 4 files changed, 67 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 972a2e95..3ee485c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,10 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve ### Fixed +- **Module target alias collision order**: `wesley compile` now rejects a + module target name that conflicts with an alias registered by an earlier + loaded module, closing an order-dependent gap in module-owned target + dispatch. - **Parity sentinel evidence contract**: `pnpm parity:ir --json` now records canonical projected legacy and Rust bytes, and Rust L1 hash checks remove top-level metadata before comparing against `wesley schema hash` or tracked diff --git a/docs/BEARING.md b/docs/BEARING.md index f03951f0..4f01e618 100644 --- a/docs/BEARING.md +++ b/docs/BEARING.md @@ -111,15 +111,17 @@ Current evidence now includes complete v0.0.5 publication proof, an expanded Rust L1 corpus for directive-heavy SDL, schema extensions, legacy aliases, and invalid duplicate-directive coverage, `pnpm parity:ir` for the `js-table-vs-rust-table.v0` compatibility projection over the first -table-compatible sentinel corpus, and the domain-empty ownership packet in -`0014`. +table-compatible sentinel corpus, the domain-empty ownership packet in `0014`, +and executable module-target dispatch coverage for no-module diagnostics, +default target discovery, requested-target validation, duplicate target +rejection, and alias conflicts in both registration orders. The next pulls are: -1. Prove module-owned target dispatch with hermetic fixture modules: - no-module diagnostics, explicit `wesley.targets`, duplicate target - rejection, alias conflict rejection, and target selection without built-in - product or database names. +1. Expand the fixture-module zoo only where it adds new boundary evidence: + target dispatch already rejects missing modules, invalid product/database + target names, duplicate names, and aliases that collide before or after the + owning target loads. 2. Pull the remaining Rust IR contract fixture card into design so fixture classes, canonical byte rules, diagnostics, and performance evidence are release-scoped instead of floating in `asap/`. diff --git a/packages/wesley-cli/src/commands/compile.mjs b/packages/wesley-cli/src/commands/compile.mjs index 559cb856..e6d21416 100644 --- a/packages/wesley-cli/src/commands/compile.mjs +++ b/packages/wesley-cli/src/commands/compile.mjs @@ -130,6 +130,14 @@ function addTargetDescriptor({ `Compile target "${name}" was registered by both module "${existing.moduleName}" and module "${moduleName}".` ); } + if (aliases.has(name)) { + const existingTargetName = aliases.get(name); + const existing = byName.get(existingTargetName); + throw new WesleyError( + 'INVALID_TARGET_CAPABILITY', + `Compile target "${name}" from module "${moduleName}" conflicts with alias registered by target "${existing?.name ?? existingTargetName}" from module "${existing?.moduleName ?? ''}".` + ); + } ordered.push(descriptor); byName.set(name, descriptor); diff --git a/packages/wesley-cli/test/module-loading.test.mjs b/packages/wesley-cli/test/module-loading.test.mjs index 5100f5f4..ea22b79b 100644 --- a/packages/wesley-cli/test/module-loading.test.mjs +++ b/packages/wesley-cli/test/module-loading.test.mjs @@ -586,3 +586,50 @@ test('program rejects compile target aliases that collide across modules', async rmSync(tempDir, { recursive: true, force: true }); } }); + +test('program rejects compile target aliases that collide with later target names', async () => { + const tempDir = mkdtempSync(path.join(os.tmpdir(), 'wesley-module-compile-alias-target-collision-')); + const io = createIo(); + + try { + writeFileSync(path.join(tempDir, 'schema.graphql'), 'type Todo { id: ID! }\n'); + const firstModule = writeCompileTargetModule(tempDir, { + fileName: 'alias-first-module.mjs', + moduleName: 'alias-first-module', + targetName: 'first-target', + aliases: ['later-target'] + }); + const secondModule = writeCompileTargetModule(tempDir, { + fileName: 'target-later-module.mjs', + moduleName: 'target-later-module', + targetName: 'later-target' + }); + + const exitCode = await program( + ['node', 'wesley', '--json', 'compile', '--schema', 'schema.graphql', '--dry-run'], + { + cwd: tempDir, + env: { + WESLEY_MODULES: [firstModule, secondModule].join(path.delimiter) + }, + fs: { + async read(targetPath) { + return readFileSync(path.resolve(tempDir, targetPath), 'utf8'); + } + }, + stdout: io.stdout, + stderr: io.stderr, + logger: nullLogger + } + ); + + assert.notEqual(exitCode, 0); + const payload = JSON.parse(io.readStderr()); + assert.equal(payload.code, 'INVALID_TARGET_CAPABILITY'); + assert.match(payload.error, /later-target/); + assert.match(payload.error, /alias-first-module/); + assert.match(payload.error, /target-later-module/); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } +}); From 5da084166596459cfa8515f6428165636eaa49be Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 22 May 2026 13:35:09 -0700 Subject: [PATCH 2/3] docs(ir): pull fixture contract into parity design --- CHANGELOG.md | 3 + docs/BEARING.md | 12 +- .../rust-core-and-wasm-capability-abi.md | 2 +- ...wesley-core-rs-ir-contract-and-fixtures.md | 104 ++++++++++++++++++ .../rust-ir-parity-sentinel.md | 11 +- docs/method/backlog/asap/README.md | 3 +- ...wesley-core-rs-ir-contract-and-fixtures.md | 35 ------ ...EVIDENCE_rust-core-performance-baseline.md | 2 +- ...URCE_wesley-core-rs-parser-parity-spike.md | 2 +- 9 files changed, 123 insertions(+), 51 deletions(-) create mode 100644 docs/design/0013-rust-ir-parity-sentinel/SOURCE_wesley-core-rs-ir-contract-and-fixtures.md delete mode 100644 docs/method/backlog/asap/SOURCE_wesley-core-rs-ir-contract-and-fixtures.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ee485c5..ec3aa291 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve ### Added +- **Rust IR fixture contract note**: Moved the core-rs IR contract and fixture + backlog card into the active `0013` design packet, naming the v0.0.6 fixture + classes, canonical byte rules, diagnostics contract, and repo evidence. - **Domain-empty core boundary packet**: Pulled the boundary card into design packet `0014`, defining what generic Wesley owns, what external modules or sibling repos own, and the first docs/dispatch audit that keeps product and diff --git a/docs/BEARING.md b/docs/BEARING.md index 4f01e618..f1d4c653 100644 --- a/docs/BEARING.md +++ b/docs/BEARING.md @@ -114,7 +114,8 @@ invalid duplicate-directive coverage, `pnpm parity:ir` for the table-compatible sentinel corpus, the domain-empty ownership packet in `0014`, and executable module-target dispatch coverage for no-module diagnostics, default target discovery, requested-target validation, duplicate target -rejection, and alias conflicts in both registration orders. +rejection, alias conflicts in both registration orders, and the Rust IR +fixture contract now housed under the active `0013` packet. The next pulls are: @@ -122,16 +123,13 @@ The next pulls are: target dispatch already rejects missing modules, invalid product/database target names, duplicate names, and aliases that collide before or after the owning target loads. -2. Pull the remaining Rust IR contract fixture card into design so fixture - classes, canonical byte rules, diagnostics, and performance evidence are - release-scoped instead of floating in `asap/`. -3. Stabilize invalid-SDL diagnostic contracts with executable coverage for +2. Stabilize invalid-SDL diagnostic contracts with executable coverage for codes and spans where available, while naming what remains intentionally unstable. -4. Define the next parity projection before broadening `pnpm parity:ir` beyond +3. Define the next parity projection before broadening `pnpm parity:ir` beyond table-compatible SDL. Schema extensions and non-table L1 facts need a fair projection before they become JS/Rust parity evidence. -5. Capture a Rust core performance baseline over the canonical corpus after +4. Capture a Rust core performance baseline over the canonical corpus after the fixture and projection boundaries are named. Do not pull `OWN_ninelives-resilience-integration.md` until the module boundary diff --git a/docs/design/0009-rust-core-and-wasm-capability-abi/rust-core-and-wasm-capability-abi.md b/docs/design/0009-rust-core-and-wasm-capability-abi/rust-core-and-wasm-capability-abi.md index b1f7a47c..6c4fd09f 100644 --- a/docs/design/0009-rust-core-and-wasm-capability-abi/rust-core-and-wasm-capability-abi.md +++ b/docs/design/0009-rust-core-and-wasm-capability-abi/rust-core-and-wasm-capability-abi.md @@ -633,7 +633,7 @@ doctrine only. Follow-on slices to create: -- [Wesley core-rs IR contract and fixtures](../../method/backlog/asap/SOURCE_wesley-core-rs-ir-contract-and-fixtures.md) +- [Wesley core-rs IR contract and fixtures](../0013-rust-ir-parity-sentinel/SOURCE_wesley-core-rs-ir-contract-and-fixtures.md) - [Wesley core-rs parser parity spike](../../method/backlog/up-next/SOURCE_wesley-core-rs-parser-parity-spike.md) - [WASM host function governance](../../method/backlog/up-next/RUNTIME_wasm-host-function-governance.md) - [WASM capability versioning and state](../../method/backlog/up-next/RUNTIME_wasm-capability-versioning-and-state.md) diff --git a/docs/design/0013-rust-ir-parity-sentinel/SOURCE_wesley-core-rs-ir-contract-and-fixtures.md b/docs/design/0013-rust-ir-parity-sentinel/SOURCE_wesley-core-rs-ir-contract-and-fixtures.md new file mode 100644 index 00000000..3a150fbc --- /dev/null +++ b/docs/design/0013-rust-ir-parity-sentinel/SOURCE_wesley-core-rs-ir-contract-and-fixtures.md @@ -0,0 +1,104 @@ +--- +title: Wesley core-rs IR contract and fixtures +legend: SOURCE +packet: 0013-rust-ir-parity-sentinel +status: active +release: v0.0.6 +--- + +# Wesley core-rs IR contract and fixtures + +## Why now + +The Rust core design is only useful if Rust can reproduce today's compiler +truth. This note pulls the old ASAP backlog card into the active +`0013-rust-ir-parity-sentinel` packet so fixture classes, canonical bytes, +diagnostics, and performance evidence are release-scoped instead of floating in +the queue. + +## Hill + +A maintainer can run one fixture command and compare current JS lowering against +the Rust lowering target using canonical JSON bytes and clear mismatch +diagnostics, without treating product or database semantics as Wesley core. + +## Contract Surface + +The fixture contract is owned by generic Wesley compiler truth: + +- Rust L1 fixtures live under `test/fixtures/ir-parity/`. +- Invalid SDL fixtures live under `test/fixtures/ir-parity-invalid/`. +- Rust golden regeneration is `pnpm fixtures:ir`. +- JS/Rust parity evidence is `pnpm parity:ir`. +- The first parity projection is `js-table-vs-rust-table.v0`. +- The Rust command surface is `cargo run --quiet -p wesley-cli -- schema ...`. +- The legacy JS anchors are the parse, lower, canonicalize, registry-hash, and + canonical JSON functions named in + [Phase 0: IR Truth Manifest](../0009-rust-core-and-wasm-capability-abi/phase-0-ir-truth-manifest.md). + +## Fixture Classes + +The v0.0.6 corpus must keep these classes explicit: + +- **Small table SDL**: a narrow compatibility fixture for fast smoke feedback. +- **Medium table SDL**: broader table, directive, and relation coverage. +- **Large SDL**: scale coverage for regeneration and later performance + baselines, not first-pass parity proof. +- **Directive-heavy SDL**: directive argument and canonical alias truth. +- **Legacy alias SDL**: compatibility input for supported core aliases. +- **Schema-extension SDL**: Rust L1 coverage that waits for a fair non-table + parity projection before default sentinel admission. +- **Invalid SDL**: negative diagnostics with stable codes and spans where the + lowerer can provide them. + +## Canonical Bytes + +- Canonical JSON is UTF-8, newline-free, sorted-object-key JSON. +- Projection-created arrays sort only when the projection contract says they + are unordered facts; authored or semantic array order is preserved. +- Top-level Rust L1 `metadata` is removed before parity-sensitive hashing. +- Directive names must be canonicalized by lowerers, not rewritten by the + comparator. +- Repeated custom directives remain ordered values unless the lowerer rejects + them as invalid under a named rule. +- Tracked `*.l1.hash` sidecars are Rust golden evidence, not JS/Rust parity + evidence. + +## Diagnostics + +Invalid SDL coverage should record: + +- a stable error code where the Rust lowerer exposes one +- the fixture path +- a stable message shape that names the violated rule +- line and column spans where the parser or lowerer can preserve them +- an explicit note when a span is unavailable or intentionally unstable + +The diagnostic contract is not allowed to hide invalid inputs by normalizing +them into fixture outputs. + +## Done looks like + +- current JS parse/lower/hash functions are listed in the truth manifest +- canonical IR JSON byte rules are written down here and in the sentinel packet +- fixture corpus covers small, medium, large, directive-heavy, invalid, + legacy-alias, and schema-extension SDL cases +- expected diagnostics include stable codes and spans where available +- baseline Rust lowering time and memory are captured for the fixture corpus +- parity failure output shows the first semantic mismatch, not just a raw diff +- packet `0009`, packet `0013`, and follow-on backlog items link to this note + +## Repo Evidence + +- `docs/design/0009-rust-core-and-wasm-capability-abi/rust-core-and-wasm-capability-abi.md` +- `docs/design/0009-rust-core-and-wasm-capability-abi/phase-0-ir-truth-manifest.md` +- `docs/design/0013-rust-ir-parity-sentinel/rust-ir-parity-sentinel.md` +- `docs/RustCore.md` +- `crates/wesley-core/src/` +- `crates/wesley-core/tests/` +- `crates/wesley-cli/tests/` +- `schemas/` +- `scripts/generate-ir-fixtures.mjs` +- `scripts/check-ir-parity.mjs` +- `test/fixtures/ir-parity/` +- `test/fixtures/ir-parity-invalid/` diff --git a/docs/design/0013-rust-ir-parity-sentinel/rust-ir-parity-sentinel.md b/docs/design/0013-rust-ir-parity-sentinel/rust-ir-parity-sentinel.md index eb02f4bb..927be17c 100644 --- a/docs/design/0013-rust-ir-parity-sentinel/rust-ir-parity-sentinel.md +++ b/docs/design/0013-rust-ir-parity-sentinel/rust-ir-parity-sentinel.md @@ -181,11 +181,14 @@ the default v0 sentinel corpus. The former still carries non-table Rust L1 coverage that needs a separate projection before it is fair parity evidence; the latter is scale coverage rather than the first compatibility sentinel. -The preceding design slice also pulled the backlog card into design, expanded -the Rust L1 corpus, and closed one blocker the sentinel would otherwise expose +The supporting +[core-rs IR contract and fixtures note](./SOURCE_wesley-core-rs-ir-contract-and-fixtures.md) +records the release-scoped fixture classes, canonical byte rules, diagnostic +contract, and repo evidence. The preceding design slice also expanded the Rust +L1 corpus and closed one blocker the sentinel would otherwise expose immediately: Rust L1 lowering now canonicalizes the core Wesley directive -aliases before writing semantic IR, rejects duplicate canonical core -directives, and preserves repeated custom directives as ordered values. +aliases before writing semantic IR, rejects duplicate canonical core directives, +and preserves repeated custom directives as ordered values. ## Playback Questions diff --git a/docs/method/backlog/asap/README.md b/docs/method/backlog/asap/README.md index 20d8e657..f0cb463d 100644 --- a/docs/method/backlog/asap/README.md +++ b/docs/method/backlog/asap/README.md @@ -13,5 +13,4 @@ database lanes as Wesley features. Current near-term pulls: -1. `SOURCE_wesley-core-rs-ir-contract-and-fixtures.md` -2. `OWN_ninelives-resilience-integration.md` +1. `OWN_ninelives-resilience-integration.md` diff --git a/docs/method/backlog/asap/SOURCE_wesley-core-rs-ir-contract-and-fixtures.md b/docs/method/backlog/asap/SOURCE_wesley-core-rs-ir-contract-and-fixtures.md deleted file mode 100644 index caf9cedc..00000000 --- a/docs/method/backlog/asap/SOURCE_wesley-core-rs-ir-contract-and-fixtures.md +++ /dev/null @@ -1,35 +0,0 @@ -# Wesley core-rs IR contract and fixtures - -- Lane: `asap` -- Legend: `SOURCE` - -## Why now - -The Rust core design is only useful if Rust can reproduce today's compiler -truth. The next move is Phase 0: freeze the canonical IR contract and fixture -corpus before any rewrite work starts. - -## Hill - -A maintainer can run one fixture command and compare current JS lowering against -the future Rust lowering target using canonical JSON bytes and clear mismatch -diagnostics. - -## Done looks like - -- current JS parse/lower/hash functions are listed -- canonical IR JSON byte rules are written down -- fixture corpus covers small, medium, large, directive-heavy, invalid, and - schema-extension SDL cases -- expected diagnostics include stable codes and spans where available -- baseline JS lowering time and memory are captured for the fixture corpus -- parity failure output shows the first semantic mismatch, not just a raw diff -- the fixture corpus is linked from design packet `0009` - -## Repo Evidence - -- `docs/design/0009-rust-core-and-wasm-capability-abi/rust-core-and-wasm-capability-abi.md` -- `docs/RustCore.md` -- `packages/wesley-core/src/` -- `packages/wesley-core/test/` -- `schemas/` diff --git a/docs/method/backlog/up-next/EVIDENCE_rust-core-performance-baseline.md b/docs/method/backlog/up-next/EVIDENCE_rust-core-performance-baseline.md index a684d8ec..2952ed29 100644 --- a/docs/method/backlog/up-next/EVIDENCE_rust-core-performance-baseline.md +++ b/docs/method/backlog/up-next/EVIDENCE_rust-core-performance-baseline.md @@ -27,5 +27,5 @@ for small, medium, and large schemas before any default cutover. ## Repo Evidence - `docs/design/0009-rust-core-and-wasm-capability-abi/rust-core-and-wasm-capability-abi.md` -- `docs/method/backlog/asap/SOURCE_wesley-core-rs-ir-contract-and-fixtures.md` +- `docs/design/0013-rust-ir-parity-sentinel/SOURCE_wesley-core-rs-ir-contract-and-fixtures.md` - `packages/wesley-core/test/` diff --git a/docs/method/backlog/up-next/SOURCE_wesley-core-rs-parser-parity-spike.md b/docs/method/backlog/up-next/SOURCE_wesley-core-rs-parser-parity-spike.md index 8eb44ac3..4db20710 100644 --- a/docs/method/backlog/up-next/SOURCE_wesley-core-rs-parser-parity-spike.md +++ b/docs/method/backlog/up-next/SOURCE_wesley-core-rs-parser-parity-spike.md @@ -28,6 +28,6 @@ of library vibes. ## Repo Evidence - `docs/design/0009-rust-core-and-wasm-capability-abi/rust-core-and-wasm-capability-abi.md` -- `docs/method/backlog/asap/SOURCE_wesley-core-rs-ir-contract-and-fixtures.md` +- `docs/design/0013-rust-ir-parity-sentinel/SOURCE_wesley-core-rs-ir-contract-and-fixtures.md` - `packages/wesley-core/src/` - `packages/wesley-core/test/` From f33f9f78b951b35891ff59e43062691cedafd527 Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 22 May 2026 13:39:48 -0700 Subject: [PATCH 3/3] fix(rust): expose stable lowering diagnostics --- CHANGELOG.md | 4 ++ crates/wesley-core/src/adapters/apollo.rs | 54 +++++++++++-------- crates/wesley-core/src/domain/error.rs | 33 ++++++++++++ .../wesley-core/tests/lowering_validation.rs | 25 +++++++++ docs/BEARING.md | 17 +++--- ...wesley-core-rs-ir-contract-and-fixtures.md | 10 +++- 6 files changed, 113 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec3aa291..5b857449 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,10 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve ### Fixed +- **Rust invalid-SDL diagnostics**: `WesleyError` now exposes a stable + diagnostic object with machine-readable codes, severity, and parser + line/column spans where Apollo provides a byte index; semantic lowering + errors keep stable codes while source spans remain intentionally absent. - **Module target alias collision order**: `wesley compile` now rejects a module target name that conflicts with an alias registered by an earlier loaded module, closing an order-dependent gap in module-owned target diff --git a/crates/wesley-core/src/adapters/apollo.rs b/crates/wesley-core/src/adapters/apollo.rs index 129b82c8..c1e6e4ee 100644 --- a/crates/wesley-core/src/adapters/apollo.rs +++ b/crates/wesley-core/src/adapters/apollo.rs @@ -14,7 +14,7 @@ use crate::domain::optic::{ }; use crate::domain::schema_delta::{diff_schema_ir, SchemaDelta}; use crate::ports::lowering::LoweringPort; -use apollo_parser::{cst, Parser}; +use apollo_parser::{cst, Error as ApolloParserError, Parser}; use async_trait::async_trait; use indexmap::IndexMap; use std::collections::{BTreeMap, BTreeSet, HashMap}; @@ -62,11 +62,7 @@ pub fn list_schema_operations_sdl(schema_sdl: &str) -> Result>(); if !errors.is_empty() { let err = &errors[0]; - return Err(WesleyError::ParseError { - message: err.message().to_string(), - line: None, - column: None, - }); + return Err(parse_error_from_apollo(schema_sdl, err)); } let doc = cst.document(); @@ -180,11 +176,7 @@ impl ApolloLoweringAdapter { let errors = cst.errors().collect::>(); if !errors.is_empty() { let err = &errors[0]; - return Err(WesleyError::ParseError { - message: err.message().to_string(), - line: None, - column: None, - }); + return Err(parse_error_from_apollo(sdl, err)); } let doc = cst.document(); @@ -958,6 +950,34 @@ fn lowering_error_value(area: &str, message: String) -> WesleyError { } } +fn parse_error_from_apollo(sdl: &str, error: &ApolloParserError) -> WesleyError { + let (line, column) = source_location_for_byte_index(sdl, error.index()); + WesleyError::ParseError { + message: error.message().to_string(), + line: Some(line), + column: Some(column), + } +} + +fn source_location_for_byte_index(source: &str, index: usize) -> (u32, u32) { + let mut line = 1; + let mut column = 1; + + for (byte_index, character) in source.char_indices() { + if byte_index >= index { + break; + } + if character == '\n' { + line += 1; + column = 1; + } else { + column += 1; + } + } + + (line, column) +} + fn canonical_core_directive_name(name: &str) -> Option<&str> { match name { "wes_table" | "wesley_table" | "table" => Some("wes_table"), @@ -1275,11 +1295,7 @@ fn parse_operation_document(operation_sdl: &str) -> Result>(); if !errors.is_empty() { let err = &errors[0]; - return Err(WesleyError::ParseError { - message: err.message().to_string(), - line: None, - column: None, - }); + return Err(parse_error_from_apollo(operation_sdl, err)); } let doc = cst.document(); @@ -3363,11 +3379,7 @@ fn extract_root_types(schema_sdl: &str) -> Result { let errors = cst.errors().collect::>(); if !errors.is_empty() { let err = &errors[0]; - return Err(WesleyError::ParseError { - message: err.message().to_string(), - line: None, - column: None, - }); + return Err(parse_error_from_apollo(schema_sdl, err)); } let mut root_types = RootTypes::default(); diff --git a/crates/wesley-core/src/domain/error.rs b/crates/wesley-core/src/domain/error.rs index b21aabe6..50b4277d 100644 --- a/crates/wesley-core/src/domain/error.rs +++ b/crates/wesley-core/src/domain/error.rs @@ -29,6 +29,39 @@ pub enum WesleyError { ResilienceError(String), } +impl WesleyError { + /// Returns the stable diagnostic contract for this error. + pub fn diagnostic(&self) -> WesleyDiagnostic { + match self { + Self::ParseError { + message, + line, + column, + } => WesleyDiagnostic { + code: "WESLEY_PARSE_ERROR".to_string(), + message: message.clone(), + severity: "ERROR".to_string(), + line: *line, + column: *column, + }, + Self::LoweringError { message, .. } => WesleyDiagnostic { + code: "WESLEY_LOWERING_ERROR".to_string(), + message: message.clone(), + severity: "ERROR".to_string(), + line: None, + column: None, + }, + Self::ResilienceError(message) => WesleyDiagnostic { + code: "WESLEY_RESILIENCE_ERROR".to_string(), + message: message.clone(), + severity: "ERROR".to_string(), + line: None, + column: None, + }, + } + } +} + /// A diagnostic message emitted by the compiler. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct WesleyDiagnostic { diff --git a/crates/wesley-core/tests/lowering_validation.rs b/crates/wesley-core/tests/lowering_validation.rs index 2cfacd42..03101575 100644 --- a/crates/wesley-core/tests/lowering_validation.rs +++ b/crates/wesley-core/tests/lowering_validation.rs @@ -139,6 +139,31 @@ async fn rejects_duplicate_canonical_directives() { message.contains("Duplicate directive '@wes_table'"), "unexpected error: {message}" ); + + let diagnostic = err.diagnostic(); + assert_eq!(diagnostic.code, "WESLEY_LOWERING_ERROR"); + assert_eq!(diagnostic.severity, "ERROR"); + assert_eq!(diagnostic.message, "Duplicate directive '@wes_table'"); + assert_eq!(diagnostic.line, None); + assert_eq!(diagnostic.column, None); +} + +#[tokio::test] +async fn parse_errors_expose_stable_diagnostics_with_spans() { + let sdl = "type Broken {\n id:\n}\n"; + + let adapter = create_adapter(); + let err = adapter + .lower_sdl(sdl) + .await + .expect_err("invalid SDL syntax should fail lowering"); + let diagnostic = err.diagnostic(); + + assert_eq!(diagnostic.code, "WESLEY_PARSE_ERROR"); + assert_eq!(diagnostic.severity, "ERROR"); + assert!(diagnostic.message.contains("expected")); + assert_eq!(diagnostic.line, Some(3)); + assert_eq!(diagnostic.column, Some(1)); } #[tokio::test] diff --git a/docs/BEARING.md b/docs/BEARING.md index f1d4c653..7d72f378 100644 --- a/docs/BEARING.md +++ b/docs/BEARING.md @@ -92,8 +92,9 @@ base platform. notes rather than accidental hash churn. - **Alias Semantics**: Legacy directive aliases are compatibility input, not a license to preserve arbitrary spelling in semantic Rust L1 output. -- **Invalid Diagnostics**: The Rust lowerer can reject invalid SDL, but stable - codes and spans are not yet part of the L1 fixture contract. +- **Invalid Diagnostics**: The Rust lowerer now exposes stable diagnostic + codes and parser spans, but semantic lowering spans are still absent and + should not be implied by tests or release notes. - **External Module Gap**: Wesley has named the domain-empty boundary, but the module seam still needs hermetic target-dispatch fixtures, runtime boundary evidence, and artifact evidence before external modules can consume it @@ -115,7 +116,10 @@ table-compatible sentinel corpus, the domain-empty ownership packet in `0014`, and executable module-target dispatch coverage for no-module diagnostics, default target discovery, requested-target validation, duplicate target rejection, alias conflicts in both registration orders, and the Rust IR -fixture contract now housed under the active `0013` packet. +fixture contract now housed under the active `0013` packet. Invalid SDL +diagnostics now expose stable `WesleyError::diagnostic()` codes and parser +line/column spans where available, while semantic lowering spans remain +explicitly absent. The next pulls are: @@ -123,13 +127,10 @@ The next pulls are: target dispatch already rejects missing modules, invalid product/database target names, duplicate names, and aliases that collide before or after the owning target loads. -2. Stabilize invalid-SDL diagnostic contracts with executable coverage for - codes and spans where available, while naming what remains intentionally - unstable. -3. Define the next parity projection before broadening `pnpm parity:ir` beyond +2. Define the next parity projection before broadening `pnpm parity:ir` beyond table-compatible SDL. Schema extensions and non-table L1 facts need a fair projection before they become JS/Rust parity evidence. -4. Capture a Rust core performance baseline over the canonical corpus after +3. Capture a Rust core performance baseline over the canonical corpus after the fixture and projection boundaries are named. Do not pull `OWN_ninelives-resilience-integration.md` until the module boundary diff --git a/docs/design/0013-rust-ir-parity-sentinel/SOURCE_wesley-core-rs-ir-contract-and-fixtures.md b/docs/design/0013-rust-ir-parity-sentinel/SOURCE_wesley-core-rs-ir-contract-and-fixtures.md index 3a150fbc..75d93838 100644 --- a/docs/design/0013-rust-ir-parity-sentinel/SOURCE_wesley-core-rs-ir-contract-and-fixtures.md +++ b/docs/design/0013-rust-ir-parity-sentinel/SOURCE_wesley-core-rs-ir-contract-and-fixtures.md @@ -68,7 +68,7 @@ The v0.0.6 corpus must keep these classes explicit: Invalid SDL coverage should record: -- a stable error code where the Rust lowerer exposes one +- a stable error code from `WesleyError::diagnostic()` - the fixture path - a stable message shape that names the violated rule - line and column spans where the parser or lowerer can preserve them @@ -77,6 +77,14 @@ Invalid SDL coverage should record: The diagnostic contract is not allowed to hide invalid inputs by normalizing them into fixture outputs. +Current v0.0.6 behavior: + +- parse errors use `WESLEY_PARSE_ERROR` and preserve line/column spans derived + from Apollo's parser byte index +- semantic lowering errors use `WESLEY_LOWERING_ERROR` +- semantic lowering errors do not yet expose source spans, so duplicate + canonical directive coverage asserts `line: null` and `column: null` + ## Done looks like - current JS parse/lower/hash functions are listed in the truth manifest