Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
15b73d4
docs: sequence echo contract hosting backlog
flyingrobots May 4, 2026
4915d40
docs: define echo contract hosting doctrine
flyingrobots May 4, 2026
06e79a7
docs: align contract backlog with existing echo substrate
flyingrobots May 4, 2026
7e9fc84
docs: inventory echo intent registry observation boundary
flyingrobots May 4, 2026
70bbcb3
docs: decide registry host boundary
flyingrobots May 4, 2026
776f687
test: prove Wesley toy contract bridge missing
flyingrobots May 4, 2026
6c391ee
docs: track relocated Wesley schema reconciliation
flyingrobots May 4, 2026
8cef2fe
feat: emit Wesley EINT observation helpers
flyingrobots May 4, 2026
dbb9019
test: compile Wesley toy consumer bridge
flyingrobots May 4, 2026
80c1207
fix: canonicalize Wesley generated vars helpers
flyingrobots May 4, 2026
340af96
docs: explain application contract hosting
flyingrobots May 4, 2026
0c5f580
test: extract Wesley toy counter fixture
flyingrobots May 4, 2026
51c047d
fix: harden Wesley generated helper output
flyingrobots May 4, 2026
4a05c20
docs: render Mermaid diagrams in VitePress
flyingrobots May 4, 2026
0879522
docs: define authenticated Wesley intent admission posture
flyingrobots May 4, 2026
1baa426
fix: namespace Wesley generated helper types
flyingrobots May 4, 2026
dced3e7
docs: address contract hosting review feedback
flyingrobots May 4, 2026
721e0ef
docs: update changelog for PR feedback
flyingrobots May 4, 2026
0e3aa81
test: cover no-std Wesley helper output
flyingrobots May 4, 2026
2d6b3ea
docs: clarify generated query helper scope
flyingrobots May 4, 2026
e45463b
docs: update changelog for follow-up review
flyingrobots May 4, 2026
c02a906
Merge branch 'main' of github.com:flyingrobots/echo into backlog/echo…
flyingrobots May 4, 2026
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
148 changes: 148 additions & 0 deletions crates/echo-wesley-gen/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,31 @@ fn generate_rust(ir: &WesleyIR, args: &Args) -> Result<String> {
tokens.extend(quote! {
// Registry provider types (Echo runtime loads an app-supplied implementation).
use echo_registry_api::{ArgDef, EnumDef, ObjectDef, OpDef, OpKind, RegistryInfo, RegistryProvider};
use echo_wasm_abi::pack_intent_v1;
});

if ir.ops.iter().any(|op| op.kind == OpKind::Query) {
tokens.extend(quote! {
use echo_wasm_abi::kernel_port::{
ObservationAt, ObservationCoordinate, ObservationFrame, ObservationProjection,
ObservationRequest, WorldlineId,
};
});
}

if ir.ops.iter().any(|op| op.kind == OpKind::Mutation) {
tokens.extend(quote! {
/// Error produced while building a generated EINT intent.
#[derive(Debug)]
pub enum GeneratedIntentError {
Comment thread
flyingrobots marked this conversation as resolved.
Outdated
/// Operation vars could not be encoded canonically.
EncodeVars(echo_wasm_abi::CanonError),
/// Encoded vars could not be packed into an EINT envelope.
PackEnvelope(echo_wasm_abi::EnvelopeError),
}
});
}

let mut enum_defs: Vec<_> = ir
.types
.iter()
Expand Down Expand Up @@ -241,6 +264,84 @@ fn generate_rust(ir: &WesleyIR, args: &Args) -> Result<String> {
});
}

for op in &ops_sorted {
let const_name = op_const_ident(&op.name, op.op_id);
let helper_name = format_ident!("{}", to_snake_case(&op.name));
let vars_name = format_ident!("{}Vars", to_pascal_case(&op.name));
Comment thread
flyingrobots marked this conversation as resolved.
let vars_fields = op.args.iter().map(|a| {
let field_name = safe_ident(&a.name);
let base_ty = map_type(&a.type_name, args);
let list_ty: TokenStream = if a.list {
quote! { Vec<#base_ty> }
} else {
quote! { #base_ty }
};

if a.required {
quote! { pub #field_name: #list_ty }
} else {
quote! { pub #field_name: Option<#list_ty> }
}
});
let encode_fn_name = format_ident!("encode_{}_vars", helper_name);
tokens.extend(quote! {
/// Canonical vars payload for this generated operation.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct #vars_name {
#(#vars_fields),*
}

/// Encode this operation's vars using Echo canonical CBOR.
pub fn #encode_fn_name(vars: &#vars_name) -> Result<Vec<u8>, echo_wasm_abi::CanonError> {
echo_wasm_abi::encode_cbor(vars)
}
});
match op.kind {
OpKind::Mutation => {
let fn_name = format_ident!("pack_{}_intent", helper_name);
let raw_fn_name = format_ident!("pack_{}_intent_raw_vars", helper_name);
tokens.extend(quote! {
/// Encode this mutation's vars and pack them into an EINT v1 intent.
pub fn #fn_name(vars: &#vars_name) -> Result<Vec<u8>, GeneratedIntentError> {
let vars_bytes = #encode_fn_name(vars).map_err(GeneratedIntentError::EncodeVars)?;
pack_intent_v1(#const_name, &vars_bytes).map_err(GeneratedIntentError::PackEnvelope)
}

/// Pack already-canonical vars bytes for this generated mutation into EINT v1.
pub fn #raw_fn_name(vars: &[u8]) -> Result<Vec<u8>, echo_wasm_abi::EnvelopeError> {
pack_intent_v1(#const_name, vars)
}
});
}
OpKind::Query => {
let fn_name = format_ident!("{}_observation_request", helper_name);
let raw_fn_name = format_ident!("{}_observation_request_raw_vars", helper_name);
tokens.extend(quote! {
/// Encode this query's vars and build a frontier query-view observation request.
pub fn #fn_name(worldline_id: WorldlineId, vars: &#vars_name) -> Result<ObservationRequest, echo_wasm_abi::CanonError> {
let vars_bytes = #encode_fn_name(vars)?;
Ok(#raw_fn_name(worldline_id, &vars_bytes))
}

/// Build a frontier query-view request from already-canonical vars bytes.
pub fn #raw_fn_name(worldline_id: WorldlineId, vars: &[u8]) -> ObservationRequest {
ObservationRequest {
coordinate: ObservationCoordinate {
worldline_id,
at: ObservationAt::Frontier,
},
frame: ObservationFrame::QueryView,
projection: ObservationProjection::Query {
query_id: #const_name,
vars_bytes: Vec::from(vars),
},
}
}
});
}
}
}

// OPS table (sorted by op_id).
let ops_entries = ops_sorted.iter().map(|op| {
let kind = match op.kind {
Expand Down Expand Up @@ -324,6 +425,53 @@ fn op_const_ident(name: &str, op_id: u32) -> proc_macro2::Ident {
format_ident!("OP_{}", out)
}

fn to_pascal_case(name: &str) -> String {
let mut out = String::new();
let mut capitalize_next = true;
for c in name.chars() {
if c.is_alphanumeric() {
if capitalize_next {
out.push(c.to_ascii_uppercase());
capitalize_next = false;
} else {
out.push(c);
}
} else {
capitalize_next = true;
}
}
if out.is_empty() {
"Op".to_string()
} else {
out
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

fn to_snake_case(name: &str) -> String {
let mut out = String::new();
let mut previous_was_separator = true;
for (index, c) in name.chars().enumerate() {
if c.is_alphanumeric() {
if c.is_uppercase() && index > 0 && !previous_was_separator {
out.push('_');
}
out.push(c.to_ascii_lowercase());
previous_was_separator = false;
} else if !previous_was_separator {
out.push('_');
previous_was_separator = true;
}
}
while out.ends_with('_') {
out.pop();
}
if out.is_empty() {
"op".to_string()
} else {
out
}
}

fn validate_version(ir: &WesleyIR) -> Result<()> {
const SUPPORTED: &str = "echo-ir/v1";
match ir.ir_version.as_deref() {
Expand Down
29 changes: 29 additions & 0 deletions crates/echo-wesley-gen/tests/fixtures/toy-counter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!-- SPDX-License-Identifier: Apache-2.0 OR LicenseRef-MIND-UCAL-1.0 -->
<!-- © James Ross Ω FLYING•ROBOTS <https://github.com/flyingrobots> -->

# Toy Counter Fixture

This is the smallest shared Echo/Wesley contract-hosting fixture.

It exists to prove:

- generated operation ids and registry metadata;
- typed operation variable generation;
- canonical operation variable encoding;
- EINT v1 packing;
- observation request generation;
- generated output compilation in a standalone consumer crate;
- future installed-host contract smoke tests.

It is not:

- a `jedit` fixture;
- a dynamic loading fixture;
- a GraphQL execution fixture;
- a host-side registry validation fixture;
- a text-editing or product-domain fixture.

Tests should consume `echo-ir-v1.json` through `include_str!(...)`. Do not copy
the toy counter IR into new tests. If the fixture needs to change, update this
single source and make the contract boundary change explicit in the test that
requires it.
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{
"ir_version": "echo-ir/v1",
"schema_sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"codec_id": "cbor-canon-v1",
"registry_version": 1,
"types": [
{
"name": "CounterValue",
"kind": "OBJECT",
"fields": [
{
"name": "value",
"type": "Int",
"required": true
}
]
},
{
"name": "IncrementInput",
"kind": "INPUT_OBJECT",
"fields": [
{
"name": "amount",
"type": "Int",
"required": true
}
]
},
{
"name": "Mutation",
"kind": "OBJECT",
"fields": [
{
"name": "increment",
"type": "CounterValue",
"required": true
}
]
},
{
"name": "Query",
"kind": "OBJECT",
"fields": [
{
"name": "counterValue",
"type": "CounterValue",
"required": true
}
]
}
],
"ops": [
{
"kind": "MUTATION",
"name": "increment",
"op_id": 1001,
"args": [
{
"name": "input",
"type": "IncrementInput",
"required": true
}
],
"result_type": "CounterValue"
},
{
"kind": "QUERY",
"name": "counterValue",
"op_id": 1002,
"args": [],
"result_type": "CounterValue"
}
]
}
Loading
Loading