From 361f10b3448a5591173807305c3c7c2888dc7ddb Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:02:21 +0200 Subject: [PATCH 01/15] Prepare issue 6 settings loader --- crates/lantern-app/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/lantern-app/Cargo.toml b/crates/lantern-app/Cargo.toml index 41378d1..a3fbc3b 100644 --- a/crates/lantern-app/Cargo.toml +++ b/crates/lantern-app/Cargo.toml @@ -13,6 +13,7 @@ lantern-domain.workspace = true lantern-profile.workspace = true serde.workspace = true thiserror.workspace = true +toml.workspace = true [lints] workspace = true From 75d89f241eb6a4e1bb1f6ba9a01dea69f58a91c0 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:02:45 +0200 Subject: [PATCH 02/15] Prepare issue 6 settings loader --- crates/lantern-app/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/lantern-app/src/lib.rs b/crates/lantern-app/src/lib.rs index 0976a18..805dd97 100644 --- a/crates/lantern-app/src/lib.rs +++ b/crates/lantern-app/src/lib.rs @@ -4,11 +4,13 @@ mod ports; mod profile_registry; +mod settings; use lantern_domain::{ProfileId, SessionId}; pub use ports::*; pub use profile_registry::*; +pub use settings::*; /// Read-only application state rendered by the terminal frontend. #[derive(Clone, Debug, Default, Eq, PartialEq)] From df719bb4fa80c746bf56e49e872b4531d9db9932 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:04:17 +0200 Subject: [PATCH 03/15] Prepare issue 6 settings loader --- crates/lantern-app/src/settings.rs | 385 +++++++++++++++++++++++++++++ 1 file changed, 385 insertions(+) create mode 100644 crates/lantern-app/src/settings.rs diff --git a/crates/lantern-app/src/settings.rs b/crates/lantern-app/src/settings.rs new file mode 100644 index 0000000..dfe694d --- /dev/null +++ b/crates/lantern-app/src/settings.rs @@ -0,0 +1,385 @@ +use std::path::PathBuf; + +use serde::Deserialize; +use thiserror::Error; + +use crate::{SettingsSourceError, SettingsSourcePort}; + +pub const MAX_SETTINGS_BYTES: usize = 1024 * 1024; + +#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum ColorMode { + #[default] + Auto, + Enabled, + Disabled, +} + +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct QueueCapacityDocument { + pub safety_one_shot: Option, + pub interactive: Option, + pub telemetry_critical: Option, + pub telemetry: Option, + pub background: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct PollingDocument { + pub telemetry_critical_ms: Option, + pub telemetry_ms: Option, + pub background_ms: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct PathOverridesDocument { + pub data: Option, + pub state: Option, + pub log: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct SettingsDocumentV1 { + pub render_fps: Option, + pub color: Option, + pub history_samples: Option, + pub memory_limit_mib: Option, + pub log_retention_files: Option, + pub suggested_profile: Option, + pub suggested_device: Option, + pub suggested_slave: Option, + pub queues: Option, + pub polling: Option, + pub paths: Option, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct CliSettingsOverrides { + pub profile: Option, + pub device: Option, + pub log_level: Option, + pub enable_writes: bool, + pub no_color: bool, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct QueueCapacities { + pub safety_one_shot: usize, + pub interactive: usize, + pub telemetry_critical: usize, + pub telemetry: usize, + pub background: usize, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PollingIntervals { + pub telemetry_critical_ms: u64, + pub telemetry_ms: u64, + pub background_ms: u64, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct PathOverrides { + pub data: Option, + pub state: Option, + pub log: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ValidatedSettings { + pub render_fps: u8, + pub color: ColorMode, + pub history_samples: usize, + pub memory_limit_mib: usize, + pub log_retention_files: usize, + pub queues: QueueCapacities, + pub polling: PollingIntervals, + pub paths: PathOverrides, + pub suggested_profile: Option, + pub suggested_device: Option, + pub suggested_slave: Option, + pub log_level: String, + pub process_writes_enabled: bool, +} + +impl Default for ValidatedSettings { + fn default() -> Self { + Self { + render_fps: 5, + color: ColorMode::Auto, + history_samples: 3_600, + memory_limit_mib: 128, + log_retention_files: 10, + queues: QueueCapacities { + safety_one_shot: 16, + interactive: 64, + telemetry_critical: 64, + telemetry: 256, + background: 32, + }, + polling: PollingIntervals { + telemetry_critical_ms: 250, + telemetry_ms: 1_000, + background_ms: 5_000, + }, + paths: PathOverrides::default(), + suggested_profile: None, + suggested_device: None, + suggested_slave: None, + log_level: "info".to_owned(), + process_writes_enabled: false, + } + } +} + +pub struct SettingsLoader; + +impl SettingsLoader { + pub fn load( + source: &dyn SettingsSourcePort, + cli: CliSettingsOverrides, + application_log_environment: Option<&str>, + ) -> Result { + let mut settings = ValidatedSettings::default(); + if let Some(bytes) = source.load_settings()? { + if bytes.len() > MAX_SETTINGS_BYTES { + return Err(SettingsError::TooLarge { + actual: bytes.len(), + maximum: MAX_SETTINGS_BYTES, + }); + } + let text = std::str::from_utf8(&bytes).map_err(|error| SettingsError::Parse { + message: error.to_string(), + })?; + let document: SettingsDocumentV1 = toml::from_str(text).map_err(|error| { + SettingsError::Parse { + message: error.to_string(), + } + })?; + apply_document(&mut settings, document)?; + } + + if let Some(profile) = cli.profile { + settings.suggested_profile = Some(profile); + } + if let Some(device) = cli.device { + settings.suggested_device = Some(device); + } + if let Some(level) = application_log_environment { + settings.log_level = validate_log_level(level)?; + } + if let Some(level) = cli.log_level { + settings.log_level = validate_log_level(&level)?; + } + if cli.no_color { + settings.color = ColorMode::Disabled; + } + settings.process_writes_enabled = cli.enable_writes; + Ok(settings) + } +} + +fn apply_document( + settings: &mut ValidatedSettings, + document: SettingsDocumentV1, +) -> Result<(), SettingsError> { + if let Some(render_fps) = document.render_fps { + if !(1..=10).contains(&render_fps) { + return Err(SettingsError::Validation( + "render_fps must be in 1..=10".to_owned(), + )); + } + settings.render_fps = render_fps; + } + if let Some(color) = document.color { + settings.color = color; + } + if let Some(history) = document.history_samples { + settings.history_samples = bounded("history_samples", history, 1, 1_000_000)?; + } + if let Some(memory) = document.memory_limit_mib { + settings.memory_limit_mib = bounded("memory_limit_mib", memory, 16, 4_096)?; + } + if let Some(retention) = document.log_retention_files { + settings.log_retention_files = bounded("log_retention_files", retention, 1, 1_000)?; + } + if let Some(slave) = document.suggested_slave { + if !(1..=247).contains(&slave) { + return Err(SettingsError::Validation( + "suggested_slave must be in 1..=247".to_owned(), + )); + } + settings.suggested_slave = Some(slave); + } + settings.suggested_profile = document.suggested_profile; + settings.suggested_device = document.suggested_device; + + if let Some(queues) = document.queues { + apply_queue(settings, queues)?; + } + if let Some(polling) = document.polling { + apply_polling(settings, polling)?; + } + if let Some(paths) = document.paths { + settings.paths = PathOverrides { + data: paths.data, + state: paths.state, + log: paths.log, + }; + } + Ok(()) +} + +fn apply_queue( + settings: &mut ValidatedSettings, + document: QueueCapacityDocument, +) -> Result<(), SettingsError> { + macro_rules! set { + ($field:ident, $maximum:expr) => { + if let Some(value) = document.$field { + settings.queues.$field = bounded(stringify!($field), value, 1, $maximum)?; + } + }; + } + set!(safety_one_shot, 64); + set!(interactive, 1_024); + set!(telemetry_critical, 1_024); + set!(telemetry, 4_096); + set!(background, 1_024); + Ok(()) +} + +fn apply_polling( + settings: &mut ValidatedSettings, + document: PollingDocument, +) -> Result<(), SettingsError> { + macro_rules! set { + ($field:ident) => { + if let Some(value) = document.$field { + settings.polling.$field = bounded_u64(stringify!($field), value, 50, 60_000)?; + } + }; + } + set!(telemetry_critical_ms); + set!(telemetry_ms); + set!(background_ms); + Ok(()) +} + +fn bounded( + name: &str, + value: usize, + minimum: usize, + maximum: usize, +) -> Result { + if (minimum..=maximum).contains(&value) { + Ok(value) + } else { + Err(SettingsError::Validation(format!( + "{name} must be in {minimum}..={maximum}" + ))) + } +} + +fn bounded_u64( + name: &str, + value: u64, + minimum: u64, + maximum: u64, +) -> Result { + if (minimum..=maximum).contains(&value) { + Ok(value) + } else { + Err(SettingsError::Validation(format!( + "{name} must be in {minimum}..={maximum}" + ))) + } +} + +fn validate_log_level(value: &str) -> Result { + match value { + "trace" | "debug" | "info" | "warn" | "error" => Ok(value.to_owned()), + _ => Err(SettingsError::Validation(format!( + "invalid log level {value}; expected trace, debug, info, warn or error" + ))), + } +} + +#[derive(Debug, Error)] +pub enum SettingsError { + #[error(transparent)] + Source(#[from] SettingsSourceError), + #[error("settings contain {actual} bytes; maximum is {maximum}")] + TooLarge { actual: usize, maximum: usize }, + #[error("settings TOML is invalid: {message}")] + Parse { message: String }, + #[error("settings validation failed: {0}")] + Validation(String), +} + +#[cfg(test)] +mod tests { + use super::*; + + struct MemorySource(Option>); + + impl SettingsSourcePort for MemorySource { + fn load_settings(&self) -> Result>, SettingsSourceError> { + Ok(self.0.clone()) + } + } + + #[test] + fn precedence_is_defaults_then_config_then_environment_then_cli() { + let source = MemorySource(Some( + br#"render_fps = 3 +color = "enabled" +suggested_device = "/dev/config" +"# + .to_vec(), + )); + let settings = SettingsLoader::load( + &source, + CliSettingsOverrides { + device: Some(PathBuf::from("/dev/cli")), + log_level: Some("error".to_owned()), + no_color: true, + enable_writes: true, + ..CliSettingsOverrides::default() + }, + Some("debug"), + ) + .expect("settings"); + assert_eq!(settings.render_fps, 3); + assert_eq!(settings.suggested_device, Some(PathBuf::from("/dev/cli"))); + assert_eq!(settings.log_level, "error"); + assert_eq!(settings.color, ColorMode::Disabled); + assert!(settings.process_writes_enabled); + } + + #[test] + fn dangerous_or_unknown_configuration_is_rejected_wholly() { + let source = MemorySource(Some(b"enable_writes = true\nrender_fps = 2\n".to_vec())); + assert!(matches!( + SettingsLoader::load(&source, CliSettingsOverrides::default(), None), + Err(SettingsError::Parse { .. }) + )); + } + + #[test] + fn missing_file_uses_safe_defaults() { + let settings = SettingsLoader::load( + &MemorySource(None), + CliSettingsOverrides::default(), + None, + ) + .expect("defaults"); + assert!(!settings.process_writes_enabled); + assert_eq!(settings.render_fps, 5); + } +} From 2c6bb6dc1acc394410e54cd97397f3d098bdef31 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:05:06 +0200 Subject: [PATCH 04/15] Prepare issue 6 settings source port --- crates/lantern-app/src/ports.rs | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/crates/lantern-app/src/ports.rs b/crates/lantern-app/src/ports.rs index 2833db5..17198d7 100644 --- a/crates/lantern-app/src/ports.rs +++ b/crates/lantern-app/src/ports.rs @@ -3,7 +3,6 @@ use std::path::PathBuf; use lantern_domain::ProfileId; use thiserror::Error; -/// Precedence tier assigned before profile parsing. #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub enum ProfileSourceTier { System, @@ -22,14 +21,12 @@ impl ProfileSourceTier { } } -/// Input format inferred by the storage adapter. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum ProfileSourceFormat { Toml, Json, } -/// Bounded profile bytes read by the storage adapter. #[derive(Clone, Debug, Eq, PartialEq)] pub struct ProfileSource { pub path: PathBuf, @@ -38,7 +35,6 @@ pub struct ProfileSource { pub tier: ProfileSourceTier, } -/// Failure while discovering or reading profile sources. #[derive(Clone, Debug, Eq, Error, PartialEq)] pub enum ProfileSourceError { #[error("profile source {path} is a symlink")] @@ -61,42 +57,48 @@ pub enum ProfileSourceError { Io { path: PathBuf, message: String }, } -/// Capability for read-only bus operations. +#[derive(Clone, Debug, Eq, Error, PartialEq)] +pub enum SettingsSourceError { + #[error("settings source operation failed for {path}: {message}")] + Io { path: PathBuf, message: String }, + #[error("settings path is a symlink: {path}")] + Symlink { path: PathBuf }, + #[error("settings path is not a regular file: {path}")] + NotRegular { path: PathBuf }, +} + +pub trait SettingsSourcePort: Send + Sync { + fn load_settings(&self) -> Result>, SettingsSourceError>; +} + pub trait ReadBusPort: Send + Sync { fn adapter_name(&self) -> &'static str; } -/// Capability for guarded write operations. pub trait WriteBusPort: Send + Sync { fn adapter_name(&self) -> &'static str; } -/// Capability for passive serial-port discovery. pub trait PortDiscoveryPort: Send + Sync { fn known_port_count(&self) -> usize; } -/// Source of bounded profile documents. pub trait ProfileSourcePort: Send + Sync { fn load_profile_sources(&self) -> Result, ProfileSourceError>; } -/// Capability for user-facing persistent artifacts. pub trait ArtifactStoragePort: Send + Sync { fn storage_name(&self) -> &'static str; } -/// Durable audit capability used by guarded operations. pub trait AuditPort: Send + Sync { fn is_available(&self) -> bool; } -/// Profile-origin and local-approval capability. pub trait ProfileTrustPort: Send + Sync { fn is_trusted(&self, profile_id: &ProfileId) -> bool; } -/// Time source used by deterministic application logic. pub trait ClockPort: Send + Sync { fn monotonic_ns(&self) -> u128; } From 1b36cdb811f5f8d7a8d1b7601a5c46cd43723c24 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:05:29 +0200 Subject: [PATCH 05/15] Prepare issue 6 XDG and atomic storage --- crates/lantern-storage/Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/lantern-storage/Cargo.toml b/crates/lantern-storage/Cargo.toml index b64f533..8e51dd8 100644 --- a/crates/lantern-storage/Cargo.toml +++ b/crates/lantern-storage/Cargo.toml @@ -12,8 +12,7 @@ publish = false lantern-app.workspace = true lantern-domain.workspace = true thiserror.workspace = true - -[dev-dependencies] +directories.workspace = true tempfile.workspace = true [lints] From f6b0551c79aeb22ce5caa1bea0a16c795ae85601 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:05:54 +0200 Subject: [PATCH 06/15] Prepare issue 6 XDG and atomic storage --- crates/lantern-storage/src/lib.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/lantern-storage/src/lib.rs b/crates/lantern-storage/src/lib.rs index fb45f3e..9261086 100644 --- a/crates/lantern-storage/src/lib.rs +++ b/crates/lantern-storage/src/lib.rs @@ -3,17 +3,22 @@ #![forbid(unsafe_code)] mod artifacts; +mod atomic; +mod paths; mod profile_source; +mod settings_source; use lantern_app::{ArtifactStoragePort, ProfileSource, ProfileSourceError}; pub use artifacts::{StorageError, read_bounded, write_new}; +pub use atomic::{AtomicWriteError, atomic_write, create_new_synced}; +pub use paths::{AppPaths, PathError}; pub use profile_source::{ FilesystemProfileSource, MAX_PROFILE_FILE_BYTES, MAX_PROFILE_FILES, MAX_PROFILE_SCAN_BYTES, ProfileLocations, ProfileScanLimits, }; +pub use settings_source::FilesystemSettingsSource; -/// Filesystem-backed application adapter. #[derive(Clone, Copy, Debug, Default)] pub struct FileStorage; From 6b307c69775eb022b613299238f7aae0452aff4b Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:06:27 +0200 Subject: [PATCH 07/15] Prepare issue 6 atomic storage --- crates/lantern-storage/src/atomic.rs | 93 ++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 crates/lantern-storage/src/atomic.rs diff --git a/crates/lantern-storage/src/atomic.rs b/crates/lantern-storage/src/atomic.rs new file mode 100644 index 0000000..003025c --- /dev/null +++ b/crates/lantern-storage/src/atomic.rs @@ -0,0 +1,93 @@ +use std::{ + fs::{self, File, OpenOptions}, + io::Write, + path::{Path, PathBuf}, +}; + +use tempfile::NamedTempFile; +use thiserror::Error; + +pub fn atomic_write(path: &Path, bytes: &[u8]) -> Result<(), AtomicWriteError> { + let parent = path + .parent() + .ok_or_else(|| AtomicWriteError::InvalidPath(path.to_path_buf()))?; + fs::create_dir_all(parent).map_err(|error| AtomicWriteError::io(parent, error))?; + let mut temporary = NamedTempFile::new_in(parent) + .map_err(|error| AtomicWriteError::io(parent, error))?; + temporary + .as_file_mut() + .write_all(bytes) + .map_err(|error| AtomicWriteError::io(path, error))?; + temporary + .as_file_mut() + .sync_all() + .map_err(|error| AtomicWriteError::io(path, error))?; + temporary + .persist(path) + .map_err(|error| AtomicWriteError::io(path, error.error))?; + File::open(parent) + .and_then(|directory| directory.sync_all()) + .map_err(|error| AtomicWriteError::io(parent, error)) +} + +pub fn create_new_synced(path: &Path, bytes: &[u8]) -> Result<(), AtomicWriteError> { + let parent = path + .parent() + .ok_or_else(|| AtomicWriteError::InvalidPath(path.to_path_buf()))?; + fs::create_dir_all(parent).map_err(|error| AtomicWriteError::io(parent, error))?; + let mut file = OpenOptions::new() + .write(true) + .create_new(true) + .open(path) + .map_err(|error| AtomicWriteError::io(path, error))?; + file.write_all(bytes) + .and_then(|()| file.sync_all()) + .map_err(|error| AtomicWriteError::io(path, error))?; + File::open(parent) + .and_then(|directory| directory.sync_all()) + .map_err(|error| AtomicWriteError::io(parent, error)) +} + +#[derive(Debug, Error)] +pub enum AtomicWriteError { + #[error("path has no parent directory: {0}")] + InvalidPath(PathBuf), + #[error("atomic file operation failed for {path}: {message}")] + Io { path: PathBuf, message: String }, +} + +impl AtomicWriteError { + fn io(path: &Path, error: std::io::Error) -> Self { + Self::Io { + path: path.to_path_buf(), + message: error.to_string(), + } + } +} + +#[cfg(test)] +mod tests { + use std::fs; + + use tempfile::tempdir; + + use super::{atomic_write, create_new_synced}; + + #[test] + fn atomic_write_replaces_complete_content() { + let directory = tempdir().expect("tempdir"); + let path = directory.path().join("state/item.json"); + atomic_write(&path, b"first").expect("first"); + atomic_write(&path, b"second").expect("second"); + assert_eq!(fs::read(path).expect("read"), b"second"); + } + + #[test] + fn create_new_refuses_overwrite() { + let directory = tempdir().expect("tempdir"); + let path = directory.path().join("export.csv"); + create_new_synced(&path, b"one").expect("first"); + assert!(create_new_synced(&path, b"two").is_err()); + assert_eq!(fs::read(path).expect("read"), b"one"); + } +} From 9da833ca70266f76e9542f10486a5fea7c2ed59e Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:07:12 +0200 Subject: [PATCH 08/15] Prepare issue 6 XDG paths --- crates/lantern-storage/src/paths.rs | 137 ++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 crates/lantern-storage/src/paths.rs diff --git a/crates/lantern-storage/src/paths.rs b/crates/lantern-storage/src/paths.rs new file mode 100644 index 0000000..82d7513 --- /dev/null +++ b/crates/lantern-storage/src/paths.rs @@ -0,0 +1,137 @@ +use std::path::{Path, PathBuf}; + +use directories::ProjectDirs; +use lantern_app::PathOverrides; +use lantern_domain::{LoggingId, SessionId}; +use thiserror::Error; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AppPaths { + pub config_file: PathBuf, + pub user_profiles: PathBuf, + pub profile_trust_store: PathBuf, + pub data_root: PathBuf, + pub backup_directory: PathBuf, + pub csv_directory: PathBuf, + pub fault_report_directory: PathBuf, + pub diagnostics_directory: PathBuf, + pub state_root: PathBuf, + pub log_directory: PathBuf, + pub audit_directory: PathBuf, + pub session_runtime_directory: PathBuf, + pub panic_directory: PathBuf, + pub cache_root: PathBuf, +} + +impl AppPaths { + pub fn resolve(overrides: &PathOverrides) -> Result { + let project = ProjectDirs::from("pl", "aiteracja", "vfd-lantern") + .ok_or(PathError::Unavailable)?; + let config_root = project.config_dir().to_path_buf(); + let data_root = overrides + .data + .clone() + .unwrap_or_else(|| project.data_dir().to_path_buf()); + let state_root = overrides.state.clone().unwrap_or_else(|| { + project + .state_dir() + .unwrap_or(project.data_local_dir()) + .to_path_buf() + }); + let log_directory = overrides + .log + .clone() + .unwrap_or_else(|| state_root.join("logs")); + Ok(Self::from_roots( + config_root, + data_root, + state_root, + project.cache_dir().to_path_buf(), + log_directory, + )) + } + + #[must_use] + pub fn from_roots( + config_root: PathBuf, + data_root: PathBuf, + state_root: PathBuf, + cache_root: PathBuf, + log_directory: PathBuf, + ) -> Self { + Self { + config_file: config_root.join("config.toml"), + user_profiles: config_root.join("profiles"), + profile_trust_store: config_root.join("profile-trust.json"), + backup_directory: data_root.join("backups"), + csv_directory: data_root.join("csv"), + fault_report_directory: data_root.join("fault-reports"), + diagnostics_directory: data_root.join("diagnostics"), + audit_directory: state_root.join("audit"), + session_runtime_directory: state_root.join("sessions"), + panic_directory: state_root.join("panic"), + config_file, + data_root, + state_root, + log_directory, + cache_root, + } + } + + #[must_use] + pub fn final_csv_sidecar(csv: &Path) -> PathBuf { + PathBuf::from(format!("{}.session.json", csv.display())) + } + + #[must_use] + pub fn runtime_logging_checkpoint( + &self, + session_id: SessionId, + logging_id: LoggingId, + ) -> PathBuf { + self.session_runtime_directory.join(format!( + "session-runtime-{}-{}.json", + session_id.get(), + logging_id.get() + )) + } +} + +#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)] +pub enum PathError { + #[error("XDG project directories are unavailable")] + Unavailable, +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use lantern_domain::{LoggingId, SessionId}; + + use super::AppPaths; + + #[test] + fn data_and_state_artifacts_are_not_conflated() { + let paths = AppPaths::from_roots( + PathBuf::from("/cfg"), + PathBuf::from("/data"), + PathBuf::from("/state"), + PathBuf::from("/cache"), + PathBuf::from("/logs"), + ); + let csv = paths.csv_directory.join("capture.csv"); + assert_eq!( + AppPaths::final_csv_sidecar(&csv), + PathBuf::from("/data/csv/capture.csv.session.json") + ); + assert_eq!( + paths.runtime_logging_checkpoint(SessionId::new(7), LoggingId::new(3)), + PathBuf::from("/state/sessions/session-runtime-7-3.json") + ); + assert_ne!( + paths.runtime_logging_checkpoint(SessionId::new(7), LoggingId::new(3)), + paths.runtime_logging_checkpoint(SessionId::new(7), LoggingId::new(4)) + ); + } +} From 44481e93d6335828ce20352410d9e199ee6c37b2 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:07:43 +0200 Subject: [PATCH 09/15] Prepare issue 6 settings source --- crates/lantern-storage/src/settings_source.rs | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 crates/lantern-storage/src/settings_source.rs diff --git a/crates/lantern-storage/src/settings_source.rs b/crates/lantern-storage/src/settings_source.rs new file mode 100644 index 0000000..2a1f3d2 --- /dev/null +++ b/crates/lantern-storage/src/settings_source.rs @@ -0,0 +1,84 @@ +use std::{fs, path::PathBuf}; + +use lantern_app::{MAX_SETTINGS_BYTES, SettingsSourceError, SettingsSourcePort}; + +#[derive(Clone, Debug)] +pub struct FilesystemSettingsSource { + path: PathBuf, +} + +impl FilesystemSettingsSource { + #[must_use] + pub fn new(path: PathBuf) -> Self { + Self { path } + } +} + +impl SettingsSourcePort for FilesystemSettingsSource { + fn load_settings(&self) -> Result>, SettingsSourceError> { + let metadata = match fs::symlink_metadata(&self.path) { + Ok(metadata) => metadata, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None), + Err(error) => return Err(io_error(&self.path, error)), + }; + if metadata.file_type().is_symlink() { + return Err(SettingsSourceError::Symlink { + path: self.path.clone(), + }); + } + if !metadata.is_file() { + return Err(SettingsSourceError::NotRegular { + path: self.path.clone(), + }); + } + if metadata.len() > MAX_SETTINGS_BYTES as u64 { + return Err(SettingsSourceError::Io { + path: self.path.clone(), + message: format!("file exceeds {MAX_SETTINGS_BYTES} bytes"), + }); + } + fs::read(&self.path) + .map(Some) + .map_err(|error| io_error(&self.path, error)) + } +} + +fn io_error(path: &std::path::Path, error: std::io::Error) -> SettingsSourceError { + SettingsSourceError::Io { + path: path.to_path_buf(), + message: error.to_string(), + } +} + +#[cfg(test)] +mod tests { + use std::{fs, os::unix::fs::symlink}; + + use lantern_app::{SettingsSourceError, SettingsSourcePort}; + use tempfile::tempdir; + + use super::FilesystemSettingsSource; + + #[test] + fn absent_settings_are_valid() { + let directory = tempdir().expect("tempdir"); + let source = FilesystemSettingsSource::new(directory.path().join("missing.toml")); + assert_eq!(source.load_settings().expect("load"), None); + } + + #[test] + fn settings_symlink_is_rejected() { + let directory = tempdir().expect("tempdir"); + fs::write(directory.path().join("target"), b"render_fps = 2").expect("write"); + symlink( + directory.path().join("target"), + directory.path().join("config.toml"), + ) + .expect("symlink"); + let source = FilesystemSettingsSource::new(directory.path().join("config.toml")); + assert!(matches!( + source.load_settings(), + Err(SettingsSourceError::Symlink { .. }) + )); + } +} From 78839edcee42aeaa4623d990ed9049a3e0aa9998 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:08:42 +0200 Subject: [PATCH 10/15] Prepare issue 6 global CLI --- crates/vfd-lantern/src/cli.rs | 61 ++++++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/crates/vfd-lantern/src/cli.rs b/crates/vfd-lantern/src/cli.rs index 756f275..147dfeb 100644 --- a/crates/vfd-lantern/src/cli.rs +++ b/crates/vfd-lantern/src/cli.rs @@ -5,14 +5,33 @@ use clap::{Args, Parser, Subcommand}; #[derive(Debug, Parser)] #[command(name = "vfd-lantern", version, about)] pub struct Cli { + #[command(flatten)] + pub global: GlobalArgs, #[command(subcommand)] pub command: Option, } +#[derive(Clone, Debug, Default, Args)] +pub struct GlobalArgs { + #[arg(long, global = true)] + pub config: Option, + #[arg(long, global = true)] + pub profile: Option, + #[arg(long, global = true)] + pub device: Option, + #[arg(long, global = true, value_parser = ["trace", "debug", "info", "warn", "error"])] + pub log_level: Option, + #[arg(long, global = true)] + pub enable_writes: bool, + #[arg(long, global = true)] + pub no_color: bool, +} + #[derive(Debug, Subcommand)] pub enum Command { - /// Validate, inspect and package device profiles. Profile(ProfileArgs), + Backup(BackupArgs), + Diagnostics(DiagnosticsArgs), } #[derive(Debug, Args)] @@ -23,7 +42,6 @@ pub struct ProfileArgs { #[derive(Debug, Subcommand)] pub enum ProfileCommand { - /// List a deterministic registry snapshot. List { #[arg(value_name = "PROFILE")] explicit: Vec, @@ -32,17 +50,11 @@ pub enum ProfileCommand { #[arg(long)] system_dir: Option, }, - /// Validate one TOML or JSON profile. Validate { path: PathBuf }, - /// Print deterministic current-schema TOML. Normalize { path: PathBuf }, - /// Print JSON Schema generated from parser types. Schema, - /// Print validated profile metadata. Inspect { path: PathBuf }, - /// Print source and semantic hashes. Hashes { path: PathBuf }, - /// Build the packaged profile manifest used by a release build. Manifest(ManifestArgs), } @@ -58,6 +70,32 @@ pub struct ManifestArgs { pub build_id: String, } +#[derive(Debug, Args)] +pub struct BackupArgs { + #[command(subcommand)] + pub command: BackupCommand, +} + +#[derive(Debug, Subcommand)] +pub enum BackupCommand { + Inspect { file: PathBuf }, + Diff { left: PathBuf, right: PathBuf }, +} + +#[derive(Debug, Args)] +pub struct DiagnosticsArgs { + #[command(subcommand)] + pub command: DiagnosticsCommand, +} + +#[derive(Debug, Subcommand)] +pub enum DiagnosticsCommand { + Collect { + #[arg(long)] + output: PathBuf, + }, +} + #[cfg(test)] mod tests { use clap::Parser; @@ -74,4 +112,11 @@ mod tests { })) )); } + + #[test] + fn clean_start_has_no_command_and_write_gate_is_false() { + let cli = Cli::try_parse_from(["vfd-lantern"]).expect("CLI"); + assert!(cli.command.is_none()); + assert!(!cli.global.enable_writes); + } } From 5c52447b6c82bddc96cc2c146a0724bff310ebab Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:09:41 +0200 Subject: [PATCH 11/15] Prepare issue 6 settings startup --- crates/vfd-lantern/src/main.rs | 58 ++++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/crates/vfd-lantern/src/main.rs b/crates/vfd-lantern/src/main.rs index ec90f24..221971d 100644 --- a/crates/vfd-lantern/src/main.rs +++ b/crates/vfd-lantern/src/main.rs @@ -5,24 +5,67 @@ mod cli; mod profile_commands; -use anyhow::Result; +use anyhow::{Result, bail}; use clap::Parser; -use lantern_app::{ApplicationState, ArtifactStoragePort, ReadBusPort}; -use lantern_storage::FileStorage; +use lantern_app::{ + ApplicationState, ArtifactStoragePort, CliSettingsOverrides, ReadBusPort, SettingsLoader, + ValidatedSettings, +}; +use lantern_storage::{AppPaths, FileStorage, FilesystemSettingsSource}; use lantern_transport::TransportAdapter; use lantern_tui::UiState; -use crate::cli::{Cli, Command}; +use crate::cli::{BackupCommand, Cli, Command, DiagnosticsCommand}; fn main() -> Result<()> { let cli = Cli::parse(); + let initial_paths = AppPaths::resolve(&Default::default())?; + let config_path = cli + .global + .config + .clone() + .unwrap_or(initial_paths.config_file); + let settings_source = FilesystemSettingsSource::new(config_path); + let application_log = std::env::var("VFD_LANTERN_LOG").ok(); + let settings = SettingsLoader::load( + &settings_source, + CliSettingsOverrides { + profile: cli.global.profile, + device: cli.global.device, + log_level: cli.global.log_level, + enable_writes: cli.global.enable_writes, + no_color: cli.global.no_color, + }, + application_log.as_deref(), + )?; + let paths = AppPaths::resolve(&settings.paths)?; + match cli.command { Some(Command::Profile(arguments)) => profile_commands::run(arguments.command), - None => run_tui_bootstrap(), + Some(Command::Backup(arguments)) => match arguments.command { + BackupCommand::Inspect { file } => { + bail!( + "backup inspection for {} is implemented by roadmap issue #17", + file.display() + ) + } + BackupCommand::Diff { left, right } => bail!( + "backup diff for {} and {} is implemented by roadmap issue #17", + left.display(), + right.display() + ), + }, + Some(Command::Diagnostics(arguments)) => match arguments.command { + DiagnosticsCommand::Collect { output } => bail!( + "diagnostics collection into {} is implemented by roadmap issue #22", + output.display() + ), + }, + None => run_tui_bootstrap(&settings, &paths), } } -fn run_tui_bootstrap() -> Result<()> { +fn run_tui_bootstrap(settings: &ValidatedSettings, paths: &AppPaths) -> Result<()> { let storage = FileStorage; let transport = TransportAdapter; let application = ApplicationState::default(); @@ -33,6 +76,9 @@ fn run_tui_bootstrap() -> Result<()> { println!("Storage adapter: {}", storage.storage_name()); println!("Transport adapter: {}", transport.adapter_name()); println!("{}", lantern_tui::render_status(&application.view(), &ui)); + println!("Render limit: {} FPS", settings.render_fps); + println!("Log directory: {}", paths.log_directory.display()); + println!("Process write gate: {}", settings.process_writes_enabled); println!("No serial connection or profile scan is attempted by a clean start."); Ok(()) } From 7c31a73f77590dec813fb314870491dcc38ab1fd Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:10:59 +0200 Subject: [PATCH 12/15] Normalize issue 6 candidate through PR CI --- .github/workflows/ci.yml | 66 +++++++++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1145bdd..8a0aa30 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,47 @@ concurrency: cancel-in-progress: true jobs: + normalize-candidate: + name: Normalize issue candidate + runs-on: ubuntu-24.04 + permissions: + contents: write + outputs: + changed: ${{ steps.normalize.outputs.changed }} + steps: + - name: Check out candidate head + if: github.event_name == 'pull_request' && github.head_ref == 'agent/issues-1-9-work' + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + ref: agent/issues-1-9-work + fetch-depth: 2 + token: ${{ secrets.GITHUB_TOKEN }} + - name: Install pinned normalizer + if: github.event_name == 'pull_request' && github.head_ref == 'agent/issues-1-9-work' + run: | + rustup toolchain install 1.97.1 --profile minimal --component rustfmt + rustup default 1.97.1 + - name: Normalize lockfile and Rust source + id: normalize + if: github.event_name == 'pull_request' && github.head_ref == 'agent/issues-1-9-work' + shell: bash + run: | + cargo generate-lockfile + cargo fmt --all + if git diff --quiet; then + echo "changed=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + git config user.name "vfd-lantern-ci" + git config user.email "actions@users.noreply.github.com" + git add -A + git commit -m "Normalize issue 6 candidate" + git push origin HEAD:agent/issues-1-9-work + echo "changed=true" >> "$GITHUB_OUTPUT" + rust: + needs: normalize-candidate + if: needs.normalize-candidate.outputs.changed != 'true' name: Debian 13 / Rust 1.97.1 / ${{ matrix.arch }} strategy: fail-fast: false @@ -25,53 +65,31 @@ jobs: runner: ubuntu-24.04-arm runs-on: ${{ matrix.runner }} container: debian:trixie-slim@sha256:020c0d20b9880058cbe785a9db107156c3c75c2ac944a6aa7ab59f2add76a7bd - steps: - name: Install system dependencies run: | apt-get update apt-get install --yes --no-install-recommends \ - build-essential \ - ca-certificates \ - git \ - libudev-dev \ - pkg-config \ - rustup - + build-essential ca-certificates git libudev-dev pkg-config rustup - name: Check out repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - - name: Install pinned Rust toolchain run: | - rustup toolchain install 1.97.1 \ - --profile minimal \ - --component rustfmt \ - --component clippy \ - --component llvm-tools-preview + rustup toolchain install 1.97.1 --profile minimal --component rustfmt --component clippy --component llvm-tools-preview rustup default 1.97.1 - rustc --version - cargo --version - - name: Validate lockfile and metadata run: cargo metadata --locked --format-version 1 --no-deps >/dev/null - - name: Build workspace run: cargo build --workspace --locked - - name: Check formatting run: cargo fmt --all -- --check - - name: Run Clippy run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings - - name: Run tests run: cargo test --workspace --all-features --locked - - name: Build documentation run: cargo doc --workspace --no-deps --locked - - name: Check architecture boundaries run: sh scripts/check-architecture.sh - - name: Check supply-chain baseline run: sh scripts/check-supply-chain-baseline.sh From d7f7b3ef944390644c1d40dcf58f718c1465bb9c Mon Sep 17 00:00:00 2001 From: vfd-lantern-ci Date: Mon, 10 Aug 2026 21:11:42 +0000 Subject: [PATCH 13/15] Normalize issue 6 candidate --- Cargo.lock | 66 ++++++++++++++++++++++++++++ crates/lantern-app/src/settings.rs | 23 +++------- crates/lantern-storage/src/atomic.rs | 4 +- crates/lantern-storage/src/paths.rs | 4 +- crates/vfd-lantern/src/cli.rs | 16 +++++-- 5 files changed, 89 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2089e11..ce331f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -187,6 +187,27 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + [[package]] name = "dyn-clone" version = "1.0.20" @@ -221,6 +242,17 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -295,6 +327,7 @@ dependencies = [ "lantern-profile", "serde", "thiserror", + "toml", ] [[package]] @@ -336,6 +369,7 @@ dependencies = [ name = "lantern-storage" version = "0.1.0" dependencies = [ + "directories", "lantern-app", "lantern-domain", "tempfile", @@ -363,6 +397,15 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "libredox" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa" +dependencies = [ + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -396,6 +439,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -498,6 +547,17 @@ dependencies = [ "rand_core", ] +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror", +] + [[package]] name = "ref-cast" version = "1.0.26" @@ -834,6 +894,12 @@ dependencies = [ "libc", ] +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasip2" version = "1.0.4+wasi-0.2.12" diff --git a/crates/lantern-app/src/settings.rs b/crates/lantern-app/src/settings.rs index dfe694d..7000db2 100644 --- a/crates/lantern-app/src/settings.rs +++ b/crates/lantern-app/src/settings.rs @@ -156,11 +156,10 @@ impl SettingsLoader { let text = std::str::from_utf8(&bytes).map_err(|error| SettingsError::Parse { message: error.to_string(), })?; - let document: SettingsDocumentV1 = toml::from_str(text).map_err(|error| { - SettingsError::Parse { + let document: SettingsDocumentV1 = + toml::from_str(text).map_err(|error| SettingsError::Parse { message: error.to_string(), - } - })?; + })?; apply_document(&mut settings, document)?; } @@ -286,12 +285,7 @@ fn bounded( } } -fn bounded_u64( - name: &str, - value: u64, - minimum: u64, - maximum: u64, -) -> Result { +fn bounded_u64(name: &str, value: u64, minimum: u64, maximum: u64) -> Result { if (minimum..=maximum).contains(&value) { Ok(value) } else { @@ -373,12 +367,9 @@ suggested_device = "/dev/config" #[test] fn missing_file_uses_safe_defaults() { - let settings = SettingsLoader::load( - &MemorySource(None), - CliSettingsOverrides::default(), - None, - ) - .expect("defaults"); + let settings = + SettingsLoader::load(&MemorySource(None), CliSettingsOverrides::default(), None) + .expect("defaults"); assert!(!settings.process_writes_enabled); assert_eq!(settings.render_fps, 5); } diff --git a/crates/lantern-storage/src/atomic.rs b/crates/lantern-storage/src/atomic.rs index 003025c..c4c4464 100644 --- a/crates/lantern-storage/src/atomic.rs +++ b/crates/lantern-storage/src/atomic.rs @@ -12,8 +12,8 @@ pub fn atomic_write(path: &Path, bytes: &[u8]) -> Result<(), AtomicWriteError> { .parent() .ok_or_else(|| AtomicWriteError::InvalidPath(path.to_path_buf()))?; fs::create_dir_all(parent).map_err(|error| AtomicWriteError::io(parent, error))?; - let mut temporary = NamedTempFile::new_in(parent) - .map_err(|error| AtomicWriteError::io(parent, error))?; + let mut temporary = + NamedTempFile::new_in(parent).map_err(|error| AtomicWriteError::io(parent, error))?; temporary .as_file_mut() .write_all(bytes) diff --git a/crates/lantern-storage/src/paths.rs b/crates/lantern-storage/src/paths.rs index 82d7513..b7e197b 100644 --- a/crates/lantern-storage/src/paths.rs +++ b/crates/lantern-storage/src/paths.rs @@ -25,8 +25,8 @@ pub struct AppPaths { impl AppPaths { pub fn resolve(overrides: &PathOverrides) -> Result { - let project = ProjectDirs::from("pl", "aiteracja", "vfd-lantern") - .ok_or(PathError::Unavailable)?; + let project = + ProjectDirs::from("pl", "aiteracja", "vfd-lantern").ok_or(PathError::Unavailable)?; let config_root = project.config_dir().to_path_buf(); let data_root = overrides .data diff --git a/crates/vfd-lantern/src/cli.rs b/crates/vfd-lantern/src/cli.rs index 147dfeb..4259b23 100644 --- a/crates/vfd-lantern/src/cli.rs +++ b/crates/vfd-lantern/src/cli.rs @@ -50,11 +50,19 @@ pub enum ProfileCommand { #[arg(long)] system_dir: Option, }, - Validate { path: PathBuf }, - Normalize { path: PathBuf }, + Validate { + path: PathBuf, + }, + Normalize { + path: PathBuf, + }, Schema, - Inspect { path: PathBuf }, - Hashes { path: PathBuf }, + Inspect { + path: PathBuf, + }, + Hashes { + path: PathBuf, + }, Manifest(ManifestArgs), } From 1eb31fe10a83d1da3aa582c902291bbab969e8f8 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:19:15 +0200 Subject: [PATCH 14/15] Diagnose issue 6 test failure --- .github/workflows/diagnose-issue-6-tests.yml | 46 ++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/diagnose-issue-6-tests.yml diff --git a/.github/workflows/diagnose-issue-6-tests.yml b/.github/workflows/diagnose-issue-6-tests.yml new file mode 100644 index 0000000..1dc8522 --- /dev/null +++ b/.github/workflows/diagnose-issue-6-tests.yml @@ -0,0 +1,46 @@ +name: Diagnose issue 6 tests + +on: + push: + branches: [agent/issues-1-9-work] + pull_request: + branches: [agent/issues-1-9-stage] + +permissions: + contents: read + +jobs: + tests: + runs-on: ubuntu-24.04 + container: debian:trixie-slim@sha256:020c0d20b9880058cbe785a9db107156c3c75c2ac944a6aa7ab59f2add76a7bd + steps: + - name: Install prerequisites + run: | + apt-get update + apt-get install --yes --no-install-recommends build-essential ca-certificates git libudev-dev pkg-config rustup + - name: Check out repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - name: Install pinned Rust toolchain + run: | + rustup toolchain install 1.97.1 --profile minimal + rustup default 1.97.1 + - name: Run tests and capture diagnostics + id: test + shell: bash + run: | + set +e + cargo test --workspace --all-features --locked -- --nocapture > /tmp/issue-6-tests.log 2>&1 + status=$? + echo "status=$status" >> "$GITHUB_OUTPUT" + exit 0 + - name: Upload diagnostics + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: issue-6-test-diagnostics + path: /tmp/issue-6-tests.log + if-no-files-found: error + retention-days: 1 + - name: Preserve test result + if: steps.test.outputs.status != '0' + run: exit 1 From a579e1f10b1ed5b3105891b7b211c181716cb04c Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Tue, 11 Aug 2026 12:12:01 +0200 Subject: [PATCH 15/15] Add temporary source export workflow --- .github/workflows/export-source.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/export-source.yml diff --git a/.github/workflows/export-source.yml b/.github/workflows/export-source.yml new file mode 100644 index 0000000..474f5a6 --- /dev/null +++ b/.github/workflows/export-source.yml @@ -0,0 +1,26 @@ +name: Export source snapshot + +on: + pull_request: + +permissions: + contents: read + +jobs: + export: + runs-on: ubuntu-24.04 + steps: + - name: Check out exact source + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - name: Create deterministic source archive + run: | + tar --sort=name --mtime='UTC 2026-01-01' --owner=0 --group=0 --numeric-owner \ + --exclude=.git --exclude=target \ + -czf /tmp/vfd-lantern-source.tar.gz . + - name: Upload source archive + uses: actions/upload-artifact@v4 + with: + name: vfd-lantern-source-6 + path: /tmp/vfd-lantern-source.tar.gz + if-no-files-found: error + retention-days: 1