Skip to content
Open
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ versions still track specification maturity rather than a released product.

## [Unreleased]

- Add an application-owned configurable-cell example command and how-to with checked Unicode/byte limits, derived result budgets, repeatable public lawpack/application builds, and pinned Echo provider witnesses.

- Correct the retained study validation helper to remove staged deletions and old rename paths from copied validation snapshots.

- Signature mismatch spans identify exactly the effect call, excluding its enclosing binding and failure handler.
Expand Down
20 changes: 20 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions crates/edict-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,10 @@ serde_json = "1"
rustix = { version = "1.1.4", features = ["fs"] }

[dev-dependencies]
tempfile = "3"
serde_json = "1"
jsonschema = { version = "0.58.6", default-features = false }

[[example]]
name = "configurable-causal-cell"
path = "examples/configurable_causal_cell/main.rs"
92 changes: 92 additions & 0 deletions crates/edict-cli/examples/configurable_causal_cell/command.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
//! Deterministic document rendering for the standalone example command.
use super::limits::{LimitError, Limits};
use serde_json::json;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CommandError {
Usage,
InvalidNumber,
InvalidDigest,
Limits(LimitError),
Encoding,
}

pub fn render(arguments: &[&str]) -> Result<String, CommandError> {
match arguments {
["lawpack", key, value, bytes] => {
let document = super::document::document(parameters(key, value, bytes)?)
.map_err(CommandError::Limits)?;
serde_json::to_string_pretty(&document).map_err(|_| CommandError::Encoding)
}
["source", key, value, bytes, digest] => {
let limits = parameters(key, value, bytes)?;
limits.budgets().map_err(CommandError::Limits)?;
if !digest.strip_prefix("sha256:").is_some_and(|hex| {
hex.len() == 64
&& hex.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
}) {
return Err(CommandError::InvalidDigest);
}
Ok(source(limits, digest))
}
["application"] => serde_json::to_string_pretty(&json!({
"schema": "edict.application/v1", "coordinate": "examples.configurable_cell@1",
"sources": ["src/create.edict"],
"lawpacks": [{"manifest": "vendor/cell/manifest.cbor", "exports": "vendor/cell/exports.cbor",
"adapter": "vendor/cell/adapter.cbor", "targetConfiguration": "vendor/cell/configuration.cbor"}],
"target": {"profile": "echo.dpo@1", "providerPackage": "provider"},
"outputDirectory": ".build/application"
})).map_err(|_| CommandError::Encoding),
_ => Err(CommandError::Usage),
}
}

fn parameters(key: &str, value: &str, bytes: &str) -> Result<Limits, CommandError> {
let parse = |text: &str| text.parse().map_err(|_| CommandError::InvalidNumber);
Ok(Limits {
key_scalars: parse(key)?,
value_scalars: parse(value)?,
replacement_bytes: parse(bytes)?,
})
}

fn source(limits: Limits, digest: &str) -> String {
format!(
r#"package examples.configurable_cell@1;

use lawpack example.cell@1 digest "{digest}" as cell;

type Created = {{
key: String<max={key}>,
value: String<max={value}>,
}};

intent create(input: cell.CreateInput) returns Created
profile cell.createIfAbsent
basis input.basis
budget <= cell.createBudget
{{
let receipt: cell.CreateReceipt = cell.createIfAbsent(input)
else {{ alreadyExists(existing) => cell.AlreadyExists }};
return {{ key: receipt.key, value: input.value }};
}}
"#,
key = limits.key_scalars,
value = limits.value_scalars,
)
}

impl std::fmt::Display for CommandError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let message = match self {
Self::Usage => "usage: configurable-causal-cell lawpack KEY_SCALARS VALUE_SCALARS VALUE_BYTES | source KEY_SCALARS VALUE_SCALARS VALUE_BYTES MANIFEST_DIGEST | application",
Self::InvalidNumber => "limits must be unsigned 64-bit integers",
Self::InvalidDigest => "manifest digest must be sha256: followed by 64 lowercase hexadecimal digits",
Self::Limits(LimitError::Zero) => "all limits must be greater than zero",
Self::Limits(LimitError::Overflow) => "limits overflow checked byte or budget arithmetic",
Self::Limits(LimitError::ScalarBytesExceedCap) => "value scalar maximum requires more UTF-8 bytes than the replacement cap",
Self::Encoding => "document JSON encoding failed",
};
formatter.write_str(message)
}
}
95 changes: 95 additions & 0 deletions crates/edict-cli/examples/configurable_causal_cell/document.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//! Application-owned authoring JSON; Edict computes all local artifact digests.
use super::limits::{ByteBudgets, LimitError, Limits};
use serde_json::{json, Value};

pub fn document(limits: Limits) -> Result<Value, LimitError> {
let budget = limits.budgets()?;
Ok(json!({
"schema": "edict.lawpack-build/v1",
"outputDirectory": "vendor/cell",
"dependencyBundles": [],
"lawpack": {
"schema": "edict.lawpack-authoring/v1",
"id": "example.cell", "version": "1",
"acceptedCoreAbi": ["edict.core/v1"], "dependencies": [],
"exportsCoordinate": "example.cell.exports/v1",
"exports": exports(limits),
"targetAdapters": [adapter(budget)],
"verifier": {"class": "declarative", "ruleset": {"local": "rules"}},
"compatibility": {"local": "compatibility"},
"conformanceFixtureCorpus": {"local": "fixtures"},
"localResources": [
{"name": "configuration", "coordinate": "echo.operation-lowering-configuration/v1",
"output": "configuration.cbor", "value": configuration(limits, budget)},
{"name": "rules", "coordinate": "example.cell.rules/v1", "output": "rules.cbor",
"value": {"effect": "example.cell@1.createIfAbsent", "writeClass": "create"}},
{"name": "compatibility", "coordinate": "example.cell.compatibility/v1", "output": "compatibility.cbor",
"value": {"core": "edict.core/v1", "target": "echo.span-ir/v1"}},
{"name": "fixtures", "coordinate": "example.cell.fixtures/v1", "output": "fixtures.cbor",
"value": {"keyScalars": limits.key_scalars, "valueScalars": limits.value_scalars,
"replacementBytes": limits.replacement_bytes}}
]
}
}))
}

fn exports(limits: Limits) -> Value {
let key = format!("String<max={},canonical=raw-utf8>", limits.key_scalars);
let value = format!("String<max={},canonical=raw-utf8>", limits.value_scalars);
json!({
"types": [
{"coordinate": "example.cell@1.CreateInput", "definition": format!("Record<basis:String<max=128,canonical=raw-utf8>,key:{key},value:{value}>")},
{"coordinate": "example.cell@1.CreateReceipt", "definition": format!("Record<key:{key}>")},
{"coordinate": "example.cell@1.ExistingValue", "definition": format!("Record<key:{key},value:{value}>")}
],
"constants": [], "pureFunctions": [],
"effects": [{
"coordinate": "example.cell@1.createIfAbsent", "typeParameters": [],
"inputType": "example.cell@1.CreateInput", "outputType": "example.cell@1.CreateReceipt",
"executionClass": "runtime", "effectKindHint": "create",
"footprintObligation": "example.cell@1.cellKeyFootprint", "costObligation": "example.cell@1.createBudget",
"effectFailures": {"alreadyExists": {"authorityClass": "domainMappable", "payloadType": "example.cell@1.ExistingValue"}},
"guardSupport": true
}],
"obstructions": [{"coordinate": "example.cell@1.AlreadyExists", "authorityClass": "domainMappable", "payloadSchema": "example.cell@1.ExistingValue"}],
"operationProfiles": {"example.cell@1.createIfAbsent": {
"opticTemplate": {"opticKind": "affectReintegration", "boundaryKind": "affect",
"supportPolicy": "example.cell@1.directSupport", "lossDisposition": "example.cell@1.lossless",
"apertureRequirement": {"kind": "abstractFootprintObligation", "reference": "example.cell@1.cellKeyFootprint"}},
"effectPredicate": "example.cell@1.createIfAbsentEffect"
}}
})
}

fn adapter(budget: ByteBudgets) -> Value {
json!({
"coordinate": "example.cell.echo-adapter/v1", "output": "adapter.cbor",
"acceptedTargetProfile": {"id": "echo.dpo@1", "digest": "sha256:2e2494121aecf5e6a2d920f5fb85408825d394765fad41484c416397c920fb04"},
"acceptedTargetIr": {"id": "echo.span-ir/v1", "digest": "sha256:0057167e68f50c99dcce087b3e1cd677d17c5d1dc238bdb52d89469e1472fc2f"},
"operationProfiles": {"example.cell@1.createIfAbsent": {
"core": "continuum.profile.create/v1", "semanticEffects": ["example.cell@1.createIfAbsent"]
}},
"effectImplementations": {"example.cell@1.createIfAbsent": {
"targetIntrinsic": "echo.dpo@1.anchored-node-attachment-create-if-absent",
"targetConfiguration": {"local": "configuration"}, "writeClass": "create",
"footprintObligation": "example.cell@1.cellKeyFootprint", "costObligation": "example.cell@1.createBudget",
"failureMappings": {"alreadyExists": "echo.executable-operation/precondition-mismatch/v1"}
}},
"budgets": {"example.cell@1.createBudget": {"maxSteps": 16,
"maxAllocatedBytes": budget.allocated, "maxOutputBytes": budget.output}}
})
}

fn configuration(limits: Limits, budget: ByteBudgets) -> Value {
json!({
"apiVersion": "echo.operation-lowering-configuration/v1",
"programKind": "anchored-node-attachment-create-if-absent/v1",
"requiredNodeTypeProfile": "causal.cell.node/value/v1",
"requiredAttachmentTypeProfile": "causal.cell.attachment/value/v1",
"maxReplacementBytes": limits.replacement_bytes,
"authorityProfile": "causal.cell.authority.application/v1",
"budgetCeiling": {"steps": 16, "readBytes": 64, "writeBytes": budget.write},
"invocationBinding": {"nodeKeyField": "key", "replacementField": "value",
"nodeIdDerivation": "sha256-utf8/v1", "warpIdSource": "action-lane/v1"}
})
}
78 changes: 78 additions & 0 deletions crates/edict-cli/examples/configurable_causal_cell/limits.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//! Parameter calculations for the application-owned causal-cell example.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Limits {
pub key_scalars: u64,
pub value_scalars: u64,
pub replacement_bytes: u64,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ByteBudgets {
pub output: u64,
pub write: u64,
pub allocated: u64,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LimitError {
Zero,
Overflow,
ScalarBytesExceedCap,
}

impl Limits {
pub fn budgets(self) -> Result<ByteBudgets, LimitError> {
if self.key_scalars == 0 || self.value_scalars == 0 || self.replacement_bytes == 0 {
return Err(LimitError::Zero);
}
let key_bytes = self
.key_scalars
.checked_mul(4)
.ok_or(LimitError::Overflow)?;
let value_bytes = self
.value_scalars
.checked_mul(4)
.ok_or(LimitError::Overflow)?;
if value_bytes > self.replacement_bytes {
return Err(LimitError::ScalarBytesExceedCap);
}
// Canonical result is { key: text, value: text }, including all headers.
let output_bytes = sum(&[
1,
text_size(3)?,
text_size(key_bytes)?,
text_size(5)?,
text_size(value_bytes)?,
])?;
// The selected create profile reserves 64 bytes beyond replacement data.
let write_bytes = self
.replacement_bytes
.checked_add(64)
.ok_or(LimitError::Overflow)?;
// Input adds a 128-scalar basis field to the same two-field shape.
let input_bytes = sum(&[output_bytes, text_size(5)?, text_size(128 * 4)?])?;
let allocated_bytes = sum(&[input_bytes, output_bytes, self.replacement_bytes])?;
Ok(ByteBudgets {
output: output_bytes,
write: write_bytes,
allocated: allocated_bytes,
})
}
}

fn text_size(bytes: u64) -> Result<u64, LimitError> {
let header = match bytes {
0..=23 => 1,
24..=255 => 2,
256..=65_535 => 3,
65_536..=4_294_967_295 => 5,
_ => 9,
};
bytes.checked_add(header).ok_or(LimitError::Overflow)
}

fn sum(values: &[u64]) -> Result<u64, LimitError> {
values.iter().try_fold(0_u64, |total, value| {
total.checked_add(*value).ok_or(LimitError::Overflow)
})
}
19 changes: 19 additions & 0 deletions crates/edict-cli/examples/configurable_causal_cell/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//! Print application-owned causal-cell documents without modifying files.
mod command;
mod document;
mod limits;

fn main() -> std::process::ExitCode {
let arguments: Vec<String> = std::env::args().skip(1).collect();
let arguments: Vec<&str> = arguments.iter().map(String::as_str).collect();
match command::render(&arguments) {
Ok(document) => {
println!("{document}");
std::process::ExitCode::SUCCESS
}
Err(error) => {
eprintln!("{error}");
std::process::ExitCode::from(2)
}
}
}
Loading
Loading