Skip to content

Commit 15cbba2

Browse files
authored
perf(core): investigate streaming validation, add AGM-006 tests (#83) (#162)
* perf(core): investigate streaming validation, add AGM-006 tests (#83) Investigation of streaming validation using par_bridge(): - Implemented par_bridge() streaming to avoid intermediate Vec - Review identified critical issues: Mutex contention, atomic overhead - par_bridge() has worse work distribution than collect+par_iter Decision: Keep current collect+par_iter approach as it performs better for typical workloads (<10k files). The memory overhead of the path Vec is negligible compared to the I/O cost of file reads. Changes made: - Minor code cleanup in AGM-006 path filtering - Add test_streaming_validation_agents_md_collection - Add test_streaming_files_checked_count Closes #83 - Performance investigation complete, current approach optimal. * docs: update CHANGELOG for #83 investigation * style: apply cargo fmt formatting * refactor: rename tests per review feedback
1 parent 4733c6e commit 15cbba2

2 files changed

Lines changed: 72 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3131

3232
### Performance
3333
- AS-015 directory size validation now short-circuits when limit exceeded, improving performance on large skill directories (#84)
34+
- Investigated streaming validation with par_bridge() - current collect+par_iter approach found optimal for typical workloads (#83)
35+
36+
### Tests
37+
- Added validation pipeline tests for AGENTS.md path collection and files_checked counter (#83)
3438

3539
### Changed
3640
- Narrowed agnix-core public API surface (#85)

crates/agnix-core/src/lib.rs

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ pub fn validate_project_with_registry(
367367

368368
let walk_root = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
369369

370-
// Collect all file paths to validate (sequential walk, parallel validation)
370+
// Collect file paths (sequential walk, parallel validation)
371371
// Note: hidden(false) includes .github directory for Copilot instruction files
372372
let paths: Vec<PathBuf> = WalkBuilder::new(&walk_root)
373373
.hidden(false)
@@ -398,9 +398,7 @@ pub fn validate_project_with_registry(
398398
.map(|entry| entry.path().to_path_buf())
399399
.collect();
400400

401-
// Count recognized files (exclude FileType::Unknown)
402-
// Note: detect_file_type is called again during validation, but it's a fast
403-
// string-only operation (no I/O) - the overhead is negligible vs file reads.
401+
// Count recognized files (detect_file_type is string-only, no I/O)
404402
let files_checked = paths
405403
.iter()
406404
.filter(|p| detect_file_type(p) != FileType::Unknown)
@@ -427,17 +425,13 @@ pub fn validate_project_with_registry(
427425

428426
// AGM-006: Check for multiple AGENTS.md files in the directory tree (project-level check)
429427
if config.is_rule_enabled("AGM-006") {
430-
let agents_files: Vec<_> = paths
428+
let agents_md_paths: Vec<_> = paths
431429
.iter()
432-
.filter(|p| {
433-
p.file_name()
434-
.and_then(|n| n.to_str())
435-
.is_some_and(|name| name == "AGENTS.md")
436-
})
430+
.filter(|p| p.file_name().and_then(|n| n.to_str()) == Some("AGENTS.md"))
437431
.collect();
438432

439-
if agents_files.len() > 1 {
440-
for agents_file in &agents_files {
433+
if agents_md_paths.len() > 1 {
434+
for agents_file in &agents_md_paths {
441435
let parent_files =
442436
schemas::agents_md::check_agents_md_hierarchy(agents_file, &paths);
443437
let description = if !parent_files.is_empty() {
@@ -450,9 +444,9 @@ pub fn validate_project_with_registry(
450444
parent_paths.join(", ")
451445
)
452446
} else {
453-
let other_paths: Vec<String> = agents_files
447+
let other_paths: Vec<String> = agents_md_paths
454448
.iter()
455-
.filter(|p| *p != agents_file)
449+
.filter(|p| p.as_path() != agents_file.as_path())
456450
.map(|p| p.to_string_lossy().to_string())
457451
.collect();
458452
format!(
@@ -1004,6 +998,66 @@ mod tests {
1004998
);
1005999
}
10061000

1001+
#[test]
1002+
fn test_validate_project_agents_md_collection() {
1003+
// Verify that validation correctly collects AGENTS.md paths for AGM-006
1004+
let temp = tempfile::TempDir::new().unwrap();
1005+
1006+
// Create multiple AGENTS.md files in different directories
1007+
std::fs::write(temp.path().join("AGENTS.md"), "# Root agents").unwrap();
1008+
1009+
let subdir = temp.path().join("subproject");
1010+
std::fs::create_dir_all(&subdir).unwrap();
1011+
std::fs::write(subdir.join("AGENTS.md"), "# Subproject agents").unwrap();
1012+
1013+
let config = LintConfig::default();
1014+
let result = validate_project(temp.path(), &config).unwrap();
1015+
1016+
// Should have AGM-006 warnings for both AGENTS.md files
1017+
let agm006_diagnostics: Vec<_> = result
1018+
.diagnostics
1019+
.iter()
1020+
.filter(|d| d.rule == "AGM-006")
1021+
.collect();
1022+
1023+
assert_eq!(
1024+
agm006_diagnostics.len(),
1025+
2,
1026+
"Expected AGM-006 diagnostic for each AGENTS.md file, got: {:?}",
1027+
agm006_diagnostics
1028+
);
1029+
}
1030+
1031+
#[test]
1032+
fn test_validate_project_files_checked_count() {
1033+
// Verify that validation correctly counts recognized file types
1034+
let temp = tempfile::TempDir::new().unwrap();
1035+
1036+
// Create recognized file types
1037+
std::fs::write(
1038+
temp.path().join("SKILL.md"),
1039+
"---\nname: test-skill\ndescription: Test skill\n---\nBody",
1040+
)
1041+
.unwrap();
1042+
std::fs::write(temp.path().join("CLAUDE.md"), "# Project memory").unwrap();
1043+
1044+
// Create unrecognized file types (should not be counted)
1045+
// Note: .md files are GenericMarkdown (recognized), so use non-markdown extensions
1046+
std::fs::write(temp.path().join("notes.txt"), "Some notes").unwrap();
1047+
std::fs::write(temp.path().join("data.json"), "{}").unwrap();
1048+
1049+
let config = LintConfig::default();
1050+
let result = validate_project(temp.path(), &config).unwrap();
1051+
1052+
// files_checked should only count recognized types (SKILL.md + CLAUDE.md = 2)
1053+
// .txt and .json (not matching MCP patterns) are FileType::Unknown
1054+
assert_eq!(
1055+
result.files_checked, 2,
1056+
"files_checked should count only recognized file types, got {}",
1057+
result.files_checked
1058+
);
1059+
}
1060+
10071061
#[test]
10081062
fn test_validate_project_plugin_detection() {
10091063
let temp = tempfile::TempDir::new().unwrap();

0 commit comments

Comments
 (0)