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
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,31 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve

## [Unreleased]

### Added

- **Rust IR parity sentinel packet**: Pulled the parity sentinel backlog item
into design packet `0013`, defining comparator inputs, normalization, hash
behavior, and failure output for the next JS/Rust parity check.
- **Expanded Rust L1 fixture corpus**: Added directive-heavy,
schema-extension, legacy-alias, and invalid duplicate-directive fixtures for
the v0.0.6 compiler-truth lane.

### Changed

- **v0.0.6 bearing reset**: Reframed `docs/BEARING.md` around Rust IR parity,
module-boundary enforcement, and explicit `wesley-postgres` preservation
after the v0.0.5 clean-house release.
- **v0.0.5 release evidence**: Replaced pending publication wording with the
actual GitHub Release, signed tag, workflow, and crates.io visibility
evidence.

### Fixed

- **Rust directive alias normalization**: Rust L1 lowering now canonicalizes
the current core Wesley directive aliases to `wes_*` names and rejects
duplicate canonical directives instead of allowing last-write-wins drift,
while repeated custom directives are preserved as ordered values.

## [0.0.5] - 2026-05-21

### Fixed
Expand Down
42 changes: 41 additions & 1 deletion crates/wesley-core/src/adapters/apollo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,8 @@ impl ApolloLoweringAdapter {
})?
.text()
.to_string();
let core_name = canonical_core_directive_name(&dir_name);
let canonical_name = core_name.unwrap_or(dir_name.as_str()).to_string();

let mut args_map = serde_json::Map::new();
if let Some(args) = dir.arguments() {
Expand All @@ -503,7 +505,14 @@ impl ApolloLoweringAdapter {
serde_json::Value::Object(args_map)
};

map.insert(dir_name, val);
if core_name.is_some() && map.contains_key(&canonical_name) {
return Err(lowering_error_value(
"directive",
format!("Duplicate directive '@{canonical_name}'"),
));
}

insert_directive_value(map, canonical_name, val);
}
Ok(())
}
Expand Down Expand Up @@ -949,6 +958,37 @@ fn lowering_error_value(area: &str, message: String) -> WesleyError {
}
}

fn canonical_core_directive_name(name: &str) -> Option<&str> {
match name {
"wes_table" | "wesley_table" | "table" => Some("wes_table"),
"wes_pk" | "wesley_pk" | "pk" | "primaryKey" => Some("wes_pk"),
"wes_fk" | "wesley_fk" | "fk" | "foreignKey" => Some("wes_fk"),
"wes_unique" | "wesley_unique" | "unique" => Some("wes_unique"),
"wes_index" | "wesley_index" | "index" => Some("wes_index"),
"wes_tenant" | "wesley_tenant" | "tenant" => Some("wes_tenant"),
"wes_default" | "wesley_default" | "default" => Some("wes_default"),
"wes_rls" | "wesley_rls" | "rls" => Some("wes_rls"),
_ => None,
}
}

fn insert_directive_value(
map: &mut IndexMap<String, serde_json::Value>,
name: String,
value: serde_json::Value,
) {
match map.get_mut(&name) {
Some(serde_json::Value::Array(values)) => values.push(value),
Some(existing) => {
let first = std::mem::take(existing);
*existing = serde_json::Value::Array(vec![first, value]);
}
None => {
map.insert(name, value);
}
}
}

/// Resolves response-path field selections from a single GraphQL operation.
pub fn resolve_operation_selections(operation_sdl: &str) -> Result<Vec<String>, WesleyError> {
let parsed = parse_operation_document(operation_sdl)?;
Expand Down
111 changes: 111 additions & 0 deletions crates/wesley-core/tests/lowering_validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,117 @@ async fn test_lower_large_schema() {
validate_schema("large-schema").await;
}

#[tokio::test]
async fn test_lower_directive_heavy_schema() {
validate_schema("directive-heavy-schema").await;
}

#[tokio::test]
async fn test_lower_schema_extensions_schema() {
validate_schema("schema-extensions-schema").await;
}

#[tokio::test]
async fn test_lower_legacy_alias_schema() {
validate_schema("legacy-alias-schema").await;
}

#[tokio::test]
async fn canonicalizes_legacy_directive_aliases() {
let sdl_path = get_fixture_path("legacy-alias-schema.graphql");
let sdl = fs::read_to_string(sdl_path).expect("Failed to read SDL fixture");

let adapter = create_adapter();
let ir = adapter
.lower_sdl(&sdl)
.await
.expect("Failed to lower SDL to L1 IR");

let tenant = find_type(&ir.types, "Tenant");
assert!(tenant.directives.contains_key("wes_table"));
assert!(tenant.directives.contains_key("wes_rls"));
assert!(!tenant.directives.contains_key("table"));
assert!(!tenant.directives.contains_key("rls"));

let tenant_id = tenant
.fields
.iter()
.find(|field| field.name == "id")
.expect("missing id field");
assert!(tenant_id.directives.contains_key("wes_pk"));
assert!(!tenant_id.directives.contains_key("pk"));

let member = find_type(&ir.types, "Member");
assert!(member.directives.contains_key("wes_table"));
assert!(member.directives.contains_key("wes_tenant"));
assert!(member.directives.contains_key("wes_rls"));
assert!(!member.directives.contains_key("wesley_table"));
assert!(!member.directives.contains_key("tenant"));

let role = member
.fields
.iter()
.find(|field| field.name == "role")
.expect("missing role field");
assert!(role.directives.contains_key("wes_default"));
assert!(!role.directives.contains_key("default"));
}

#[tokio::test]
async fn rejects_duplicate_canonical_directives() {
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("../../test/fixtures/ir-parity-invalid/duplicate-directive-alias.graphql");
let sdl = fs::read_to_string(path).expect("Failed to read invalid SDL fixture");

let adapter = create_adapter();
let err = adapter
.lower_sdl(&sdl)
.await
.expect_err("duplicate canonical directives should fail lowering");
let message = err.to_string();

assert!(
message.contains("Duplicate directive '@wes_table'"),
"unexpected error: {message}"
);
}

#[tokio::test]
async fn preserves_repeated_custom_directives_as_ordered_values() {
let sdl = r#"
directive @tag(name: String!) repeatable on FIELD_DEFINITION

type Thing {
name: String @tag(name: "alpha") @tag(name: "beta")
}
"#;

let adapter = create_adapter();
let ir = adapter
.lower_sdl(sdl)
.await
.expect("repeatable custom directives should lower");
let thing = find_type(&ir.types, "Thing");
let name = thing
.fields
.iter()
.find(|field| field.name == "name")
.expect("missing name field");
let tag_values = name.directives["tag"]
.as_array()
.expect("repeated custom directive should be preserved as an array");

assert_eq!(tag_values.len(), 2);
assert_eq!(
tag_values[0]["name"],
serde_json::Value::String("alpha".into())
);
assert_eq!(
tag_values[1]["name"],
serde_json::Value::String("beta".into())
);
}

#[tokio::test]
async fn lowers_graphql_type_families_into_l1_ir() {
let sdl = r#"
Expand Down
121 changes: 64 additions & 57 deletions docs/BEARING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,24 @@ Current direction and active tensions. Historical ship data is in

```mermaid
timeline
Phase 1 : Clean House Release : Domain-Empty Core : Backlog Truth
Phase 2 : IR Truth : Rust Parity : Stable Fixture Corpus
Phase 3 : Module Boundaries : External Targets : Artifact Evidence
Phase 1 : v0.0.5 Shipped : Clean House : Domain-Empty Backlog
Phase 2 : v0.0.6 : Rust IR Parity : Fixture Truth
Phase 3 : Module Boundary : External Targets : Artifact Evidence
Phase 4 : Core Release : Legacy Node Retirement : Postgres Module Cutover
```

## Active Gravity

### 1. Clean House Release
### 1. v0.0.6 Rust IR Parity
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update docs front door for v0.0.6 release center

This change declares v0.0.6 as the active gravity in BEARING, but docs/README.md still says the active release center is v0.0.5 (line 33 in that file), so the two top-level navigation docs now contradict each other about what is current. That inconsistency was introduced here and will misroute readers who start from the docs front door; please update docs/README.md in the same change to mark v0.0.5 as shipped/history and point active work to the 0013 parity packet.

Useful? React with 👍 / 👎.


The next Wesley hill is an introspective cleanup release.
The next Wesley hill is not another product lane. It is a compiler-truth
release that makes the Rust core harder to drift from the legacy JS lowering
surface while Wesley finishes moving toward one native compiler brain.

Wesley should stop orbiting old Echo, jedit, Continuum, PostgreSQL, and
Supabase implementation lanes. Those repos or their external module homes now
own the product/domain work. Wesley owns the compiler kernel, generic module
contracts, generic artifact/evidence plumbing, and compatibility evidence that
those external consumers can inspect.

The cleanup release should make the repository's backlog, docs, tests, and
front doors say that consistently.
v0.0.5 closed the clean-house release. v0.0.6 should turn that cleanup into
evidence: richer canonical fixtures, clearer compatibility diagnostics, and a
separate JS/Rust parity sentinel that proves whether current Rust L1 bytes
still match the legacy truth anchors where they are expected to match.

### 2. Domain-Empty Core

Expand All @@ -39,80 +37,89 @@ front doors say that consistently.
`wesley-core`, Wesley generators, generic host packages, or generic task
execution packages.

### 3. IR Truth And Rust Parity
### 3. Rust L1 Fixture Truth

- Treat the Rust workspace as the primary compiler surface.
- Freeze the canonical IR contract and fixture corpus before broad rewrites.
- Remove nondeterministic metadata from parity-sensitive IR bytes.
- Keep a JS/Rust parity sentinel over canonical fixtures until legacy Node
lowering is retired or deliberately demoted.
- Expand the canonical fixture corpus before broad rewrites.
- Keep nondeterministic metadata out of parity-sensitive IR bytes.
- Preserve directive spelling, alias normalization, extension folding, and
invalid-SDL diagnostics as explicit tests instead of tribal knowledge.
- Keep jedit-shaped consumer fixtures as compiler coverage, not as jedit
product ownership.

### 4. External Optic Admission Split

The current optic-admission ownership split is:
### 4. Parity Sentinel Before Retirement

- Wesley compiles artifacts and registration descriptors.
- Echo registers artifacts, returns runtime-local handles, admits or obstructs
invocations, instruments access, and emits witnesses/readings.
- Authority layers issue grants and capability presentations.
- Applications hide artifact handles, basis references, and runtime
coordinates behind product-facing adapters.
- Continuum coordinates the shared role map, but should not freeze a shared
protocol family until the compiled-artifact, registration, invocation, and
witness path is proven in the owning repos.
`pnpm fixtures:ir` regenerates Rust L1 golden files. It is not JS/Rust parity
proof.

That split belongs in repo bearings and external backlogs, not as hidden Wesley
product work.
The new sentinel work lives in
[0013-rust-ir-parity-sentinel](./design/0013-rust-ir-parity-sentinel/rust-ir-parity-sentinel.md).
It should compare normalized semantic IR from the legacy JS lowerer and the
Rust lowerer over an explicit corpus, then fail with a useful mismatch path and
hash evidence. Only after that evidence exists should Wesley retire or demote
legacy Node lowering.

### 5. Module Capability Runtime
### 5. Module Capability Boundary

- Use the module capability registry as the seam between loaded modules and
Wesley base verbs.
- Keep `wesley compile` dispatching only through module-owned `wesley.targets`.
- Keep Wesley core CI independent of external product and database repos by
exercising hermetic fixture modules across supported capability collections.
- Move product/runtime/database semantics to owning repos or modules before
deleting generic compatibility evidence that external consumers still need.

### 6. Wesley-Postgres Preservation

`wesley-postgres` is the PostgreSQL-family extraction home. It should not be
abandoned while Wesley cleans house. Database semantics removed from Wesley
need explicit homes and follow-through there before more Postgres-shaped code
is deleted or reshaped in generic Wesley.
`wesley-postgres` is the PostgreSQL-family extraction home. It is active and
must not be abandoned while Wesley tightens its domain-empty boundary.

That repo remains the home for PostgreSQL/Supabase generation, PostgreSQL
execution adapters, and database safety primitives. Wesley should coordinate
by preserving generic module seams and avoiding new database semantics in the
base platform.

## Tensions

- **Backlog Residue**: Several older cards still read like Echo, jedit,
Continuum, or database implementation work belongs in Wesley. The next
cleanup slice must move, archive, or rewrite those cards.
- **Compatibility Churn**: IR, hash, directive, or generated-artifact changes
can affect Echo and jedit fixtures. Those changes need explicit compatibility
notes rather than accidental hash churn.
- **Legacy NPM Front Door**: README and guide now point core work at Cargo, but
package scripts, docs drift checks, and old generator commands still assume
legacy Node surfaces.
- **Two-Brain Confusion**: Rust and Node surfaces still coexist. The intended
shape is one compiler brain (`crates/wesley-core`), one native command body
(`crates/wesley-cli`), and legacy Node support surfaces under `packages/`
until ported, extracted, or retired.
- **Fixture Churn**: IR, hash, directive, or generated-artifact changes can
affect Echo and jedit fixtures. Those changes need explicit compatibility
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.
- **External Module Gap**: Wesley can name the domain-empty boundary, but
external modules still need enough capability runtime and artifact evidence
to consume it cleanly.
- **Sibling Repo Coordination**: Wesley should reference `wesley-postgres` as
the database authority without editing or overwriting sibling work from this
repo.

## Next Target

The immediate focus is **v0.0.5 clean house**:

1. Execute design packet
[0012-product-leftover-cleanup](./design/0012-product-leftover-cleanup/product-leftover-cleanup.md).
2. Finish the active backlog verification pass so product/runtime/database
cards are moved, retired, or rewritten as external-module compatibility
work.
3. Treat the old `v0.1.0` lane as retired historical/extraction context.
4. Freeze canonical IR fixtures and nondeterministic metadata policy.
5. Install JS/Rust parity evidence before deeper Rust-native cleanup.
6. Keep `wesley-postgres` visible as the database extraction home.
The immediate focus is **v0.0.6 Rust IR parity and module-boundary
enforcement**:

Current evidence now includes complete v0.0.5 publication proof and an expanded
Rust L1 corpus for directive-heavy SDL, schema extensions, legacy aliases, and
invalid duplicate-directive coverage.

The next pulls are:

1. Implement the JS/Rust parity sentinel command from design packet
[0013-rust-ir-parity-sentinel](./design/0013-rust-ir-parity-sentinel/rust-ir-parity-sentinel.md).
2. Land the `js-table-vs-rust-table.v0` projection/crosswalk before comparing
legacy JS table IR with Rust L1 bytes.
3. Pull the domain-empty core boundary card into enforcement work so product
and database behavior stays outside generic Wesley.
4. Continue the IR contract fixture lane for stable invalid-SDL diagnostics,
including codes and spans where available.
5. Keep `wesley-postgres` visible as the database extraction home and avoid
reshaping sibling work from Wesley release branches.

Echo and jedit do not need more Wesley feature gravity for their current work.
Wesley should coordinate on compatibility only when a concrete artifact, hash,
Expand Down
Loading
Loading