Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -34,6 +37,14 @@ 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
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
Expand Down
54 changes: 33 additions & 21 deletions crates/wesley-core/src/adapters/apollo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -62,11 +62,7 @@ pub fn list_schema_operations_sdl(schema_sdl: &str) -> Result<Vec<SchemaOperatio
let errors = cst.errors().collect::<Vec<_>>();
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();
Expand Down Expand Up @@ -180,11 +176,7 @@ impl ApolloLoweringAdapter {
let errors = cst.errors().collect::<Vec<_>>();
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();
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -1275,11 +1295,7 @@ fn parse_operation_document(operation_sdl: &str) -> Result<ParsedOperationDocume
let errors = cst.errors().collect::<Vec<_>>();
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();
Expand Down Expand Up @@ -3363,11 +3379,7 @@ fn extract_root_types(schema_sdl: &str) -> Result<RootTypes, WesleyError> {
let errors = cst.errors().collect::<Vec<_>>();
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();
Expand Down
33 changes: 33 additions & 0 deletions crates/wesley-core/src/domain/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
25 changes: 25 additions & 0 deletions crates/wesley-core/tests/lowering_validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
33 changes: 17 additions & 16 deletions docs/BEARING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -111,25 +112,25 @@ 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, alias conflicts in both registration orders, and the Rust IR
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:

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.
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
codes and spans where available, while naming what remains intentionally
unstable.
4. Define the next parity projection before broadening `pnpm parity:ir` beyond
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. 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
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
---
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 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
- 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.

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
- 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/`
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 1 addition & 2 deletions docs/method/backlog/asap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Loading
Loading