chore: xtask tree rule (max 12 loose files per source folder), AGENTS.md rule, ROADMAP pending list
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,7 @@ NotesCraft is a native, Obsidian-compatible notes app written in **Rust only** (
|
||||
- One `thiserror` enum `Error` and `pub type Result<T>` per crate. Libraries do not use `anyhow`.
|
||||
- Every public item has rustdoc, every module has a `//!` comment. `clippy::pedantic` must be clean; no blanket allows.
|
||||
- Files stay under about 400 lines; split by concern. Smallest design that works; no speculative abstractions.
|
||||
- **Source folders stay shallow and grouped**: no directory under a crate's `src/` (or an app's) holds more than 12 loose `.rs` files (`mod.rs` and `lib.rs` do not count). Group files into sub-folders by concern (`foo.rs` + `foo/`), and nest a group inside a parent folder when it grows. Moving files must not change public paths: re-export from `lib.rs`. Enforced by `cargo xtask tree` (part of `cargo xtask ci`); directories still waiting for their restructure are listed in `xtask/src/tree.rs` and in ROADMAP.md.
|
||||
- Named exports, `import type`-style discipline, no magic numbers (named constants), `get` = derived, `fetch` = IO, `is`/`has`/`can` = predicate.
|
||||
- Tests: unit in `src`, integration in `tests/`, benches/perf in `perf/` or `#[ignore]`d tests, examples in `examples/`. Write the failing test first. Every crate has at least one.
|
||||
- Every write to disk goes through `craft-atomic`.
|
||||
|
||||
+6
-4
@@ -31,10 +31,12 @@ Also built beyond the phase list: Vim mode (`vim` crate and app-wide navigation)
|
||||
|
||||
## Next up
|
||||
|
||||
1. First tagged release: the macOS `.dmg` dry run passes locally (ad-hoc signed, signature valid from the mounted image, app starts); still to do: run `release.yml` on the remote (signing secrets, the shared `craft-common-actions` workflow), then install-test the Windows MSI and the Linux packages on real machines; embed the app icon into the Windows `.exe` (needs a build dependency such as `winresource`, owner approval). The runtime window icon is set on Windows and Linux.
|
||||
2. Tab row clears the macOS traffic lights when the sidebar is collapsed to its rail; Visual-mode edits have no per-block caret memory across undo.
|
||||
3. Binary clipboard image paste (needs the `arboard` dependency approved), `craft-print` hook for PDF export, typeset math in PDF.
|
||||
4. Housekeeping: `craftrules` reconciliation (0.10), regex dependency decision for search, `testkit`.
|
||||
1. Assistant overhaul (in progress on branch `feat/assistant-overhaul`): several connections (ACP agents and OpenAI-compatible endpoints), a connections manager in Settings, `@` mentions and context chips, "Add to chat" from the Source and Visual selections, a new panel layout, static agent icons.
|
||||
2. Folder restructure of the remaining crowded source directories, then remove their entries from `PENDING` in `xtask/src/tree.rs`: `crates/ui-egui/src` (53 loose files) and its `app/`, `text/`, `whiteboard_view/`, `assistant_panel/` folders, and `crates/engine/src` (47). Also `crates/acp`. Run after item 1 lands so moves do not collide; keep public paths stable through re-exports; update `docs/ARCHITECTURE.md` with the new trees. Every other crate is already grouped (rule in `AGENTS.md`, checked by `cargo xtask tree`).
|
||||
3. First tagged release: the macOS `.dmg` dry run passes locally (ad-hoc signed, signature valid from the mounted image, app starts); still to do: run `release.yml` on the remote (signing secrets, the shared `craft-common-actions` workflow), then install-test the Windows MSI and the Linux packages on real machines; embed the app icon into the Windows `.exe` (needs a build dependency such as `winresource`, owner approval). The runtime window icon is set on Windows and Linux.
|
||||
4. Polish: the tab row clears the macOS traffic lights when the sidebar is collapsed to its rail; animated icons: author the set in EffectCraft instead of generating it (its Lottie export adds fields such as `ml` that the player rejects) and tune individual icons as they are noticed (`notescraft-icon-motion` skill); egui's own menus and tooltips still close without animation.
|
||||
5. Binary clipboard image paste (needs the `arboard` dependency approved), `craft-print` hook for PDF export, typeset math in PDF.
|
||||
6. Housekeeping: `craftrules` reconciliation (0.10), regex dependency decision for search, `testkit`, split `crates/markdown/src/lib.rs` (568 lines).
|
||||
|
||||
## Known issues and review findings
|
||||
|
||||
|
||||
+6
-1
@@ -10,6 +10,7 @@ mod lex;
|
||||
#[cfg(test)]
|
||||
mod lex_tests;
|
||||
mod rules;
|
||||
mod tree;
|
||||
mod version;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -23,7 +24,8 @@ commands:
|
||||
verify cargo fmt --all --check; cargo clippy --workspace --all-targets -- -D warnings;
|
||||
cargo test --workspace (stops at first failure, non-zero exit)
|
||||
layers enforce the crate dependency layering (SPEC §3.1)
|
||||
ci verify, layers, i18n --strict
|
||||
tree fail when a source directory holds more than 12 loose .rs files
|
||||
ci verify, layers, tree, i18n --strict
|
||||
ico ico OUT.ico IN.png... packs PNGs into a Windows .ico (packaging/icons.sh)
|
||||
version print the workspace version; `version set X.Y.Z[-pre]` changes it
|
||||
i18n scan ui-egui/src and apps/*/src for hardcoded user strings (SPEC §12);
|
||||
@@ -35,6 +37,7 @@ fn main() -> ExitCode {
|
||||
let result = match args.first().map(String::as_str) {
|
||||
Some("verify") => cmd_verify(),
|
||||
Some("layers") => cmd_layers(),
|
||||
Some("tree") => tree::run(&root()),
|
||||
Some("ci") => cmd_ci(),
|
||||
Some("i18n") => cmd_i18n(&args),
|
||||
Some("ico") => ico::run(&args.iter().skip(1).map(String::as_str).collect::<Vec<_>>()),
|
||||
@@ -151,6 +154,8 @@ fn cmd_ci() -> Result<(), String> {
|
||||
println!();
|
||||
cmd_layers()?;
|
||||
println!();
|
||||
tree::run(&root())?;
|
||||
println!();
|
||||
i18n::run(&[String::from("--strict")])
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
//! Source-folder hygiene: no directory under a crate's `src/` may hold more than [`MAX_LOOSE`] loose `.rs`
|
||||
//! files. Group files into sub-folders by concern instead (a folder may itself sit inside a parent folder).
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
/// Most `.rs` files one source directory may hold directly (`mod.rs` and `lib.rs` do not count).
|
||||
pub const MAX_LOOSE: usize = 12;
|
||||
|
||||
/// Directories (workspace-relative) still waiting for their restructure; remove each entry when it is done.
|
||||
/// ROADMAP.md lists the same work under "Next up".
|
||||
const PENDING: &[&str] = &[
|
||||
"crates/ui-egui/src",
|
||||
"crates/ui-egui/src/whiteboard_view",
|
||||
"crates/ui-egui/src/text",
|
||||
"crates/ui-egui/src/app",
|
||||
"crates/ui-egui/src/assistant_panel",
|
||||
"crates/engine/src",
|
||||
];
|
||||
|
||||
/// Source directories over the limit, as `(path, loose file count)`, skipping [`PENDING`] ones.
|
||||
pub fn crowded(root: &Path) -> Vec<(String, usize)> {
|
||||
let mut out = Vec::new();
|
||||
for top in ["crates", "apps"] {
|
||||
let Ok(entries) = std::fs::read_dir(root.join(top)) else { continue };
|
||||
for entry in entries.flatten() {
|
||||
walk(root, &entry.path().join("src"), &mut out);
|
||||
}
|
||||
}
|
||||
out.retain(|(path, _)| !PENDING.contains(&path.as_str()));
|
||||
out.sort();
|
||||
out
|
||||
}
|
||||
|
||||
fn walk(root: &Path, dir: &Path, out: &mut Vec<(String, usize)>) {
|
||||
let Ok(entries) = std::fs::read_dir(dir) else { return };
|
||||
let mut loose = 0;
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
walk(root, &path, out);
|
||||
} else if path.extension().is_some_and(|ext| ext == "rs") && !path.file_name().is_some_and(|name| name == "mod.rs" || name == "lib.rs") {
|
||||
loose += 1;
|
||||
}
|
||||
}
|
||||
if loose > MAX_LOOSE {
|
||||
let relative = dir.strip_prefix(root).unwrap_or(dir).to_string_lossy().replace('\\', "/");
|
||||
out.push((relative, loose));
|
||||
}
|
||||
}
|
||||
|
||||
/// `cargo xtask tree`: fails when a source directory holds too many loose files.
|
||||
pub fn run(root: &Path) -> Result<(), String> {
|
||||
let crowded = crowded(root);
|
||||
if crowded.is_empty() {
|
||||
println!("OK: no source directory holds more than {MAX_LOOSE} loose files ({} pending restructure).", PENDING.len());
|
||||
return Ok(());
|
||||
}
|
||||
for (path, count) in &crowded {
|
||||
println!(" - {path}: {count} loose files (max {MAX_LOOSE}); group them into sub-folders by concern");
|
||||
}
|
||||
Err(format!("{} crowded source director{}", crowded.len(), if crowded.len() == 1 { "y" } else { "ies" }))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_crowded_directory_is_reported_and_mod_rs_does_not_count() {
|
||||
let root = std::env::temp_dir().join(format!("nc-tree-test-{}", std::process::id()));
|
||||
let src = root.join("crates/demo/src");
|
||||
assert!(std::fs::create_dir_all(src.join("grouped")).is_ok());
|
||||
for index in 0..=MAX_LOOSE {
|
||||
assert!(std::fs::write(src.join(format!("file{index}.rs")), "").is_ok());
|
||||
}
|
||||
assert!(std::fs::write(src.join("lib.rs"), "").is_ok());
|
||||
assert!(std::fs::write(src.join("grouped/mod.rs"), "").is_ok());
|
||||
assert_eq!(crowded(&root), vec![("crates/demo/src".to_owned(), MAX_LOOSE + 1)]);
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_workspace_itself_is_within_the_limit() {
|
||||
assert_eq!(crowded(&crate::root()), Vec::<(String, usize)>::new());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user