From 483439d47f3754d00f9c836dfb9ab8118f6c3ecb Mon Sep 17 00:00:00 2001 From: James Ross Date: Thu, 21 May 2026 23:47:27 -0700 Subject: [PATCH 1/3] feat(ir): expand parity fixture coverage --- CHANGELOG.md | 24 ++ crates/wesley-core/src/adapters/apollo.rs | 24 +- .../wesley-core/tests/lowering_validation.rs | 75 ++++++ docs/BEARING.md | 117 ++++----- .../phase-0-ir-truth-manifest.md | 22 +- .../rust-ir-parity-sentinel.md | 146 ++++++++++++ docs/design/README.md | 1 + docs/method/backlog/asap/README.md | 9 +- .../SOURCE_wesley-core-rs-parity-sentinel.md | 27 --- docs/method/releases/v0.0.5/verification.md | 11 +- .../duplicate-directive-alias.graphql | 3 + .../ir-parity/directive-heavy-schema.graphql | 21 ++ .../ir-parity/directive-heavy-schema.l1.hash | 1 + .../ir-parity/directive-heavy-schema.l1.json | 221 +++++++++++++++++ .../ir-parity/legacy-alias-schema.graphql | 11 + .../ir-parity/legacy-alias-schema.l1.hash | 1 + .../ir-parity/legacy-alias-schema.l1.json | 106 +++++++++ .../schema-extensions-schema.graphql | 55 +++++ .../schema-extensions-schema.l1.hash | 1 + .../schema-extensions-schema.l1.json | 222 ++++++++++++++++++ 20 files changed, 1000 insertions(+), 98 deletions(-) create mode 100644 docs/design/0013-rust-ir-parity-sentinel/rust-ir-parity-sentinel.md delete mode 100644 docs/method/backlog/asap/SOURCE_wesley-core-rs-parity-sentinel.md create mode 100644 test/fixtures/ir-parity-invalid/duplicate-directive-alias.graphql create mode 100644 test/fixtures/ir-parity/directive-heavy-schema.graphql create mode 100644 test/fixtures/ir-parity/directive-heavy-schema.l1.hash create mode 100644 test/fixtures/ir-parity/directive-heavy-schema.l1.json create mode 100644 test/fixtures/ir-parity/legacy-alias-schema.graphql create mode 100644 test/fixtures/ir-parity/legacy-alias-schema.l1.hash create mode 100644 test/fixtures/ir-parity/legacy-alias-schema.l1.json create mode 100644 test/fixtures/ir-parity/schema-extensions-schema.graphql create mode 100644 test/fixtures/ir-parity/schema-extensions-schema.l1.hash create mode 100644 test/fixtures/ir-parity/schema-extensions-schema.l1.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ce906f3..fd16731a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,30 @@ 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. + ## [0.0.5] - 2026-05-21 ### Fixed diff --git a/crates/wesley-core/src/adapters/apollo.rs b/crates/wesley-core/src/adapters/apollo.rs index 67c7a143..5ec6518c 100644 --- a/crates/wesley-core/src/adapters/apollo.rs +++ b/crates/wesley-core/src/adapters/apollo.rs @@ -486,6 +486,7 @@ impl ApolloLoweringAdapter { })? .text() .to_string(); + let canonical_name = canonical_directive_name(&dir_name).to_string(); let mut args_map = serde_json::Map::new(); if let Some(args) = dir.arguments() { @@ -503,7 +504,14 @@ impl ApolloLoweringAdapter { serde_json::Value::Object(args_map) }; - map.insert(dir_name, val); + if map.contains_key(&canonical_name) { + return Err(lowering_error_value( + "directive", + format!("Duplicate directive '@{canonical_name}'"), + )); + } + + map.insert(canonical_name, val); } Ok(()) } @@ -949,6 +957,20 @@ fn lowering_error_value(area: &str, message: String) -> WesleyError { } } +fn canonical_directive_name(name: &str) -> &str { + match name { + "wesley_table" | "table" => "wes_table", + "wesley_pk" | "pk" | "primaryKey" => "wes_pk", + "wesley_fk" | "fk" | "foreignKey" => "wes_fk", + "wesley_unique" | "unique" => "wes_unique", + "wesley_index" | "index" => "wes_index", + "wesley_tenant" | "tenant" => "wes_tenant", + "wesley_default" | "default" => "wes_default", + "wesley_rls" | "rls" => "wes_rls", + _ => name, + } +} + /// Resolves response-path field selections from a single GraphQL operation. pub fn resolve_operation_selections(operation_sdl: &str) -> Result, WesleyError> { let parsed = parse_operation_document(operation_sdl)?; diff --git a/crates/wesley-core/tests/lowering_validation.rs b/crates/wesley-core/tests/lowering_validation.rs index 786a897e..fe88d539 100644 --- a/crates/wesley-core/tests/lowering_validation.rs +++ b/crates/wesley-core/tests/lowering_validation.rs @@ -66,6 +66,81 @@ 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 lowers_graphql_type_families_into_l1_ir() { let sdl = r#" diff --git a/docs/BEARING.md b/docs/BEARING.md index 3a818bfb..c935f73c 100644 --- a/docs/BEARING.md +++ b/docs/BEARING.md @@ -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 -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 @@ -39,80 +37,85 @@ 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. + +As of this bearing reset, `~/git/wesley-postgres` has local `main` ahead of +`origin/main` with additional uncommitted bootstrap work. 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 State**: `wesley-postgres` has active local work outside this + PR. Wesley should reference it as the database authority without editing or + overwriting that 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**: + +1. Keep v0.0.5 publication evidence complete and inspectable. +2. Execute design packet + [0013-rust-ir-parity-sentinel](./design/0013-rust-ir-parity-sentinel/rust-ir-parity-sentinel.md). +3. Expand the L1 fixture corpus for directive-heavy SDL, schema extensions, + legacy directive aliases, and invalid-SDL failure coverage. +4. Implement the JS/Rust parity sentinel as a separate check from + `pnpm fixtures:ir`. +5. Pull the domain-empty core boundary card into enforcement work so product + and database behavior stays outside generic Wesley. +6. 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, diff --git a/docs/design/0009-rust-core-and-wasm-capability-abi/phase-0-ir-truth-manifest.md b/docs/design/0009-rust-core-and-wasm-capability-abi/phase-0-ir-truth-manifest.md index 04857be0..5bdbd354 100644 --- a/docs/design/0009-rust-core-and-wasm-capability-abi/phase-0-ir-truth-manifest.md +++ b/docs/design/0009-rust-core-and-wasm-capability-abi/phase-0-ir-truth-manifest.md @@ -44,6 +44,10 @@ The fixture corpus is stored in `test/fixtures/ir-parity` and consists of - `*.l1.json` stores the Rust-native L1 IR emitted by `wesley schema lower`. - `*.l1.hash` stores the Rust-native L1 hash emitted by `wesley schema hash`. +Invalid SDL fixtures live in `test/fixtures/ir-parity-invalid`. They are not +processed by `pnpm fixtures:ir`; they are consumed by explicit negative Rust +tests. + ### Stable L1 Hashes | Fixture | Hash (SHA-256) | @@ -51,6 +55,9 @@ The fixture corpus is stored in `test/fixtures/ir-parity` and consists of | `small-schema.graphql` | `b484bf6741686314aea381b51d5d26805b08fa27517225bbe4b736d9f39c606f` | | `medium-schema.graphql` | `853d939364506680535ae865438d897efc9fee2dc8e5b21d1118cae3cfe5664b` | | `large-schema.graphql` | `dfd5a42ab6a03570294764e4e9bdd791b5dd42fc02db5feb9543849a67d14726` | +| `directive-heavy-schema.graphql` | `e2e831e55a3b439322c49057e6ad2c6e28e6446e0b6f79fa1cae2a8b102053e3` | +| `schema-extensions-schema.graphql` | `72d4d2db0d705fb59117a4c9f2e55ade187e435829253bb862aabd6dee5c9f99` | +| `legacy-alias-schema.graphql` | `95b4c726cfccf7874ba2e5d01a216cb1f31c0abce0ea060885899a5d79281aa6` | ### Categories @@ -59,13 +66,16 @@ The fixture corpus is stored in `test/fixtures/ir-parity` and consists of (**COMPLETE**) 3. **Large:** 100+ types to test performance and memory scaling. (**COMPLETE**) 4. **Directive-Heavy:** Extensive use of `@wes_rls`, `@wes_tenant`, and - `@wes_default`. (PENDING) + `@wes_default`, including directive arguments with arrays and object + values. (**COMPLETE**) 5. **Invalid:** SDL cases that MUST trigger specific `WesleyParseError` codes. - (PENDING) -6. **Schema-Extensions:** Testing JS and Rust `extend type` folding. - (**STARTED**) -7. **Legacy-Aliases:** Using `@table`, `@pk`, etc., to ensure alias - normalization works. (PENDING) + (**STARTED**; current Rust coverage rejects duplicate canonical directives, + while stable diagnostic codes and spans remain future work.) +6. **Schema-Extensions:** Testing JS and Rust extension folding for scalar, + object, interface, union, enum, and input object types. (**COMPLETE**) +7. **Legacy-Aliases:** Using `@table`, `@pk`, `@primaryKey`, `@tenant`, and + related core aliases to ensure Rust L1 emits canonical `@wes_*` directive + names. (**COMPLETE for the current core compiler alias set**) ## Baseline Performance (JS) 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 new file mode 100644 index 00000000..d61acb8d --- /dev/null +++ b/docs/design/0013-rust-ir-parity-sentinel/rust-ir-parity-sentinel.md @@ -0,0 +1,146 @@ +--- +title: Rust IR Parity Sentinel +legend: SOURCE +packet: 0013-rust-ir-parity-sentinel +status: active +release: v0.0.6 +--- + +# Rust IR Parity Sentinel + +## Sponsors + +- Human: I can change the Rust compiler kernel and know whether I broke legacy + JS compatibility, changed the canonical Rust L1 truth intentionally, or hit a + known compatibility break that needs release notes. +- Agent: I can run one purpose-built parity check and get a small evidence + bundle instead of treating Rust fixture regeneration as implicit JS/Rust + proof. + +## Hill + +Wesley has a separate parity sentinel that compares the legacy JS lowerer and +the Rust lowerer over an explicit corpus after agreed non-semantic envelope +fields are normalized away. + +`pnpm fixtures:ir` remains the Rust L1 golden-regeneration command. The parity +sentinel is a different check. + +## Why This Cycle Exists + +v0.0.5 cleaned up product backlog gravity and made Rust L1 fixture +regeneration honest. The next risk is semantic drift while Rust becomes the +primary compiler surface. + +The old JS implementation is still the compatibility anchor for several +consumer-shaped schemas. Rust should not diverge silently on directive +spelling, extension folding, type shape, canonical JSON bytes, or registry +hashes. + +## Comparator Contract + +### Inputs + +The sentinel consumes an explicit fixture list, not every `.graphql` file by +accident. + +For each fixture it records: + +- fixture path +- legacy JS semantic IR bytes +- Rust semantic IR bytes +- legacy JS semantic hash +- Rust semantic hash +- normalizer version +- command versions or commit identifiers when available + +The first corpus should draw from `test/fixtures/ir-parity` after excluding +fixtures that intentionally assert Rust-only target-state behavior. + +### Lowerers + +The Rust side uses: + +```bash +cargo run --quiet -p wesley-cli -- schema lower --schema --json +cargo run --quiet -p wesley-cli -- schema hash --schema +``` + +The legacy JS side uses the current truth anchors named in +[Phase 0: IR Truth Manifest](../0009-rust-core-and-wasm-capability-abi/phase-0-ir-truth-manifest.md): + +- `GraphQLSchemaParser.parse` +- `buildIRFromAST` +- `canonicalize` +- `registryHash` +- `canonicalizeJSON` + +### Normalization + +The normalizer removes envelope-only data and keeps semantic data intact. + +- Remove top-level `metadata`. +- Sort object keys with Wesley canonical JSON ordering before hashing. +- Preserve array order. +- Preserve directive argument values exactly after each lowerer has produced + semantic IR. +- Require lowerers to emit canonical directive names for core Wesley aliases. +- Do not rewrite legacy alias spellings in the comparator. Alias + normalization belongs in the lowerer, where schema semantics are known. + +### Hash Behavior + +The sentinel compares normalized semantic bytes and their SHA-256 digests. + +It also verifies that the Rust `schema hash` command agrees with the digest of +the normalized Rust semantic bytes. If those disagree, the Rust CLI/hash path +is inconsistent even before JS parity is considered. + +Tracked `*.l1.hash` files remain Rust golden outputs, not JS/Rust parity +evidence. + +### Failure Output + +Failure output must identify the first semantic mismatch without forcing the +reviewer to inspect a raw wall of JSON. + +Each failure should include: + +- fixture path +- legacy hash +- Rust hash +- mismatch JSON pointer path +- compact legacy/Rust value previews at that path +- whether the Rust tracked `.l1.hash` still matches the current Rust output +- the next decision: fix Rust, fix JS compatibility, update Rust goldens, or + record an intentional compatibility break + +## Current Slice + +This first v0.0.6 slice does not implement the sentinel command yet. + +It does pull the backlog card into design, expands the Rust L1 corpus, and +closes one blocker the sentinel would otherwise expose immediately: Rust L1 +lowering now canonicalizes the core Wesley directive aliases before writing +semantic IR and rejects duplicate canonical directives. + +## Playback Questions + +1. Is Rust fixture regeneration still separate from JS/Rust parity proof? +2. Does the design define comparator inputs, lowerers, normalization, hash + behavior, and failure output? +3. Does the fixture corpus now cover directive-heavy SDL, schema extensions, + legacy aliases, and at least one invalid-SDL case? +4. Does Rust L1 preserve canonical directive names for supported aliases? +5. Is the next implementation slice narrow enough to add a `pnpm parity:ir` + check without changing the Rust golden-regeneration command? + +## Non-Goals + +- Do not retire legacy Node lowering in this packet. +- Do not treat product-specific Echo, jedit, Continuum, or database semantics + as generic Wesley compiler work. +- Do not make the comparator hide semantic differences by rewriting IR after + lowering. +- Do not turn invalid fixtures into inputs for `pnpm fixtures:ir`; invalid SDL + belongs in explicit negative tests. diff --git a/docs/design/README.md b/docs/design/README.md index e1ba15fa..c102754a 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -19,6 +19,7 @@ Current packets: - [`0010`](./0010-wesley-graft-mcp-boundary/wesley-graft-mcp-boundary.md): Wesley+Graft MCP boundary for legal agent optics - [`0011`](./0011-causal-suffix-bundle-family-and-runtime-sync/causal-suffix-bundle-family-and-runtime-sync.md): Causal suffix bundle family and runtime sync - [`0012`](./0012-product-leftover-cleanup/product-leftover-cleanup.md): Product leftover cleanup for the v0.0.5 clean-house release +- [`0013`](./0013-rust-ir-parity-sentinel/rust-ir-parity-sentinel.md): Rust IR parity sentinel for the v0.0.6 compiler-truth release - [Module Contract](./wesley-module-contract.md): Generic core boundary versus external module-owned domain surfaces - [Module Capability Contract](./wesley-module-capability-contract.md): The capability surfaces external modules should implement - [Contract / Artifact / Runtime Boundary](./wesley-contract-family-artifact-runtime-value.md): GraphQL-authored families, Wesley-emitted artifacts, and later runtime values diff --git a/docs/method/backlog/asap/README.md b/docs/method/backlog/asap/README.md index 0b887bb3..8eff9f14 100644 --- a/docs/method/backlog/asap/README.md +++ b/docs/method/backlog/asap/README.md @@ -7,13 +7,12 @@ cycle. Keep items narrow, evidence-backed, and explicit about which shared seam or compiler boundary they are trying to freeze next. -The active ASAP hill is domain-empty Wesley: remove product/database ownership -from this repo, build the external module capability seam, and stop treating -historical product lanes as Wesley features. +The active ASAP hill is v0.0.6 compiler truth: finish Rust IR parity evidence, +enforce the domain-empty core boundary, and stop treating historical product or +database lanes as Wesley features. Current near-term pulls: 1. `SOURCE_domain-empty-wesley-core-boundary.md` 2. `SOURCE_wesley-core-rs-ir-contract-and-fixtures.md` -3. `SOURCE_wesley-core-rs-parity-sentinel.md` -4. `OWN_ninelives-resilience-integration.md` +3. `OWN_ninelives-resilience-integration.md` diff --git a/docs/method/backlog/asap/SOURCE_wesley-core-rs-parity-sentinel.md b/docs/method/backlog/asap/SOURCE_wesley-core-rs-parity-sentinel.md deleted file mode 100644 index b483bc83..00000000 --- a/docs/method/backlog/asap/SOURCE_wesley-core-rs-parity-sentinel.md +++ /dev/null @@ -1,27 +0,0 @@ -# SOURCE: Wesley core-rs Parity Sentinel - -- Lane: `asap` -- Legend: `SOURCE` - -## Why (Cool Idea) - -As we move the compiler truth to Rust, we risk "Semantic Drift" where the -legacy JS implementation and the new Rust kernel produce slightly different IR -or hashes for the same SDL. - -## Done looks like - -- `pnpm fixtures:ir` remains the Rust L1 golden-regeneration command only. -- A separate GitHub Action or pre-push hook runs the legacy JS lowerer and - Rust lowerer against an explicit parity corpus. -- The sentinel normalizes agreed non-semantic envelope fields before comparing - semantic IR and hashes. -- It fails if JS and Rust diverge, forcing the developer to fix drift, update - the Rust L1 truth manifest, or record an explicit compatibility break. - -## Repo Evidence - -- `scripts/generate-ir-fixtures.mjs` -- `docs/design/0009-rust-core-and-wasm-capability-abi/phase-0-ir-truth-manifest.md` -- `packages/wesley-runtime-node/src/GraphQLAdapter.mjs` -- `crates/wesley-core/tests/lowering_validation.rs` diff --git a/docs/method/releases/v0.0.5/verification.md b/docs/method/releases/v0.0.5/verification.md index 981183f9..d40e689e 100644 --- a/docs/method/releases/v0.0.5/verification.md +++ b/docs/method/releases/v0.0.5/verification.md @@ -44,5 +44,12 @@ ## Publication Evidence -Publication is pending merge of the release finalization branch, creation of -tag `v0.0.5`, and the tag-triggered GitHub Actions release workflow. +| Check | Evidence | +| --- | --- | +| Release finalization PR | PR #512 merged into `main` as `fa42c71832520b70e55447e865e0425e467db2a5`. | +| Release tag | Signed annotated tag `v0.0.5` points to `fa42c71832520b70e55447e865e0425e467db2a5`. | +| Tag signature | `git tag -v v0.0.5` passed with RSA key `01A63D8E9DBEEDE32918AF9C39560E0406CA9135`. | +| GitHub Release | Published at on `2026-05-21T23:05:14Z`; draft and prerelease flags are false. | +| CI workflow | `CI` run `26258049691` completed successfully for `v0.0.5` at `fa42c71832520b70e55447e865e0425e467db2a5`: . | +| Crates workflow | `Release Crates` run `26258049690` completed successfully for `v0.0.5` at `fa42c71832520b70e55447e865e0425e467db2a5`: . | +| Crates.io visibility | Direct crates.io API checks returned HTTP `200` for `wesley-core`, `wesley-emit-rust`, `wesley-emit-typescript`, and `wesley-cli` version `0.0.5`. | diff --git a/test/fixtures/ir-parity-invalid/duplicate-directive-alias.graphql b/test/fixtures/ir-parity-invalid/duplicate-directive-alias.graphql new file mode 100644 index 00000000..e6d67f59 --- /dev/null +++ b/test/fixtures/ir-parity-invalid/duplicate-directive-alias.graphql @@ -0,0 +1,3 @@ +type Account @wes_table @table { + id: ID! @wes_pk @pk +} diff --git a/test/fixtures/ir-parity/directive-heavy-schema.graphql b/test/fixtures/ir-parity/directive-heavy-schema.graphql new file mode 100644 index 00000000..ad030a74 --- /dev/null +++ b/test/fixtures/ir-parity/directive-heavy-schema.graphql @@ -0,0 +1,21 @@ +type Organization @wes_table(name: "organizations") @wes_rls { + id: ID! @wes_pk + slug: String! @wes_unique @wes_index(name: "organizations_slug_idx", using: "btree") + name: String! @wes_default(value: "unnamed") + created_at: String! @wes_default(value: "now()") +} + +type Account @wes_table(name: "accounts") @wes_tenant(by: "org_id") @wes_rls(preset: "tenant", roles: ["member", "admin"], options: {mode: "strict", audit: true, limit: 5}) { + id: ID! @wes_pk + org_id: ID! @wes_fk(ref: "organizations.id") @wes_index + email: String! @wes_unique + created_at: String! @wes_default(value: "now()") + active: Boolean! @wes_default(value: "true") +} + +type AccountEvent @wes_table(name: "account_events") @wes_tenant(by: "account_id") @wes_rls { + id: ID! @wes_pk + account_id: ID! @wes_fk(ref: "accounts.id") @wes_index(name: "account_events_account_idx") + kind: String! @wes_default(value: "created") + payload: String +} diff --git a/test/fixtures/ir-parity/directive-heavy-schema.l1.hash b/test/fixtures/ir-parity/directive-heavy-schema.l1.hash new file mode 100644 index 00000000..06cd05d8 --- /dev/null +++ b/test/fixtures/ir-parity/directive-heavy-schema.l1.hash @@ -0,0 +1 @@ +e2e831e55a3b439322c49057e6ad2c6e28e6446e0b6f79fa1cae2a8b102053e3 diff --git a/test/fixtures/ir-parity/directive-heavy-schema.l1.json b/test/fixtures/ir-parity/directive-heavy-schema.l1.json new file mode 100644 index 00000000..c04cc5b7 --- /dev/null +++ b/test/fixtures/ir-parity/directive-heavy-schema.l1.json @@ -0,0 +1,221 @@ +{ + "version": "1.0.0", + "types": [ + { + "name": "Account", + "kind": "OBJECT", + "directives": { + "wes_table": { + "name": "accounts" + }, + "wes_tenant": { + "by": "org_id" + }, + "wes_rls": { + "preset": "tenant", + "roles": [ + "member", + "admin" + ], + "options": { + "mode": "strict", + "audit": true, + "limit": 5 + } + } + }, + "fields": [ + { + "name": "id", + "type": { + "base": "ID", + "nullable": false, + "isList": false + }, + "directives": { + "wes_pk": true + } + }, + { + "name": "org_id", + "type": { + "base": "ID", + "nullable": false, + "isList": false + }, + "directives": { + "wes_fk": { + "ref": "organizations.id" + }, + "wes_index": true + } + }, + { + "name": "email", + "type": { + "base": "String", + "nullable": false, + "isList": false + }, + "directives": { + "wes_unique": true + } + }, + { + "name": "created_at", + "type": { + "base": "String", + "nullable": false, + "isList": false + }, + "directives": { + "wes_default": { + "value": "now()" + } + } + }, + { + "name": "active", + "type": { + "base": "Boolean", + "nullable": false, + "isList": false + }, + "directives": { + "wes_default": { + "value": "true" + } + } + } + ] + }, + { + "name": "AccountEvent", + "kind": "OBJECT", + "directives": { + "wes_table": { + "name": "account_events" + }, + "wes_tenant": { + "by": "account_id" + }, + "wes_rls": true + }, + "fields": [ + { + "name": "id", + "type": { + "base": "ID", + "nullable": false, + "isList": false + }, + "directives": { + "wes_pk": true + } + }, + { + "name": "account_id", + "type": { + "base": "ID", + "nullable": false, + "isList": false + }, + "directives": { + "wes_fk": { + "ref": "accounts.id" + }, + "wes_index": { + "name": "account_events_account_idx" + } + } + }, + { + "name": "kind", + "type": { + "base": "String", + "nullable": false, + "isList": false + }, + "directives": { + "wes_default": { + "value": "created" + } + } + }, + { + "name": "payload", + "type": { + "base": "String", + "nullable": true, + "isList": false + }, + "directives": {} + } + ] + }, + { + "name": "Organization", + "kind": "OBJECT", + "directives": { + "wes_table": { + "name": "organizations" + }, + "wes_rls": true + }, + "fields": [ + { + "name": "id", + "type": { + "base": "ID", + "nullable": false, + "isList": false + }, + "directives": { + "wes_pk": true + } + }, + { + "name": "slug", + "type": { + "base": "String", + "nullable": false, + "isList": false + }, + "directives": { + "wes_unique": true, + "wes_index": { + "name": "organizations_slug_idx", + "using": "btree" + } + } + }, + { + "name": "name", + "type": { + "base": "String", + "nullable": false, + "isList": false + }, + "directives": { + "wes_default": { + "value": "unnamed" + } + } + }, + { + "name": "created_at", + "type": { + "base": "String", + "nullable": false, + "isList": false + }, + "directives": { + "wes_default": { + "value": "now()" + } + } + } + ] + } + ] +} diff --git a/test/fixtures/ir-parity/legacy-alias-schema.graphql b/test/fixtures/ir-parity/legacy-alias-schema.graphql new file mode 100644 index 00000000..083a1a1c --- /dev/null +++ b/test/fixtures/ir-parity/legacy-alias-schema.graphql @@ -0,0 +1,11 @@ +type Tenant @table(name: "tenants") @rls { + id: ID! @pk + slug: String! @unique @index(name: "tenant_slug_idx") +} + +type Member @wesley_table(name: "members") @tenant(by: "tenant_id") @wesley_rls { + id: ID! @primaryKey + tenant_id: ID! @foreignKey(ref: "tenants.id") @index + email: String! @wesley_unique + role: String! @default(value: "member") +} diff --git a/test/fixtures/ir-parity/legacy-alias-schema.l1.hash b/test/fixtures/ir-parity/legacy-alias-schema.l1.hash new file mode 100644 index 00000000..716f77a0 --- /dev/null +++ b/test/fixtures/ir-parity/legacy-alias-schema.l1.hash @@ -0,0 +1 @@ +95b4c726cfccf7874ba2e5d01a216cb1f31c0abce0ea060885899a5d79281aa6 diff --git a/test/fixtures/ir-parity/legacy-alias-schema.l1.json b/test/fixtures/ir-parity/legacy-alias-schema.l1.json new file mode 100644 index 00000000..dbf263a7 --- /dev/null +++ b/test/fixtures/ir-parity/legacy-alias-schema.l1.json @@ -0,0 +1,106 @@ +{ + "version": "1.0.0", + "types": [ + { + "name": "Member", + "kind": "OBJECT", + "directives": { + "wes_table": { + "name": "members" + }, + "wes_tenant": { + "by": "tenant_id" + }, + "wes_rls": true + }, + "fields": [ + { + "name": "id", + "type": { + "base": "ID", + "nullable": false, + "isList": false + }, + "directives": { + "wes_pk": true + } + }, + { + "name": "tenant_id", + "type": { + "base": "ID", + "nullable": false, + "isList": false + }, + "directives": { + "wes_fk": { + "ref": "tenants.id" + }, + "wes_index": true + } + }, + { + "name": "email", + "type": { + "base": "String", + "nullable": false, + "isList": false + }, + "directives": { + "wes_unique": true + } + }, + { + "name": "role", + "type": { + "base": "String", + "nullable": false, + "isList": false + }, + "directives": { + "wes_default": { + "value": "member" + } + } + } + ] + }, + { + "name": "Tenant", + "kind": "OBJECT", + "directives": { + "wes_table": { + "name": "tenants" + }, + "wes_rls": true + }, + "fields": [ + { + "name": "id", + "type": { + "base": "ID", + "nullable": false, + "isList": false + }, + "directives": { + "wes_pk": true + } + }, + { + "name": "slug", + "type": { + "base": "String", + "nullable": false, + "isList": false + }, + "directives": { + "wes_unique": true, + "wes_index": { + "name": "tenant_slug_idx" + } + } + } + ] + } + ] +} diff --git a/test/fixtures/ir-parity/schema-extensions-schema.graphql b/test/fixtures/ir-parity/schema-extensions-schema.graphql new file mode 100644 index 00000000..d194d182 --- /dev/null +++ b/test/fixtures/ir-parity/schema-extensions-schema.graphql @@ -0,0 +1,55 @@ +scalar DateTime @specifiedBy(url: "https://example.com/datetime") + +extend scalar DateTime @wes_critical + +interface Node { + id: ID! +} + +interface Named { + name: String! +} + +interface Timestamped implements Node { + id: ID! + created_at: DateTime! +} + +extend interface Timestamped { + updated_at: DateTime +} + +type User implements Node & Named @wes_table(name: "users") { + id: ID! @wes_pk + name: String! +} + +extend type User implements Timestamped { + created_at: DateTime! @wes_default(value: "now()") + updated_at: DateTime +} + +type Team implements Node & Named @wes_table(name: "teams") { + id: ID! @wes_pk + name: String! +} + +union SearchResult = User + +extend union SearchResult = Team + +enum Status { + ACTIVE +} + +extend enum Status { + ARCHIVED +} + +input UserFilter { + ids: [ID!]! +} + +extend input UserFilter { + status: Status = ACTIVE +} diff --git a/test/fixtures/ir-parity/schema-extensions-schema.l1.hash b/test/fixtures/ir-parity/schema-extensions-schema.l1.hash new file mode 100644 index 00000000..a8b2908a --- /dev/null +++ b/test/fixtures/ir-parity/schema-extensions-schema.l1.hash @@ -0,0 +1 @@ +72d4d2db0d705fb59117a4c9f2e55ade187e435829253bb862aabd6dee5c9f99 diff --git a/test/fixtures/ir-parity/schema-extensions-schema.l1.json b/test/fixtures/ir-parity/schema-extensions-schema.l1.json new file mode 100644 index 00000000..d7f57172 --- /dev/null +++ b/test/fixtures/ir-parity/schema-extensions-schema.l1.json @@ -0,0 +1,222 @@ +{ + "version": "1.0.0", + "types": [ + { + "name": "DateTime", + "kind": "SCALAR", + "directives": { + "specifiedBy": { + "url": "https://example.com/datetime" + }, + "wes_critical": true + } + }, + { + "name": "Named", + "kind": "INTERFACE", + "directives": {}, + "fields": [ + { + "name": "name", + "type": { + "base": "String", + "nullable": false, + "isList": false + }, + "directives": {} + } + ] + }, + { + "name": "Node", + "kind": "INTERFACE", + "directives": {}, + "fields": [ + { + "name": "id", + "type": { + "base": "ID", + "nullable": false, + "isList": false + }, + "directives": {} + } + ] + }, + { + "name": "SearchResult", + "kind": "UNION", + "directives": {}, + "unionMembers": [ + "User", + "Team" + ] + }, + { + "name": "Status", + "kind": "ENUM", + "directives": {}, + "enumValues": [ + "ACTIVE", + "ARCHIVED" + ] + }, + { + "name": "Team", + "kind": "OBJECT", + "directives": { + "wes_table": { + "name": "teams" + } + }, + "implements": [ + "Node", + "Named" + ], + "fields": [ + { + "name": "id", + "type": { + "base": "ID", + "nullable": false, + "isList": false + }, + "directives": { + "wes_pk": true + } + }, + { + "name": "name", + "type": { + "base": "String", + "nullable": false, + "isList": false + }, + "directives": {} + } + ] + }, + { + "name": "Timestamped", + "kind": "INTERFACE", + "directives": {}, + "implements": [ + "Node" + ], + "fields": [ + { + "name": "id", + "type": { + "base": "ID", + "nullable": false, + "isList": false + }, + "directives": {} + }, + { + "name": "created_at", + "type": { + "base": "DateTime", + "nullable": false, + "isList": false + }, + "directives": {} + }, + { + "name": "updated_at", + "type": { + "base": "DateTime", + "nullable": true, + "isList": false + }, + "directives": {} + } + ] + }, + { + "name": "User", + "kind": "OBJECT", + "directives": { + "wes_table": { + "name": "users" + } + }, + "implements": [ + "Node", + "Named", + "Timestamped" + ], + "fields": [ + { + "name": "id", + "type": { + "base": "ID", + "nullable": false, + "isList": false + }, + "directives": { + "wes_pk": true + } + }, + { + "name": "name", + "type": { + "base": "String", + "nullable": false, + "isList": false + }, + "directives": {} + }, + { + "name": "created_at", + "type": { + "base": "DateTime", + "nullable": false, + "isList": false + }, + "directives": { + "wes_default": { + "value": "now()" + } + } + }, + { + "name": "updated_at", + "type": { + "base": "DateTime", + "nullable": true, + "isList": false + }, + "directives": {} + } + ] + }, + { + "name": "UserFilter", + "kind": "INPUT_OBJECT", + "directives": {}, + "fields": [ + { + "name": "ids", + "type": { + "base": "ID", + "nullable": false, + "isList": true, + "listItemNullable": false + }, + "directives": {} + }, + { + "name": "status", + "type": { + "base": "Status", + "nullable": true, + "isList": false + }, + "defaultValue": "ACTIVE", + "directives": {} + } + ] + } + ] +} From 0ac0e6450fc06665d0d46a617b897e3efe8fcf62 Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 22 May 2026 01:03:38 -0700 Subject: [PATCH 2/3] fix(ir): preserve repeated custom directives --- CHANGELOG.md | 3 +- crates/wesley-core/src/adapters/apollo.rs | 44 ++++++++++---- .../wesley-core/tests/lowering_validation.rs | 36 +++++++++++ docs/BEARING.md | 36 ++++++----- .../phase-0-ir-truth-manifest.md | 4 ++ .../rust-ir-parity-sentinel.md | 59 +++++++++++++++---- 6 files changed, 141 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd16731a..19994c7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,8 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve - **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. + 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 diff --git a/crates/wesley-core/src/adapters/apollo.rs b/crates/wesley-core/src/adapters/apollo.rs index 5ec6518c..129b82c8 100644 --- a/crates/wesley-core/src/adapters/apollo.rs +++ b/crates/wesley-core/src/adapters/apollo.rs @@ -486,7 +486,8 @@ impl ApolloLoweringAdapter { })? .text() .to_string(); - let canonical_name = canonical_directive_name(&dir_name).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() { @@ -504,14 +505,14 @@ impl ApolloLoweringAdapter { serde_json::Value::Object(args_map) }; - if map.contains_key(&canonical_name) { + if core_name.is_some() && map.contains_key(&canonical_name) { return Err(lowering_error_value( "directive", format!("Duplicate directive '@{canonical_name}'"), )); } - map.insert(canonical_name, val); + insert_directive_value(map, canonical_name, val); } Ok(()) } @@ -957,17 +958,34 @@ fn lowering_error_value(area: &str, message: String) -> WesleyError { } } -fn canonical_directive_name(name: &str) -> &str { +fn canonical_core_directive_name(name: &str) -> Option<&str> { match name { - "wesley_table" | "table" => "wes_table", - "wesley_pk" | "pk" | "primaryKey" => "wes_pk", - "wesley_fk" | "fk" | "foreignKey" => "wes_fk", - "wesley_unique" | "unique" => "wes_unique", - "wesley_index" | "index" => "wes_index", - "wesley_tenant" | "tenant" => "wes_tenant", - "wesley_default" | "default" => "wes_default", - "wesley_rls" | "rls" => "wes_rls", - _ => 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, + 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); + } } } diff --git a/crates/wesley-core/tests/lowering_validation.rs b/crates/wesley-core/tests/lowering_validation.rs index fe88d539..2cfacd42 100644 --- a/crates/wesley-core/tests/lowering_validation.rs +++ b/crates/wesley-core/tests/lowering_validation.rs @@ -141,6 +141,42 @@ async fn rejects_duplicate_canonical_directives() { ); } +#[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#" diff --git a/docs/BEARING.md b/docs/BEARING.md index c935f73c..3e6453f6 100644 --- a/docs/BEARING.md +++ b/docs/BEARING.md @@ -74,11 +74,10 @@ legacy Node lowering. `wesley-postgres` is the PostgreSQL-family extraction home. It is active and must not be abandoned while Wesley tightens its domain-empty boundary. -As of this bearing reset, `~/git/wesley-postgres` has local `main` ahead of -`origin/main` with additional uncommitted bootstrap work. 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. +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 @@ -96,25 +95,30 @@ module seams and avoiding new database semantics in the base platform. - **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 State**: `wesley-postgres` has active local work outside this - PR. Wesley should reference it as the database authority without editing or - overwriting that work from this repo. +- **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.6 Rust IR parity and module-boundary enforcement**: -1. Keep v0.0.5 publication evidence complete and inspectable. -2. Execute design packet +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). -3. Expand the L1 fixture corpus for directive-heavy SDL, schema extensions, - legacy directive aliases, and invalid-SDL failure coverage. -4. Implement the JS/Rust parity sentinel as a separate check from - `pnpm fixtures:ir`. -5. Pull the domain-empty core boundary card into enforcement work so product +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. -6. Keep `wesley-postgres` visible as the database extraction home and avoid +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. diff --git a/docs/design/0009-rust-core-and-wasm-capability-abi/phase-0-ir-truth-manifest.md b/docs/design/0009-rust-core-and-wasm-capability-abi/phase-0-ir-truth-manifest.md index 5bdbd354..ecfb43d4 100644 --- a/docs/design/0009-rust-core-and-wasm-capability-abi/phase-0-ir-truth-manifest.md +++ b/docs/design/0009-rust-core-and-wasm-capability-abi/phase-0-ir-truth-manifest.md @@ -34,6 +34,10 @@ rules are MANDATORY: IR. It MUST be stripped before computing the parity hash. The JS adapter now emits a stable `generatedAt` value for compatibility so repeated parses of identical SDL do not change IR bytes solely because wall-clock time advanced. +7. **Directive Multiplicity:** Core Wesley directive aliases MUST lower to the + canonical `@wes_*` directive name and duplicate canonical core directives + MUST fail. Repeated custom directives are preserved as ordered JSON arrays + under the directive name. ## Fixture Corpus 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 d61acb8d..d90538cd 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 @@ -47,10 +47,11 @@ accident. For each fixture it records: - fixture path -- legacy JS semantic IR bytes -- Rust semantic IR bytes -- legacy JS semantic hash -- Rust semantic hash +- comparison projection name +- legacy JS projected semantic bytes +- Rust projected semantic bytes +- legacy JS projected semantic hash +- Rust projected semantic hash - normalizer version - command versions or commit identifiers when available @@ -75,6 +76,38 @@ The legacy JS side uses the current truth anchors named in - `registryHash` - `canonicalizeJSON` +### Projection + +The sentinel must not compare raw legacy JS table IR bytes directly against +raw Rust L1 bytes. Those shapes are intentionally different today: the legacy +JS adapter emits table-centered IR, while Rust L1 emits consolidated GraphQL +type definitions. + +The first implementation uses a named `js-table-vs-rust-table.v0` projection. +Both lowerers must project into that shared comparison shape before bytes or +hashes are compared. + +The projection includes: + +- object types admitted as tables by `@wes_table` or its supported aliases +- effective table names +- field names and GraphQL type references +- canonical core Wesley directives and directive arguments +- table index, tenant, primary-key, default, and foreign-key facts derivable + from the projected directives + +The projection excludes: + +- non-table scalar, enum, union, interface, and input-object-only semantics +- Rust-only extension-family coverage that has no legacy JS table-IR + equivalent yet +- generated relationship records unless the projection derives the same fact + from both lowerers + +Each fixture admitted to the sentinel corpus must name the projection it uses. +Fixtures with no coherent legacy/Rust common projection remain Rust L1 corpus +fixtures until a separate crosswalk is designed. + ### Normalization The normalizer removes envelope-only data and keeps semantic data intact. @@ -90,11 +123,12 @@ The normalizer removes envelope-only data and keeps semantic data intact. ### Hash Behavior -The sentinel compares normalized semantic bytes and their SHA-256 digests. +The sentinel compares normalized projected semantic bytes and their SHA-256 +digests. It also verifies that the Rust `schema hash` command agrees with the digest of -the normalized Rust semantic bytes. If those disagree, the Rust CLI/hash path -is inconsistent even before JS parity is considered. +the normalized Rust L1 semantic bytes. If those disagree, the Rust CLI/hash +path is inconsistent even before JS parity is considered. Tracked `*.l1.hash` files remain Rust golden outputs, not JS/Rust parity evidence. @@ -122,17 +156,20 @@ This first v0.0.6 slice does not implement the sentinel command yet. It does pull the backlog card into design, expands the Rust L1 corpus, and closes one blocker the sentinel would otherwise expose immediately: Rust L1 lowering now canonicalizes the core Wesley directive aliases before writing -semantic IR and rejects duplicate canonical directives. +semantic IR, rejects duplicate canonical core directives, and preserves +repeated custom directives as ordered values. ## Playback Questions 1. Is Rust fixture regeneration still separate from JS/Rust parity proof? 2. Does the design define comparator inputs, lowerers, normalization, hash behavior, and failure output? -3. Does the fixture corpus now cover directive-heavy SDL, schema extensions, +3. Does the design forbid raw legacy table IR versus raw Rust L1 comparison + and name a projection before comparing bytes? +4. Does the fixture corpus now cover directive-heavy SDL, schema extensions, legacy aliases, and at least one invalid-SDL case? -4. Does Rust L1 preserve canonical directive names for supported aliases? -5. Is the next implementation slice narrow enough to add a `pnpm parity:ir` +5. Does Rust L1 preserve canonical directive names for supported aliases? +6. Is the next implementation slice narrow enough to add a `pnpm parity:ir` check without changing the Rust golden-regeneration command? ## Non-Goals From 9789f8e3e827f4ecd2364d7548fdfaceaf1e6df3 Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 22 May 2026 02:33:59 -0700 Subject: [PATCH 3/3] docs(readme): align current release center --- docs/README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/README.md b/docs/README.md index 3a8e9de5..93fb3012 100644 --- a/docs/README.md +++ b/docs/README.md @@ -30,15 +30,20 @@ The most recent Continuum cycle packet is It closed as a `partial` landing in [its retro packet](./method/retro/0003-continuum-contract-compiler/continuum-contract-compiler.md). -The active release center is now the v0.0.5 clean-house packet: +The v0.0.5 clean-house packet is shipped release context: - [Product Leftover Cleanup](./design/0012-product-leftover-cleanup/product-leftover-cleanup.md) +The active release center is now v0.0.6 Rust IR parity: + +- [Rust IR Parity Sentinel](./design/0013-rust-ir-parity-sentinel/rust-ir-parity-sentinel.md) + The old Continuum-heavy `v0.1.0/` lane has been retired to [graveyard/v0.1.0](./method/graveyard/v0.1.0/README.md). Treat those notes as historical extraction context, not as active Wesley release commitments. -The repo already has the important generic building block around that hill: +The repo already has the important generic building blocks around that +direction: - a module-driven `wesley compile` surface where targets come from loaded external modules