Skip to content
Merged
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve

### Fixed

- **Rust Holmes assurance review fixes**: The new Holmes artifact locator now
returns stable Holmes diagnostics for invalid and escaping paths, rejects
platform-specific backslash and drive-path input before normalization, the
schema-version registry now fails closed when a family requirement is absent,
semantic-version parsing rejects leading-zero identifiers, in-memory port
writes are readable through the same fake store, and the crate metadata/docs
no longer point at nonexistent or unpublished documentation.
- **`weslaw` semantic diff review fixes**: Law diffs now classify existing
channel and invariant law modifications as modification events instead of
additions, emit registry/tag/schema-hash events so changed `lawHash` values
Expand All @@ -33,6 +40,12 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve

### Added

- **Rust Holmes assurance foundation**: Added the unpublished
`crates/wesley-holmes` workspace crate with a hexagonal module shell, domain
dependency-boundary tests, deterministic port traits and fakes, a structured
diagnostic envelope, typed `HolmesLawEvidenceBundle` model, safe
workspace-relative artifact path locator, and artifact-family schema-version
registry for the first ten Holmes implementation slices.
- **`weslaw` v1 consumer payoff**: `wesley emit rust --law <path>` now emits
law-backed helper validators for integer scalar semantics and discriminated
input variant rules, `wesley law capabilities` emits report-only
Expand Down
7 changes: 7 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ members = [
"crates/wesley-core",
"crates/wesley-emit-typescript",
"crates/wesley-emit-rust",
"crates/wesley-holmes",
"crates/wesley-cli",
"xtask",
]
Expand Down
13 changes: 13 additions & 0 deletions crates/wesley-holmes/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "wesley-holmes"
version = "0.0.5"
edition = "2021"
description = "Rust Holmes law assurance foundation for Wesley semantic evidence"
license = "MIT"
repository = "https://github.com/flyingrobots/wesley"
homepage = "https://github.com/flyingrobots/wesley"
readme = "README.md"
publish = false

[dependencies]
serde = { version = "1.0", features = ["derive"] }
31 changes: 31 additions & 0 deletions crates/wesley-holmes/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# wesley-holmes

`wesley-holmes` is the Rust foundation for Holmes law assurance work inside
Wesley. It consumes Wesley-published law evidence, policy, witness, MCP, and
GitHub payload artifacts; validates their envelope shape and version posture;
and prepares deterministic diagnostics and reporting surfaces for later CLI,
API, and MCP interfaces.

This crate is intentionally not published yet. It is a workspace implementation
crate for the Holmes redesign described in the Wesley design packet:

- [Holmes `weslaw` Assurance PRD/Test Plan](https://github.com/flyingrobots/wesley/blob/main/docs/design/0020-holmes-weslaw-assurance-prd-test-plan/holmes-weslaw-assurance-prd-test-plan.md)
- [Holmes Assurance Hexagon](https://github.com/flyingrobots/wesley/blob/main/docs/design/0018-holmes-assurance-hexagon/holmes-assurance-hexagon.md)

## Boundary

The crate follows the planned hexagonal boundary:

- `domain`: pure law-assurance data, diagnostics, evidence models, and version
rules. Domain code must not import filesystem, network, process, GitHub, MCP,
or wall-clock dependencies.
- `application`: deterministic orchestration utilities that bind domain facts
to ports without owning external side effects.
- `ports`: abstract clock, artifact, policy, reporting, GitHub, MCP, and command
I/O traits plus deterministic fakes for tests.
- `adapters`: future concrete integrations for filesystem, GitHub, MCP, and CLI
surfaces.
- `reporting`: future renderer-facing DTOs and report assembly helpers.

The current slice establishes the foundation only. No public Holmes CLI command
is exposed from Wesley yet.
5 changes: 5 additions & 0 deletions crates/wesley-holmes/src/adapters/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
//! Concrete adapter boundary for future filesystem, GitHub, MCP, and CLI integrations.
//!
//! The first Holmes implementation slice deliberately leaves this namespace
//! empty. Concrete adapters will land only after the domain and port contracts
//! have proven stable.
108 changes: 108 additions & 0 deletions crates/wesley-holmes/src/application/artifact_locator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//! Workspace-relative artifact path resolution.

use std::path::{Component, Path};

use crate::domain::{HolmesDiagnostic, HolmesDiagnosticCode, HolmesResult, HolmesSeverity};

/// A normalized artifact path that stays inside the configured workspace root.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedArtifactPath {
/// Workspace-relative path using `/` separators.
pub workspace_relative: String,
}

/// Resolves `weslaw` artifact references without touching the filesystem.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WeslawArtifactLocator {
workspace_root: String,
}

impl WeslawArtifactLocator {
/// Create a locator for a workspace root label.
pub fn new(workspace_root: impl Into<String>) -> Self {
Self {
workspace_root: workspace_root.into(),
}
}

/// Return the configured workspace root label.
pub fn workspace_root(&self) -> &str {
&self.workspace_root
}

/// Normalize a user-authored artifact path relative to the workspace.
///
/// The resolver is lexical by design. It rejects absolute paths, Windows
/// prefixes, empty paths, and `..` components that would escape the
/// workspace root. It does not perform symlink or filesystem
/// canonicalization.
pub fn resolve(&self, path: &str) -> HolmesResult<ResolvedArtifactPath> {
if path.trim().is_empty() {
return Err(invalid_path("artifact path must not be empty"));
}

if path.contains('\\') {
return Err(path_escape("artifact path must use `/` separators"));
}

if looks_like_windows_drive_path(path) {
return Err(path_escape("artifact path must be workspace-relative"));
}

let path = Path::new(path);
if path.is_absolute() {
return Err(path_escape("artifact path must be workspace-relative"));
}

let mut normalized = Vec::new();
for component in path.components() {
match component {
Component::CurDir => {}
Component::Normal(segment) => {
normalized.push(segment.to_string_lossy().into_owned())
}
Component::ParentDir => {
if normalized.pop().is_none() {
return Err(path_escape(
"artifact path must not escape the workspace root",
));
}
}
Component::Prefix(_) | Component::RootDir => {
return Err(path_escape("artifact path must be workspace-relative"));
}
}
}

if normalized.is_empty() {
return Err(invalid_path("artifact path must reference a file"));
}

Ok(ResolvedArtifactPath {
workspace_relative: normalized.join("/"),
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}

fn invalid_path(message: impl Into<String>) -> HolmesDiagnostic {
HolmesDiagnostic::new(
HolmesDiagnosticCode::HlawArtifactPathInvalid,
HolmesSeverity::Error,
message,
)
.at_field("path")
}

fn path_escape(message: impl Into<String>) -> HolmesDiagnostic {
HolmesDiagnostic::new(
HolmesDiagnosticCode::HlawArtifactPathEscape,
HolmesSeverity::Error,
message,
)
.at_field("path")
}

fn looks_like_windows_drive_path(path: &str) -> bool {
let bytes = path.as_bytes();
bytes.len() >= 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':'
}
5 changes: 5 additions & 0 deletions crates/wesley-holmes/src/application/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
//! Application services for deterministic Holmes law-assurance orchestration.

mod artifact_locator;

pub use artifact_locator::{ResolvedArtifactPath, WeslawArtifactLocator};
100 changes: 100 additions & 0 deletions crates/wesley-holmes/src/domain/diagnostic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//! Deterministic diagnostic envelopes for Holmes law assurance.

use std::error::Error;
use std::fmt;

use serde::{Deserialize, Serialize};

/// Stable diagnostic code emitted by Holmes validation and ingest paths.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum HolmesDiagnosticCode {
/// A required `schemaVersion` field was absent or blank.
HlawSchemaVersionMissing,
/// A `schemaVersion` field was not valid semantic version syntax.
HlawSchemaVersionMalformed,
/// A `schemaVersion` major version is not supported by this Holmes build.
HlawSchemaVersionUnsupportedMajor,
/// A `schemaVersion` minor version is newer than this Holmes build accepts.
HlawSchemaVersionUnsupportedMinor,
/// No local version requirement was configured for an artifact family.
HlawSchemaVersionRequirementMissing,
/// An artifact path attempted to escape the workspace root.
HlawArtifactPathEscape,
/// An artifact path was malformed before resolution.
HlawArtifactPathInvalid,
/// A law evidence bundle was missing a required artifact reference.
HlawEvidenceBundleInvalid,
/// A requested artifact was unavailable through its port.
HlawArtifactUnavailable,
}

/// Severity attached to a Holmes diagnostic.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum HolmesSeverity {
/// A hard failure that prevents safe continuation.
Error,
/// A non-blocking issue that should be visible in reports.
Warning,
/// Informational context attached to a report.
Info,
}

/// Structured diagnostic envelope shared by validation and ingest flows.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HolmesDiagnostic {
/// Stable diagnostic code.
pub code: HolmesDiagnosticCode,
/// Diagnostic severity.
pub severity: HolmesSeverity,
/// Human-readable explanation.
pub message: String,
/// Optional artifact family associated with this diagnostic.
#[serde(skip_serializing_if = "Option::is_none")]
pub artifact_family: Option<String>,
/// Optional field path associated with this diagnostic.
#[serde(skip_serializing_if = "Option::is_none")]
pub field_path: Option<String>,
}

impl HolmesDiagnostic {
/// Create a new diagnostic envelope.
pub fn new(
code: HolmesDiagnosticCode,
severity: HolmesSeverity,
message: impl Into<String>,
) -> Self {
Self {
code,
severity,
message: message.into(),
artifact_family: None,
field_path: None,
}
}

/// Attach an artifact-family label.
pub fn for_family(mut self, family: impl Into<String>) -> Self {
self.artifact_family = Some(family.into());
self
}

/// Attach a field path.
pub fn at_field(mut self, field_path: impl Into<String>) -> Self {
self.field_path = Some(field_path.into());
self
}
}

impl fmt::Display for HolmesDiagnostic {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{:?}: {}", self.code, self.message)
}
}

impl Error for HolmesDiagnostic {}

/// Result alias for Holmes domain and port operations.
pub type HolmesResult<T> = Result<T, HolmesDiagnostic>;
Loading
Loading