Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
710 changes: 710 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[workspace]
resolver = "2"

members = [
members = ["apps/frivault-cli",
"crates/frilvault-core"
]
]
Comment on lines +4 to +6
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Likely typo in crate directory name: frivault-cli vs project frilvault.

The CLI member is apps/frivault-cli (missing an l), while the core crate is frilvault-core and the project is FrilVault. If unintentional, rename the directory/crate now while it's still a scaffold to avoid churn later.

✏️ Proposed fix
-members = ["apps/frivault-cli",
+members = ["apps/frilvault-cli",
     "crates/frilvault-core"
 ]

Note: also rename the apps/frivault-cli directory and update its Cargo.toml package name accordingly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Cargo.toml` around lines 4 - 6, The workspace member in Cargo.toml lists
"apps/frivault-cli" which looks like a typo versus the project name FrilVault
and the core crate "frilvault-core"; rename the directory and crate to match:
change the workspace member to "apps/frilvault-cli", rename the filesystem
folder apps/frivault-cli → apps/frilvault-cli, and update the package name in
that crate's Cargo.toml (the package.name field) to "frilvault-cli" so the
workspace entry, directory name, and crate name are consistent.

6 changes: 6 additions & 0 deletions apps/frivault-cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[package]
name = "frivault-cli"
version = "0.1.0"
edition = "2024"

[dependencies]
3 changes: 3 additions & 0 deletions apps/frivault-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn main() {
println!("Hello, world!");
}
5 changes: 5 additions & 0 deletions crates/frilvault-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@ version = "0.1.0"
edition = "2024"

[dependencies]
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1", features = ["derive"] }
serde_yml = "0.0.12"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Latest serde_yml version on crates.io
curl -s https://crates.io/api/v1/crates/serde_yml | jq '.crate.max_stable_version, .crate.newest_version'

# Security advisories for serde_yml
gh api graphql -f query='
{
  securityVulnerabilities(first: 5, ecosystem: RUST, package: "serde_yml") {
    nodes {
      advisory { summary severity publishedAt }
      vulnerableVersionRange
      firstPatchedVersion { identifier }
    }
  }
}'

Repository: FrilLab/frilvault

Length of output: 313


Address the serde_yml advisory for serde_yml = "0.0.12".

GitHub reports serde_yml as “unsound and unmaintained” (MODERATE) with vulnerableVersionRange <= 0.0.12 and no firstPatchedVersion. The currently pinned 0.0.12 falls in the vulnerable range, so this dependency needs replacement/mitigation (e.g., switch to a maintained YAML backend or another vetted crate/fork) rather than just version confirmation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/frilvault-core/Cargo.toml` at line 9, The dependency serde_yml =
"0.0.12" is flagged as unsound/unmaintained; replace or mitigate it by switching
to a maintained YAML crate (e.g., serde_yaml) or a vetted fork and updating all
code paths that import/use serde_yml to the new crate name (search for uses of
serde_yml in Cargo.toml and in imports/usages throughout the crate); update
Cargo.toml dependency entry and adjust any feature flags or API call sites to
match the chosen replacement, run cargo build/test to ensure compatibility, and
remove serde_yml once all references are migrated.

thiserror = "2"
uuid = { version = "1", features = ["v4", "serde"] }
3 changes: 3 additions & 0 deletions crates/frilvault-core/src/constants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub const VAULT_DIR_NAME: &str = ".vault";
pub const NOTE_FILE_EXTENSION: &str = "yml";
pub const SCHEMA_VERSION: u32 = 1;
13 changes: 13 additions & 0 deletions crates/frilvault-core/src/error/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use thiserror::Error;

#[derive(Debug, Error)]
pub enum FrilVaultError {
#[error("io error: {0}")]
Io(#[from] std::io::Error),

#[error("yaml error: {0}")]
Yaml(#[from] serde_yml::Error),

#[error("source file path is outside workspace root")]
SourcePathOutsideWorkspace,
}
3 changes: 3 additions & 0 deletions crates/frilvault-core/src/error/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod errors;

pub use errors::FrilVaultError;
26 changes: 14 additions & 12 deletions crates/frilvault-core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
pub mod constants;
pub mod error;
pub mod note;
pub mod parser;
pub mod storage;
pub mod workspace;

#[cfg(test)]
mod tests {
use super::*;
pub use error::FrilVaultError;
pub use note::{AddNoteInput, Note, NoteFile, NoteService};
pub use storage::YamlNoteRepository;
pub use workspace::{PathResolver, Workspace};

pub type FrilVaultResult<T> = Result<T, FrilVaultError>;

#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
#[cfg(test)]
mod tests;
90 changes: 90 additions & 0 deletions crates/frilvault-core/src/note/entity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use uuid::Uuid;

// TODO: Regex parser
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type")]
pub enum NoteAnchor {
Line(LineAnchor),
Symbol(SymbolAnchor),
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LineAnchor {
pub line: u32,
pub column: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SymbolAnchor {
pub name: String,
pub kind: SymbolKind,
pub signature: Option<String>,
pub line_hint: Option<u32>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum SymbolKind {
Function,
Struct,
Enum,
Trait,
Impl,
Method,
Unknown,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Note {
/// id
pub id: Uuid,

/// source_file
pub source_file: PathBuf,

/// anchor: the position where the note is anchored. it can be either line-based or symbol-based.
pub anchor: NoteAnchor,

/// content: the content of the note.
pub content: String,

/// created_at: the time when the note was created.
pub created_at: DateTime<Utc>,

/// updated_at: the time when the note was last updated. initially same as created_at.
pub updated_at: DateTime<Utc>,
}

#[derive(Debug, Clone)]
pub struct AddNoteInput {
/// source_file: the path to the source file where the note is located.
pub source_file: PathBuf,

/// anchor: the position where the note is anchored. it can be either line-based or symbol-based.
pub anchor: NoteAnchor,

/// content: the content of the note to be saved.
pub content: String,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct NoteFile {
pub notes: Vec<Note>,
}

impl Note {
pub fn new(input: AddNoteInput) -> Self {
let now = Utc::now();

Self {
id: Uuid::new_v4(),
source_file: input.source_file,
anchor: input.anchor,
content: input.content,
created_at: now,
updated_at: now,
}
}
}
5 changes: 5 additions & 0 deletions crates/frilvault-core/src/note/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod entity;
mod service;

pub use entity::*;
pub use service::*;
31 changes: 31 additions & 0 deletions crates/frilvault-core/src/note/service.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use std::path::Path;

use crate::{
FrilVaultResult,
note::{AddNoteInput, Note},
storage::YamlNoteRepository,
};

pub struct NoteService {
repository: YamlNoteRepository,
}

impl NoteService {
pub fn new(repository: YamlNoteRepository) -> Self {
Self { repository }
}

pub fn add_note(&self, input: AddNoteInput) -> FrilVaultResult<Note> {
let note = Note::new(input);

self.repository.append_note(&note)?;

Ok(note)
}

pub fn list_notes(&self, source_file: impl AsRef<Path>) -> FrilVaultResult<Vec<Note>> {
let note_file = self.repository.load_by_source_file(source_file.as_ref())?;

Ok(note_file.notes)
}
}
5 changes: 5 additions & 0 deletions crates/frilvault-core/src/parser/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pub mod note_parser;
pub mod yaml_parser;

pub use note_parser::NoteParser;
pub use yaml_parser::YamlParser;
8 changes: 8 additions & 0 deletions crates/frilvault-core/src/parser/note_parser.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
use crate::FrilVaultResult;
use crate::note::NoteFile;

pub trait NoteParser {
fn serialize(&self, note_file: &NoteFile) -> FrilVaultResult<String>;

fn deserialize(&self, content: &str) -> FrilVaultResult<NoteFile>;
}
16 changes: 16 additions & 0 deletions crates/frilvault-core/src/parser/yaml_parser.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use crate::FrilVaultResult;
use crate::note::NoteFile;
use crate::parser::NoteParser;

#[derive(Debug, Default, Clone)]
pub struct YamlParser;

impl NoteParser for YamlParser {
fn serialize(&self, note_file: &NoteFile) -> FrilVaultResult<String> {
Ok(serde_yml::to_string(note_file)?)
}

fn deserialize(&self, content: &str) -> FrilVaultResult<NoteFile> {
Ok(serde_yml::from_str(content)?)
}
}
3 changes: 3 additions & 0 deletions crates/frilvault-core/src/storage/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod yaml_repository;

pub use yaml_repository::*;
69 changes: 69 additions & 0 deletions crates/frilvault-core/src/storage/yaml_repository.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use crate::FrilVaultResult;
use crate::note::{Note, NoteFile};
use crate::parser::{NoteParser, YamlParser};
use crate::workspace::PathResolver;
use std::fs;
use std::path::{Path, PathBuf};

#[derive(Debug, Clone)]
pub struct YamlNoteRepository {
path_resolver: PathResolver,
parser: YamlParser,
}

impl YamlNoteRepository {
// Create a YamlNoteRepository with the given PathResolver.
pub fn new(path_resolver: PathResolver) -> Self {
Self {
path_resolver,
parser: YamlParser,
}
}

// Read the existing note file, append the new note, and save it again.
pub fn append_note(&self, note: &Note) -> FrilVaultResult<()> {
let mut note_file = self.load_by_source_file(&note.source_file)?;

note_file.notes.push(note.clone());

self.save_by_source_file(&note.source_file, &note_file)?;

Ok(())
}

// If the file does not exist, it returns an empty NoteFile.
pub fn load_by_source_file(&self, source_file: &Path) -> FrilVaultResult<NoteFile> {
let note_path = self.path_resolver.resolve_note_path(source_file);

self.load_by_note_path(note_path)
}

pub fn save_by_source_file(
&self,
source_file: &Path,
note_file: &NoteFile,
) -> FrilVaultResult<()> {
let note_path = self.path_resolver.resolve_note_path(source_file);

if let Some(parent) = note_path.parent() {
fs::create_dir_all(parent)?;
}

let yaml = self.parser.serialize(note_file)?;

fs::write(note_path, yaml)?;

Ok(())
}

fn load_by_note_path(&self, note_path: PathBuf) -> FrilVaultResult<NoteFile> {
if !note_path.exists() {
return Ok(NoteFile::default());
}

let content = fs::read_to_string(note_path)?;
let note_file = self.parser.deserialize(&content)?;

Ok(note_file)
}
}
11 changes: 11 additions & 0 deletions crates/frilvault-core/src/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#[cfg(test)]
mod note_entity_test;

#[cfg(test)]
mod note_resolver_test;

#[cfg(test)]
mod note_service_test;

#[cfg(test)]
mod yaml_parser_test;
41 changes: 41 additions & 0 deletions crates/frilvault-core/src/tests/note_entity_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use crate::{
AddNoteInput,
note::{LineAnchor, Note, NoteAnchor},
};

#[test]
fn create_note_from_input() {
let input = AddNoteInput {
source_file: "src/main.rs".into(),
anchor: NoteAnchor::Line(LineAnchor {
line: 10,
column: 5,
}),
content: "test note".to_string(),
};

let note = Note::new(input);

assert_eq!(note.content, "test note");

match note.anchor {
NoteAnchor::Line(anchor) => {
assert_eq!(anchor.line, 10);
assert_eq!(anchor.column, 5);
}
_ => panic!("expected line anchor"),
}
}

#[test]
fn create_note_generates_uuid() {
let input = AddNoteInput {
source_file: "src/main.rs".into(),
anchor: NoteAnchor::Line(LineAnchor { line: 1, column: 1 }),
content: "uuid test".to_string(),
};

let note = Note::new(input);

assert_ne!(note.id.to_string(), "");
}
21 changes: 21 additions & 0 deletions crates/frilvault-core/src/tests/note_resolver_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use crate::PathResolver;

#[test]
fn resolve_note_path_returns_vault_path() {
let resolver = PathResolver::new("/workspace");

let path = resolver.resolve_note_path("src/main.rs");

assert!(path.ends_with(".vault/src/main.rs.yml"));
}

#[test]
fn workspace_relative_path() {
let resolver = PathResolver::new("/workspace");

let relative = resolver
.to_workspace_relative("/workspace/src/main.rs")
.unwrap();

assert_eq!(relative.to_string_lossy(), "src/main.rs");
}
Loading