Skip to content

Commit 682e4f7

Browse files
author
Claude Sonnet (coordinator)
committed
Merge origin/main
2 parents 7d97037 + bf2f401 commit 682e4f7

18 files changed

Lines changed: 278 additions & 146 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,6 @@ prd.md
1616
# Local environment secrets
1717
.env
1818
.env.*
19+
20+
# Generated local state sidecars (should not be committed)
21+
*.ledgerr-state.json

Cargo.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ledger-core/src/tags.rs

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,40 @@ impl Tag {
4242
Ok(Self(format!("#{normalized}")))
4343
}
4444

45+
fn sanitize_body(body: &str) -> String {
46+
let mut builder = String::new();
47+
let mut last_was_hyphen = false;
48+
49+
for c in body.trim().chars() {
50+
if c.is_ascii_alphanumeric() {
51+
builder.push(c.to_ascii_lowercase());
52+
last_was_hyphen = false;
53+
} else if !builder.is_empty() && !last_was_hyphen {
54+
builder.push('-');
55+
last_was_hyphen = true;
56+
}
57+
}
58+
59+
let sanitized = builder.trim_matches('-').to_string();
60+
if sanitized.is_empty() {
61+
"tag".to_string()
62+
} else {
63+
sanitized
64+
}
65+
}
66+
4567
/// Infallible parse that prefixes '#' if missing and normalizes.
4668
pub fn normalize(raw: &str) -> Self {
47-
let s = if raw.starts_with('#') {
48-
raw.to_string()
69+
let s = raw.trim();
70+
let s = if s.starts_with('#') {
71+
s.to_string()
4972
} else {
50-
format!("#{raw}")
73+
format!("#{s}")
5174
};
52-
Self::new(&s).unwrap_or_else(|_| Self(s.to_ascii_lowercase()))
75+
Self::new(&s).unwrap_or_else(|_| {
76+
let body = s.strip_prefix('#').unwrap_or(&s);
77+
Self(format!("#{}", Self::sanitize_body(body)))
78+
})
5379
}
5480

5581
pub fn as_str(&self) -> &str {

crates/ledgerr-host/src/settings/schema.rs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,13 @@ use serde::{Deserialize, Serialize};
22

33
use crate::notify::{NotificationBackend, NotificationTestResult};
44

5-
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
5+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
66
#[serde(rename_all = "snake_case")]
77
pub enum SettingsSchemaVersion {
8+
#[default]
89
V1,
910
}
1011

11-
impl Default for SettingsSchemaVersion {
12-
fn default() -> Self {
13-
Self::V1
14-
}
15-
}
16-
1712
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1813
pub struct ShowNotificationsFor {
1914
pub approval_required: bool,

crates/ledgerr-host/src/settings/store.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ impl SettingsStore {
4949
temp_file.write_all(&json)?;
5050
temp_file.flush()?;
5151
drop(temp_file);
52+
53+
// On Windows, `std::fs::rename` does not overwrite an existing destination,
54+
// so we remove the destination first if it exists to keep the swap atomic
55+
// enough for single-user local operation.
56+
#[cfg(windows)]
57+
if self.path.exists() {
58+
std::fs::remove_file(&self.path)?;
59+
}
60+
5261
std::fs::rename(temp_path, &self.path)?;
5362
Ok(())
5463
}

crates/ledgerr-host/src/tray/runtime.rs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -359,14 +359,6 @@ fn handle_command(
359359
sync_state(state, &settings, tray);
360360
Ok(false)
361361
}
362-
TrayCommand::ToggleStartMinimizedToTray(enabled) => {
363-
let mut settings = store.load()?;
364-
settings.start_minimized_to_tray = enabled;
365-
store.save(&settings)?;
366-
367-
sync_state(state, &settings, tray);
368-
Ok(false)
369-
}
370362
TrayCommand::ToggleWindowVisibleOnStart(enabled) => {
371363
let mut settings = store.load()?;
372364
settings.window_visible_on_start = enabled;

crates/ledgerr-host/tests/settings_atomicity.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ fn atomic_save_replaces_old_file_without_partial_contents() {
1616
let store = SettingsStore::new(path.clone());
1717
store.save(&AppSettings::default()).unwrap();
1818

19-
let mut updated = AppSettings::default();
20-
updated.toast_enabled = false;
21-
updated.start_minimized_to_tray = true;
19+
let updated = AppSettings {
20+
toast_enabled: false,
21+
start_minimized_to_tray: true,
22+
..AppSettings::default()
23+
};
2224
store.save(&updated).unwrap();
2325

2426
let raw = std::fs::read_to_string(path).unwrap();

crates/ledgerr-mcp/Cargo.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ schemars = { version = "0.8", features = ["derive"] }
1616
serde = { version = "1", features = ["derive"] }
1717
serde_json = "1"
1818
thiserror = "2"
19-
tokio = { workspace = true, optional = true }
2019
tracing = { workspace = true }
2120

2221
# plugin_info — always available (cross-platform system metadata)
@@ -40,8 +39,8 @@ ontology = ["core"]
4039
classification = ["core"]
4140
audit = ["core"]
4241
tax = ["core"]
43-
xero = ["core", "dep:ledgerr-xero", "dep:tokio"]
44-
llm = ["core", "dep:ledgerr-llm", "dep:tokio"]
42+
xero = ["core", "dep:ledgerr-xero"]
43+
llm = ["core", "dep:ledgerr-llm"]
4544
full = ["core", "events", "reconciliation", "hsm", "ontology", "classification", "audit", "tax", "xero", "llm"]
4645
self-update = ["dep:reqwest", "dep:semver"]
4746

crates/ledgerr-mcp/src/bin/regen-docs.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/// Regenerates docs/ and scripts/ files from the Rust contract source.
22
/// Run: cargo run -p ledgerr-mcp --bin regen-docs
3-
use std::path::PathBuf;
3+
use std::path::{Path, PathBuf};
44

55
use ledgerr_mcp::contract;
66

@@ -13,7 +13,7 @@ fn repo_root() -> PathBuf {
1313
.to_path_buf()
1414
}
1515

16-
fn write(root: &PathBuf, rel: &str, content: &str) {
16+
fn write(root: &Path, rel: &str, content: &str) {
1717
let path = root.join(rel);
1818
std::fs::write(&path, content).unwrap_or_else(|e| panic!("write {rel}: {e}"));
1919
println!("wrote {rel}");

crates/ledgerr-mcp/src/contract.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -595,7 +595,7 @@ pub fn generated_capability_contract_markdown() -> String {
595595
Rust code is the only source of truth for the published MCP surface. If this file drifts from the contract module, tests should fail.\n\n",
596596
);
597597
doc.push_str(
598-
"The default catalog is intentionally small: 7 top-level `ledgerr_*` tools. Each tool uses a required `action` field so the major capability families stay visible while related operations are grouped under one top-level command.\n\n",
598+
"The default catalog is intentionally small: 8 top-level `ledgerr_*` tools. Each tool uses a required `action` field so the major capability families stay visible while related operations are grouped under one top-level command.\n\n",
599599
);
600600
doc.push_str("## Published MCP Tools\n\n");
601601
doc.push_str("| Tool | Purpose | Common actions |\n|---|---|---|\n");
@@ -622,7 +622,7 @@ The server still accepts older `l3dg3rr_*` and proxy-style call names as hidden
622622
doc.push_str(
623623
"## Internal Service API\n\n\
624624
Canonical trait:\n[TurboLedgerTools in crates/ledgerr-mcp/src/lib.rs](../crates/ledgerr-mcp/src/lib.rs#L289)\n\n\
625-
Important distinction:\n- The MCP surface is the reduced 7-tool catalog defined in Rust.\n- The internal service trait remains more granular and implementation-oriented.\n\n\
625+
Important distinction:\n- The MCP surface is the 8-tool catalog defined in Rust.\n- The internal service trait remains more granular and implementation-oriented.\n\n\
626626
API layering:\n1. `ledgerr-mcp-server` (stdio transport)\n2. `contract` (published tool families, action enums, generated schema/doc artifacts)\n3. `mcp_adapter` (dispatch + envelope shaping)\n4. `TurboLedgerService` (domain logic, guardrails, state/event/HSM ops)\n5. `ledger-core` (ingest, filename validation, classification primitives)\n\n",
627627
);
628628
doc.push_str("## Example Flow\n\n");
@@ -665,7 +665,7 @@ pub fn generated_agent_runbook_markdown() -> String {
665665
This file is generated from `crates/ledgerr-mcp/src/contract.rs`.\n\n\
666666
Agent workflows must use `initialize`, `notifications/initialized`, `tools/list`, and `tools/call` over stdio.\n\n\
667667
## Runtime Model\n\n\
668-
The default published surface is the reduced 7-tool catalog:\n\n{}\n\
668+
The default published surface is the 8-tool catalog:\n\n{}\n\
669669
Each tool requires an `action` argument.\n\n\
670670
## Bootstrap\n\n\
671671
From repo root:\n\n```bash\ncargo build -p ledgerr-mcp --bin ledgerr-mcp-server\n```\n\n\

0 commit comments

Comments
 (0)