diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index cba4b09..4cc14cb 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -11,9 +11,12 @@ Describe your changes. - [ ] test - [ ] chore -## Testing +### Required -Describe testing performed. +- [ ] cargo check passes +- [ ] cargo fmt --check passes +- [ ] cargo clippy --workspace --all-targets -- -D warnings passes +- [ ] cargo test passes ## Checklist diff --git a/README.md b/README.md index e7cdbdc..4c87720 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # FrilVault -> Personal Knowledge Vault for Developers +> Your Personal Knowledge Vault for Code ๐Ÿšง Early development stage -FrilVault is a developer-focused knowledge vault that allows you to attach private notes to source code without modifying the code itself. +FrilVault is a developer-focused personal knowledge vault that allows you to attach private notes to source code without modifying the code itself. -Instead of adding temporary comments, TODOs, research notes, or learning records directly into a repository, FrilVault stores them separately in a local `.vault` directory. +Instead of adding temporary comments, TODOs, research notes, debugging records, or learning materials directly into source files, FrilVault stores them separately inside a local `.vault` directory. This keeps source code clean while preserving valuable knowledge. @@ -22,6 +22,7 @@ When studying large codebases, contributing to open source projects, or maintain - TODO items - Learning notes - AI context +- Reverse engineering notes Adding these notes directly into source files creates several problems: @@ -34,32 +35,89 @@ FrilVault solves this by storing notes outside of the source code. --- +## Quick Start + +Add a note: + +```bash +flv add \ + --file src/main.rs \ + --line 10 \ + --column 5 \ + --content "parser ๊ฐœ์„  ํ•„์š”" +``` + +List notes: + +```bash +flv list \ + --file src/main.rs +``` + +Search notes: + +```bash +flv search parser +``` + +Update a note: + +```bash +flv update \ + --file src/main.rs \ + --id \ + --content "parser ๊ตฌ์กฐ ์žฌ์„ค๊ณ„ ํ•„์š”" +``` + +Delete a note: + +```bash +flv delete \ + --file src/main.rs \ + --id +``` + +--- + ## Features ### Private Notes -Store personal notes without modifying source files. - -### Symbol-Based Notes +Store personal notes without modifying source code. -Attach notes to functions, methods, structs, classes, or modules. +### Line Anchors -### Local First +Attach notes to specific locations inside source files. -All data is stored locally. +```yaml +anchor: + type: Line + line: 10 + column: 5 +``` ### Search -Search notes by symbol, file, tag, or keyword. +Search notes using keywords. + +```bash +flv search parser +``` -### AI Ready +### Local First -Use personal notes as context for AI-assisted development workflows. +All data is stored locally. + +No external services are required. ### Clean Repository Keep repositories free from temporary comments and personal annotations. +### Developer Knowledge Base + +Build a personal knowledge layer on top of any codebase. + --- ## Example @@ -72,61 +130,145 @@ pub fn calculate_damage() { } ``` +Personal note: + +```bash +flv add \ + --file src/combat.rs \ + --line 1 \ + --column 1 \ + --content "Consider SIMD optimization in the future" +``` + Stored note: ```yaml notes: - - symbol: calculate_damage + - id: '15b7c4b3-f4a6-4cc1-accb-428f344cc597' + + source_file: src/combat.rs - tags: - - analysis + anchor: + type: Line + line: 1 + column: 1 - comment: | - Consider SIMD optimization in the future. + content: Consider SIMD optimization in the future + + created_at: '2026-06-03T17:42:17.853037Z' + updated_at: '2026-06-03T17:42:17.853037Z' ``` --- -## Project Structure +## Storage Structure + +Current storage layout: ```text -frilvault/ -โ”œโ”€โ”€ crates/ -โ”‚ โ””โ”€โ”€ frilvault-core/ -โ”‚ -โ”œโ”€โ”€ apps/ -โ”‚ โ””โ”€โ”€ vscode-extension/ +project/ +โ”œโ”€โ”€ src/ โ”‚ โ””โ”€โ”€ .vault/ - โ”œโ”€โ”€ notes/ - โ”œโ”€โ”€ cache/ - โ””โ”€โ”€ index/ + โ””โ”€โ”€ src/ + โ”œโ”€โ”€ main.rs.yml + โ”œโ”€โ”€ lib.rs.yml + โ””โ”€โ”€ service.rs.yml +``` + +Example: + +```yaml +notes: + - id: '15b7c4b3-f4a6-4cc1-accb-428f344cc597' + + source_file: src/lib.rs + + anchor: + type: Line + line: 3 + column: 1 + + content: Parser trait ๊ฒ€ํ†  + + created_at: '2026-06-03T17:42:17.853037Z' + updated_at: '2026-06-03T17:42:17.853037Z' ``` --- +## Current Status + +### Core + +- YAML note storage +- Line anchors +- CRUD operations +- Keyword search + +### CLI + +- add +- list +- update +- delete +- search + +--- + +## Use Cases + +### Open Source Analysis + +Study libraries and frameworks without modifying upstream code. + +### Reverse Engineering + +Document control flow and implementation details. + +### Personal Documentation + +Store architecture notes and design decisions. + +### Learning Notes + +Record discoveries while exploring unfamiliar codebases. + +### AI-Assisted Development + +Build project-specific context for future AI workflows. + +--- + ## Roadmap -### Phase 1 +### Phase 1 (Current) - Rust core library -- VSCode extension -- YAML note storage -- Symbol-based notes +- YAML storage +- CLI support +- CRUD operations +- Keyword search ### Phase 2 +- Symbol anchors +- JSON output +- VSCode extension + +### Phase 3 + - Project indexing -- Search engine - Workspace explorer +- Cached search -### Phase 3 +### Phase 4 +- AI Context Engine - Semantic search -- AI context integration -- RAG support +- RAG integration -### Phase 4 +### Phase 5 - JetBrains plugin - Desktop application diff --git a/apps/frilvault-cli/Cargo.toml b/apps/frilvault-cli/Cargo.toml index 5139d8e..c145de9 100644 --- a/apps/frilvault-cli/Cargo.toml +++ b/apps/frilvault-cli/Cargo.toml @@ -3,6 +3,10 @@ name = "frilvault-cli" version = "0.1.0" edition = "2024" +[[bin]] +name = "flvt" +path = "src/main.rs" + [dependencies] clap = { version = "4", features = ["derive"] } anyhow = "1" diff --git a/apps/frilvault-cli/src/cli/mod.rs b/apps/frilvault-cli/src/cli/mod.rs index e0c9e30..a1a091e 100644 --- a/apps/frilvault-cli/src/cli/mod.rs +++ b/apps/frilvault-cli/src/cli/mod.rs @@ -3,19 +3,17 @@ use clap::{Parser, Subcommand}; pub mod add; pub mod delete; pub mod list; +pub mod search; pub mod update; use add::AddCommand; use delete::DeleteCommand; use list::ListCommand; +use search::SearchCommand; use update::UpdateCommand; #[derive(Parser)] -#[command( - name = "frilvault", - version, - about = "Personal note vault for source code" -)] +#[command(name = "flvt", version, about = "Personal note vault for source code")] pub struct Cli { #[command(subcommand)] pub command: Commands, @@ -27,4 +25,5 @@ pub enum Commands { List(ListCommand), Update(UpdateCommand), Delete(DeleteCommand), + Search(SearchCommand), } diff --git a/apps/frilvault-cli/src/cli/search.rs b/apps/frilvault-cli/src/cli/search.rs new file mode 100644 index 0000000..9909253 --- /dev/null +++ b/apps/frilvault-cli/src/cli/search.rs @@ -0,0 +1,6 @@ +use clap::Args; + +#[derive(Debug, Args)] +pub struct SearchCommand { + pub keyword: String, +} diff --git a/apps/frilvault-cli/src/command/list.rs b/apps/frilvault-cli/src/command/list.rs index 849ff42..01027c8 100644 --- a/apps/frilvault-cli/src/command/list.rs +++ b/apps/frilvault-cli/src/command/list.rs @@ -1,22 +1,16 @@ use anyhow::Result; -use crate::{cli::list::ListCommand, command::create_note_service}; +use crate::{cli::list::ListCommand, command::create_note_service, output}; pub fn execute(command: ListCommand) -> Result<()> { let service = create_note_service()?; let notes = service.list_notes(&command.file)?; - println!("Found {} notes", notes.len()); + output::print_note_count(notes.len()); for note in notes { - println!("\nID: {}", note.id); - - println!("\nFilename: {:?}", note.source_file); - - println!("Content: {}", note.content); - - println!("Anchor: {:?}", note.anchor); + output::print_note(¬e); } Ok(()) diff --git a/apps/frilvault-cli/src/command/mod.rs b/apps/frilvault-cli/src/command/mod.rs index 8bd27d1..3efa0ef 100644 --- a/apps/frilvault-cli/src/command/mod.rs +++ b/apps/frilvault-cli/src/command/mod.rs @@ -5,6 +5,7 @@ use frilvault_core::{NoteService, PathResolver, YamlNoteRepository}; pub mod add; pub mod delete; pub mod list; +pub mod search; pub mod update; pub fn create_note_service() -> Result { diff --git a/apps/frilvault-cli/src/command/search.rs b/apps/frilvault-cli/src/command/search.rs new file mode 100644 index 0000000..0a693f0 --- /dev/null +++ b/apps/frilvault-cli/src/command/search.rs @@ -0,0 +1,17 @@ +use anyhow::Result; + +use crate::{cli::search::SearchCommand, command::create_note_service, output}; + +pub fn execute(command: SearchCommand) -> Result<()> { + let service = create_note_service()?; + + let notes = service.search_notes(&command.keyword)?; + + output::print_note_count(notes.len()); + + for note in notes { + output::print_note(¬e); + } + + Ok(()) +} diff --git a/apps/frilvault-cli/src/main.rs b/apps/frilvault-cli/src/main.rs index 2e820eb..e756340 100644 --- a/apps/frilvault-cli/src/main.rs +++ b/apps/frilvault-cli/src/main.rs @@ -1,5 +1,6 @@ mod cli; mod command; +mod output; use anyhow::Result; use clap::Parser; @@ -24,6 +25,10 @@ fn main() -> Result<()> { Commands::Delete(cmd) => { command::delete::execute(cmd)?; } + + Commands::Search(cmd) => { + command::search::execute(cmd)?; + } } Ok(()) diff --git a/apps/frilvault-cli/src/output.rs b/apps/frilvault-cli/src/output.rs new file mode 100644 index 0000000..d6f82b0 --- /dev/null +++ b/apps/frilvault-cli/src/output.rs @@ -0,0 +1,36 @@ +use frilvault_core::{Note, NoteAnchor}; + +// pub enum OutputFormat { +// Text, +// Json, +// } + +pub fn print_note(note: &Note) { + println!("โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€"); + + println!("ID: {}", note.id); + + match ¬e.anchor { + NoteAnchor::Line(anchor) => { + println!("Location: {}:{}", anchor.line, anchor.column); + } + + NoteAnchor::Symbol(anchor) => { + println!("Symbol: {}", anchor.name); + } + } + + println!("File: {}", note.source_file.display()); + + println!(); + + println!("{}", note.content); + + println!(); +} + +pub fn print_note_count(count: usize) { + println!("Found {} notes", count); + + println!(); +} diff --git a/crates/frilvault-core/src/error/errors.rs b/crates/frilvault-core/src/error/errors.rs index fa6bb91..139be8f 100644 --- a/crates/frilvault-core/src/error/errors.rs +++ b/crates/frilvault-core/src/error/errors.rs @@ -1,4 +1,5 @@ use thiserror::Error; +use uuid::Uuid; #[derive(Debug, Error)] pub enum FrilVaultError { @@ -8,6 +9,9 @@ pub enum FrilVaultError { #[error("yaml error: {0}")] Yaml(#[from] serde_yml::Error), - #[error("source file path is outside workspace root")] + #[error("source path is outside workspace")] SourcePathOutsideWorkspace, + + #[error("note not found: {0}")] + NoteNotFound(Uuid), } diff --git a/crates/frilvault-core/src/lib.rs b/crates/frilvault-core/src/lib.rs index 03423c1..19fb22d 100644 --- a/crates/frilvault-core/src/lib.rs +++ b/crates/frilvault-core/src/lib.rs @@ -5,10 +5,11 @@ pub mod parser; pub mod storage; pub mod workspace; -pub use error::FrilVaultError; +pub use error::*; pub use note::*; -pub use storage::YamlNoteRepository; -pub use workspace::{PathResolver, Workspace}; +pub use parser::*; +pub use storage::*; +pub use workspace::*; pub type FrilVaultResult = Result; diff --git a/crates/frilvault-core/src/note/service.rs b/crates/frilvault-core/src/note/service.rs index a2b64e3..bce14ab 100644 --- a/crates/frilvault-core/src/note/service.rs +++ b/crates/frilvault-core/src/note/service.rs @@ -4,7 +4,7 @@ use chrono::Utc; use uuid::Uuid; use crate::{ - FrilVaultResult, + FrilVaultError, FrilVaultResult, note::{AddNoteInput, Note}, storage::YamlNoteRepository, }; @@ -46,8 +46,14 @@ impl NoteService { let mut notes = self.load_notes(source_file)?; + let before = notes.len(); + notes.retain(|note| note.id != note_id); + if notes.len() == before { + return Err(FrilVaultError::NoteNotFound(note_id)); + } + self.repository.replace_notes(source_file, notes)?; Ok(()) @@ -63,14 +69,34 @@ impl NoteService { let mut notes = self.load_notes(source_file)?; - if let Some(note) = notes.iter_mut().find(|note| note.id == note_id) { - // TODO: FrilVaultError::NoteNotFound - note.content = content; - note.updated_at = Utc::now(); - } + let note = notes + .iter_mut() + .find(|note| note.id == note_id) + .ok_or(FrilVaultError::NoteNotFound(note_id))?; + + note.content = content; + note.updated_at = Utc::now(); self.save_notes(source_file, notes)?; Ok(()) } + + pub fn search_notes(&self, keyword: &str) -> FrilVaultResult> { + let note_files = self.repository.list_all_note_files()?; + + let keyword = keyword.to_lowercase(); + + let mut matches = Vec::new(); + + for note_file in note_files { + for note in note_file.notes { + if note.content.to_lowercase().contains(&keyword) { + matches.push(note); + } + } + } + + Ok(matches) + } } diff --git a/crates/frilvault-core/src/parser/mod.rs b/crates/frilvault-core/src/parser/mod.rs index 0ad212a..5a9b66d 100644 --- a/crates/frilvault-core/src/parser/mod.rs +++ b/crates/frilvault-core/src/parser/mod.rs @@ -1,5 +1,5 @@ -pub mod note_parser; -pub mod yaml_parser; +mod note_parser; +mod yaml_parser; pub use note_parser::NoteParser; pub use yaml_parser::YamlParser; diff --git a/crates/frilvault-core/src/storage/yaml_repository.rs b/crates/frilvault-core/src/storage/yaml_repository.rs index 15a6d8e..aab4c7c 100644 --- a/crates/frilvault-core/src/storage/yaml_repository.rs +++ b/crates/frilvault-core/src/storage/yaml_repository.rs @@ -1,4 +1,5 @@ use crate::FrilVaultResult; +use crate::constants::VAULT_DIR_NAME; use crate::note::{Note, NoteFile}; use crate::parser::{NoteParser, YamlParser}; use crate::workspace::PathResolver; @@ -12,6 +13,10 @@ pub struct YamlNoteRepository { } impl YamlNoteRepository { + fn vault_root(&self) -> PathBuf { + self.path_resolver.workspace_root().join(VAULT_DIR_NAME) + } + // Create a YamlNoteRepository with the given PathResolver. pub fn new(path_resolver: PathResolver) -> Self { Self { @@ -72,4 +77,43 @@ impl YamlNoteRepository { self.save_by_source_file(source_file, ¬e_file) } + + pub fn list_all_note_files(&self) -> FrilVaultResult> { + let mut result = Vec::new(); + let vault_root = self.vault_root(); + + if !vault_root.exists() { + return Ok(result); + } + + self.collect_note_files(&vault_root, &mut result)?; + + Ok(result) + } + + fn collect_note_files( + &self, + directory: &Path, + result: &mut Vec, + ) -> FrilVaultResult<()> { + for entry in fs::read_dir(directory)? { + let entry = entry?; + let path = entry.path(); + + if path.is_dir() { + self.collect_note_files(&path, result)?; + continue; + } + + if path.extension().and_then(|ext| ext.to_str()) != Some("yml") { + continue; + } + + let note_file = self.load_by_note_path(path)?; + + result.push(note_file); + } + + Ok(()) + } } diff --git a/crates/frilvault-core/src/tests/mod.rs b/crates/frilvault-core/src/tests/mod.rs index c836ebd..a0b0355 100644 --- a/crates/frilvault-core/src/tests/mod.rs +++ b/crates/frilvault-core/src/tests/mod.rs @@ -9,3 +9,6 @@ mod note_service_test; #[cfg(test)] mod yaml_parser_test; + +#[cfg(test)] +mod yaml_repository_test; diff --git a/crates/frilvault-core/src/tests/note_service_test.rs b/crates/frilvault-core/src/tests/note_service_test.rs index c8da77c..fa8a875 100644 --- a/crates/frilvault-core/src/tests/note_service_test.rs +++ b/crates/frilvault-core/src/tests/note_service_test.rs @@ -1,5 +1,6 @@ use crate::{ - AddNoteInput, NoteService, PathResolver, YamlNoteRepository, constants::NOTE_FILE_EXTENSION, + AddNoteInput, LineAnchor, NoteAnchor, NoteService, PathResolver, YamlNoteRepository, + constants::NOTE_FILE_EXTENSION, }; use std::fs; @@ -174,3 +175,61 @@ fn update_note_changes_content() { fs::remove_dir_all(workspace_root).unwrap(); } + +#[test] +fn search_notes_finds_matching_notes() { + let workspace_root = + std::env::temp_dir().join(format!("frilvault-test-{}", uuid::Uuid::new_v4())); + + fs::create_dir_all(&workspace_root).unwrap(); + + let resolver = PathResolver::new(&workspace_root); + + let repository = YamlNoteRepository::new(resolver); + + let service = NoteService::new(repository); + + service + .add_note(AddNoteInput { + source_file: "src/main.rs".into(), + + anchor: NoteAnchor::Line(LineAnchor { + line: 10, + column: 5, + }), + + content: "parser ๊ฐœ์„  ํ•„์š”".to_string(), + }) + .unwrap(); + + service + .add_note(AddNoteInput { + source_file: "src/lib.rs".into(), + + anchor: NoteAnchor::Line(LineAnchor { line: 3, column: 1 }), + + content: "Parser trait ๊ฒ€ํ† ".to_string(), + }) + .unwrap(); + + service + .add_note(AddNoteInput { + source_file: "src/mod.rs".into(), + + anchor: NoteAnchor::Line(LineAnchor { line: 1, column: 1 }), + + content: "hello world".to_string(), + }) + .unwrap(); + + let notes = service.search_notes("parser").unwrap(); + + assert_eq!(notes.len(), 2,); + + // case_insensitive + let notes = service.search_notes("PARSER").unwrap(); + + assert_eq!(notes.len(), 2,); + + fs::remove_dir_all(workspace_root).unwrap(); +} diff --git a/crates/frilvault-core/src/tests/yaml_parser_test.rs b/crates/frilvault-core/src/tests/yaml_parser_test.rs index 6939ae0..9d54840 100644 --- a/crates/frilvault-core/src/tests/yaml_parser_test.rs +++ b/crates/frilvault-core/src/tests/yaml_parser_test.rs @@ -1,7 +1,4 @@ -use crate::{ - note::{LineAnchor, Note, NoteAnchor, NoteFile}, - parser::{NoteParser, YamlParser}, -}; +use crate::{LineAnchor, Note, NoteAnchor, NoteFile, NoteParser, YamlParser}; use chrono::Utc; use uuid::Uuid; diff --git a/crates/frilvault-core/src/tests/yaml_repository_test.rs b/crates/frilvault-core/src/tests/yaml_repository_test.rs new file mode 100644 index 0000000..1c978a4 --- /dev/null +++ b/crates/frilvault-core/src/tests/yaml_repository_test.rs @@ -0,0 +1,53 @@ +use crate::{AddNoteInput, LineAnchor, NoteAnchor, NoteService, PathResolver, YamlNoteRepository}; + +use std::fs; + +fn create_service(workspace_root: &std::path::Path) -> NoteService { + let resolver = PathResolver::new(workspace_root); + let repository = YamlNoteRepository::new(resolver); + + NoteService::new(repository) +} + +fn create_repository(workspace_root: &std::path::Path) -> YamlNoteRepository { + let resolver = PathResolver::new(workspace_root); + + YamlNoteRepository::new(resolver) +} + +#[test] +fn list_all_note_files_returns_all_note_files() { + let workspace_root = + std::env::temp_dir().join(format!("frilvault-test-{}", uuid::Uuid::new_v4())); + + fs::create_dir_all(&workspace_root).unwrap(); + + let service = create_service(&workspace_root); + + service + .add_note(AddNoteInput { + source_file: "src/main.rs".into(), + anchor: NoteAnchor::Line(LineAnchor { + line: 10, + column: 5, + }), + content: "main note".to_string(), + }) + .unwrap(); + + service + .add_note(AddNoteInput { + source_file: "src/lib.rs".into(), + anchor: NoteAnchor::Line(LineAnchor { line: 3, column: 1 }), + content: "lib note".to_string(), + }) + .unwrap(); + + let repository = create_repository(&workspace_root); + + let note_files = repository.list_all_note_files().unwrap(); + + assert_eq!(note_files.len(), 2); + + fs::remove_dir_all(workspace_root).unwrap(); +} diff --git a/crates/frilvault-core/src/workspace/path_resolver.rs b/crates/frilvault-core/src/workspace/path_resolver.rs index 2f85c71..34ce269 100644 --- a/crates/frilvault-core/src/workspace/path_resolver.rs +++ b/crates/frilvault-core/src/workspace/path_resolver.rs @@ -20,6 +20,10 @@ impl PathResolver { format!("{}.{}", source_file.as_ref().display(), NOTE_FILE_EXTENSION) } + pub fn workspace_root(&self) -> &Path { + &self.workspace_root + } + // Convert the relative path of the source file to the path '.vault/{source_file}.yml' pub fn resolve_note_path(&self, source_file: impl AsRef) -> PathBuf { let note_file_name = Self::note_file_name(source_file);