From 239b469d87b7470ae152d4df96fccb46105e8e54 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 3 Nov 2026 05:23:33 +0000 Subject: [PATCH 1/8] Resolve Rust crate-name, self, and super imports. use wikifier::... from src/main.rs and tests/*.rs was dropped because resolve_rust only kept mod and crate::. Read the root Cargo.toml package and lib names, resolve self:: and super:: from the file, and keep crate:: inside that file's crate. A Cargo-layout fixture and a prepare-edit dogfood check on this repo's src/api.rs lock the dependent set. Co-authored-by: Aron Amos --- AGENTS.md | 2 +- AGENTS.md.wiki.md | 2 +- CHANGELOG.md | 3 +- CHANGELOG.md.wiki.md | 2 +- CLAUDE.md | 2 +- README.md | 2 +- skills/run.md | 2 +- skills/run.md.wiki.md | 2 +- src/parse.rs | 622 +++++++++++++++++++++++++++++++++++---- src/parse.rs.wiki.md | 6 +- tests/dogfood.rs | 68 ++++- tests/dogfood.rs.wiki.md | 5 + tests/parity.rs | 23 +- tests/parity.rs.wiki.md | 5 + 14 files changed, 672 insertions(+), 74 deletions(-) create mode 100644 tests/dogfood.rs.wiki.md create mode 100644 tests/parity.rs.wiki.md diff --git a/AGENTS.md b/AGENTS.md index 4fddd6d..b603b0e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,7 +32,7 @@ Prefer the CLI when the connected MCP server is still `python3 -m wikifier.mcp.s - Do not hand-edit `library.md`, `file_health.md`, `file_health.json`, or `pending_updates.md`. `update-maps` rewrites `library.md`. Health and pending change only through the commands. They take `.wikifier_staging/.lock`. - `init` copies `index.html` only when it is missing, and it does not clobber an existing path list. `diagnostics.html` stays in this repo. `index.v2.html` and `diagnostics.v2.html` are comparison pages and are not deployed. - This repo's map roots are `src/` and `tests/` (`map_paths.txt`). `monitored_paths.txt` is the extra watch list for docs and `wikifier.sh`. -- The import scan is comment-masked regex, plus named JS/TS re-exports to depth 8 (`parser: "regex+reexport"`). An indented Python import is conditional. `import()` is dynamic. The accuracy gate is `accuracy_fixtures_match_frozen_edges` (four frozen fixtures). Do not describe the scan as a CPython `ast` walk, and do not claim it has been run on Babylon.js. It does not read tsconfig paths, package exports, or workspaces. +- The import scan is comment-masked regex, plus named JS/TS re-exports to depth 8 (`parser: "regex+reexport"`). An indented Python import is conditional. `import()` is dynamic. Rust resolves `mod`, `crate::`, `self::`, `super::`, and the root `Cargo.toml` package or lib name (`-` to `_`). `crate::` stays in that file's crate (`tests/*.rs` and `src/bin/*.rs` included). The package or lib name points at the library, which is how `src/main.rs` and `tests/*.rs` show up as dependents. The accuracy gate is `accuracy_fixtures_match_frozen_edges` (the frozen layouts, including a Cargo crate). Do not describe the scan as a CPython `ast` walk, and do not claim it has been run on Babylon.js. It does not read tsconfig paths, package exports, or workspaces. - `mark_green` checks Python and JS/TS public names against the wiki. Rust, Go, C/C++, C#, Java, and Markdown come back `unchecked`. That is expected. - `wikifier serve` binds `127.0.0.1:8787`. Do not bind port 8080. - Do not set the project root to a parent that contains several repos. diff --git a/AGENTS.md.wiki.md b/AGENTS.md.wiki.md index 6cada42..f31836f 100644 --- a/AGENTS.md.wiki.md +++ b/AGENTS.md.wiki.md @@ -2,4 +2,4 @@ **Role:** Entry point for any coding agent in this repository. -It points at `skills/run.md` (protocol v0.7) and names the live Rust binary, the daily loop, and the files that are historical Python-era records. Map roots stay `src/` and `tests/`. The import scan stays comment-masked regex plus named re-exports. +It points at `skills/run.md` (protocol v0.7) and names the live Rust binary, the daily loop, and the files that are historical Python-era records. Map roots stay `src/` and `tests/`. The import scan stays comment-masked regex plus named re-exports. Rust also resolves `crate::`, `self::`, `super::`, and the root Cargo.toml package or lib name, so `src/main.rs` and `tests/*.rs` are dependents of the library modules they import. diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b6df6f..a806356 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The CI empty-dependency check reads `Cargo.toml` with `awk`. The workflow does not run Python. - The live daemon actions are `start`, `stop`, `status`, and `run`. - This repo's import-map roots are `src/` and `tests/`. -- The import scan remains comment-masked regex plus named JS/TS re-exports to depth 8 (`parser: "regex+reexport"`). It is not a CPython `ast` walk, and it has not been run on Babylon.js. The accuracy gate is still the four frozen fixtures. +- The import scan remains comment-masked regex plus named JS/TS re-exports to depth 8 (`parser: "regex+reexport"`). It is not a CPython `ast` walk, and it has not been run on Babylon.js. The accuracy gate is `accuracy_fixtures_match_frozen_edges`, now including a Cargo crate layout. +- Rust `mod`, `crate::`, `self::`, `super::`, and the root `Cargo.toml` package or lib name resolve to files. Hyphens in the package name become underscores. `crate::` stays inside the file's crate (`tests/*.rs` and `src/bin/*.rs` are separate crates). The package or lib name points at the library, so binaries and integration tests are dependents of the library modules they import. ## [4.7.0] - 2026-10-01 diff --git a/CHANGELOG.md.wiki.md b/CHANGELOG.md.wiki.md index ab841c6..5cdb479 100644 --- a/CHANGELOG.md.wiki.md +++ b/CHANGELOG.md.wiki.md @@ -2,4 +2,4 @@ **Role:** Keep a Changelog history for package 4.7.x. -The 4.7.0 section is the record of that release, including the Python package that shipped then. Unreleased notes describe the Rust binary: map artifacts, dependency flags, wiki checks, deletion, background commands, the dashboard serve routes, MCP error reporting, `heal-stubs --dry-run`, and `init DIR`. The import scan is recorded as `regex+reexport`. CI checks the empty dependency list without Python. +The 4.7.0 section is the record of that release, including the Python package that shipped then. Unreleased notes describe the Rust binary: map artifacts, dependency flags, wiki checks, deletion, background commands, the dashboard serve routes, MCP error reporting, `heal-stubs --dry-run`, and `init DIR`. The import scan is recorded as `regex+reexport`. Unreleased also notes that Rust crate-name imports resolve to the library. CI checks the empty dependency list without Python. diff --git a/CLAUDE.md b/CLAUDE.md index f4c6eb8..e8e09c7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -60,7 +60,7 @@ Pipeline: **scan → parse → resolve → cache → health → artifacts**, imp - `src/api.rs` — the library surface: `session_bootstrap`, `check_changes`, `prepare_edit`, `suggest_next_actions`, `record_change`, `record_changes`, `record_deletion`, `mark_green`, `verify_wikis`, `update_maps` / `update_maps_run`, `dependency_report`, `load_dependents`, `cycles`, `init_project`, `serve_loop`, `monitor_loop`, `daemon_ctl`. `check_changes` watches mapped files, Green files, and `monitored_paths.txt`. `record_changes` reads `git status` (git is optional). - `src/scope.rs` — project root, `map_paths.txt`, `monitored_paths.txt`, excludes, and the file walk. A missing configured map directory is a scope error. Three or more child projects make the root a container and the walk stops. -- `src/parse.rs` — comment-masked regex scans for Python, JS/TS, Rust, Go, C/C++, C#, and Java. Named JS/TS re-exports are followed to depth 8 (`via_barrel`). An indented Python import is `conditional`. `import()` is `dynamic`. This is not a CPython `ast` parser and it does not read tsconfig paths, package `exports`, or workspaces. The accuracy gate is `accuracy_fixtures_match_frozen_edges`. +- `src/parse.rs` — comment-masked regex scans for Python, JS/TS, Rust, Go, C/C++, C#, and Java. Named JS/TS re-exports are followed to depth 8 (`via_barrel`). An indented Python import is `conditional`. `import()` is `dynamic`. Rust resolves `mod`, `crate::`, `self::`, `super::`, and the root `Cargo.toml` package or lib name (`-` to `_`). `crate::` stays in the file's crate; the package or lib name points at the library. This is not a CPython `ast` parser and it does not read tsconfig paths, package `exports`, or workspaces. The accuracy gate is `accuracy_fixtures_match_frozen_edges`. - `src/cache.rs` + `src/cache_commit.rs` — text index at `.wikifier_staging/wk_cache` (per-file payload, `by_src`, `by_dst`, notes, barrels). One commit per `update-maps`. Size and mtime skip unchanged files. `--full` rehashes. `src/legacy_cache.rs` imports an empty cache from `import_cache.sqlite` (the `sqlite3` CLI) or `import_cache.json`. - `src/artifacts.rs` — `file_health.json` (schema version 2) plus the `file_health.md` view, pending lines, and the journal. Identical journal retries within 120 seconds are skipped. `mark_green` stores `public_symbols`, `wiki_hash`, and `source_mtime_ns`, and refuses a Python or JS/TS wiki that misses the current public names, still mentions a removed name in backticks, or is unchanged after the public surface changed. - `src/maps_out.rs` — `library.md` (a file tree, a Mermaid block capped at 80 edges, and the dependency table) and sharded folder cards at `.wikifier_staging/maps/_root.json` and `maps/{prefix}/_index.json`. diff --git a/README.md b/README.md index 41f3829..7c74cc8 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Works from small scripts to large monorepos. **Deep import/include maps** (zero- |----------|------------|--------| | Python | `.py` | comment-masked regex; relative imports and same-tree module paths; an indented import is flagged conditional | | JavaScript / TypeScript | `.js` `.ts` `.jsx` `.tsx` `.mjs` `.cjs` `.mts` `.cts` | comment-masked regex; named re-exports followed to depth 8; `import()` flagged dynamic | -| Rust | `.rs` | `use` / `mod` / `extern crate` | +| Rust | `.rs` | `mod`, `use`, `crate::`, `self::`, `super::`, and the root `Cargo.toml` package or lib name (`-` becomes `_`). A name points at the library; `crate::` stays inside the file's own crate, including `tests/` and `src/bin/` | | Go | `.go` | `import` / import blocks | | C / C++ | `.c` `.h` `.cpp` `.cc` `.cxx` `.hpp` `.hh` | `#include` (local + system) | | C# | `.cs` | `using` namespaces | diff --git a/skills/run.md b/skills/run.md index 36fdb3f..5df72dd 100644 --- a/skills/run.md +++ b/skills/run.md @@ -158,7 +158,7 @@ The program is a std-only Rust binary under `src/`. `Cargo.toml` has an empty `[ - `record_deletion` appends the journal line, marks the health row Red, drops the file from the graph, and removes its pending line. The file does not have to exist. - `record_changes` matches reasons by exact path, then the longest `dir/` prefix, then the longest glob (`*` and `?`). `--only`, `--green`, `--dry-run`, and deletions are applied. `--green` lists verification failures and leaves those files Yellow. - `mark_green` extracts Python top-level `def`/`class` names (or `__all__`) and JS/TS `export` names, and requires each current name to appear in the wiki. With a previous baseline, a removed name still wrapped in backticks is `wiki_mentions_removed_symbols`, and a changed public surface with an unchanged wiki hash is `wiki_not_updated_for_api_change`. Rust, Go, C/C++, C#, and Java have no symbol extractor, so their rows are `unchecked`. `health --summary` includes `green_breakdown`. A successful mark removes that file's pending line. -- The scan ignores comments and strings. It does not use CPython `ast`. It does not read tsconfig `paths`, package `exports`, or workspace maps. Named re-exports are the barrel follow. Accuracy is the four frozen fixtures (`accuracy_fixtures_match_frozen_edges`). This parser has not been run on Babylon.js. +- The scan ignores comments and strings. It does not use CPython `ast`. It does not read tsconfig `paths`, package `exports`, or workspace maps. Named re-exports are the barrel follow. Rust reads the root `Cargo.toml` `[package].name` and `[lib].name` only (hyphens become underscores) and resolves `crate::`, `self::`, `super::`, that crate name, and `mod` to files. `crate::` stays in the file's crate (`tests/*.rs` and `src/bin/*.rs` are their own crates). A package or lib name points at the library. Accuracy is `accuracy_fixtures_match_frozen_edges`, including the Rust crate layout. This parser has not been run on Babylon.js. - MCP `tools/call` sets `isError` when the payload has `success: false`. `resources/read` and `prompts/get` answer. A call waits at most `WIKIFIER_MCP_DEADLINE_S` seconds (default 60) and then returns `timed_out` and `may_still_complete`. An identical journal action, file, and reason inside 120 seconds sets `duplicate_skipped` and does not append another line. - Commands that write health, pending, the map, or the cache take `.wikifier_staging/.lock` before the cache lock. The lock is re-entrant on the same thread, waits 30 seconds, and is stolen when the recorded pid is dead. A timeout returns `error_type: "LockTimeoutError"` and `timed_out: true`. - `init` keeps a path list that already has a non-comment entry. It seeds `src/` only when a list is empty and `src/` exists. It copies `index.html` only when that file is absent, and it writes `.wikifier/marker`. diff --git a/skills/run.md.wiki.md b/skills/run.md.wiki.md index 218c466..cb19d3d 100644 --- a/skills/run.md.wiki.md +++ b/skills/run.md.wiki.md @@ -4,4 +4,4 @@ Part 1 is the session loop: bootstrap, check-changes, suggest-next, prepare-edit, record-change, mark-green, update-maps. Part 2 is the reference for health, wiki verification, batch recording, dependencies, and commands. -The implementation note records how the std-only Rust binary fills that contract: `wk_cache`, regex-plus-reexport parsing, map artifacts, deletion, MCP errors, the project lock, and a standing `serve` that feeds the dashboard. Daemon actions are `start`, `stop`, `status`, and `run`. `init DIR`, `verify-wikis DIR`, and `search-journal --file` apply. The protocol version stays v0.7. The live version command is `wikifier --version`. +The implementation note records how the std-only Rust binary fills that contract: `wk_cache`, regex-plus-reexport parsing, Rust paths from `crate::` / `self::` / `super::` / the root Cargo.toml crate name, map artifacts, deletion, MCP errors, the project lock, and a standing `serve` that feeds the dashboard. Daemon actions are `start`, `stop`, `status`, and `run`. `init DIR`, `verify-wikis DIR`, and `search-journal --file` apply. The protocol version stays v0.7. The live version command is `wikifier --version`. diff --git a/src/parse.rs b/src/parse.rs index 84c0a2e..b0b5508 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1,4 +1,4 @@ -//! Import extraction for the languages Python Wikifier parses. +//! Import extraction for the languages Wikifier parses. //! Unknown suffixes (COBOL, Swift, assembly) are not languages and emit no edges. use std::collections::{BTreeMap, BTreeSet, HashMap}; @@ -14,6 +14,8 @@ pub struct RawImport { pub names: Vec, pub kind: Kind, pub conditional: bool, + /// Inline `mod name { ... }` frames around this import. `super::` pops these before leaving the file. + pub inline_depth: usize, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -22,6 +24,8 @@ pub enum Kind { ExportFrom, ExportStar, Dynamic, + /// A `::` path in an expression, not a `use` or `mod` item. + PathExpr, } #[derive(Clone, Debug, Default)] @@ -132,7 +136,7 @@ pub fn resolve_project(root: &Path, files: &[PathBuf]) -> BTreeMap, file: &str } for imp in &scan.imports { if imp.kind == Kind::ExportFrom && imp.names.iter().any(|n| n == name) { - let target = resolver.resolve(file, &imp.spec, "javascript")?; + let target = resolver.resolve(file, &imp.spec, "javascript", 0)?; return defining_file(resolver, scans, &target, name, depth + 1).or(Some(target)); } } for imp in &scan.imports { if imp.kind == Kind::ExportStar { - if let Some(target) = resolver.resolve(file, &imp.spec, "javascript") { + if let Some(target) = resolver.resolve(file, &imp.spec, "javascript", 0) { if let Some(found) = defining_file(resolver, scans, &target, name, depth + 1) { return Some(found); } @@ -182,6 +186,7 @@ struct Resolver { files: BTreeSet, ts_paths: Vec<(String, String, String)>, packages: Vec, + rust_crates: Vec, } struct Package { @@ -195,14 +200,15 @@ impl Resolver { let files: BTreeSet = rels.iter().cloned().collect(); let ts_paths = load_ts_paths(root); let packages = load_packages(root); - Resolver { files, ts_paths, packages } + let rust_crates = load_rust_crate_names(root); + Resolver { files, ts_paths, packages, rust_crates } } - fn resolve(&self, from_rel: &str, spec: &str, lang: &str) -> Option { + fn resolve(&self, from_rel: &str, spec: &str, lang: &str, inline_depth: usize) -> Option { match lang { "python" => None, // python resolves inside scan_python via statements; handled below "javascript" => self.resolve_js(from_rel, spec), - "rust" => self.resolve_rust(from_rel, spec), + "rust" => self.resolve_rust(from_rel, spec, inline_depth), "go" => self.resolve_go(from_rel, spec), "c" => self.resolve_c(from_rel, spec), "java" => self.resolve_java(spec), @@ -288,48 +294,175 @@ impl Resolver { None } - fn resolve_rust(&self, from_rel: &str, spec: &str) -> Option { + fn resolve_rust(&self, from_rel: &str, spec: &str, inline_depth: usize) -> Option { if let Some(rest) = spec.strip_prefix("mod:") { - let base = Path::new(from_rel).parent().unwrap_or(Path::new("")); - let a = normalize(&base.join(format!("{rest}.rs"))); - if self.files.contains(&a) { - return Some(a); - } - let b = normalize(&base.join(rest).join("mod.rs")); - if self.files.contains(&b) { - return Some(b); - } + return self.resolve_rust_mod(from_rel, rest); + } + self.resolve_rust_path(from_rel, spec, inline_depth) + } + + /// `mod` names a file next to the declaring module. Inline `mod` frames are path segments. + fn resolve_rust_mod(&self, from_rel: &str, rest: &str) -> Option { + let parts: Vec<&str> = rest.split('/').filter(|part| !part.is_empty()).collect(); + if parts.is_empty() { return None; } - if let Some(rest) = spec.strip_prefix("crate::") { - let parts: Vec<&str> = rest.split("::").filter(|p| !p.is_empty()).collect(); - return self.rust_under("src", &parts).or_else(|| self.rust_under("", &parts)); + let mut dir = rust_submodule_dir(from_rel); + for (i, part) in parts.iter().enumerate() { + if i + 1 == parts.len() { + return self.rust_module_at(&dir, part); + } + dir.push(part); } None } - fn rust_under(&self, prefix: &str, parts: &[&str]) -> Option { - if parts.is_empty() { + fn resolve_rust_path(&self, from_rel: &str, spec: &str, inline_depth: usize) -> Option { + let segments: Vec<&str> = spec.split("::").map(str::trim).filter(|part| !part.is_empty() && *part != "*").collect(); + if segments.is_empty() { + return None; + } + let head = segments[0]; + if head == "crate" { + let root = self.file_crate_root(from_rel); + let dir = root.as_deref().map(rust_submodule_dir).unwrap_or_else(|| PathBuf::from("src")); + return self.rust_walk(&dir, &segments[1..], root.as_deref()); + } + if self.rust_crates.iter().any(|name| name == head) { + let root = self.library_root_file(); + let dir = root.as_deref().map(rust_submodule_dir).unwrap_or_else(|| PathBuf::from("src")); + return self.rust_walk(&dir, &segments[1..], root.as_deref()); + } + let mut dir = rust_submodule_dir(from_rel); + let mut depth = inline_depth; + let mut index = 0; + if head == "self" { + index = 1; + } + while index < segments.len() && segments[index] == "super" { + if depth > 0 { + depth -= 1; + } else if dir.as_os_str().is_empty() { + return None; + } else { + dir.pop(); + } + index += 1; + } + if depth > 0 { + return None; + } + if index == 0 && !self.rust_child_exists(&dir, head) { return None; } - let mut p = PathBuf::from(prefix); - for part in parts { - p.push(part); + self.rust_walk(&dir, &segments[index..], None) + } + + fn rust_spec_is_internal(&self, from_rel: &str, spec: &str) -> bool { + if spec.starts_with("mod:") { + return true; + } + let head = spec.split("::").next().unwrap_or("").trim(); + if matches!(head, "crate" | "self" | "super") { + return true; + } + if self.rust_crates.iter().any(|name| name == head) { + return true; } - let file = normalize(&p).trim_end_matches('/').to_string() + ".rs"; + self.rust_child_exists(&rust_submodule_dir(from_rel), head) + } + + fn rust_child_exists(&self, dir: &Path, name: &str) -> bool { + self.rust_module_at(dir, name).is_some() + } + + fn rust_module_at(&self, dir: &Path, name: &str) -> Option { + let file = format!("{}.rs", normalize(&dir.join(name))); if self.files.contains(&file) { return Some(file); } - let dirmod = normalize(&p.join("mod.rs")); + let dirmod = normalize(&dir.join(name).join("mod.rs")); if self.files.contains(&dirmod) { return Some(dirmod); } - if parts.len() > 1 { - return self.rust_under(prefix, &parts[..parts.len() - 1]); + None + } + + fn rust_walk(&self, dir: &Path, parts: &[&str], crate_root: Option<&str>) -> Option { + if parts.is_empty() { + return self.module_file_for(dir, crate_root); + } + let part = parts[0]; + if part == "self" { + return self.rust_walk(dir, &parts[1..], crate_root); + } + if let Some(found) = self.rust_module_at(dir, part) { + let next = dir.join(part); + if parts.len() == 1 { + return Some(found); + } + if let Some(hit) = self.rust_walk(&next, &parts[1..], None) { + return Some(hit); + } + return Some(found); + } + if parts.len() == 1 { + return self.module_file_for(dir, crate_root); + } + None + } + + fn module_file_for(&self, dir: &Path, crate_root: Option<&str>) -> Option { + if let Some(root) = crate_root { + if self.files.contains(root) && normalize(&rust_submodule_dir(root)) == normalize(dir) { + return Some(root.to_string()); + } + } + if dir.as_os_str().is_empty() { + for name in ["mod.rs", "lib.rs", "main.rs"] { + if self.files.contains(name) { + return Some(name.to_string()); + } + } + return None; + } + for name in ["mod.rs", "lib.rs", "main.rs"] { + let cand = normalize(&dir.join(name)); + if self.files.contains(&cand) { + return Some(cand); + } + } + let file = format!("{}.rs", normalize(dir)); + if self.files.contains(&file) { + return Some(file); } None } + fn library_root_file(&self) -> Option { + if self.files.contains("src/lib.rs") { + Some("src/lib.rs".into()) + } else if self.files.contains("src/main.rs") { + Some("src/main.rs".into()) + } else if self.files.contains("lib.rs") { + Some("lib.rs".into()) + } else { + None + } + } + + fn file_crate_root(&self, from_rel: &str) -> Option { + if let Some(root) = separate_crate_root(from_rel) { + if self.files.contains(&root) { + return Some(root); + } + } + if from_rel == "src/main.rs" { + return Some("src/main.rs".into()); + } + self.library_root_file() + } + fn resolve_go(&self, from_rel: &str, spec: &str) -> Option { if spec.starts_with('.') { let base = Path::new(from_rel).parent().unwrap_or(Path::new("")); @@ -502,7 +635,7 @@ fn scan_python(src: &str) -> Scan { let (masked, dynamic) = mask_python(src); let mut imports = Vec::new(); for spec in dynamic { - imports.push(RawImport { spec, names: Vec::new(), kind: Kind::Dynamic, conditional: false }); + imports.push(RawImport { spec, names: Vec::new(), kind: Kind::Dynamic, conditional: false, inline_depth: 0 }); } for stmt in python_statements(&masked) { imports.push(stmt); @@ -688,7 +821,7 @@ fn parse_py_stmt(stmt: &str) -> Option { if names.is_empty() { return None; } - return Some(RawImport { spec: format!("py:import:0::{names}"), names: Vec::new(), kind: Kind::Import, conditional: false }); + return Some(RawImport { spec: format!("py:import:0::{names}"), names: Vec::new(), kind: Kind::Import, conditional: false, inline_depth: 0 }); } if let Some(rest) = stmt.strip_prefix("from ") { let import_at = rest.find(" import ")?; @@ -715,7 +848,7 @@ fn parse_py_stmt(stmt: &str) -> Option { spec: format!("py:from:{level}:{module}:{}", names.join(",")), names, kind: Kind::Import, - conditional: false, + conditional: false, inline_depth: 0, }); } None @@ -770,7 +903,7 @@ fn scan_js(src: &str) -> Scan { let after = i + "import".len(); if after < b.len() && b[after] == b'(' { if let Some((spec, next)) = read_call_spec(src, after) { - imports.push(RawImport { spec, names: Vec::new(), kind: Kind::Dynamic, conditional: false }); + imports.push(RawImport { spec, names: Vec::new(), kind: Kind::Dynamic, conditional: false, inline_depth: 0 }); i = next; tail.clear(); continue; @@ -804,7 +937,7 @@ fn scan_js(src: &str) -> Scan { let after = i + "require".len(); if after < b.len() && b[after] == b'(' { if let Some((spec, next)) = read_call_spec(src, after) { - imports.push(RawImport { spec, names: Vec::new(), kind: Kind::Dynamic, conditional: false }); + imports.push(RawImport { spec, names: Vec::new(), kind: Kind::Dynamic, conditional: false, inline_depth: 0 }); i = next; tail.clear(); continue; @@ -950,7 +1083,7 @@ fn read_import_statement(rest: &str) -> Option<(RawImport, usize)> { let lead = rest.len() - t.len(); if t.starts_with('\'') || t.starts_with('"') { let (spec, n) = read_quoted(t)?; - return Some((RawImport { spec, names: Vec::new(), kind: Kind::Import, conditional: false }, lead + n)); + return Some((RawImport { spec, names: Vec::new(), kind: Kind::Import, conditional: false, inline_depth: 0 }, lead + n)); } let from = t.find(" from ")?; let head = &t[..from]; @@ -958,7 +1091,7 @@ fn read_import_statement(rest: &str) -> Option<(RawImport, usize)> { let ws = after.len() - after.trim_start().len(); let (spec, n) = read_quoted(after.trim_start())?; let names = brace_names(head); - Some((RawImport { spec, names, kind: Kind::Import, conditional: false }, lead + from + " from ".len() + ws + n)) + Some((RawImport { spec, names, kind: Kind::Import, conditional: false, inline_depth: 0 }, lead + from + " from ".len() + ws + n)) } fn read_export_from(trimmed: &str) -> Option<(RawImport, usize)> { @@ -967,7 +1100,7 @@ fn read_export_from(trimmed: &str) -> Option<(RawImport, usize)> { let after = trimmed[from + " from ".len()..].trim_start(); let (spec, n) = read_quoted(after)?; let consumed = from + " from ".len() + (trimmed[from + " from ".len()..].len() - after.len()) + n; - return Some((RawImport { spec, names: Vec::new(), kind: Kind::ExportStar, conditional: false }, consumed)); + return Some((RawImport { spec, names: Vec::new(), kind: Kind::ExportStar, conditional: false, inline_depth: 0 }, consumed)); } if trimmed.starts_with('{') { let from = trimmed.find(" from ")?; @@ -975,7 +1108,7 @@ fn read_export_from(trimmed: &str) -> Option<(RawImport, usize)> { let after = trimmed[from + " from ".len()..].trim_start(); let (spec, n) = read_quoted(after)?; let consumed = from + " from ".len() + (trimmed[from + " from ".len()..].len() - after.len()) + n; - return Some((RawImport { spec, names, kind: Kind::ExportFrom, conditional: false }, consumed)); + return Some((RawImport { spec, names, kind: Kind::ExportFrom, conditional: false, inline_depth: 0 }, consumed)); } None } @@ -1026,31 +1159,388 @@ fn read_quoted(s: &str) -> Option<(String, usize)> { } fn scan_rust(src: &str) -> Scan { + let b = src.as_bytes(); let mut imports = Vec::new(); - for line in strip_c_comments(src).lines() { - let t = line.trim(); - if let Some(rest) = t.strip_prefix("mod ") { - let name = rest.trim().trim_end_matches(';').split_whitespace().next().unwrap_or(""); - if is_ident(name) { - imports.push(RawImport { spec: format!("mod:{name}"), names: Vec::new(), kind: Kind::Import, conditional: false }); + let mut i = 0; + let mut brace: i32 = 0; + let mut inline: Vec<(String, i32)> = Vec::new(); + while i < b.len() { + if b[i].is_ascii_whitespace() { + i += 1; + continue; + } + if b[i] == b'/' && i + 1 < b.len() && (b[i + 1] == b'/' || b[i + 1] == b'*') { + i = skip_rust_comment(b, i); + continue; + } + if let Some(end) = skip_rust_string(b, i) { + i = end; + continue; + } + if b[i] == b'\'' { + i = skip_rust_char_or_lifetime(b, i); + continue; + } + if b[i] == b'{' { + brace += 1; + i += 1; + continue; + } + if b[i] == b'}' { + brace -= 1; + while inline.last().is_some_and(|(_, depth)| *depth > brace) { + inline.pop(); } - } else if let Some(rest) = t.strip_prefix("pub mod ") { - let name = rest.trim().trim_end_matches(';').split_whitespace().next().unwrap_or(""); - if is_ident(name) { - imports.push(RawImport { spec: format!("mod:{name}"), names: Vec::new(), kind: Kind::Import, conditional: false }); + i += 1; + continue; + } + if starts_word(b, i, "mod") { + if let Some((name, end, body)) = read_rust_mod(b, i + 3) { + if body { + inline.push((name, brace + 1)); + } else { + let mut spec = String::from("mod:"); + for (frame, _) in &inline { + spec.push_str(frame); + spec.push('/'); + } + spec.push_str(&name); + imports.push(rust_import(spec, Kind::Import, inline.len())); + } + i = end; + continue; + } + } + if starts_word(b, i, "use") { + if let Some((end, paths)) = read_rust_use(b, i + 3) { + let depth = inline.len(); + for path in paths { + if !path.is_empty() { + imports.push(rust_import(path, Kind::Import, depth)); + } + } + i = end; + continue; } - } else if let Some(rest) = t.strip_prefix("use ") { - let rest = rest.trim().trim_end_matches(';'); - if rest.starts_with("crate::") { - let path = rest.split("::").take_while(|p| !p.contains('{') && !p.contains(' ')).collect::>().join("::"); - // drop the last segment if it looks like an item (lowercase function) — keep module path - imports.push(RawImport { spec: path, names: Vec::new(), kind: Kind::Import, conditional: false }); + } + if let Some((first, end)) = read_colon_path(b, i) { + if path_is_module_ref(&first) && first.split("::").nth(1).is_some() { + imports.push(rust_import(first, Kind::PathExpr, inline.len())); } + i = end; + continue; + } + if let Some((_, end)) = read_ident(b, i) { + i = end; + continue; } + i += 1; } + let mut seen = BTreeSet::new(); + imports.retain(|imp| seen.insert(format!("{}:{}:{}", imp.inline_depth, kind_tag(imp.kind), imp.spec))); Scan { imports, local_exports: Vec::new() } } +fn rust_import(spec: String, kind: Kind, inline_depth: usize) -> RawImport { + RawImport { spec, names: Vec::new(), kind, conditional: false, inline_depth } +} + +fn kind_tag(kind: Kind) -> u8 { + match kind { + Kind::Import => 0, + Kind::ExportFrom => 1, + Kind::ExportStar => 2, + Kind::Dynamic => 3, + Kind::PathExpr => 4, + } +} + +fn path_is_module_ref(path: &str) -> bool { + matches!(path.chars().next(), Some(c) if c.is_ascii_lowercase() || c == '_') +} + +fn read_rust_mod(b: &[u8], start: usize) -> Option<(String, usize, bool)> { + let i = skip_noise(b, start); + let (name, mut j) = read_ident(b, i)?; + j = skip_noise(b, j); + if j < b.len() && b[j] == b';' { + return Some((name, j + 1, false)); + } + if j < b.len() && b[j] == b'{' { + return Some((name, j, true)); + } + None +} + +fn read_rust_use(b: &[u8], start: usize) -> Option<(usize, Vec)> { + let i = skip_noise(b, start); + let (end, paths) = parse_use_tree(b, i, &[])?; + let end = skip_noise(b, end); + let end = if end < b.len() && b[end] == b';' { end + 1 } else { end }; + Some((end, paths)) +} + +fn parse_use_tree(b: &[u8], start: usize, prefix: &[String]) -> Option<(usize, Vec)> { + let i = skip_noise(b, start); + if i >= b.len() { + return None; + } + if b[i] == b'{' { + return parse_use_group(b, i + 1, prefix); + } + if b[i] == b'*' { + return Some((i + 1, vec![prefix.join("::")])); + } + let (ident, mut j) = read_ident(b, i)?; + j = skip_noise(b, j); + if starts_word(b, j, "as") { + let alias_at = skip_noise(b, j + 2); + let (_, after_alias) = read_ident(b, alias_at)?; + j = skip_noise(b, after_alias); + } + if j + 1 < b.len() && b[j] == b':' && b[j + 1] == b':' { + let next = skip_noise(b, j + 2); + let mut deeper = prefix.to_vec(); + if ident != "self" { + deeper.push(ident); + } + return parse_use_tree(b, next, &deeper); + } + let mut segs = prefix.to_vec(); + if ident != "self" { + segs.push(ident); + } + let path = segs.join("::"); + let paths = if path.is_empty() { Vec::new() } else { vec![path] }; + Some((j, paths)) +} + +fn parse_use_group(b: &[u8], mut i: usize, prefix: &[String]) -> Option<(usize, Vec)> { + let mut out = Vec::new(); + loop { + i = skip_noise(b, i); + if i >= b.len() { + return None; + } + if b[i] == b'}' { + return Some((i + 1, out)); + } + if b[i] == b',' { + i += 1; + continue; + } + let (end, paths) = parse_use_tree(b, i, prefix)?; + out.extend(paths); + i = end; + } +} + +/// `ident::ident` starting at `i`. The returned string omits whitespace around `::`. +fn read_colon_path(b: &[u8], i: usize) -> Option<(String, usize)> { + let (first, mut j) = read_ident(b, i)?; + let mut segs = vec![first]; + loop { + let k = skip_noise(b, j); + if k + 1 < b.len() && b[k] == b':' && b[k + 1] == b':' { + let next = skip_noise(b, k + 2); + if next < b.len() && b[next] == b'{' { + return Some((segs.join("::"), next)); + } + if let Some((ident, end)) = read_ident(b, next) { + segs.push(ident); + j = end; + continue; + } + return Some((segs.join("::"), next)); + } + if segs.len() < 2 { + return None; + } + return Some((segs.join("::"), j)); + } +} + +fn read_ident(b: &[u8], i: usize) -> Option<(String, usize)> { + if i >= b.len() { + return None; + } + let start = b[i]; + if !(start.is_ascii_alphabetic() || start == b'_') { + return None; + } + let mut j = i + 1; + while j < b.len() && (b[j].is_ascii_alphanumeric() || b[j] == b'_') { + j += 1; + } + Some((String::from_utf8_lossy(&b[i..j]).into_owned(), j)) +} + +fn skip_noise(b: &[u8], mut i: usize) -> usize { + loop { + while i < b.len() && b[i].is_ascii_whitespace() { + i += 1; + } + if i + 1 < b.len() && b[i] == b'/' && (b[i + 1] == b'/' || b[i + 1] == b'*') { + i = skip_rust_comment(b, i); + continue; + } + return i; + } +} + +fn skip_rust_comment(b: &[u8], i: usize) -> usize { + if i + 1 < b.len() && b[i] == b'/' && b[i + 1] == b'/' { + let mut j = i + 2; + while j < b.len() && b[j] != b'\n' { + j += 1; + } + return j; + } + if i + 1 < b.len() && b[i] == b'/' && b[i + 1] == b'*' { + let mut j = i + 2; + while j + 1 < b.len() && !(b[j] == b'*' && b[j + 1] == b'/') { + j += 1; + } + return (j + 2).min(b.len()); + } + i +} + +fn skip_rust_string(b: &[u8], i: usize) -> Option { + let mut i = i; + if i < b.len() && (b[i] == b'b' || b[i] == b'c') { + let next = *b.get(i + 1)?; + if next == b'"' || next == b'\'' || next == b'r' { + i += 1; + } else { + return None; + } + } + if i < b.len() && b[i] == b'r' { + i += 1; + let mut hashes = 0usize; + while i < b.len() && b[i] == b'#' { + hashes += 1; + i += 1; + } + if i >= b.len() || b[i] != b'"' { + return None; + } + i += 1; + while i < b.len() { + if b[i] == b'"' { + let mut k = 0; + while k < hashes && i + 1 + k < b.len() && b[i + 1 + k] == b'#' { + k += 1; + } + if k == hashes { + return Some(i + 1 + hashes); + } + } + i += 1; + } + return Some(b.len()); + } + if i < b.len() && b[i] == b'"' { + i += 1; + while i < b.len() { + if b[i] == b'\\' { + i += 2; + continue; + } + if b[i] == b'"' { + return Some(i + 1); + } + i += 1; + } + return Some(b.len()); + } + None +} + +fn skip_rust_char_or_lifetime(b: &[u8], i: usize) -> usize { + let next = b.get(i + 1).copied().unwrap_or(0); + if next.is_ascii_alphabetic() || next == b'_' { + let mut j = i + 2; + while j < b.len() && (b[j].is_ascii_alphanumeric() || b[j] == b'_') { + j += 1; + } + return j; + } + if next == b'\\' { + return (i + 4).min(b.len()); + } + (i + 3).min(b.len()) +} + +fn rust_submodule_dir(from_rel: &str) -> PathBuf { + let path = Path::new(from_rel); + let name = path.file_name().and_then(|s| s.to_str()).unwrap_or(""); + let parent = path.parent().unwrap_or(Path::new("")); + if name == "mod.rs" || name == "lib.rs" || name == "main.rs" { + return parent.to_path_buf(); + } + let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("mod"); + parent.join(stem) +} + +fn separate_crate_root(from_rel: &str) -> Option { + let parts: Vec<&str> = from_rel.split('/').filter(|part| !part.is_empty()).collect(); + if parts.len() >= 2 && matches!(parts[0], "tests" | "examples" | "benches") { + let stem = parts[1].trim_end_matches(".rs"); + return Some(format!("{}/{stem}.rs", parts[0])); + } + if parts.len() >= 3 && parts[0] == "src" && parts[1] == "bin" { + let stem = parts[2].trim_end_matches(".rs"); + return Some(format!("src/bin/{stem}.rs")); + } + None +} + +fn load_rust_crate_names(root: &Path) -> Vec { + let text = fs::read_to_string(root.join("Cargo.toml")).unwrap_or_default(); + let mut section = String::new(); + let mut package = String::new(); + let mut lib_name = String::new(); + for line in text.lines() { + let line = line.split('#').next().unwrap_or("").trim(); + if line.is_empty() { + continue; + } + if line.starts_with('[') && line.ends_with(']') { + section = line[1..line.len() - 1].trim().to_string(); + continue; + } + if section != "package" && section != "lib" { + continue; + } + let Some((key, value)) = line.split_once('=') else { continue }; + if key.trim() != "name" { + continue; + } + let value = value.trim().trim_matches(|c| c == '"' || c == '\''); + if value.is_empty() { + continue; + } + if section == "package" && package.is_empty() { + package = value.to_string(); + } else if section == "lib" && lib_name.is_empty() { + lib_name = value.to_string(); + } + } + let mut names = Vec::new(); + if !package.is_empty() { + names.push(package.replace('-', "_")); + } + if !lib_name.is_empty() { + let normalized = lib_name.replace('-', "_"); + if !names.contains(&normalized) { + names.push(normalized); + } + } + names +} + fn scan_go(src: &str) -> Scan { let mut imports = Vec::new(); let text = strip_c_comments(src); @@ -1067,7 +1557,7 @@ fn scan_go(src: &str) -> Scan { while j < bytes.len() && bytes[j] != b')' { if bytes[j] == b'"' { if let Some((spec, n)) = read_quoted(&text[j..]) { - imports.push(RawImport { spec, names: Vec::new(), kind: Kind::Import, conditional: false }); + imports.push(RawImport { spec, names: Vec::new(), kind: Kind::Import, conditional: false, inline_depth: 0 }); j += n; continue; } @@ -1079,7 +1569,7 @@ fn scan_go(src: &str) -> Scan { } if j < bytes.len() && bytes[j] == b'"' { if let Some((spec, n)) = read_quoted(&text[j..]) { - imports.push(RawImport { spec, names: Vec::new(), kind: Kind::Import, conditional: false }); + imports.push(RawImport { spec, names: Vec::new(), kind: Kind::Import, conditional: false, inline_depth: 0 }); i = j + n; continue; } @@ -1097,9 +1587,9 @@ fn scan_c(src: &str) -> Scan { if let Some(rest) = t.strip_prefix("#include") { let rest = rest.trim(); if let Some(inner) = rest.strip_prefix('"').and_then(|r| r.strip_suffix('"')) { - imports.push(RawImport { spec: inner.to_string(), names: Vec::new(), kind: Kind::Import, conditional: false }); + imports.push(RawImport { spec: inner.to_string(), names: Vec::new(), kind: Kind::Import, conditional: false, inline_depth: 0 }); } else if let Some(inner) = rest.strip_prefix('<').and_then(|r| r.strip_suffix('>')) { - imports.push(RawImport { spec: inner.to_string(), names: Vec::new(), kind: Kind::Import, conditional: false }); + imports.push(RawImport { spec: inner.to_string(), names: Vec::new(), kind: Kind::Import, conditional: false, inline_depth: 0 }); } } } @@ -1113,7 +1603,7 @@ fn scan_java(src: &str) -> Scan { if let Some(rest) = t.strip_prefix("import ") { let rest = rest.trim_start_matches("static ").trim(); if !rest.is_empty() { - imports.push(RawImport { spec: rest.to_string(), names: Vec::new(), kind: Kind::Import, conditional: false }); + imports.push(RawImport { spec: rest.to_string(), names: Vec::new(), kind: Kind::Import, conditional: false, inline_depth: 0 }); } } } @@ -1130,7 +1620,7 @@ fn scan_csharp(src: &str) -> Scan { continue; } if !rest.is_empty() && !rest.starts_with('(') { - imports.push(RawImport { spec: rest.to_string(), names: Vec::new(), kind: Kind::Import, conditional: false }); + imports.push(RawImport { spec: rest.to_string(), names: Vec::new(), kind: Kind::Import, conditional: false, inline_depth: 0 }); } } } @@ -1542,7 +2032,7 @@ fn analyze_files_filtered(root: &Path, files: &[PathBuf], only: Option { - if imp.spec.starts_with('.') { + if lang == "rust" { + if target.is_none() && imp.kind != Kind::PathExpr && resolver.rust_spec_is_internal(rel, &imp.spec) { + note_unresolved(&mut facts, imp.spec.clone(), line, imp.kind); + } else if target.is_none() && imp.kind != Kind::PathExpr && !imp.spec.is_empty() && !resolver.rust_spec_is_internal(rel, &imp.spec) { + facts.external.push(imp.spec.clone()); + } + } else if imp.spec.starts_with('.') { note_unresolved(&mut facts, imp.spec.clone(), line, imp.kind); } else if !imp.spec.is_empty() && !imp.spec.starts_with("mod:") && !imp.spec.starts_with("crate::") { facts.external.push(imp.spec.clone()); - } else if imp.spec.starts_with('.') || (target.is_none() && imp.spec.starts_with("mod:")) { + } else if target.is_none() && imp.spec.starts_with("mod:") { note_unresolved(&mut facts, imp.spec.clone(), line, imp.kind); } } diff --git a/src/parse.rs.wiki.md b/src/parse.rs.wiki.md index bf28768..3356fb3 100644 --- a/src/parse.rs.wiki.md +++ b/src/parse.rs.wiki.md @@ -4,4 +4,8 @@ Unknown suffixes emit no edges. Named JS/TS re-exports are followed to depth 8 and flagged `via_barrel`. A Python import with indent greater than zero is `conditional`. `import()` is `dynamic`. Direct edges clear `via_barrel` when the same destination is also reached that way. -The scan does not use CPython `ast` and does not read tsconfig paths, package exports, or workspaces. The frozen fixtures are the accuracy gate. +Rust `mod` names a file beside the declaring module (`lib.rs`, `main.rs`, and `mod.rs` use the parent directory; other files use a directory with the file's stem). Inline `mod name { ... }` is not a file. `crate::` stays in the file's crate, so `tests/*.rs` and `src/bin/*.rs` do not point `crate::` at the library. `self::` and `super::` walk from that file; a `super::` inside an inline module pops the inline frame before it leaves the file. The root `Cargo.toml` `[package].name` and `[lib].name` (hyphens become underscores) both point at the library root (`src/lib.rs` when it exists). That is how `use wikifier::...` from `src/main.rs` and `tests/*.rs` becomes an edge. A path expression with the same shape counts as an edge too. `use` of any other crate is external. The scan does not read workspace members. + +The scan does not use CPython `ast` and does not read tsconfig paths, package exports, or workspaces. The frozen fixtures, including the Cargo crate layout, are the accuracy gate. + +Public scan entry points: `language`, `read_utf8`, `scan_file_warned`, `scan_file`, `resolve_project`, `resolve_python_spec`, `analyze_files`, `analyze_changed`, `edges_for_files`. Public types: `RawImport`, `Kind`, `Scan`, `FactEdge`, `FileFacts`, `Analysis`. diff --git a/tests/dogfood.rs b/tests/dogfood.rs index 4fd1a92..d149a6f 100644 --- a/tests/dogfood.rs +++ b/tests/dogfood.rs @@ -6,7 +6,7 @@ use std::path::{Path, PathBuf}; use std::process::Command; use wikifier::jsonutil::{parse, Json}; -use wikifier::{session_bootstrap, update_maps}; +use wikifier::{prepare_edit, session_bootstrap, update_maps}; fn temp_root(name: &str) -> PathBuf { let dir = std::env::temp_dir().join(format!("wk-dog-{name}-{}", std::process::id())); @@ -106,3 +106,69 @@ fn containers_are_refused_and_scoped_children_map() { let _ = fs::remove_dir_all(©); let _ = fs::remove_dir_all(&cobol); } + +fn copy_tree(from: &Path, to: &Path) { + fs::create_dir_all(to).unwrap(); + for entry in fs::read_dir(from).unwrap() { + let entry = entry.unwrap(); + let target = to.join(entry.file_name()); + if entry.path().is_dir() { + copy_tree(&entry.path(), &target); + } else { + fs::copy(entry.path(), target).unwrap(); + } + } +} + +fn string_list(value: &Json, key: &str) -> Vec { + value + .get(key) + .and_then(|item| item.as_array()) + .unwrap_or(&[]) + .iter() + .filter_map(|item| item.as_str().map(|text| text.to_string())) + .collect() +} + +#[test] +fn prepare_edit_on_this_repo_lists_api_dependents() { + let live = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let root = temp_root("self-api"); + fs::copy(live.join("Cargo.toml"), root.join("Cargo.toml")).unwrap(); + fs::copy(live.join("map_paths.txt"), root.join("map_paths.txt")).unwrap(); + copy_tree(&live.join("src"), &root.join("src")); + copy_tree(&live.join("tests"), &root.join("tests")); + + let built = update_maps(&root); + assert_eq!(built.get("success").and_then(|s| s.as_bool()), Some(true), "{built}"); + assert_eq!(built.get("map_complete").and_then(|s| s.as_bool()), Some(true), "{built}"); + + let prep = prepare_edit(&root, "src/api.rs"); + assert_eq!(prep.get("success").and_then(|s| s.as_bool()), Some(true), "{prep}"); + let mut dependents = string_list(&prep, "dependents"); + dependents.sort(); + let expected = vec![ + "src/lib.rs", + "src/main.rs", + "src/mcp.rs", + "tests/cli_surface.rs", + "tests/contract_gaps.rs", + "tests/protocol_v07.rs", + "tests/robustness.rs", + "tests/skip_maps.rs", + ]; + assert_eq!(dependents, expected, "prepare-edit src/api.rs dependents"); + + let main = prepare_edit(&root, "src/main.rs"); + assert!(main.get("dependencies").and_then(|item| item.as_array()).is_some_and(|rows| !rows.is_empty()), "{main}"); + let main_deps: Vec = main + .get("dependencies") + .and_then(|item| item.as_array()) + .unwrap_or(&[]) + .iter() + .filter_map(|item| item.get("path").and_then(|path| path.as_str()).map(|text| text.to_string())) + .collect(); + assert!(main_deps.iter().any(|path| path == "src/api.rs"), "main.rs dependencies: {main_deps:?}"); + assert!(main_deps.iter().any(|path| path == "src/timeutil.rs"), "main.rs dependencies: {main_deps:?}"); + let _ = fs::remove_dir_all(&root); +} diff --git a/tests/dogfood.rs.wiki.md b/tests/dogfood.rs.wiki.md new file mode 100644 index 0000000..ad425a4 --- /dev/null +++ b/tests/dogfood.rs.wiki.md @@ -0,0 +1,5 @@ +# tests/dogfood.rs + +**Role:** Dogfood checks against fixture trees and this repository's own sources. + +`containers_are_refused_and_scoped_children_map` refuses a root that contains several child projects and still maps a scoped child. `prepare_edit_on_this_repo_lists_api_dependents` copies this repo's `Cargo.toml`, `src/`, and `tests/` into a temp root, runs `update_maps`, and checks `prepare_edit` on `src/api.rs`. The dependent set is `src/lib.rs`, `src/mcp.rs`, `src/main.rs`, and the test files that import `wikifier::api` (`cli_surface`, `contract_gaps`, `protocol_v07`, `robustness`, `skip_maps`). diff --git a/tests/parity.rs b/tests/parity.rs index 7777849..3c5f067 100644 --- a/tests/parity.rs +++ b/tests/parity.rs @@ -3,7 +3,6 @@ use std::collections::BTreeSet; use std::fs; -use std::path::Path; use wikifier::{mapped_edges, update_maps, Json}; @@ -126,6 +125,28 @@ const FIXTURES: &[Fixture] = &[ ("lib/use.py", "lib/helpers.py"), ], }, + Fixture { + name: "rust_crate", + files: &[ + ("Cargo.toml", "[package]\nname = \"demo-crate\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[lib]\nname = \"demo_lib\"\n"), + ("src/lib.rs", "pub mod child;\n\npub fn root_item() -> i32 {\n 1\n}\n"), + ("src/child.rs", "pub mod nested;\n\npub fn from_child() -> i32 {\n 2\n}\n\nuse self::nested::from_nested;\n\n#[cfg(test)]\nmod tests {\n use super::*;\n}\n"), + ("src/child/nested.rs", "pub fn from_nested() -> i32 {\n 3\n}\n\nuse super::from_child;\nuse crate::child::from_child as again;\n"), + ("src/main.rs", "use demo_crate::child::from_child;\n\nfn main() {\n let _ = from_child();\n let _ = demo_lib::root_item();\n}\n"), + ("src/bin/tool.rs", "use demo_lib::child;\n\nfn main() {\n let _ = child::from_child();\n}\n"), + ("tests/use_lib.rs", "use demo_lib::child::nested::from_nested;\nuse demo_crate::root_item;\n\n#[test]\nfn calls_the_library() {\n let _ = from_nested();\n let _ = root_item();\n}\n"), + ], + edges: &[ + ("src/lib.rs", "src/child.rs"), + ("src/child.rs", "src/child/nested.rs"), + ("src/child/nested.rs", "src/child.rs"), + ("src/main.rs", "src/child.rs"), + ("src/main.rs", "src/lib.rs"), + ("src/bin/tool.rs", "src/child.rs"), + ("tests/use_lib.rs", "src/child/nested.rs"), + ("tests/use_lib.rs", "src/lib.rs"), + ], + }, ]; #[test] diff --git a/tests/parity.rs.wiki.md b/tests/parity.rs.wiki.md new file mode 100644 index 0000000..83acf04 --- /dev/null +++ b/tests/parity.rs.wiki.md @@ -0,0 +1,5 @@ +# tests/parity.rs + +**Role:** Accuracy fixtures for the import scan. + +`accuracy_fixtures_match_frozen_edges` builds each layout in a temp directory, runs `update_maps`, and compares the edge set to the frozen list. The layouts are the Django app, the `src/` package, the TypeScript monorepo, the comment and string traps, and a Cargo crate (`src/lib.rs`, `src/main.rs`, `src/bin/tool.rs`, `tests/use_lib.rs`) whose package name and lib name differ. `each_parsed_language_emits_a_local_edge` checks one local edge per parsed language. `unknown_suffixes_emit_no_edges` checks that COBOL, Swift, and assembly contribute no edges. From d795cdab72207075dc6e91067cd5db633481da0c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 3 Nov 2026 05:23:41 +0000 Subject: [PATCH 2/8] Note Rust crate-name resolution in the CLAUDE.md wiki. Co-authored-by: Aron Amos --- CLAUDE.md.wiki.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md.wiki.md b/CLAUDE.md.wiki.md index 459b004..953dc15 100644 --- a/CLAUDE.md.wiki.md +++ b/CLAUDE.md.wiki.md @@ -2,4 +2,4 @@ **Role:** Repo guidance for an agent working in this tree. -Hard constraints: empty Cargo dependencies, no hand-edits of `library.md` or the health and pending files, and the human dashboard stays secondary. It points agents at `AGENTS.md` and `skills/run.md`. The architecture section names the Rust modules under `src/`, the library file tree, and this repo's map roots `src/` and `tests/`. JSON failures use `success: false`. +Hard constraints: empty Cargo dependencies, no hand-edits of `library.md` or the health and pending files, and the human dashboard stays secondary. It points agents at `AGENTS.md` and `skills/run.md`. The architecture section names the Rust modules under `src/`, the library file tree, and this repo's map roots `src/` and `tests/`. The parse note includes Rust `crate::`, `self::`, `super::`, and the Cargo.toml crate name. JSON failures use `success: false`. From 4b22f9c359810d8e73c6b7319df02cda61bb36d6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 3 Nov 2026 05:23:57 +0000 Subject: [PATCH 3/8] Remove the README lookup benchmark. The section cited scripts/benchmark_lookup.py and numbers from the Python package. That script is not in this repo, so the figures cannot be reproduced. The performance table that remains already says it measured the Python package. Co-authored-by: Aron Amos --- CHANGELOG.md | 3 +++ README.md | 14 -------------- README.md.wiki.md | 2 +- 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a806356..240721e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The import scan remains comment-masked regex plus named JS/TS re-exports to depth 8 (`parser: "regex+reexport"`). It is not a CPython `ast` walk, and it has not been run on Babylon.js. The accuracy gate is `accuracy_fixtures_match_frozen_edges`, now including a Cargo crate layout. - Rust `mod`, `crate::`, `self::`, `super::`, and the root `Cargo.toml` package or lib name resolve to files. Hyphens in the package name become underscores. `crate::` stays inside the file's crate (`tests/*.rs` and `src/bin/*.rs` are separate crates). The package or lib name points at the library, so binaries and integration tests are dependents of the library modules they import. +### Removed +- The README section "Does it pay off? (measured)" is gone. It cited `scripts/benchmark_lookup.py`, which left with the Python package, and its numbers were measured on that package. The performance table that remains is still labeled as Python-era. + ## [4.7.0] - 2026-10-01 Full-codebase review release. Details and measurements: `Findings/2026-10-01-review-4.7.0.md`; diff --git a/README.md b/README.md index 7c74cc8..3d6263d 100644 --- a/README.md +++ b/README.md @@ -113,20 +113,6 @@ Advanced intel as needed: `list_paths` (names-only folder expand), `get_dependen - **Zero core dependencies** — empty `[dependencies]` in `Cargo.toml`. - **Honest failures** — a missing `project_root`, missing file, or lock timeout is an error result, never a silent fallback. `wikifier.sh` launches the Rust binary. -## Does it pay off? (measured) - -`scripts/benchmark_lookup.py` asks the questions an agent asks before an edit, on real repositories, answered by Wikifier and by grep, scored against Python's own bytecode import scanner ([details](Findings/2026-10-01-lookup-benchmark.md)). 40 targets per repo: - -| Question | Repo | Wikifier: exactly right / median tokens | grep: exactly right / median tokens | -|---|---|---|---| -| Which files import M? | llama-index-core (724 files) | **40/40** · 72 | 27/40 · 84 | -| | airflow-core (1,143 files) | **39/40** · 111 | 23/40 · 266 | -| What could break if I change M? (depth 3) | llama-index-core | **40/40** · 487 | 11/40 · 209 (15 over a 200k-token budget) | -| | airflow-core | **36/40** · 584 | 12/40 · 21,676 (8 over budget) | -| What does M import? | both | **40/40** · ~270 | reading the file: ~1,100 | - -Typical lookups cost about the same; grep goes wrong on common module names (`base`, `utils`) and its cost explodes on them (up to 100k tokens for one question). Mapping from scratch: 2.3 s for 724 files, 21 s for 7,174. - ## Performance (measured) Full / heavy runs (historical order-of-magnitude): diff --git a/README.md.wiki.md b/README.md.wiki.md index 7f35144..42437f0 100644 --- a/README.md.wiki.md +++ b/README.md.wiki.md @@ -2,4 +2,4 @@ **Role:** The public entry point for the std-only Rust program. -It tells agents to build with `cargo build --release`, start in this repo at `AGENTS.md`, and follow `skills/run.md`. Bootstrap a target with `wikifier init`. The language table describes the comment-masked regex scans. Historical benchmark tables stay labeled as measurements of the earlier package. MCP is `wikifier mcp`. +It tells agents to build with `cargo build --release`, start in this repo at `AGENTS.md`, and follow `skills/run.md`. Bootstrap a target with `wikifier init`. The language table describes the comment-masked regex scans, including Rust crate-name imports. The lookup benchmark that cited a missing `scripts/benchmark_lookup.py` is gone. The performance table that remains says its numbers are from the Python package. MCP is `wikifier mcp`. From 265a0432dd59b8974656bd5899a028c213fe72a0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 3 Nov 2026 05:29:15 +0000 Subject: [PATCH 4/8] Verify Rust wikis against top-level pub items. mark-green now extracts pub fn, struct, enum, trait, mod, type, const, static, and pub use names. pub(crate) and impl methods stay out of the check, so a Rust file can be verified Green. Go, C, C#, and Java stay unchecked. Char escapes such as '\u{1e}' no longer confuse the scan. Co-authored-by: Aron Amos --- AGENTS.md | 2 +- AGENTS.md.wiki.md | 2 +- CHANGELOG.md | 1 + CHANGELOG.md.wiki.md | 2 +- CLAUDE.md | 2 +- CLAUDE.md.wiki.md | 2 +- README.md | 2 +- README.md.wiki.md | 2 +- skills/run.md | 4 +- skills/run.md.wiki.md | 2 +- src/api.rs | 3 + src/api.rs.wiki.md | 4 +- src/artifacts.rs.wiki.md | 2 + src/cache.rs.wiki.md | 2 + src/cache_commit.rs.wiki.md | 2 +- src/legacy_cache.rs.wiki.md | 2 +- src/lib.rs.wiki.md | 2 +- src/maps_out.rs.wiki.md | 2 +- src/mcp.rs.wiki.md | 2 +- src/parse.rs | 181 +++++++++++++++++++++++++++++++++- src/parse.rs.wiki.md | 2 +- tests/protocol_v07.rs | 47 +++++++++ tests/protocol_v07.rs.wiki.md | 2 +- 23 files changed, 252 insertions(+), 22 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b603b0e..6c9b7ab 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,7 +33,7 @@ Prefer the CLI when the connected MCP server is still `python3 -m wikifier.mcp.s - `init` copies `index.html` only when it is missing, and it does not clobber an existing path list. `diagnostics.html` stays in this repo. `index.v2.html` and `diagnostics.v2.html` are comparison pages and are not deployed. - This repo's map roots are `src/` and `tests/` (`map_paths.txt`). `monitored_paths.txt` is the extra watch list for docs and `wikifier.sh`. - The import scan is comment-masked regex, plus named JS/TS re-exports to depth 8 (`parser: "regex+reexport"`). An indented Python import is conditional. `import()` is dynamic. Rust resolves `mod`, `crate::`, `self::`, `super::`, and the root `Cargo.toml` package or lib name (`-` to `_`). `crate::` stays in that file's crate (`tests/*.rs` and `src/bin/*.rs` included). The package or lib name points at the library, which is how `src/main.rs` and `tests/*.rs` show up as dependents. The accuracy gate is `accuracy_fixtures_match_frozen_edges` (the frozen layouts, including a Cargo crate). Do not describe the scan as a CPython `ast` walk, and do not claim it has been run on Babylon.js. It does not read tsconfig paths, package exports, or workspaces. -- `mark_green` checks Python and JS/TS public names against the wiki. Rust, Go, C/C++, C#, Java, and Markdown come back `unchecked`. That is expected. +- `mark_green` checks Python, JS/TS, and Rust public names against the wiki. Rust names are top-level `pub` items, not `pub(crate)` and not methods inside `impl`. Go, C/C++, C#, Java, and Markdown come back `unchecked`. That is expected. - `wikifier serve` binds `127.0.0.1:8787`. Do not bind port 8080. - Do not set the project root to a parent that contains several repos. - Git: honor hooks. Do not use `--no-verify`. Do not commit `.cursor/`, `target/`, or gitignored leftovers (`wikifier/*.pyc`, `build/`, `dist/`, `.venv-build/`). diff --git a/AGENTS.md.wiki.md b/AGENTS.md.wiki.md index f31836f..a50bec8 100644 --- a/AGENTS.md.wiki.md +++ b/AGENTS.md.wiki.md @@ -2,4 +2,4 @@ **Role:** Entry point for any coding agent in this repository. -It points at `skills/run.md` (protocol v0.7) and names the live Rust binary, the daily loop, and the files that are historical Python-era records. Map roots stay `src/` and `tests/`. The import scan stays comment-masked regex plus named re-exports. Rust also resolves `crate::`, `self::`, `super::`, and the root Cargo.toml package or lib name, so `src/main.rs` and `tests/*.rs` are dependents of the library modules they import. +It points at `skills/run.md` (protocol v0.7) and names the live Rust binary, the daily loop, and the files that are historical Python-era records. Map roots stay `src/` and `tests/`. The import scan stays comment-masked regex plus named re-exports. Rust also resolves `crate::`, `self::`, `super::`, and the root Cargo.toml package or lib name, so `src/main.rs` and `tests/*.rs` are dependents of the library modules they import. `mark-green` checks Rust top-level `pub` names. diff --git a/CHANGELOG.md b/CHANGELOG.md index 240721e..0a3a8a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - This repo's import-map roots are `src/` and `tests/`. - The import scan remains comment-masked regex plus named JS/TS re-exports to depth 8 (`parser: "regex+reexport"`). It is not a CPython `ast` walk, and it has not been run on Babylon.js. The accuracy gate is `accuracy_fixtures_match_frozen_edges`, now including a Cargo crate layout. - Rust `mod`, `crate::`, `self::`, `super::`, and the root `Cargo.toml` package or lib name resolve to files. Hyphens in the package name become underscores. `crate::` stays inside the file's crate (`tests/*.rs` and `src/bin/*.rs` are separate crates). The package or lib name points at the library, so binaries and integration tests are dependents of the library modules they import. +- `mark-green` checks Rust top-level `pub` items (`fn`, `struct`, `enum`, `trait`, `mod`, `type`, `const`, `static`, and `pub use` names) the same way it checks Python and JS/TS names. `pub(crate)` and items inside `impl` stay out of that set. A Rust file can be `verified` Green. Go, C/C++, C#, and Java stay `unchecked`. ### Removed - The README section "Does it pay off? (measured)" is gone. It cited `scripts/benchmark_lookup.py`, which left with the Python package, and its numbers were measured on that package. The performance table that remains is still labeled as Python-era. diff --git a/CHANGELOG.md.wiki.md b/CHANGELOG.md.wiki.md index 5cdb479..8da6012 100644 --- a/CHANGELOG.md.wiki.md +++ b/CHANGELOG.md.wiki.md @@ -2,4 +2,4 @@ **Role:** Keep a Changelog history for package 4.7.x. -The 4.7.0 section is the record of that release, including the Python package that shipped then. Unreleased notes describe the Rust binary: map artifacts, dependency flags, wiki checks, deletion, background commands, the dashboard serve routes, MCP error reporting, `heal-stubs --dry-run`, and `init DIR`. The import scan is recorded as `regex+reexport`. Unreleased also notes that Rust crate-name imports resolve to the library. CI checks the empty dependency list without Python. +The 4.7.0 section is the record of that release, including the Python package that shipped then. Unreleased notes describe the Rust binary: map artifacts, dependency flags, wiki checks, deletion, background commands, the dashboard serve routes, MCP error reporting, `heal-stubs --dry-run`, and `init DIR`. The import scan is recorded as `regex+reexport`. Unreleased notes that Rust crate-name imports resolve to the library, that the unreproducible lookup benchmark was removed, and that Rust top-level `pub` names can be verified Green. CI checks the empty dependency list without Python. diff --git a/CLAUDE.md b/CLAUDE.md index e8e09c7..bb662c1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -69,7 +69,7 @@ Pipeline: **scan → parse → resolve → cache → health → artifacts**, imp Generated artifacts (in the target project root): `file_health.md/.json`, `library.md`, `pending_updates.md`, `journal/YYYY/MM/DD.md`, per-file `*.wiki.md`, `.wikifier_staging/` (`wk_cache`, folder maps, `incremental_status.json`, locks, daemon state). Scoping: `monitored_paths.txt` (extra watch list), `map_paths.txt` (import-map roots), `exclude_patterns.txt`. This repo's map roots are `src/` and `tests/`. -Adding a language means a scan in `src/parse.rs` with the same edge shape, a frozen fixture kept green by `accuracy_fixtures_match_frozen_edges`, and, when wiki verification should refuse that language, a symbol extractor beside `python_public_symbols` / `js_public_symbols` in `src/api.rs`. Languages without an extractor are marked `unchecked`. +Adding a language means a scan in `src/parse.rs` with the same edge shape, a frozen fixture kept green by `accuracy_fixtures_match_frozen_edges`, and, when wiki verification should refuse that language, a symbol extractor beside `python_public_symbols` / `js_public_symbols` / `rust_public_symbols`. Languages without an extractor are marked `unchecked`. Rust's extractor is `rust_public_symbols` in `src/parse.rs`. ## Versioning & Releases diff --git a/CLAUDE.md.wiki.md b/CLAUDE.md.wiki.md index 953dc15..d9bcfd7 100644 --- a/CLAUDE.md.wiki.md +++ b/CLAUDE.md.wiki.md @@ -2,4 +2,4 @@ **Role:** Repo guidance for an agent working in this tree. -Hard constraints: empty Cargo dependencies, no hand-edits of `library.md` or the health and pending files, and the human dashboard stays secondary. It points agents at `AGENTS.md` and `skills/run.md`. The architecture section names the Rust modules under `src/`, the library file tree, and this repo's map roots `src/` and `tests/`. The parse note includes Rust `crate::`, `self::`, `super::`, and the Cargo.toml crate name. JSON failures use `success: false`. +Hard constraints: empty Cargo dependencies, no hand-edits of `library.md` or the health and pending files, and the human dashboard stays secondary. It points agents at `AGENTS.md` and `skills/run.md`. The architecture section names the Rust modules under `src/`, the library file tree, and this repo's map roots `src/` and `tests/`. The parse note includes Rust `crate::`, `self::`, `super::`, the Cargo.toml crate name, and `rust_public_symbols` for wiki checks. JSON failures use `success: false`. diff --git a/README.md b/README.md index 3d6263d..009d525 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ Advanced intel as needed: `list_paths` (names-only folder expand), `get_dependen - **Import analysis** — comment-masked regex for Python, JS/TS, Rust, Go, C/C++, C#, and Java. Named JS/TS re-exports are followed to depth 8. Edges carry conditional, dynamic, and via-barrel flags, and a confidence read from those flags. - **Incremental pipeline** — `update-maps` parses files whose size or mtime changed, then writes the `wk_cache` text index, reverse edges, `library.md`, and sharded folder cards. - **Two path lists** — `map_paths.txt` = map package roots; `monitored_paths.txt` = extra watch list (docs, scripts). `check-changes` always watches mapped files and anything ever marked Green, so a narrow list never hides edits. -- **Green you can trust** — `mark-green` checks the wiki against Python and JS/TS public symbols and refuses when added symbols are undocumented, removed symbols are still in backticks, or the public surface changed while the wiki file did not. Other languages are recorded as unchecked. `verify-wikis` audits every Green wiki. `health --summary` includes `green_breakdown`. +- **Green you can trust** — `mark-green` checks the wiki against Python, JS/TS, and Rust public symbols and refuses when a public name is missing, a removed name is still in backticks, or the public surface changed while the wiki file did not. Rust names are top-level `pub` items: `fn`, `struct`, `enum`, `trait`, `mod`, `type`, `const`, `static`, and the names a `pub use` exports. `pub(crate)` and items inside `impl` are not in that set. Go, C/C++, C#, and Java are recorded as unchecked. `verify-wikis` re-checks every Green wiki. `health --summary` includes `green_breakdown`. - **Partial-map honesty** — `--max-files` returns `map_complete=false` and `files_remaining_dirty`. Run the same command again to resume. Already-parsed files are stat-skipped. - **Cache** — `.wikifier_staging/wk_cache`. `wikifier cache-status` reports the backend and the file count. An empty cache imports `import_cache.sqlite` through the `sqlite3` CLI, or `import_cache.json`. - **Selective agent work** — health and `suggest-next` stay on 🔴 and actionable 🟡. Diagnostics list ACS reason codes and `actionable_low_conf_edges`. diff --git a/README.md.wiki.md b/README.md.wiki.md index 42437f0..d7b59d6 100644 --- a/README.md.wiki.md +++ b/README.md.wiki.md @@ -2,4 +2,4 @@ **Role:** The public entry point for the std-only Rust program. -It tells agents to build with `cargo build --release`, start in this repo at `AGENTS.md`, and follow `skills/run.md`. Bootstrap a target with `wikifier init`. The language table describes the comment-masked regex scans, including Rust crate-name imports. The lookup benchmark that cited a missing `scripts/benchmark_lookup.py` is gone. The performance table that remains says its numbers are from the Python package. MCP is `wikifier mcp`. +It tells agents to build with `cargo build --release`, start in this repo at `AGENTS.md`, and follow `skills/run.md`. Bootstrap a target with `wikifier init`. The language table describes the comment-masked regex scans, including Rust crate-name imports. The lookup benchmark that cited a missing `scripts/benchmark_lookup.py` is gone. The performance table that remains says its numbers are from the Python package. `mark-green` checks Rust top-level `pub` names as well as Python and JS/TS. MCP is `wikifier mcp`. diff --git a/skills/run.md b/skills/run.md index 5df72dd..0d0fbf6 100644 --- a/skills/run.md +++ b/skills/run.md @@ -73,7 +73,7 @@ Never skip record_change after an edit; never skip mark_green after refreshing t ### Wiki verification (mark_green) -`mark_green` compares the file's public symbols (Python: `__all__` or top-level public functions/classes; JS/TS: exports) with those recorded at the previous mark-green, and refuses (`error_type: "WikiVerificationError"`, `issues[]`) when: +`mark_green` compares the file's public symbols (Python: `__all__` or top-level public functions/classes; JS/TS: exports; Rust: top-level `pub` items) with those recorded at the previous mark-green, and refuses (`error_type: "WikiVerificationError"`, `issues[]`) when: | Issue code | Meaning | |---|---| @@ -157,7 +157,7 @@ The program is a std-only Rust binary under `src/`. `Cargo.toml` has an empty `[ - `dependencies --full` returns `imports[]` with `confidence_score`, `confidence_reasons`, `acs`, `is_dynamic`, `is_conditional`, and `in_cycle`. Compact rows always include `conditional`, `dynamic`, `via_barrel`, and `confidence`. - `record_deletion` appends the journal line, marks the health row Red, drops the file from the graph, and removes its pending line. The file does not have to exist. - `record_changes` matches reasons by exact path, then the longest `dir/` prefix, then the longest glob (`*` and `?`). `--only`, `--green`, `--dry-run`, and deletions are applied. `--green` lists verification failures and leaves those files Yellow. -- `mark_green` extracts Python top-level `def`/`class` names (or `__all__`) and JS/TS `export` names, and requires each current name to appear in the wiki. With a previous baseline, a removed name still wrapped in backticks is `wiki_mentions_removed_symbols`, and a changed public surface with an unchanged wiki hash is `wiki_not_updated_for_api_change`. Rust, Go, C/C++, C#, and Java have no symbol extractor, so their rows are `unchecked`. `health --summary` includes `green_breakdown`. A successful mark removes that file's pending line. +- `mark_green` extracts Python top-level `def`/`class` names (or `__all__`), JS/TS `export` names, and Rust top-level `pub` items (`fn`, `struct`, `enum`, `trait`, `mod`, `type`, `const`, `static`, and `pub use` names). Each current name must appear in the wiki. `pub(crate)` and items inside `impl` are not Rust public names. With a previous baseline, a removed name still wrapped in backticks is `wiki_mentions_removed_symbols`, and a changed public surface with an unchanged wiki hash is `wiki_not_updated_for_api_change`. Go, C/C++, C#, and Java have no symbol extractor, so their rows are `unchecked`. `health --summary` includes `green_breakdown`. A successful mark removes that file's pending line. - The scan ignores comments and strings. It does not use CPython `ast`. It does not read tsconfig `paths`, package `exports`, or workspace maps. Named re-exports are the barrel follow. Rust reads the root `Cargo.toml` `[package].name` and `[lib].name` only (hyphens become underscores) and resolves `crate::`, `self::`, `super::`, that crate name, and `mod` to files. `crate::` stays in the file's crate (`tests/*.rs` and `src/bin/*.rs` are their own crates). A package or lib name points at the library. Accuracy is `accuracy_fixtures_match_frozen_edges`, including the Rust crate layout. This parser has not been run on Babylon.js. - MCP `tools/call` sets `isError` when the payload has `success: false`. `resources/read` and `prompts/get` answer. A call waits at most `WIKIFIER_MCP_DEADLINE_S` seconds (default 60) and then returns `timed_out` and `may_still_complete`. An identical journal action, file, and reason inside 120 seconds sets `duplicate_skipped` and does not append another line. - Commands that write health, pending, the map, or the cache take `.wikifier_staging/.lock` before the cache lock. The lock is re-entrant on the same thread, waits 30 seconds, and is stolen when the recorded pid is dead. A timeout returns `error_type: "LockTimeoutError"` and `timed_out: true`. diff --git a/skills/run.md.wiki.md b/skills/run.md.wiki.md index cb19d3d..0494c95 100644 --- a/skills/run.md.wiki.md +++ b/skills/run.md.wiki.md @@ -2,6 +2,6 @@ **Role:** Agent Protocol v0.7. This is the contract for the daily loop. `AGENTS.md` is the repository entry point and points here. -Part 1 is the session loop: bootstrap, check-changes, suggest-next, prepare-edit, record-change, mark-green, update-maps. Part 2 is the reference for health, wiki verification, batch recording, dependencies, and commands. +Part 1 is the session loop: bootstrap, check-changes, suggest-next, prepare-edit, record-change, mark-green, update-maps. Part 2 is the reference for health, wiki verification, batch recording, dependencies, and commands. Wiki verification covers Python, JS/TS, and Rust top-level `pub` items. The implementation note records how the std-only Rust binary fills that contract: `wk_cache`, regex-plus-reexport parsing, Rust paths from `crate::` / `self::` / `super::` / the root Cargo.toml crate name, map artifacts, deletion, MCP errors, the project lock, and a standing `serve` that feeds the dashboard. Daemon actions are `start`, `stop`, `status`, and `run`. `init DIR`, `verify-wikis DIR`, and `search-journal --file` apply. The protocol version stays v0.7. The live version command is `wikifier --version`. diff --git a/src/api.rs b/src/api.rs index 925c4f7..b9ad282 100644 --- a/src/api.rs +++ b/src/api.rs @@ -936,6 +936,9 @@ fn public_symbols(path: &Path) -> SymbolScan { if matches!(ext.as_str(), "js" | "ts" | "jsx" | "tsx" | "mjs" | "cjs" | "mts" | "cts") { return SymbolScan { names: js_public_symbols(&text), warning, extractor: true }; } + if ext == "rs" { + return SymbolScan { names: crate::parse::rust_public_symbols(&text), warning, extractor: true }; + } SymbolScan { names: Vec::new(), warning, extractor: false } } diff --git a/src/api.rs.wiki.md b/src/api.rs.wiki.md index 48bfe5b..b99e56e 100644 --- a/src/api.rs.wiki.md +++ b/src/api.rs.wiki.md @@ -4,6 +4,8 @@ `update_maps_run` walks the map roots, skips unchanged files by size and mtime, commits the text cache, then writes `library.md` and the folder cards. `--directory`, `--max-files`, and `--stream` are arguments of that run. A file budget leaves the map incomplete and the next run resumes. -`record_deletion` journals the removal, marks the health row Red, and drops the file from the graph. `record_changes_opts` takes the git change list and matches reasons by exact path, directory prefix, then glob. `mark_green` checks Python and JS/TS public names against the wiki and removes that file's pending line. +`record_deletion` journals the removal, marks the health row Red, and drops the file from the graph. `record_changes_opts` takes the git change list and matches reasons by exact path, directory prefix, then glob. `mark_green` checks Python, JS/TS, and Rust public names against the wiki and removes that file's pending line. Rust names come from `rust_public_symbols`. + +Public names: `MapRun`, `VERSION`, `MCP_TOOLS`, `update_maps`, `update_maps_opts`, `update_maps_run`, `mapped_edges`, `load_dependencies`, `dependency_report`, `load_dependents`, `list_paths`, `session_bootstrap`, `suggest_next_actions`, `check_changes`, `prepare_edit`, `record_change`, `mark_green`, `health`, `read_health`, `read_journal`, `cache_status`, `validate`, `cycles`, `verify_wikis`, `record_changes`, `record_changes_opts`, `record_deletion`, `heal_stubs`, `prune_health`, `prune_health_opts`, `prune_pending`, `stale_wikis`, `prune_barrels`, `metrics_snapshot`, `healing_stats`, `resolution_diagnostics`, `barrel_reports`, `incremental_status`, `serve_once`, `serve_loop`, `monitor_loop`, `daemon_ctl`, `dispatch`, `CLI_COMMANDS`, `ensure_root`. `serve_loop` answers the JSON API, `/__wikifier/status`, dashboard `run` and `shutdown`, and project files such as `file_health.json` and `library.md`. `monitor_loop` and `daemon_ctl` (`start`, `stop`, `status`, `run`) are the other standing processes. `heal_stubs` with `dry_run` counts long Initial stubs and does not write. `seed-health` returns `HealthError` when the write fails. `verify_wikis_under` can limit the check to one file or directory. `search-journal --file` keeps one path. Mutating commands take `.wikifier_staging/.lock` before the cache lock. `prune_health` and `prune_pending` keep files named in `monitored_paths.txt`. `prune-health --deleted-missing` also drops rows whose path is no longer a file. diff --git a/src/artifacts.rs.wiki.md b/src/artifacts.rs.wiki.md index ee6a24d..d07035d 100644 --- a/src/artifacts.rs.wiki.md +++ b/src/artifacts.rs.wiki.md @@ -5,3 +5,5 @@ `file_health.json` is schema version 2. A row can store `public_symbols`, `wiki_hash`, and `source_mtime_ns` from the last mark-green. Those fields are written only when they are set, so an older health file is not rewritten just by being read. `append_journal` skips an identical action, file, and reason inside 120 seconds. `folder_card` reads `.wikifier_staging/maps/_root.json` or a directory `_index.json`. + +Public names: `HealthRow`, `read_health`, `write_health`, `upsert_health_row`, `JournalEntry`, `journal_path`, `read_journal`, `parse_journal`, `JournalWrite`, `append_journal`, `read_pending`, `folder_card`. diff --git a/src/cache.rs.wiki.md b/src/cache.rs.wiki.md index e04fa2a..f1a9a21 100644 --- a/src/cache.rs.wiki.md +++ b/src/cache.rs.wiki.md @@ -5,3 +5,5 @@ Each mapped file has a payload, a `by_src` edge list, a `by_dst` edge list, notes, and an optional barrel record. An edge line is destination, line, names, then optional flag letters `c`, `d`, and `b`. Confidence is computed when an edge is read: dynamic 0.4, conditional 0.55, via barrel 0.7, static 0.9. The ACS reason is `dynamic_literal`, `conditional_guard`, `via_barrel`, or `resolved_static`. + +Public names: `check_consistency`, `rebuild`, `Consistency`, `Session`, `payload_path`, `by_src_path`, `commit_pending_path`, `load_dependencies`, `InternalEdge`, `confidence_of`, `format_edge_line`, `DependencyView`, `load_dependency_view`, `load_dependents`, `list_prefix`, `all_paths`, `FileUpdate`, `upsert_file`, `remove_file`, `indexed_rows`, `payload_bytes`, `stat_ns`, `edges`, `rel_string`, `write_barrel_record`, `prune_barrel_files`. diff --git a/src/cache_commit.rs.wiki.md b/src/cache_commit.rs.wiki.md index 21c444d..ba5895e 100644 --- a/src/cache_commit.rs.wiki.md +++ b/src/cache_commit.rs.wiki.md @@ -2,4 +2,4 @@ **Role:** One commit for a map run. -The session edits the index in memory and writes `by_src` before `by_dst`, with the generation file last. A crash leaves `commit.pending`. Unresolved notes are stored as `U`, the raw specifier, the line, and a reason. Removing a file also drops its barrel record on commit. The cache lock is `wk.lock`. +The `Session` edits the index in memory and writes `by_src` before `by_dst`, with the generation file last. A crash leaves `commit.pending`. Unresolved notes are stored as `U`, the raw specifier, the line, and a reason. Removing a file also drops its barrel record on commit. The cache lock is `wk.lock`. `check_consistency` compares the forward and reverse indexes. `rebuild` rewrites them. `Consistency` is that comparison's result. diff --git a/src/legacy_cache.rs.wiki.md b/src/legacy_cache.rs.wiki.md index ee76564..f4af190 100644 --- a/src/legacy_cache.rs.wiki.md +++ b/src/legacy_cache.rs.wiki.md @@ -2,4 +2,4 @@ **Role:** Fill an empty `wk_cache` from an older import cache. -If `paths.txt` is empty and `import_cache.sqlite` exists, the `sqlite3` CLI reads `files` and `edges`. `import_cache.json` is the other fallback. Imported rows use mtime 0 and hash `sha256:legacy`, so the next `update-maps` re-parses them. A sqlite file that cannot be read returns the warning `sqlite_present_unread`. +`import_if_empty` runs when the text cache has no paths. If `import_cache.sqlite` exists, the `sqlite3` CLI reads `files` and `edges`. `import_cache.json` is the other fallback. Imported rows use mtime 0 and hash `sha256:legacy`, so the next `update-maps` re-parses them. A sqlite file that cannot be read returns the warning `sqlite_present_unread`. diff --git a/src/lib.rs.wiki.md b/src/lib.rs.wiki.md index e59ac7d..3793643 100644 --- a/src/lib.rs.wiki.md +++ b/src/lib.rs.wiki.md @@ -2,4 +2,4 @@ **Role:** Crate root for the std-only Wikifier core. -Public modules are `api`, `artifacts`, `cache`, `jsonutil`, `maps_out`, `mcp`, `parse`, `scope`, `sha256`, and `timeutil`. `legacy_cache`, `project_lock`, `cache_commit`, and `proc` stay private. The re-exports are the daily loop: bootstrap, check, prepare, record, mark-green, update-maps, and dependency lookup. +Public modules are `api`, `artifacts`, `cache`, `jsonutil`, `maps_out`, `mcp`, `parse`, `scope`, `sha256`, and `timeutil`. `legacy_cache`, `project_lock`, `cache_commit`, and `proc` stay private. Re-exported names: `cache_status`, `check_changes`, `health`, `list_paths`, `load_dependencies`, `load_dependents`, `mapped_edges`, `mark_green`, `prepare_edit`, `read_health`, `read_journal`, `record_change`, `session_bootstrap`, `suggest_next_actions`, `update_maps`, `VERSION`, and `Json`. diff --git a/src/maps_out.rs.wiki.md b/src/maps_out.rs.wiki.md index 97f1563..1374e6f 100644 --- a/src/maps_out.rs.wiki.md +++ b/src/maps_out.rs.wiki.md @@ -2,4 +2,4 @@ **Role:** Human `library.md` and the sharded agent folder cards. -`write` rebuilds `.wikifier_staging/maps/_root.json` and one `_index.json` per directory prefix. `library.md` has a file tree, then a Mermaid block capped at 80 edges, then the dependency table. `write_status` stores `incremental_status.json` for `map_complete` and `files_remaining_dirty`. +`write` rebuilds `.wikifier_staging/maps/_root.json` and one `_index.json` per directory prefix. `library.md` has a file tree, then a Mermaid block capped at 80 edges, then the dependency table. `write_status` stores `incremental_status.json` for `map_complete` and `files_remaining_dirty`. `read_status` reads that file back. diff --git a/src/mcp.rs.wiki.md b/src/mcp.rs.wiki.md index c5212d9..c6228e7 100644 --- a/src/mcp.rs.wiki.md +++ b/src/mcp.rs.wiki.md @@ -2,4 +2,4 @@ **Role:** Stdio JSON-RPC MCP adapter over `src/api.rs`. -`tools/call` sets `isError` when the payload has `success: false`. `resources/read` serves the library, health, pending, and journal resources. `prompts/get` returns a short user message. Each call runs on a thread and stops waiting after `WIKIFIER_MCP_DEADLINE_S` seconds (default 60). A timeout payload has `timed_out` and `may_still_complete`. +`tool_schema` describes one tool. `handle_message` answers one JSON-RPC request. `serve_stdio` reads messages from stdin. `tools/call` sets `isError` when the payload has `success: false`. `resources/read` serves the library, health, pending, and journal resources. `prompts/get` returns a short user message. Each call runs on a thread and stops waiting after `WIKIFIER_MCP_DEADLINE_S` seconds (default 60). A timeout payload has `timed_out` and `may_still_complete`. diff --git a/src/parse.rs b/src/parse.rs index b0b5508..dbe8f0b 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1460,17 +1460,39 @@ fn skip_rust_string(b: &[u8], i: usize) -> Option { fn skip_rust_char_or_lifetime(b: &[u8], i: usize) -> usize { let next = b.get(i + 1).copied().unwrap_or(0); - if next.is_ascii_alphabetic() || next == b'_' { + if next == b'\\' { + let mut j = i + 2; + if b.get(j) == Some(&b'u') && b.get(j + 1) == Some(&b'{') { + j += 2; + while j < b.len() && b[j] != b'}' { + j += 1; + } + j += 1; + } else if b.get(j) == Some(&b'x') { + j += 3; + } else { + j += 1; + } + if j < b.len() && b[j] == b'\'' { + j += 1; + } + return j.min(b.len()); + } + if (next.is_ascii_alphabetic() || next == b'_') && b.get(i + 2) != Some(&b'\'') { let mut j = i + 2; while j < b.len() && (b[j].is_ascii_alphanumeric() || b[j] == b'_') { j += 1; } return j; } - if next == b'\\' { - return (i + 4).min(b.len()); + let mut j = i + 1; + while j < b.len() && b[j] != b'\'' && j < i + 8 { + j += 1; + } + if j < b.len() && b[j] == b'\'' { + j += 1; } - (i + 3).min(b.len()) + j.min(b.len()) } fn rust_submodule_dir(from_rel: &str) -> PathBuf { @@ -1497,6 +1519,157 @@ fn separate_crate_root(from_rel: &str) -> Option { None } +/// Top-level `pub` items. `pub(crate)` and items inside `impl` or a function are not part of this set. +pub fn rust_public_symbols(text: &str) -> Vec { + let b = text.as_bytes(); + let mut symbols = Vec::new(); + let mut i = 0; + let mut brace: i32 = 0; + while i < b.len() { + if b[i].is_ascii_whitespace() { + i += 1; + continue; + } + if b[i] == b'/' && i + 1 < b.len() && (b[i + 1] == b'/' || b[i + 1] == b'*') { + i = skip_rust_comment(b, i); + continue; + } + if let Some(end) = skip_rust_string(b, i) { + i = end; + continue; + } + if b[i] == b'\'' { + i = skip_rust_char_or_lifetime(b, i); + continue; + } + if b[i] == b'{' { + brace += 1; + i += 1; + continue; + } + if b[i] == b'}' { + brace -= 1; + i += 1; + continue; + } + if brace == 0 && starts_word(b, i, "pub") { + let after = i + 3; + if after < b.len() && b[after] == b'(' { + i = after; + continue; + } + if let Some((names, end)) = rust_pub_names(b, after) { + for name in names { + if !name.is_empty() && !symbols.iter().any(|have: &String| have == &name) { + symbols.push(name); + } + } + i = end; + continue; + } + } + if let Some((_, end)) = read_ident(b, i) { + i = end; + continue; + } + i += 1; + } + symbols +} + +fn rust_pub_names(b: &[u8], start: usize) -> Option<(Vec, usize)> { + let mut i = skip_noise(b, start); + loop { + if starts_word(b, i, "async") { + i = skip_noise(b, i + "async".len()); + continue; + } + if starts_word(b, i, "unsafe") { + i = skip_noise(b, i + "unsafe".len()); + continue; + } + if starts_word(b, i, "const") { + let next = skip_noise(b, i + "const".len()); + if starts_word(b, next, "fn") { + i = next; + continue; + } + let (name, end) = read_ident(b, next)?; + return Some((vec![name], end)); + } + break; + } + for (word, len) in [("fn", 2usize), ("struct", 6), ("enum", 4), ("trait", 5), ("mod", 3), ("type", 4), ("static", 6)] { + if starts_word(b, i, word) { + let mut next = skip_noise(b, i + len); + if word == "static" && starts_word(b, next, "mut") { + next = skip_noise(b, next + 3); + } + let (name, end) = read_ident(b, next)?; + return Some((vec![name], end)); + } + } + if starts_word(b, i, "use") { + return use_export_names(b, i + 3); + } + None +} + +fn use_export_names(b: &[u8], start: usize) -> Option<(Vec, usize)> { + fn walk(b: &[u8], start: usize) -> Option<(usize, Vec)> { + let i = skip_noise(b, start); + if i >= b.len() { + return None; + } + if b[i] == b'{' { + return walk_group(b, i + 1); + } + if b[i] == b'*' { + return Some((i + 1, Vec::new())); + } + let (ident, mut j) = read_ident(b, i)?; + j = skip_noise(b, j); + let mut alias = None; + if starts_word(b, j, "as") { + let alias_at = skip_noise(b, j + 2); + let (name, after) = read_ident(b, alias_at)?; + alias = Some(name); + j = skip_noise(b, after); + } + if j + 1 < b.len() && b[j] == b':' && b[j + 1] == b':' { + return walk(b, skip_noise(b, j + 2)); + } + let name = alias.unwrap_or(ident); + if name == "self" || name == "super" || name == "crate" { + return Some((j, Vec::new())); + } + Some((j, vec![name])) + } + fn walk_group(b: &[u8], mut i: usize) -> Option<(usize, Vec)> { + let mut out = Vec::new(); + loop { + i = skip_noise(b, i); + if i >= b.len() { + return None; + } + if b[i] == b'}' { + return Some((i + 1, out)); + } + if b[i] == b',' { + i += 1; + continue; + } + let (end, names) = walk(b, i)?; + out.extend(names); + i = end; + } + } + let (end, names) = walk(b, skip_noise(b, start))?; + let end = skip_noise(b, end); + let end = if end < b.len() && b[end] == b';' { end + 1 } else { end }; + Some((names, end)) +} + fn load_rust_crate_names(root: &Path) -> Vec { let text = fs::read_to_string(root.join("Cargo.toml")).unwrap_or_default(); let mut section = String::new(); diff --git a/src/parse.rs.wiki.md b/src/parse.rs.wiki.md index 3356fb3..73ed1d5 100644 --- a/src/parse.rs.wiki.md +++ b/src/parse.rs.wiki.md @@ -8,4 +8,4 @@ Rust `mod` names a file beside the declaring module (`lib.rs`, `main.rs`, and `m The scan does not use CPython `ast` and does not read tsconfig paths, package exports, or workspaces. The frozen fixtures, including the Cargo crate layout, are the accuracy gate. -Public scan entry points: `language`, `read_utf8`, `scan_file_warned`, `scan_file`, `resolve_project`, `resolve_python_spec`, `analyze_files`, `analyze_changed`, `edges_for_files`. Public types: `RawImport`, `Kind`, `Scan`, `FactEdge`, `FileFacts`, `Analysis`. +Public scan entry points: `language`, `read_utf8`, `scan_file_warned`, `scan_file`, `resolve_project`, `resolve_python_spec`, `analyze_files`, `analyze_changed`, `edges_for_files`, `rust_public_symbols`. Public types: `RawImport`, `Kind`, `Scan`, `FactEdge`, `FileFacts`, `Analysis`. `rust_public_symbols` lists top-level `pub` items (`fn`, `struct`, `enum`, `trait`, `mod`, `type`, `const`, `static`, and the names a `pub use` exports). `pub(crate)` and items inside `impl` are left out. diff --git a/tests/protocol_v07.rs b/tests/protocol_v07.rs index 33e1dfb..2b90cbd 100644 --- a/tests/protocol_v07.rs +++ b/tests/protocol_v07.rs @@ -375,6 +375,53 @@ fn verify_wikis_prefix_checks_one_file() { let _ = fs::remove_dir_all(&root); } +#[test] +fn rust_mark_green_requires_top_level_pub_items() { + let root = scratch("rust-green"); + let source = "\ +pub fn kept() -> i32 { 1 } +pub struct Widget; +pub enum Phase { On } +pub trait Run { fn run(&self); } +pub mod child; +pub type Alias = Widget; +pub const FLAG: i32 = 1; +pub use child::exported; +pub(crate) fn hidden() {} +impl Widget { pub fn method(&self) {} } +#[cfg(test)] +mod tests { pub fn inside() {} } +"; + fs::write(root.join("src/lib.rs"), source).unwrap(); + fs::write(root.join("src/child.rs"), "pub fn exported() {}\n").unwrap(); + fs::write(root.join("src/lib.rs.wiki.md"), "Names kept, Widget, Phase, Run, child, Alias, FLAG, exported.\n").unwrap(); + let marked = mark_green(&root, "src/lib.rs", "wiki names the public items", false); + assert_eq!(marked.get("success").and_then(|flag| flag.as_bool()), Some(true), "{marked}"); + assert_eq!(marked.get("verification").and_then(|flag| flag.as_str()), Some("verified"), "{marked}"); + let row = read_health(&root).into_iter().find(|row| row.file == "src/lib.rs").unwrap(); + assert!(row.public_symbols.contains("kept"), "{}", row.public_symbols); + assert!(!row.public_symbols.contains("hidden"), "pub(crate) counted: {}", row.public_symbols); + assert!(!row.public_symbols.contains("method"), "impl method counted: {}", row.public_symbols); + assert!(!row.public_symbols.contains("inside"), "nested pub counted: {}", row.public_symbols); + + let wiki = fs::read_to_string(root.join("src/lib.rs.wiki.md")).unwrap(); + fs::write(root.join("src/lib.rs"), "pub fn kept() -> i32 { 1 }\npub fn added() -> i32 { 2 }\n").unwrap(); + fs::write(root.join("src/lib.rs.wiki.md"), &wiki).unwrap(); + let refused = mark_green(&root, "src/lib.rs", "same wiki", false); + assert_eq!(refused.get("error_type").and_then(|flag| flag.as_str()), Some("WikiVerificationError"), "{refused}"); + assert!(refused.to_string().contains("undocumented_new_symbols: added"), "{refused}"); + + fs::write(root.join("src/lib.rs.wiki.md"), "kept stays. `Widget` was removed. added is the new function.\n").unwrap(); + let removed = mark_green(&root, "src/lib.rs", "wiki still names Widget", false); + assert_eq!(removed.get("error_type").and_then(|flag| flag.as_str()), Some("WikiVerificationError"), "{removed}"); + assert!(removed.to_string().contains("wiki_mentions_removed_symbols: Widget"), "{removed}"); + + fs::write(root.join("src/lib.rs.wiki.md"), "kept and added are the public functions.\n").unwrap(); + let again = mark_green(&root, "src/lib.rs", "wiki names kept and added", false); + assert_eq!(again.get("verification").and_then(|flag| flag.as_str()), Some("verified"), "{again}"); + let _ = fs::remove_dir_all(&root); +} + fn http_exchange(port: u16, request: &str) -> String { use std::io::{Read, Write}; let mut stream = std::net::TcpStream::connect(("127.0.0.1", port)).unwrap(); diff --git a/tests/protocol_v07.rs.wiki.md b/tests/protocol_v07.rs.wiki.md index 5089372..295083f 100644 --- a/tests/protocol_v07.rs.wiki.md +++ b/tests/protocol_v07.rs.wiki.md @@ -2,4 +2,4 @@ **Role:** Integration tests for the agent-facing contract that the early Rust port left thin. -Covers `library.md` and folder cards, `--directory` / `--max-files` resume, `record_deletion` plus journal de-duplication, `record_changes` globs and `--green`, dependency confidence and cycle `break_hint`, JS wiki refusal, legacy JSON import, `init` keeping path lists, `init DIR` as the project root, `heal-stubs --dry-run`, `search-journal --file`, `verify-wikis` limited to one file, MCP `isError` plus resource and prompt reads, prune keeping a monitored file that sits outside the map roots, and `serve` reading `file_health.json` and `library.md` before shutdown. +Covers `library.md` and folder cards, `--directory` / `--max-files` resume, `record_deletion` plus journal de-duplication, `record_changes` globs and `--green`, dependency confidence and cycle `break_hint`, JS wiki refusal, Rust `mark_green` on top-level `pub` items, legacy JSON import, `init` keeping path lists, `init DIR` as the project root, `heal-stubs --dry-run`, `search-journal --file`, `verify-wikis` limited to one file, MCP `isError` plus resource and prompt reads, prune keeping a monitored file that sits outside the map roots, and `serve` reading `file_health.json` and `library.md` before shutdown. From bffc9d10c5e9d04f5717f44f7c9c3ad8a26c1d55 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 3 Nov 2026 05:31:29 +0000 Subject: [PATCH 5/8] Test the deadline, check-changes cap, sqlite import, and low-confidence edges. WIKIFIER_MCP_DEADLINE_S stops a blocking monitor call and still finishes health. A cap of 1 truncates check-changes and names the env var. An empty cache imports import_cache.sqlite, a filled cache does not, and a corrupt sqlite file warns sqlite_present_unread. Conditional edges are actionable; dynamic edges are not. Co-authored-by: Aron Amos --- CHANGELOG.md | 1 + tests/behaviour_gaps.rs | 211 ++++++++++++++++++++++++++++++++ tests/behaviour_gaps.rs.wiki.md | 11 ++ 3 files changed, 223 insertions(+) create mode 100644 tests/behaviour_gaps.rs create mode 100644 tests/behaviour_gaps.rs.wiki.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a3a8a5..8fad8cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The import scan remains comment-masked regex plus named JS/TS re-exports to depth 8 (`parser: "regex+reexport"`). It is not a CPython `ast` walk, and it has not been run on Babylon.js. The accuracy gate is `accuracy_fixtures_match_frozen_edges`, now including a Cargo crate layout. - Rust `mod`, `crate::`, `self::`, `super::`, and the root `Cargo.toml` package or lib name resolve to files. Hyphens in the package name become underscores. `crate::` stays inside the file's crate (`tests/*.rs` and `src/bin/*.rs` are separate crates). The package or lib name points at the library, so binaries and integration tests are dependents of the library modules they import. - `mark-green` checks Rust top-level `pub` items (`fn`, `struct`, `enum`, `trait`, `mod`, `type`, `const`, `static`, and `pub use` names) the same way it checks Python and JS/TS names. `pub(crate)` and items inside `impl` stay out of that set. A Rust file can be `verified` Green. Go, C/C++, C#, and Java stay `unchecked`. +- Tests cover `WIKIFIER_MCP_DEADLINE_S`, `WIKIFIER_CHECK_CHANGES_MAX`, an `import_cache.sqlite` import (including `sqlite_present_unread`), and `actionable_low_conf_edges`. ### Removed - The README section "Does it pay off? (measured)" is gone. It cited `scripts/benchmark_lookup.py`, which left with the Python package, and its numbers were measured on that package. The performance table that remains is still labeled as Python-era. diff --git a/tests/behaviour_gaps.rs b/tests/behaviour_gaps.rs new file mode 100644 index 0000000..5008604 --- /dev/null +++ b/tests/behaviour_gaps.rs @@ -0,0 +1,211 @@ +//! Behaviour the review found untested: MCP deadline, check-changes cap, +//! sqlite cache import, and actionable low-confidence edges. + +use std::io::{BufRead, BufReader, Write}; +use std::path::PathBuf; +use std::process::{Command, Stdio}; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +use wikifier::cache::indexed_rows; +use wikifier::jsonutil::{parse, Json}; +use wikifier::mcp; +use wikifier::{load_dependencies, session_bootstrap, update_maps}; + +fn scratch(name: &str) -> PathBuf { + let n = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos(); + let dir = std::env::temp_dir().join(format!("wk-gap-{name}-{n}")); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(dir.join("src")).unwrap(); + std::fs::write(dir.join("map_paths.txt"), "src\n").unwrap(); + std::fs::write(dir.join("monitored_paths.txt"), "src\n").unwrap(); + dir +} + +fn json_text(value: &Json) -> String { + value.to_string() +} + +fn mcp_call(bin: &str, deadline: Option<&str>, body: &str) -> (Json, Duration) { + let mut cmd = Command::new(bin); + cmd.arg("mcp").stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::null()); + match deadline { + Some(value) => { + cmd.env("WIKIFIER_MCP_DEADLINE_S", value); + } + None => { + cmd.env_remove("WIKIFIER_MCP_DEADLINE_S"); + } + } + let mut child = cmd.spawn().unwrap(); + let mut stdin = child.stdin.take().unwrap(); + let stdout = child.stdout.take().unwrap(); + let started = Instant::now(); + writeln!(stdin, "{body}").unwrap(); + let mut line = String::new(); + BufReader::new(stdout).read_line(&mut line).unwrap(); + let elapsed = started.elapsed(); + let _ = child.kill(); + let _ = child.wait(); + let parsed = parse(line.trim()).unwrap_or_else(|err| panic!("mcp json {err}: {line}")); + (parsed, elapsed) +} + +fn tool_body(root: &PathBuf, name: &str) -> String { + let root_text = root.to_str().unwrap().replace('\\', "\\\\").replace('"', "\\\""); + format!( + r#"{{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{{"name":"{name}","arguments":{{"project_root":"{root_text}"}}}}}}"# + ) +} + +fn structured(message: &Json) -> &Json { + message.get("result").and_then(|result| result.get("structuredContent")).unwrap_or_else(|| panic!("no structured content in {message}")) +} + +#[test] +fn mcp_deadline_stops_a_blocking_call_and_lets_a_fast_call_finish() { + let root = scratch("deadline"); + std::fs::write(root.join("src/a.py"), "x = 1\n").unwrap(); + let bin = env!("CARGO_BIN_EXE_wikifier"); + + let (fast, fast_elapsed) = mcp_call(bin, Some("1"), &tool_body(&root, "health")); + let fast_body = structured(&fast); + assert_eq!(fast_body.get("success").and_then(|flag| flag.as_bool()), Some(true), "{fast}"); + assert!(fast_elapsed < Duration::from_secs(1), "health took {fast_elapsed:?}"); + + let (slow, slow_elapsed) = mcp_call(bin, Some("1"), &tool_body(&root, "monitor")); + let slow_body = structured(&slow); + assert_eq!(slow_body.get("success").and_then(|flag| flag.as_bool()), Some(false), "{slow}"); + assert_eq!(slow_body.get("error_type").and_then(|flag| flag.as_str()), Some("DeadlineExceeded"), "{slow}"); + assert_eq!(slow_body.get("timed_out").and_then(|flag| flag.as_bool()), Some(true), "{slow}"); + assert_eq!(slow_body.get("may_still_complete").and_then(|flag| flag.as_bool()), Some(true), "{slow}"); + assert!(slow_body.get("error").and_then(|flag| flag.as_str()).unwrap_or("").contains("WIKIFIER_MCP_DEADLINE_S") || slow_body.to_string().contains("deadline"), "{slow}"); + assert!(slow_elapsed >= Duration::from_millis(700), "deadline 1 returned in {slow_elapsed:?}"); + assert!(slow_elapsed < Duration::from_secs(8), "deadline 1 hung for {slow_elapsed:?}"); + assert_eq!(fast.get("result").and_then(|result| result.get("isError")).and_then(|flag| flag.as_bool()), Some(false), "{fast}"); + assert_eq!(slow.get("result").and_then(|result| result.get("isError")).and_then(|flag| flag.as_bool()), Some(true), "{slow}"); + + let (zero, zero_elapsed) = mcp_call(bin, Some("0"), &tool_body(&root, "monitor")); + let zero_body = structured(&zero); + assert_eq!(zero_body.get("timed_out").and_then(|flag| flag.as_bool()), Some(true), "{zero}"); + assert!(zero_elapsed >= Duration::from_millis(700), "deadline 0 was not raised to 1s ({zero_elapsed:?})"); + let _ = std::fs::remove_dir_all(&root); +} + +#[test] +fn check_changes_cap_stops_the_pass_and_names_the_env_var() { + let root = scratch("cap"); + for name in ["a.py", "b.py", "c.py"] { + std::fs::write(root.join("src").join(name), format!("{name} = 1\n")).unwrap(); + } + let mapped = update_maps(&root); + assert_eq!(mapped.get("success").and_then(|flag| flag.as_bool()), Some(true), "{mapped}"); + let bin = env!("CARGO_BIN_EXE_wikifier"); + + let capped = Command::new(bin) + .args(["--target", root.to_str().unwrap(), "check-changes"]) + .env("WIKIFIER_CHECK_CHANGES_MAX", "1") + .output() + .unwrap(); + let capped_json = parse(&String::from_utf8(capped.stdout.clone()).unwrap()).unwrap_or_else(|err| panic!("{err}: {}", String::from_utf8_lossy(&capped.stdout))); + assert_eq!(capped_json.get("success").and_then(|flag| flag.as_bool()), Some(true), "{capped_json}"); + assert_eq!(capped_json.get("truncated").and_then(|flag| flag.as_bool()), Some(true), "{capped_json}"); + assert!(json_text(&capped_json).contains("WIKIFIER_CHECK_CHANGES_MAX"), "{capped_json}"); + assert!(json_text(&capped_json).contains("stopped at 1 files"), "{capped_json}"); + + let open = Command::new(bin) + .args(["--target", root.to_str().unwrap(), "check-changes"]) + .env("WIKIFIER_CHECK_CHANGES_MAX", "50") + .output() + .unwrap(); + let open_json = parse(&String::from_utf8(open.stdout).unwrap()).unwrap(); + assert_eq!(open_json.get("truncated"), None, "{open_json}"); + + let ignored = Command::new(bin) + .args(["--target", root.to_str().unwrap(), "check-changes"]) + .env("WIKIFIER_CHECK_CHANGES_MAX", "nope") + .output() + .unwrap(); + let ignored_json = parse(&String::from_utf8(ignored.stdout).unwrap()).unwrap(); + assert_eq!(ignored_json.get("truncated"), None, "a non-numeric cap must stay at the default: {ignored_json}"); + let _ = std::fs::remove_dir_all(&root); +} + +#[test] +fn sqlite_import_fills_an_empty_cache_and_reports_an_unreadable_file() { + let root = scratch("sqlite"); + std::fs::create_dir_all(root.join(".wikifier_staging")).unwrap(); + std::fs::write(root.join("src/a.py"), "x = 1\n").unwrap(); + std::fs::write(root.join("src/b.py"), "y = 2\n").unwrap(); + let db = root.join(".wikifier_staging/import_cache.sqlite"); + let created = Command::new("sqlite3") + .arg(&db) + .arg("CREATE TABLE files (rel_path TEXT); CREATE TABLE edges (src TEXT, dst TEXT); INSERT INTO files VALUES ('src/a.py'); INSERT INTO files VALUES ('src/b.py'); INSERT INTO edges VALUES ('src/a.py', 'src/b.py');") + .status() + .unwrap_or_else(|err| panic!("sqlite3 CLI is required for this test: {err}")); + assert!(created.success(), "sqlite3 could not create the cache"); + + let deps = load_dependencies(&root, "src/a.py"); + assert_eq!(deps.get("success").and_then(|flag| flag.as_bool()), Some(true), "{deps}"); + let internal = deps.get("internal").and_then(|item| item.as_array()).unwrap_or(&[]); + assert!(internal.iter().any(|edge| edge.get("path").and_then(|path| path.as_str()) == Some("src/b.py")), "{deps}"); + let rows = indexed_rows(&root); + assert!(rows.iter().any(|row| row.0 == "src/a.py" && row.3 == "sha256:legacy" && row.1 == 0), "{rows:?}"); + + let extra = Command::new("sqlite3") + .arg(&db) + .arg("INSERT INTO edges VALUES ('src/a.py', 'src/c.py');") + .status() + .unwrap(); + assert!(extra.success()); + let again = load_dependencies(&root, "src/a.py"); + let paths: Vec<_> = again.get("internal").and_then(|item| item.as_array()).unwrap_or(&[]).iter().filter_map(|edge| edge.get("path").and_then(|path| path.as_str())).collect(); + assert_eq!(paths, ["src/b.py"], "a filled cache must ignore a later sqlite file: {again}"); + + let broken = scratch("sqlite-bad"); + std::fs::create_dir_all(broken.join(".wikifier_staging")).unwrap(); + std::fs::write(broken.join(".wikifier_staging/import_cache.sqlite"), b"not a sqlite database").unwrap(); + std::fs::write(broken.join("src/a.py"), "x = 1\n").unwrap(); + let boot = session_bootstrap(&broken); + assert_eq!(boot.get("success").and_then(|flag| flag.as_bool()), Some(true), "{boot}"); + let warnings = json_text(boot.get("warnings").unwrap_or(&Json::Null)); + assert!(warnings.contains("sqlite_present_unread"), "{boot}"); + let _ = std::fs::remove_dir_all(&root); + let _ = std::fs::remove_dir_all(&broken); +} + +#[test] +fn actionable_low_conf_edges_keep_conditional_and_drop_dynamic() { + let root = scratch("low"); + std::fs::create_dir_all(root.join("src/pkg")).unwrap(); + std::fs::write(root.join("src/pkg/__init__.py"), "").unwrap(); + std::fs::write(root.join("src/pkg/b.py"), "VALUE = 1\n").unwrap(); + std::fs::write(root.join("src/pkg/c.py"), "VALUE = 2\n").unwrap(); + std::fs::write(root.join("src/pkg/d.py"), "VALUE = 3\n").unwrap(); + std::fs::write( + root.join("src/pkg/a.py"), + "from . import b\n\ndef load():\n from . import c\n import importlib\n return importlib.import_module('pkg.d')\n", + ).unwrap(); + let mapped = update_maps(&root); + assert_eq!(mapped.get("success").and_then(|flag| flag.as_bool()), Some(true), "{mapped}"); + + let root_text = root.to_str().unwrap().replace('\\', "\\\\").replace('"', "\\\""); + let raw = format!( + r#"{{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{{"name":"get_resolution_diagnostics","arguments":{{"project_root":"{root_text}"}}}}}}"# + ); + let message = mcp::handle_message(&parse(&raw).unwrap()).unwrap(); + let body = structured(&message); + assert_eq!(body.get("success").and_then(|flag| flag.as_bool()), Some(true), "{body}"); + let low = body.get("actionable_low_conf_edges").and_then(|item| item.as_array()).unwrap_or(&[]); + assert!(low.iter().any(|edge| { + edge.get("file").and_then(|value| value.as_str()) == Some("src/pkg/a.py") + && edge.get("path").and_then(|value| value.as_str()) == Some("src/pkg/c.py") + && edge.get("reason").and_then(|value| value.as_str()) == Some("conditional_guard") + && matches!(edge.get("confidence"), Some(Json::Float(n)) if (*n - 0.55).abs() < 0.001) + }), "{body}"); + assert!(low.iter().all(|edge| edge.get("path").and_then(|value| value.as_str()) != Some("src/pkg/b.py")), "static edge was treated as low confidence: {body}"); + assert!(low.iter().all(|edge| edge.get("reason").and_then(|value| value.as_str()) != Some("dynamic_literal")), "dynamic edges are noise, not actionable: {body}"); + let counts = body.get("reason_code_counts").unwrap_or(&Json::Null); + assert!(counts.get("conditional_guard").is_some(), "{counts}"); + assert!(counts.get("dynamic_literal").is_some(), "{counts}"); + let _ = std::fs::remove_dir_all(&root); +} diff --git a/tests/behaviour_gaps.rs.wiki.md b/tests/behaviour_gaps.rs.wiki.md new file mode 100644 index 0000000..faf3c57 --- /dev/null +++ b/tests/behaviour_gaps.rs.wiki.md @@ -0,0 +1,11 @@ +# tests/behaviour_gaps.rs + +**Role:** Behaviour tests for four paths that had no coverage. + +`mcp_deadline_stops_a_blocking_call_and_lets_a_fast_call_finish` runs `wikifier mcp` with `WIKIFIER_MCP_DEADLINE_S`. A `health` call finishes. A `monitor` call returns `DeadlineExceeded`, `timed_out`, and `may_still_complete`. A value of `0` still waits at least one second. + +`check_changes_cap_stops_the_pass_and_names_the_env_var` sets `WIKIFIER_CHECK_CHANGES_MAX` to 1 and expects `truncated` plus the env var in the warning. A numeric cap above the file count, and a non-numeric value, leave the pass uncapped. + +`sqlite_import_fills_an_empty_cache_and_reports_an_unreadable_file` builds `import_cache.sqlite` with the `sqlite3` CLI, checks the imported edge and the `sha256:legacy` hash, checks that a later sqlite write does not replace a filled cache, and checks that a corrupt sqlite file warns `sqlite_present_unread`. + +`actionable_low_conf_edges_keep_conditional_and_drop_dynamic` maps a conditional import and a dynamic `import_module`. Only the conditional edge is actionable, at confidence 0.55 with reason `conditional_guard`. From bc8ad3d95684c566ed8ae0e3c8b1cadeeba1344e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 3 Nov 2026 05:31:35 +0000 Subject: [PATCH 6/8] Mention the new behaviour tests in the changelog wiki. Co-authored-by: Aron Amos --- CHANGELOG.md.wiki.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md.wiki.md b/CHANGELOG.md.wiki.md index 8da6012..27365f9 100644 --- a/CHANGELOG.md.wiki.md +++ b/CHANGELOG.md.wiki.md @@ -2,4 +2,4 @@ **Role:** Keep a Changelog history for package 4.7.x. -The 4.7.0 section is the record of that release, including the Python package that shipped then. Unreleased notes describe the Rust binary: map artifacts, dependency flags, wiki checks, deletion, background commands, the dashboard serve routes, MCP error reporting, `heal-stubs --dry-run`, and `init DIR`. The import scan is recorded as `regex+reexport`. Unreleased notes that Rust crate-name imports resolve to the library, that the unreproducible lookup benchmark was removed, and that Rust top-level `pub` names can be verified Green. CI checks the empty dependency list without Python. +The 4.7.0 section is the record of that release, including the Python package that shipped then. Unreleased notes describe the Rust binary: map artifacts, dependency flags, wiki checks, deletion, background commands, the dashboard serve routes, MCP error reporting, `heal-stubs --dry-run`, and `init DIR`. The import scan is recorded as `regex+reexport`. Unreleased notes that Rust crate-name imports resolve to the library, that the unreproducible lookup benchmark was removed, that Rust top-level `pub` names can be verified Green, and that the MCP deadline, check-changes cap, sqlite import, and low-confidence edges have tests. CI checks the empty dependency list without Python. From 90c648dbd9c6df1e16397f52016837b9981b0d73 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 3 Nov 2026 05:32:27 +0000 Subject: [PATCH 7/8] Pin the Rust toolchain and require the Windows CI job. CI and the release workflow use dtolnay/rust-toolchain@1.83.0 instead of @stable. Ubuntu, macOS, and Windows all run cargo test --locked, and a Windows failure fails the workflow. The sqlite import assertions still run wherever the sqlite3 CLI is installed; the unread warning runs on every platform. Co-authored-by: Aron Amos --- .github/workflows/ci.yml | 9 ++------- .github/workflows/publish.yml | 4 ++-- CHANGELOG.md | 1 + CHANGELOG.md.wiki.md | 2 +- CONTRIBUTING.md | 2 +- CONTRIBUTING.md.wiki.md | 2 +- tests/behaviour_gaps.rs | 27 ++++++++++++++++++--------- tests/behaviour_gaps.rs.wiki.md | 2 +- 8 files changed, 27 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 25cc934..38d5a98 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,21 +13,16 @@ jobs: test: name: ${{ matrix.os }} / Rust runs-on: ${{ matrix.os }} - continue-on-error: ${{ matrix.experimental }} strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest] - experimental: [false] - include: - - os: windows-latest - experimental: true + os: [ubuntu-latest, macos-latest, windows-latest] steps: - uses: actions/checkout@v6 - name: Set up Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@1.83.0 - name: Test run: cargo test --locked diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index efbfc52..b8c0d7e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@1.83.0 - name: Test run: cargo test --locked @@ -25,7 +25,7 @@ jobs: needs: test steps: - uses: actions/checkout@v6 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@1.83.0 - name: Build release binary run: cargo build --release --locked - name: Create GitHub Release diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fad8cf..ef299a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Rust `mod`, `crate::`, `self::`, `super::`, and the root `Cargo.toml` package or lib name resolve to files. Hyphens in the package name become underscores. `crate::` stays inside the file's crate (`tests/*.rs` and `src/bin/*.rs` are separate crates). The package or lib name points at the library, so binaries and integration tests are dependents of the library modules they import. - `mark-green` checks Rust top-level `pub` items (`fn`, `struct`, `enum`, `trait`, `mod`, `type`, `const`, `static`, and `pub use` names) the same way it checks Python and JS/TS names. `pub(crate)` and items inside `impl` stay out of that set. A Rust file can be `verified` Green. Go, C/C++, C#, and Java stay `unchecked`. - Tests cover `WIKIFIER_MCP_DEADLINE_S`, `WIKIFIER_CHECK_CHANGES_MAX`, an `import_cache.sqlite` import (including `sqlite_present_unread`), and `actionable_low_conf_edges`. +- CI pins Rust 1.83.0 (`dtolnay/rust-toolchain@1.83.0`). Ubuntu, macOS, and Windows all run `cargo test --locked`, and a Windows failure fails the workflow. ### Removed - The README section "Does it pay off? (measured)" is gone. It cited `scripts/benchmark_lookup.py`, which left with the Python package, and its numbers were measured on that package. The performance table that remains is still labeled as Python-era. diff --git a/CHANGELOG.md.wiki.md b/CHANGELOG.md.wiki.md index 27365f9..7455466 100644 --- a/CHANGELOG.md.wiki.md +++ b/CHANGELOG.md.wiki.md @@ -2,4 +2,4 @@ **Role:** Keep a Changelog history for package 4.7.x. -The 4.7.0 section is the record of that release, including the Python package that shipped then. Unreleased notes describe the Rust binary: map artifacts, dependency flags, wiki checks, deletion, background commands, the dashboard serve routes, MCP error reporting, `heal-stubs --dry-run`, and `init DIR`. The import scan is recorded as `regex+reexport`. Unreleased notes that Rust crate-name imports resolve to the library, that the unreproducible lookup benchmark was removed, that Rust top-level `pub` names can be verified Green, and that the MCP deadline, check-changes cap, sqlite import, and low-confidence edges have tests. CI checks the empty dependency list without Python. +The 4.7.0 section is the record of that release, including the Python package that shipped then. Unreleased notes describe the Rust binary: map artifacts, dependency flags, wiki checks, deletion, background commands, the dashboard serve routes, MCP error reporting, `heal-stubs --dry-run`, and `init DIR`. The import scan is recorded as `regex+reexport`. Unreleased notes that Rust crate-name imports resolve to the library, that the unreproducible lookup benchmark was removed, that Rust top-level `pub` names can be verified Green, and that the MCP deadline, check-changes cap, sqlite import, and low-confidence edges have tests. CI pins Rust 1.83.0 and requires Ubuntu, macOS, and Windows. CI checks the empty dependency list without Python. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index be935db..3a8bdc7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,7 +37,7 @@ Please use the appropriate GitHub issue template: ```bash wikifier mark-green "path/to/your/file" ``` -5. Before opening a PR: `cargo test`, then `wikifier update-maps` and `wikifier validate` on this repo. Parser changes must keep `accuracy_fixtures_match_frozen_edges` green. +5. Before opening a PR: `cargo test`, then `wikifier update-maps` and `wikifier validate` on this repo. Parser changes must keep `accuracy_fixtures_match_frozen_edges` green. CI pins Rust 1.83.0 and runs `cargo test --locked` on Ubuntu, macOS, and Windows. All three jobs are required. ### 3. Pull Request Guidelines diff --git a/CONTRIBUTING.md.wiki.md b/CONTRIBUTING.md.wiki.md index a5e8462..06e31b5 100644 --- a/CONTRIBUTING.md.wiki.md +++ b/CONTRIBUTING.md.wiki.md @@ -2,4 +2,4 @@ **Role:** How to change Wikifier without adding dependencies. -New commands go in `src/api.rs` and `src/main.rs`, with a test and a `skills/run.md` update. The launchers only exec the binary. After an edit, record the change, refresh that file's wiki, and mark it green. +New commands go in `src/api.rs` and `src/main.rs`, with a test and a `skills/run.md` update. The launchers only exec the binary. After an edit, record the change, refresh that file's wiki, and mark it green. CI pins Rust 1.83.0 and runs the tests on Ubuntu, macOS, and Windows. diff --git a/tests/behaviour_gaps.rs b/tests/behaviour_gaps.rs index 5008604..c3fcaa2 100644 --- a/tests/behaviour_gaps.rs +++ b/tests/behaviour_gaps.rs @@ -130,8 +130,26 @@ fn check_changes_cap_stops_the_pass_and_names_the_env_var() { let _ = std::fs::remove_dir_all(&root); } +fn sqlite3_available() -> bool { + Command::new("sqlite3").arg("-version").output().map(|out| out.status.success()).unwrap_or(false) +} + #[test] fn sqlite_import_fills_an_empty_cache_and_reports_an_unreadable_file() { + let broken = scratch("sqlite-bad"); + std::fs::create_dir_all(broken.join(".wikifier_staging")).unwrap(); + std::fs::write(broken.join(".wikifier_staging/import_cache.sqlite"), b"not a sqlite database").unwrap(); + std::fs::write(broken.join("src/a.py"), "x = 1\n").unwrap(); + let boot = session_bootstrap(&broken); + assert_eq!(boot.get("success").and_then(|flag| flag.as_bool()), Some(true), "{boot}"); + let warnings = json_text(boot.get("warnings").unwrap_or(&Json::Null)); + assert!(warnings.contains("sqlite_present_unread"), "{boot}"); + let _ = std::fs::remove_dir_all(&broken); + if !sqlite3_available() { + eprintln!("sqlite3 CLI is not on PATH; the import assertions run where the CLI is installed"); + return; + } + let root = scratch("sqlite"); std::fs::create_dir_all(root.join(".wikifier_staging")).unwrap(); std::fs::write(root.join("src/a.py"), "x = 1\n").unwrap(); @@ -161,16 +179,7 @@ fn sqlite_import_fills_an_empty_cache_and_reports_an_unreadable_file() { let paths: Vec<_> = again.get("internal").and_then(|item| item.as_array()).unwrap_or(&[]).iter().filter_map(|edge| edge.get("path").and_then(|path| path.as_str())).collect(); assert_eq!(paths, ["src/b.py"], "a filled cache must ignore a later sqlite file: {again}"); - let broken = scratch("sqlite-bad"); - std::fs::create_dir_all(broken.join(".wikifier_staging")).unwrap(); - std::fs::write(broken.join(".wikifier_staging/import_cache.sqlite"), b"not a sqlite database").unwrap(); - std::fs::write(broken.join("src/a.py"), "x = 1\n").unwrap(); - let boot = session_bootstrap(&broken); - assert_eq!(boot.get("success").and_then(|flag| flag.as_bool()), Some(true), "{boot}"); - let warnings = json_text(boot.get("warnings").unwrap_or(&Json::Null)); - assert!(warnings.contains("sqlite_present_unread"), "{boot}"); let _ = std::fs::remove_dir_all(&root); - let _ = std::fs::remove_dir_all(&broken); } #[test] diff --git a/tests/behaviour_gaps.rs.wiki.md b/tests/behaviour_gaps.rs.wiki.md index faf3c57..6793122 100644 --- a/tests/behaviour_gaps.rs.wiki.md +++ b/tests/behaviour_gaps.rs.wiki.md @@ -6,6 +6,6 @@ `check_changes_cap_stops_the_pass_and_names_the_env_var` sets `WIKIFIER_CHECK_CHANGES_MAX` to 1 and expects `truncated` plus the env var in the warning. A numeric cap above the file count, and a non-numeric value, leave the pass uncapped. -`sqlite_import_fills_an_empty_cache_and_reports_an_unreadable_file` builds `import_cache.sqlite` with the `sqlite3` CLI, checks the imported edge and the `sha256:legacy` hash, checks that a later sqlite write does not replace a filled cache, and checks that a corrupt sqlite file warns `sqlite_present_unread`. +`sqlite_import_fills_an_empty_cache_and_reports_an_unreadable_file` always checks that a corrupt sqlite file warns `sqlite_present_unread`. When the `sqlite3` CLI is on PATH it also builds a database, checks the imported edge and the `sha256:legacy` hash, and checks that a later sqlite write does not replace a filled cache. `actionable_low_conf_edges_keep_conditional_and_drop_dynamic` maps a conditional import and a dynamic `import_module`. Only the conditional edge is actionable, at confidence 0.55 with reason `conditional_guard`. From 55a37cab69fa63951ea0fae12a0c20cb59688de6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 3 Nov 2026 05:36:00 +0000 Subject: [PATCH 8/8] Clear clippy warnings and archive the Python-era plans. Drop the dead list-paths loop and the clippy findings in the library and the binary. Move the two Python-era plans to Findings/archive and delete the wiki for the removed Python test. Refresh the dependency map and mark the touched Rust wikis verified. Co-authored-by: Aron Amos --- AGENTS.md | 4 +- AGENTS.md.wiki.md | 2 +- CLAUDE.md | 2 +- CLAUDE.md.wiki.md | 2 +- .../archive/MODULARIZATION_PLAN.md | 2 + .../archive/SIMPLIFICATION_PLAN_AND_LOG.md | 2 + file_health.json | 2 +- file_health.md | 45 ++++--- journal/2026/11/03.md | 96 ++++++++++++++ library.md | 120 +++++++++++++----- src/api.rs | 120 +++++++++--------- src/cache_commit.rs | 2 +- src/jsonutil.rs | 2 +- src/jsonutil.rs.wiki.md | 7 + src/legacy_cache.rs | 2 +- src/main.rs | 34 ++--- src/mcp.rs | 13 +- src/parse.rs | 11 +- src/project_lock.rs.wiki.md | 2 + tests/behaviour_gaps.rs | 2 +- tests/test_gap_closure.py.wiki.md | 5 - 21 files changed, 312 insertions(+), 165 deletions(-) rename MODULARIZATION_PLAN.md => Findings/archive/MODULARIZATION_PLAN.md (99%) rename SIMPLIFICATION_PLAN_AND_LOG.md => Findings/archive/SIMPLIFICATION_PLAN_AND_LOG.md (99%) create mode 100644 src/jsonutil.rs.wiki.md delete mode 100644 tests/test_gap_closure.py.wiki.md diff --git a/AGENTS.md b/AGENTS.md index 6c9b7ab..9f64bdd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,8 +56,8 @@ Prefer the CLI when the connected MCP server is still `python3 -m wikifier.mcp.s These files still describe the deleted Python package. They are records. Do not implement them, and do not rewrite them to sound current. -- `MODULARIZATION_PLAN.md` -- `SIMPLIFICATION_PLAN_AND_LOG.md` +- `Findings/archive/MODULARIZATION_PLAN.md` +- `Findings/archive/SIMPLIFICATION_PLAN_AND_LOG.md` - `Findings/` and `Findings/archive/` - `journal/` entries from before the Rust cutover - Released sections of `CHANGELOG.md` (Unreleased is the live note) diff --git a/AGENTS.md.wiki.md b/AGENTS.md.wiki.md index a50bec8..1c875fe 100644 --- a/AGENTS.md.wiki.md +++ b/AGENTS.md.wiki.md @@ -2,4 +2,4 @@ **Role:** Entry point for any coding agent in this repository. -It points at `skills/run.md` (protocol v0.7) and names the live Rust binary, the daily loop, and the files that are historical Python-era records. Map roots stay `src/` and `tests/`. The import scan stays comment-masked regex plus named re-exports. Rust also resolves `crate::`, `self::`, `super::`, and the root Cargo.toml package or lib name, so `src/main.rs` and `tests/*.rs` are dependents of the library modules they import. `mark-green` checks Rust top-level `pub` names. +It points at `skills/run.md` (protocol v0.7) and names the live Rust binary, the daily loop, and the Python-era records kept under `Findings/archive/`. Map roots stay `src/` and `tests/`. The import scan stays comment-masked regex plus named re-exports. Rust also resolves `crate::`, `self::`, `super::`, and the root Cargo.toml package or lib name, so `src/main.rs` and `tests/*.rs` are dependents of the library modules they import. `mark-green` checks Rust top-level `pub` names. diff --git a/CLAUDE.md b/CLAUDE.md index bb662c1..ff369c4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -83,5 +83,5 @@ Adding a language means a scan in `src/parse.rs` with the same edge shape, a fro - `skills/run.md` — the agent protocol (primary contract for any LLM operating Wikifier) - `docs/spec.md` — original requirements, then the living Core Principles - `Findings/` — dogfood evidence (`README.md` index; superseded plans in `Findings/archive/`). Historical. Do not treat it as the current design. -- `SIMPLIFICATION_PLAN_AND_LOG.md`, `MODULARIZATION_PLAN.md` — Python-era logs. Do not implement them. +- `Findings/archive/SIMPLIFICATION_PLAN_AND_LOG.md`, `Findings/archive/MODULARIZATION_PLAN.md` — Python-era logs. Do not implement them. - `file_health.md`, `pending_updates.md`, `journal/`, `library.md` — this repo's own live wiki state (dogfood) diff --git a/CLAUDE.md.wiki.md b/CLAUDE.md.wiki.md index d9bcfd7..a0d766a 100644 --- a/CLAUDE.md.wiki.md +++ b/CLAUDE.md.wiki.md @@ -2,4 +2,4 @@ **Role:** Repo guidance for an agent working in this tree. -Hard constraints: empty Cargo dependencies, no hand-edits of `library.md` or the health and pending files, and the human dashboard stays secondary. It points agents at `AGENTS.md` and `skills/run.md`. The architecture section names the Rust modules under `src/`, the library file tree, and this repo's map roots `src/` and `tests/`. The parse note includes Rust `crate::`, `self::`, `super::`, the Cargo.toml crate name, and `rust_public_symbols` for wiki checks. JSON failures use `success: false`. +Hard constraints: empty Cargo dependencies, no hand-edits of `library.md` or the health and pending files, and the human dashboard stays secondary. The Python-era plan logs live under `Findings/archive/`. It points agents at `AGENTS.md` and `skills/run.md`. The architecture section names the Rust modules under `src/`, the library file tree, and this repo's map roots `src/` and `tests/`. The parse note includes Rust `crate::`, `self::`, `super::`, the Cargo.toml crate name, and `rust_public_symbols` for wiki checks. JSON failures use `success: false`. diff --git a/MODULARIZATION_PLAN.md b/Findings/archive/MODULARIZATION_PLAN.md similarity index 99% rename from MODULARIZATION_PLAN.md rename to Findings/archive/MODULARIZATION_PLAN.md index 0a96e85..59e3a62 100644 --- a/MODULARIZATION_PLAN.md +++ b/Findings/archive/MODULARIZATION_PLAN.md @@ -1,3 +1,5 @@ +> Archived Python-era record. The package it describes is gone. Kept so the history stays readable. + # Wikifier Modularization Plan > **Status (4.7.0, 2026-10-01):** `cache/` and `health_pkg/` are done for real — `cache/_core.py` and diff --git a/SIMPLIFICATION_PLAN_AND_LOG.md b/Findings/archive/SIMPLIFICATION_PLAN_AND_LOG.md similarity index 99% rename from SIMPLIFICATION_PLAN_AND_LOG.md rename to Findings/archive/SIMPLIFICATION_PLAN_AND_LOG.md index 68dc63c..68864e6 100644 --- a/SIMPLIFICATION_PLAN_AND_LOG.md +++ b/Findings/archive/SIMPLIFICATION_PLAN_AND_LOG.md @@ -1,3 +1,5 @@ +> Archived Python-era record. The package it describes is gone. Kept so the history stays readable. + # Wikifier Simplification Plan & Execution Log (Post M5 Audit) **Date started**: 2026-06-07 diff --git a/file_health.json b/file_health.json index b49bacd..fdfa347 100644 --- a/file_health.json +++ b/file_health.json @@ -1 +1 @@ -{"entries":{".github/workflows/ci.yml":{"last_updated":"2026-11-03 11:39:32","reason":"Check the empty Cargo dependency list with awk (subid=cutover-verify)","source_content_hash":"sha256:6ef211562304efbb3f88054aa56caecd45b83a7fa8ce4dde53c7ddbf53e88897","source_mtime_ns":"1793666205853895471","status":"🟢 Green","verification":"no_wiki"},"AGENTS.md":{"last_updated":"2026-11-03 11:25:28","reason":"Add the agent entry point: live Rust binary, daily loop, and which logs are historical (subid=agents-md)","source_content_hash":"sha256:8a41eab6d250ce8dc4d1af01b703e543f5fc23869854f9a708cb04a697f51dcb","source_mtime_ns":"1793665504610718741","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:d542f260d91bd793a440e95760e8b7e0160e3b7e4791b3f61980dc307f5113a4"},"CHANGELOG.md":{"last_updated":"2026-11-03 11:39:32","reason":"Note dry-run, init DIR, the journal file filter, and the Python-free dependency check (subid=cutover-verify)","source_content_hash":"sha256:d9aa668ec2df9b27fe102aae7c3cb38a9911430c5bd0192f90da62191f42a645","source_mtime_ns":"1793666283191280114","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:cab78215c2f657756833565079c4fc4b2a8d66b65ec146759aeda06b68e275a7"},"CLAUDE.md":{"last_updated":"2026-11-03 11:25:28","reason":"Point at AGENTS.md, describe the library file tree, and return JSON success false (subid=agents-md)","source_content_hash":"sha256:b4156317fbfce0672763f5d2b1f5a0fcf5923de6c47e38a9fe6c0dc5ce564168","source_mtime_ns":"1793665504689348038","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:30e9dbf68f9f91489ce4cdb5e2d4b108415651b1f06bdc80fbdfc0419dc0d4d3"},"CONTRIBUTING.md":{"last_updated":"2026-11-03 08:00:11","reason":"Point new commands at src/api.rs and src/main.rs (subid=protocol-v07)","source_content_hash":"sha256:a78d26038b94a1b99a5f276faec61656087836255eaf97025696fa8487a2ac45","source_mtime_ns":"1793652983313369222","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:b70375bc79af81d2d769a714f1dfdda45e423c968ca633e46d35c7134d12dc75"},"Findings/M5-Dogfood-Assessment-Report.md":{"last_updated":"2026-07-09 05:02:27","reason":"kept; stale yellow cleared","source_content_hash":"sha256:238435188b1699e5bcf4753b62a6fe55cb064413a291259f0b696b5bd3c1692f","source_mtime_ns":"1780519956000000000","status":"🟢 Green"},"Findings/M5-Dogfood-Progress.md":{"last_updated":"2026-11-03 08:06:46","reason":"Wiki marks this file as the historical M5 log (subid=protocol-v07)","source_content_hash":"sha256:ef55167e819806060dfd7dfe7cdfd4d02024ca97706a7faa243710e2630b65fe","source_mtime_ns":"1793408707259550076","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:b6b1fa4bbee0c7e8c4035cd925f6d07e1221ba483e3472878d5b88236c5cf7f9"},"Findings/M5.1-cross-hardening-analysis.md":{"last_updated":"2026-07-09 05:02:27","reason":"kept; stale yellow cleared","source_content_hash":"sha256:a2c2414e5b6c9b1a802bec8734a7924850d546cd7746a8087216f3685ca36cb4","source_mtime_ns":"1780519762000000000","status":"🟢 Green"},"Findings/Milestones-Overview.md":{"last_updated":"2026-07-09 05:02:27","reason":"kept; stale yellow cleared","source_content_hash":"sha256:f8b7030ec8980774cf3a2560ad7aee6537263eadf8374e650718f7c6793c6d50","source_mtime_ns":"1780800604000000000","status":"🟢 Green"},"Findings/p6_real_world_validation_report.md":{"last_updated":"2026-07-09 05:02:27","reason":"kept; stale yellow cleared","source_content_hash":"sha256:0468224675888f2747674324726c0ea9a5dddded39d9a1a50f22fc3357dafc0f","source_mtime_ns":"1780520008000000000","status":"🟢 Green"},"README.md":{"last_updated":"2026-11-03 11:25:28","reason":"Point agents at AGENTS.md and skills/run.md (subid=agents-md)","source_content_hash":"sha256:47aec0c4974b8ece2fbf0c23ee7f70d799e4fc44af63bc4a49190ca29687e731","source_mtime_ns":"1793665504624626252","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:ae16d62e3ccf88f452ac38bf74948e99abed4f30ac6d3458937ecc10f40bba77"},"docs/spec.md":{"last_updated":"2026-11-03 08:00:11","reason":"Name the Rust binary and protocol v0.7 in the living principles (subid=protocol-v07)","source_content_hash":"sha256:31fe6b3b3d705628692eb0512cf44cbb21861ccfc5bb02afdd46d796ec066c6e","source_mtime_ns":"1793653008580824356","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:a58b929d25d510f15f9fe176d7823e852f6d089e398accc2e22e1ed189f260ac"},"monitored_paths.txt":{"last_updated":"2026-11-03 11:25:28","reason":"Watch AGENTS.md with the other live docs (subid=agents-md)","source_content_hash":"sha256:0f8ec022bd6174009db02e1cd0cfc181c7d74d71b65ed59d119eb54645082540","source_mtime_ns":"1793665504614684923","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:d262fc32a1f49ebc9618a49b6481676a0aefd44d0c6ff90c8d5d724ec87aae15"},"skills/run.md":{"last_updated":"2026-11-03 11:39:32","reason":"Match the command table to the Rust binary for daemon, init, verify-wikis, search-journal, and issues (subid=cutover-verify)","source_content_hash":"sha256:4e5c125f3dd09a38e313b8ee50bf658f64b63527a04bbaa71a28271a23e1d4c8","source_mtime_ns":"1793666283294132435","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:62a3e8d19fd95e06b5bd89d01dfc325bdb9d6408c2689ccafe09dfd0c23fa9e9"},"src/api.rs":{"last_updated":"2026-11-03 11:39:32","reason":"Honor heal-stubs --dry-run, return seed-health write errors, limit verify-wikis to a path, and apply search-journal --file (subid=cutover-verify)","source_content_hash":"sha256:512f94ffbaa7ba404a284fc505799240d79943390737a313a53146f19cbef1a0","source_mtime_ns":"1793666290185264246","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:71c70490c21d123dd4bc7d04b7478a0e2ce0808324b732c507f28e36ddd8aa35"},"src/artifacts.rs":{"last_updated":"2026-11-03 08:00:11","reason":"Finish the 4.7 agent contract in the Rust binary: map artifacts, deletion, update-maps flags, record-changes, dependency flags, wiki checks, legacy cache import, serve/monitor/daemon, MCP errors, project lock, and pending cleanup on mark-green (subid=protocol-v07)","source_content_hash":"sha256:37b29254a15e6c09e76a54d1c5a7d0aa7fc54b25044e17db84f345e55cbfa790","source_mtime_ns":"1793652255568501563","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:4997604124c322823cbd6cff65aadff310f26aa8709b48102ea97efd286e6fce"},"src/cache.rs":{"last_updated":"2026-11-03 08:00:11","reason":"Finish the 4.7 agent contract in the Rust binary: map artifacts, deletion, update-maps flags, record-changes, dependency flags, wiki checks, legacy cache import, serve/monitor/daemon, MCP errors, project lock, and pending cleanup on mark-green (subid=protocol-v07)","source_content_hash":"sha256:79faa2b50bc95a573a1bd3555bcafbca271221af61849201df999cb229b4be63","source_mtime_ns":"1793652200465491668","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:c7c9e6236183325eca600d09d7993b8149da60a0f90d1d392698984083bb0ec8"},"src/cache_commit.rs":{"last_updated":"2026-11-03 08:00:11","reason":"Finish the 4.7 agent contract in the Rust binary: map artifacts, deletion, update-maps flags, record-changes, dependency flags, wiki checks, legacy cache import, serve/monitor/daemon, MCP errors, project lock, and pending cleanup on mark-green (subid=protocol-v07)","source_content_hash":"sha256:06aa10848438ee6a8aedef669d6183542cb4cb295c629f0de592d198fc6919ee","source_mtime_ns":"1793652206851311504","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:74842cbb202e2c66f6de608fab76f6b763b696115be05868a60caca10cfdbd90"},"src/jsonutil.rs":{"last_updated":"2026-11-03 07:58:59","reason":"Initial stub","source_content_hash":"sha256:ad0f1ae6798a944e099fbccf291a50451efa34fb4562e11f371dab7ac04ae11b","status":"🟡 Yellow","verification":"no_wiki"},"src/legacy_cache.rs":{"last_updated":"2026-11-03 08:00:12","reason":"Finish the 4.7 agent contract in the Rust binary: map artifacts, deletion, update-maps flags, record-changes, dependency flags, wiki checks, legacy cache import, serve/monitor/daemon, MCP errors, project lock, and pending cleanup on mark-green (subid=protocol-v07)","source_content_hash":"sha256:710e61512291b7ef926ff2e0081f80223c8c3920866ae2bf16d1ef7ddc723685","source_mtime_ns":"1793652200409326774","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:3be890822244677c21b9f9c9d5b5136cdcf29673e705b656c1956be6c8fb0104"},"src/lib.rs":{"last_updated":"2026-11-03 08:00:12","reason":"Finish the 4.7 agent contract in the Rust binary: map artifacts, deletion, update-maps flags, record-changes, dependency flags, wiki checks, legacy cache import, serve/monitor/daemon, MCP errors, project lock, and pending cleanup on mark-green (subid=protocol-v07)","source_content_hash":"sha256:d3afbbbe2faf0f68c7e4c4148f8aec42a728b1c529c8615689798a1a9f9b039d","source_mtime_ns":"1793652206847021832","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:5b01d22ccd91e784944b8e619900bfe98026c2e73337b036159c1359bebd9f96"},"src/main.rs":{"last_updated":"2026-11-03 11:39:32","reason":"Use init DIR as the project root unless --target is set (subid=cutover-verify)","source_content_hash":"sha256:26c13ecab80ed6117b2921ccf163ffdb92ca9dc9977b5b9cd87f1d96151ab763","source_mtime_ns":"1793666283262123457","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:db6e1cd5beeb90e5a65b3e00a37ee2c73ea4c53750771151e655459a1b823ff5"},"src/maps_out.rs":{"last_updated":"2026-11-03 08:04:45","reason":"Write a file tree into library.md so the dashboard can show the map (subid=protocol-v07)","source_content_hash":"sha256:6f4523bfae50356ce8c90f2c840f04a79eb99c4ce889e985dd80432cba35d410","source_mtime_ns":"1793653444467054359","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:b1b8f44db626085c48332fcda37addca0a96f4160aee8f662f07df4fbf0909e4"},"src/mcp.rs":{"last_updated":"2026-11-03 08:00:12","reason":"Finish the 4.7 agent contract in the Rust binary: map artifacts, deletion, update-maps flags, record-changes, dependency flags, wiki checks, legacy cache import, serve/monitor/daemon, MCP errors, project lock, and pending cleanup on mark-green (subid=protocol-v07)","source_content_hash":"sha256:d3660d18bb3b40873eb0ac6f6007fc611991387c3f2fa51460e5753d0c84bf31","source_mtime_ns":"1793652550939549457","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:c0a453f26eaef4457d43caa513fa0df518353492dd485f2d8b740dc41902f5e2"},"src/parse.rs":{"last_updated":"2026-11-03 08:00:12","reason":"Finish the 4.7 agent contract in the Rust binary: map artifacts, deletion, update-maps flags, record-changes, dependency flags, wiki checks, legacy cache import, serve/monitor/daemon, MCP errors, project lock, and pending cleanup on mark-green (subid=protocol-v07)","source_content_hash":"sha256:22e48dafa4cfae44f25971764b19e58eeffa655ff8a4cb40dfc30ab77d1ea4ae","source_mtime_ns":"1793652238261402800","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:3099ea2b8a39ea2a90677ce2e19bb5d470dcbceb57f0e1a75a843df9d6f871d9"},"src/proc.rs":{"last_updated":"2026-11-03 07:58:59","reason":"Initial stub","source_content_hash":"sha256:a4751d64c912e3eb9b5608410b1555a0495ee7114174ea03500fffd5336ea776","status":"🟡 Yellow","verification":"no_wiki"},"src/project_lock.rs":{"last_updated":"2026-11-03 08:00:12","reason":"Finish the 4.7 agent contract in the Rust binary: map artifacts, deletion, update-maps flags, record-changes, dependency flags, wiki checks, legacy cache import, serve/monitor/daemon, MCP errors, project lock, and pending cleanup on mark-green (subid=protocol-v07)","source_content_hash":"sha256:d7f0448fccc96b9f9f4294f0abadb9044ee24551418180530eafd331b71b4ab5","source_mtime_ns":"1793652170248409300","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:c0727d50a42c024fa536275c4520acad909aa849b965e779a4e893eccd62d5b1"},"src/scope.rs":{"last_updated":"2026-11-03 07:58:59","reason":"Initial stub","source_content_hash":"sha256:4d80b5432823864d0b95db68a558cd25aa0fe7d35b3f5796fbbdf107bee709aa","status":"🟡 Yellow","verification":"no_wiki"},"src/sha256.rs":{"last_updated":"2026-11-03 07:58:59","reason":"Initial stub","source_content_hash":"sha256:a68691e08d8260937e0f6c2534faf02ef58bb8332efa2cdd1b0a59f8fdf0d77d","status":"🟡 Yellow","verification":"no_wiki"},"src/timeutil.rs":{"last_updated":"2026-11-03 07:58:59","reason":"Initial stub","source_content_hash":"sha256:a90359c6ab1c816b7d7b2f9ce12c799d3f6c8d560de0ed491c7f069be6fbaa14","status":"🟡 Yellow","verification":"no_wiki"},"tests/cache_and_artifacts.rs":{"last_updated":"2026-11-03 07:58:59","reason":"Initial stub","source_content_hash":"sha256:e147bf88f37a4748bde1c20657b911da6ca329b99867d854eea6f30a7562fd30","status":"🟡 Yellow","verification":"no_wiki"},"tests/cli_surface.rs":{"last_updated":"2026-11-03 07:58:59","reason":"Initial stub","source_content_hash":"sha256:c88cd894134edacd5cfca1895d38fc25b1f12ebfd86d7be98033863238b5414c","status":"🟡 Yellow","verification":"no_wiki"},"tests/contract_gaps.rs":{"last_updated":"2026-11-03 07:58:59","reason":"Initial stub","source_content_hash":"sha256:ece7c1aa0d6c311280d6f93a902d8faa9e4645295730299598cabfec469ed56d","status":"🟡 Yellow","verification":"no_wiki"},"tests/dogfood.rs":{"last_updated":"2026-11-03 07:58:59","reason":"Initial stub","source_content_hash":"sha256:c9659ec1dde825936912f1928d68fca671f27271a7a312bcaf48df69e03bcec8","status":"🟡 Yellow","verification":"no_wiki"},"tests/parity.rs":{"last_updated":"2026-11-03 07:58:59","reason":"Initial stub","source_content_hash":"sha256:691fe4be6f6bdda15e687ebf80686ca9484dacb037f3312d835f51c19219b223","status":"🟡 Yellow","verification":"no_wiki"},"tests/protocol_v07.rs":{"last_updated":"2026-11-03 11:39:32","reason":"Cover heal dry-run, init DIR, search-journal --file, and verify-wikis prefix (subid=cutover-verify)","source_content_hash":"sha256:bde6b07a7ea19a56c2b4e4371e51b33877ecda439393eb251017e7366a2c1b33","source_mtime_ns":"1793666321918020150","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:06e65b290b98df3ec732d6a0b156995cd87f9a62b7466f6c448c662c9c125512"},"tests/robustness.rs":{"last_updated":"2026-11-03 07:58:59","reason":"Initial stub","source_content_hash":"sha256:1216cf8aa62c31215bba2107580e3c6824bed736ebd212670f68d8a33ecf0103","status":"🟡 Yellow","verification":"no_wiki"},"tests/skip_maps.rs":{"last_updated":"2026-11-03 07:58:59","reason":"Initial stub","source_content_hash":"sha256:4c5706c7ce384e330517848e51c690369d71e00b22b25ddfa19c9485f9988c43","status":"🟡 Yellow","verification":"no_wiki"},"wikifier.sh":{"last_updated":"2026-11-03 08:06:46","reason":"Wiki describes the Rust binary launcher (subid=protocol-v07)","source_content_hash":"sha256:4f467e2117e8c361882054998cb0506717d9555e2aa570f0e176fc18d9c2f4aa","source_mtime_ns":"1793410024331635951","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:3876708eaec60862b9b8b6f73e2ae87e4abca2f228c456f973ddb7c9ee36854a"}},"version":2} \ No newline at end of file +{"entries":{"AGENTS.md":{"last_updated":"2026-11-03 05:34:35","reason":"Address the 3 Nov review findings (subid=review-2026-11-03)","source_content_hash":"sha256:6f68ebef06e7d09223b625a0e1f96bb94de5ddfc1ccb03649df64edbd35fcc83","source_mtime_ns":"1793684038394977086","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:109e3acac311f59909ef8b43dd1287904a70e27c50bce5b98229a44aa8d49e02"},"CHANGELOG.md":{"last_updated":"2026-11-03 05:34:35","reason":"Address the 3 Nov review findings (subid=review-2026-11-03)","source_content_hash":"sha256:e8ebc4db70717e7816a8c6282b66a19f70e99e66270d6e166436bbc8efcf80fd","source_mtime_ns":"1793683924472409471","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:b48347653b8beaaaf269916f07f1229d32714780de3371534ae9a1f31bf5f254"},"CLAUDE.md":{"last_updated":"2026-11-03 05:34:35","reason":"Address the 3 Nov review findings (subid=review-2026-11-03)","source_content_hash":"sha256:e7b296172e1b71a3051863b86830cbf6367a01b13c85ba3f94c447dcfc60abf4","source_mtime_ns":"1793684038394977086","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:1c4309d8e8dedd8ae9dc03f3b808bb7f3c582900d30f4f6cbf64931f2f4a63fe"},"CONTRIBUTING.md":{"last_updated":"2026-11-03 05:34:35","reason":"Address the 3 Nov review findings (subid=review-2026-11-03)","source_content_hash":"sha256:d090280cd3d1c3ab1cf342d69392471ed3835512ff7381bf4409bada57bbe89c","source_mtime_ns":"1793683924316411256","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:ec0bd9342b8e9dfeefaabc8dcddeb577f6bd4f1c760d7286b21d709c2a372b29"},"Findings/M5-Dogfood-Assessment-Report.md":{"last_updated":"2026-07-09 05:02:27","reason":"kept; stale yellow cleared","source_content_hash":"sha256:238435188b1699e5bcf4753b62a6fe55cb064413a291259f0b696b5bd3c1692f","source_mtime_ns":"1780519956000000000","status":"🟢 Green"},"Findings/M5-Dogfood-Progress.md":{"last_updated":"2026-11-03 08:06:46","reason":"Wiki marks this file as the historical M5 log (subid=protocol-v07)","source_content_hash":"sha256:ef55167e819806060dfd7dfe7cdfd4d02024ca97706a7faa243710e2630b65fe","source_mtime_ns":"1793408707259550076","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:b6b1fa4bbee0c7e8c4035cd925f6d07e1221ba483e3472878d5b88236c5cf7f9"},"Findings/M5.1-cross-hardening-analysis.md":{"last_updated":"2026-07-09 05:02:27","reason":"kept; stale yellow cleared","source_content_hash":"sha256:a2c2414e5b6c9b1a802bec8734a7924850d546cd7746a8087216f3685ca36cb4","source_mtime_ns":"1780519762000000000","status":"🟢 Green"},"Findings/Milestones-Overview.md":{"last_updated":"2026-07-09 05:02:27","reason":"kept; stale yellow cleared","source_content_hash":"sha256:f8b7030ec8980774cf3a2560ad7aee6537263eadf8374e650718f7c6793c6d50","source_mtime_ns":"1780800604000000000","status":"🟢 Green"},"Findings/p6_real_world_validation_report.md":{"last_updated":"2026-07-09 05:02:27","reason":"kept; stale yellow cleared","source_content_hash":"sha256:0468224675888f2747674324726c0ea9a5dddded39d9a1a50f22fc3357dafc0f","source_mtime_ns":"1780520008000000000","status":"🟢 Green"},"README.md":{"last_updated":"2026-11-03 05:34:35","reason":"Address the 3 Nov review findings (subid=review-2026-11-03)","source_content_hash":"sha256:9f1e7c23b08ea002d053ee722f979628e0d0bd98b79fc5ad7dc5ee122af2aef7","source_mtime_ns":"1793683696766187580","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:e2c2b383144e498adad61999d960fb6d862ec35220a5bf3b563784582199b49f"},"docs/spec.md":{"last_updated":"2026-11-03 08:00:11","reason":"Name the Rust binary and protocol v0.7 in the living principles (subid=protocol-v07)","source_content_hash":"sha256:31fe6b3b3d705628692eb0512cf44cbb21861ccfc5bb02afdd46d796ec066c6e","source_mtime_ns":"1793653008580824356","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:a58b929d25d510f15f9fe176d7823e852f6d089e398accc2e22e1ed189f260ac"},"skills/run.md":{"last_updated":"2026-11-03 05:34:35","reason":"Address the 3 Nov review findings (subid=review-2026-11-03)","source_content_hash":"sha256:51a921f43bf2eb02d9fba6c66ae0c9209fc1316aeb3b48f38b8035038dcf7e39","source_mtime_ns":"1793683697246186161","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:327e668717490617ac1b1285d36d0a699044918fb932504b23fc8b4c254cb74f"},"src/api.rs":{"last_updated":"2026-11-03 05:34:35","public_symbols":"MapRun\nVERSION\nMCP_TOOLS\nupdate_maps\nupdate_maps_opts\nupdate_maps_run\nmapped_edges\nload_dependencies\ndependency_report\nload_dependents\nlist_paths\nsession_bootstrap\nsuggest_next_actions\ncheck_changes\nprepare_edit\nrecord_change\nmark_green\nhealth\nread_health\nread_journal\ncache_status\nvalidate\ncycles\nverify_wikis\nrecord_changes\nrecord_changes_opts\nrecord_deletion\nheal_stubs\nprune_health\nprune_health_opts\nprune_pending\nstale_wikis\nprune_barrels\nmetrics_snapshot\nhealing_stats\nresolution_diagnostics\nbarrel_reports\nincremental_status\nserve_once\nserve_loop\nmonitor_loop\ndaemon_ctl\ndispatch\nCLI_COMMANDS\nensure_root","reason":"Address the 3 Nov review findings (subid=review-2026-11-03)","source_content_hash":"sha256:bb9cda81c00a44543c1888c3b0e7f493390d9baee9b8675ad4b1580799e53ca8","source_mtime_ns":"1793684003575439733","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:69ac1101639d23ac6893adf37e501a820e277ace0ddeaf3c2c80afba28557e54"},"src/artifacts.rs":{"last_updated":"2026-11-03 05:35:24","public_symbols":"HealthRow\nread_health\nwrite_health\nupsert_health_row\nJournalEntry\njournal_path\nread_journal\nparse_journal\nJournalWrite\nappend_journal\nread_pending\nfolder_card","reason":"Wiki names the public health and journal types (subid=review-w3)","source_content_hash":"sha256:37b29254a15e6c09e76a54d1c5a7d0aa7fc54b25044e17db84f345e55cbfa790","source_mtime_ns":"1793678007374624809","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:8b48f279108946f7861b81cd31e632977037c1c4fc84b7b17cbbd99eb2e74730"},"src/cache.rs":{"last_updated":"2026-11-03 05:35:24","public_symbols":"check_consistency\nrebuild\nConsistency\nSession\npayload_path\nby_src_path\ncommit_pending_path\nload_dependencies\nInternalEdge\nconfidence_of\nformat_edge_line\nDependencyView\nload_dependency_view\nload_dependents\nlist_prefix\nall_paths\nFileUpdate\nupsert_file\nremove_file\nindexed_rows\npayload_bytes\nstat_ns\nedges\nrel_string\nwrite_barrel_record\nprune_barrel_files","reason":"Wiki names the cache index surface (subid=review-w3)","source_content_hash":"sha256:79faa2b50bc95a573a1bd3555bcafbca271221af61849201df999cb229b4be63","source_mtime_ns":"1793678007370625780","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:5b6aed776d0ef82fb156bed8a6a507f9823ef6f2a873a19916bcd69f3900e106"},"src/cache_commit.rs":{"last_updated":"2026-11-03 05:34:35","public_symbols":"Consistency\nSession\ncheck_consistency\nrebuild","reason":"Address the 3 Nov review findings (subid=review-2026-11-03)","source_content_hash":"sha256:71ce0df880b4ba70abac47509ee22f03a4835d51f4ca91cd9249720abd5833e8","source_mtime_ns":"1793683981823718483","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:7eea56868007852f1bdc2eaca8f53687eb265a243958fe9681deb38279307526"},"src/jsonutil.rs":{"last_updated":"2026-11-03 05:34:35","public_symbols":"Json\nparse\nparse_jsonc\nstrip_json_comments","reason":"Address the 3 Nov review findings (subid=review-2026-11-03)","source_content_hash":"sha256:18a53e0aca984c6ba4e58132ec21a194cc08733a5e7ad4fc22c003c04a5174fd","source_mtime_ns":"1793683981803718735","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:0b67d6238457fa9e27bc3cd657576648f311bcb5095db70df8f5607deb699996"},"src/legacy_cache.rs":{"last_updated":"2026-11-03 05:34:35","public_symbols":"import_if_empty","reason":"Address the 3 Nov review findings (subid=review-2026-11-03)","source_content_hash":"sha256:5bfe43e2a43b138f28d5daeecb8576f90a9c82bf01569f4d9d3e84339f1a2507","source_mtime_ns":"1793683981803718735","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:a6fe6c79185a46cc4b30d215475bab28f517f6e5a6fd7bce6423c2792814aac8"},"src/lib.rs":{"last_updated":"2026-11-03 05:35:24","public_symbols":"api\nartifacts\ncache\njsonutil\nmaps_out\nmcp\nparse\nscope\nsha256\ntimeutil\ncache_status\ncheck_changes\nhealth\nlist_paths\nload_dependencies\nload_dependents\nmapped_edges\nmark_green\nprepare_edit\nread_health\nread_journal\nrecord_change\nsession_bootstrap\nsuggest_next_actions\nupdate_maps\nVERSION\nJson","reason":"Wiki names the crate re-exports (subid=review-w3)","source_content_hash":"sha256:d3afbbbe2faf0f68c7e4c4148f8aec42a728b1c529c8615689798a1a9f9b039d","source_mtime_ns":"1793678007370625780","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:d7e771a3ff63ab55a7967195e3b403dd75d5ae020a383adc56f0e5c09d7b03a0"},"src/main.rs":{"last_updated":"2026-11-03 05:34:35","reason":"Address the 3 Nov review findings (subid=review-2026-11-03)","source_content_hash":"sha256:c29545879d40673183e8e9477dc2f80ec56326a1f33f19a640350f207a7a94e6","source_mtime_ns":"1793683990351610198","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:db6e1cd5beeb90e5a65b3e00a37ee2c73ea4c53750771151e655459a1b823ff5"},"src/maps_out.rs":{"last_updated":"2026-11-03 05:35:24","public_symbols":"write\nwrite_status\nread_status","reason":"Wiki names read_status (subid=review-w3)","source_content_hash":"sha256:6f4523bfae50356ce8c90f2c840f04a79eb99c4ce889e985dd80432cba35d410","source_mtime_ns":"1793678007370625780","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:d441c395fe565828a87c54d5545e4c89e4ec4cfc49308a08525e6dcf3c744e75"},"src/mcp.rs":{"last_updated":"2026-11-03 05:34:35","public_symbols":"tool_schema\nhandle_message\nserve_stdio","reason":"Address the 3 Nov review findings (subid=review-2026-11-03)","source_content_hash":"sha256:2b6a92af8522c7a5047fe64beee86bf1301992ec07d2e65263fe93e593bfc3c5","source_mtime_ns":"1793683982335712018","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:8414b1757bf3acc39a66a52443e9625cb027173b2783d996e39e9f707a9a1933"},"src/parse.rs":{"last_updated":"2026-11-03 05:34:35","public_symbols":"RawImport\nKind\nScan\nlanguage\nread_utf8\nscan_file_warned\nscan_file\nresolve_project\nrust_public_symbols\nresolve_python_spec\nFactEdge\nFileFacts\nAnalysis\nanalyze_files\nanalyze_changed\nedges_for_files","reason":"Address the 3 Nov review findings (subid=review-2026-11-03)","source_content_hash":"sha256:364a6fe8cdc730f57c8be17f92d8adc704bfcc1ac8a101b935f40f4231c3e447","source_mtime_ns":"1793683982995703683","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:34dbd74cd48200e734284c5b999ffd93965537cee71444261151d81647ea8e5d"},"src/proc.rs":{"last_updated":"2026-11-03 07:58:59","reason":"Initial stub","source_content_hash":"sha256:a4751d64c912e3eb9b5608410b1555a0495ee7114174ea03500fffd5336ea776","status":"🟡 Yellow","verification":"no_wiki"},"src/project_lock.rs":{"last_updated":"2026-11-03 05:35:15","public_symbols":"ProjectLock","reason":"Wiki names ProjectLock after Rust symbol checks (subid=review-w3)","source_content_hash":"sha256:d7f0448fccc96b9f9f4294f0abadb9044ee24551418180530eafd331b71b4ab5","source_mtime_ns":"1793678007370625780","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:892ea30f94deaf281fd74d94946c81991c90e32c633e2f4fc1cf868ec4c0b60d"},"src/scope.rs":{"last_updated":"2026-11-03 07:58:59","reason":"Initial stub","source_content_hash":"sha256:4d80b5432823864d0b95db68a558cd25aa0fe7d35b3f5796fbbdf107bee709aa","status":"🟡 Yellow","verification":"no_wiki"},"src/sha256.rs":{"last_updated":"2026-11-03 07:58:59","reason":"Initial stub","source_content_hash":"sha256:a68691e08d8260937e0f6c2534faf02ef58bb8332efa2cdd1b0a59f8fdf0d77d","status":"🟡 Yellow","verification":"no_wiki"},"src/timeutil.rs":{"last_updated":"2026-11-03 07:58:59","reason":"Initial stub","source_content_hash":"sha256:a90359c6ab1c816b7d7b2f9ce12c799d3f6c8d560de0ed491c7f069be6fbaa14","status":"🟡 Yellow","verification":"no_wiki"},"tests/behaviour_gaps.rs":{"last_updated":"2026-11-03 05:34:35","reason":"Address the 3 Nov review findings (subid=review-2026-11-03)","source_content_hash":"sha256:a97b21cd9a5c7faa265b8537920f140935718c02c0d79c81c63b17a74cd830a5","source_mtime_ns":"1793684003387442177","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:0b89bd19ab27fb71a6975e099d5d6ac5c47b4a24cd3caab85dcff9ebce75f43e"},"tests/cache_and_artifacts.rs":{"last_updated":"2026-11-03 07:58:59","reason":"Initial stub","source_content_hash":"sha256:e147bf88f37a4748bde1c20657b911da6ca329b99867d854eea6f30a7562fd30","status":"🟡 Yellow","verification":"no_wiki"},"tests/cli_surface.rs":{"last_updated":"2026-11-03 07:58:59","reason":"Initial stub","source_content_hash":"sha256:c88cd894134edacd5cfca1895d38fc25b1f12ebfd86d7be98033863238b5414c","status":"🟡 Yellow","verification":"no_wiki"},"tests/contract_gaps.rs":{"last_updated":"2026-11-03 07:58:59","reason":"Initial stub","source_content_hash":"sha256:ece7c1aa0d6c311280d6f93a902d8faa9e4645295730299598cabfec469ed56d","status":"🟡 Yellow","verification":"no_wiki"},"tests/dogfood.rs":{"last_updated":"2026-11-03 05:34:35","reason":"Address the 3 Nov review findings (subid=review-2026-11-03)","source_content_hash":"sha256:4efe688e0e9b58c8f0b0ca986dad90dfd837c4e36816579353023ede19044d3a","source_mtime_ns":"1793683337392806763","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:9546cc2062f19584c348419322f07d25d353c7f8ba0257e992372f63d798c020"},"tests/parity.rs":{"last_updated":"2026-11-03 05:34:35","reason":"Address the 3 Nov review findings (subid=review-2026-11-03)","source_content_hash":"sha256:4eb986e4a3f922d917672f2df45644726f1f3a8cd132b0bf089740c91678a6d0","source_mtime_ns":"1793683314773282057","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:32753c244642df6e08f8578c6203f77225363783c15bc57c33a84f03989f2378"},"tests/protocol_v07.rs":{"last_updated":"2026-11-03 05:34:35","reason":"Address the 3 Nov review findings (subid=review-2026-11-03)","source_content_hash":"sha256:6d20f5a2375f7a06c6dc12e4d4ad3aea75bdca4b724e73cc6cdedc346f122c04","source_mtime_ns":"1793683731550052721","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:c382f216277633704637f8b520e3814b7ae53fb3fce1b0d3c24af1238a286190"},"tests/robustness.rs":{"last_updated":"2026-11-03 07:58:59","reason":"Initial stub","source_content_hash":"sha256:1216cf8aa62c31215bba2107580e3c6824bed736ebd212670f68d8a33ecf0103","status":"🟡 Yellow","verification":"no_wiki"},"tests/skip_maps.rs":{"last_updated":"2026-11-03 07:58:59","reason":"Initial stub","source_content_hash":"sha256:4c5706c7ce384e330517848e51c690369d71e00b22b25ddfa19c9485f9988c43","status":"🟡 Yellow","verification":"no_wiki"},"wikifier.sh":{"last_updated":"2026-11-03 08:06:46","reason":"Wiki describes the Rust binary launcher (subid=protocol-v07)","source_content_hash":"sha256:4f467e2117e8c361882054998cb0506717d9555e2aa570f0e176fc18d9c2f4aa","source_mtime_ns":"1793410024331635951","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:3876708eaec60862b9b8b6f73e2ae87e4abca2f228c456f973ddb7c9ee36854a"}},"version":2} \ No newline at end of file diff --git a/file_health.md b/file_health.md index b3a3558..378a78d 100644 --- a/file_health.md +++ b/file_health.md @@ -2,42 +2,41 @@ | File | Status | Last Updated | Reason / Intent | |------|--------|--------------|-----------------| -| .github/workflows/ci.yml | 🟢 Green | 2026-11-03 11:39:32 | Check the empty Cargo dependency list with awk (subid=cutover-verify) | -| AGENTS.md | 🟢 Green | 2026-11-03 11:25:28 | Add the agent entry point: live Rust binary, daily loop, and which logs are historical (subid=agents-md) | -| CHANGELOG.md | 🟢 Green | 2026-11-03 11:39:32 | Note dry-run, init DIR, the journal file filter, and the Python-free dependency check (subid=cutover-verify) | -| CLAUDE.md | 🟢 Green | 2026-11-03 11:25:28 | Point at AGENTS.md, describe the library file tree, and return JSON success false (subid=agents-md) | -| CONTRIBUTING.md | 🟢 Green | 2026-11-03 08:00:11 | Point new commands at src/api.rs and src/main.rs (subid=protocol-v07) | +| AGENTS.md | 🟢 Green | 2026-11-03 05:34:35 | Address the 3 Nov review findings (subid=review-2026-11-03) | +| CHANGELOG.md | 🟢 Green | 2026-11-03 05:34:35 | Address the 3 Nov review findings (subid=review-2026-11-03) | +| CLAUDE.md | 🟢 Green | 2026-11-03 05:34:35 | Address the 3 Nov review findings (subid=review-2026-11-03) | +| CONTRIBUTING.md | 🟢 Green | 2026-11-03 05:34:35 | Address the 3 Nov review findings (subid=review-2026-11-03) | | Findings/M5-Dogfood-Assessment-Report.md | 🟢 Green | 2026-07-09 05:02:27 | kept; stale yellow cleared | | Findings/M5-Dogfood-Progress.md | 🟢 Green | 2026-11-03 08:06:46 | Wiki marks this file as the historical M5 log (subid=protocol-v07) | | Findings/M5.1-cross-hardening-analysis.md | 🟢 Green | 2026-07-09 05:02:27 | kept; stale yellow cleared | | Findings/Milestones-Overview.md | 🟢 Green | 2026-07-09 05:02:27 | kept; stale yellow cleared | | Findings/p6_real_world_validation_report.md | 🟢 Green | 2026-07-09 05:02:27 | kept; stale yellow cleared | -| README.md | 🟢 Green | 2026-11-03 11:25:28 | Point agents at AGENTS.md and skills/run.md (subid=agents-md) | +| README.md | 🟢 Green | 2026-11-03 05:34:35 | Address the 3 Nov review findings (subid=review-2026-11-03) | | docs/spec.md | 🟢 Green | 2026-11-03 08:00:11 | Name the Rust binary and protocol v0.7 in the living principles (subid=protocol-v07) | -| monitored_paths.txt | 🟢 Green | 2026-11-03 11:25:28 | Watch AGENTS.md with the other live docs (subid=agents-md) | -| skills/run.md | 🟢 Green | 2026-11-03 11:39:32 | Match the command table to the Rust binary for daemon, init, verify-wikis, search-journal, and issues (subid=cutover-verify) | -| src/api.rs | 🟢 Green | 2026-11-03 11:39:32 | Honor heal-stubs --dry-run, return seed-health write errors, limit verify-wikis to a path, and apply search-journal --file (subid=cutover-verify) | -| src/artifacts.rs | 🟢 Green | 2026-11-03 08:00:11 | Finish the 4.7 agent contract in the Rust binary: map artifacts, deletion, update-maps flags, record-changes, dependency flags, wiki checks, legacy cache import, serve/monitor/daemon, MCP errors, project lock, and pending cleanup on mark-green (subid=protocol-v07) | -| src/cache.rs | 🟢 Green | 2026-11-03 08:00:11 | Finish the 4.7 agent contract in the Rust binary: map artifacts, deletion, update-maps flags, record-changes, dependency flags, wiki checks, legacy cache import, serve/monitor/daemon, MCP errors, project lock, and pending cleanup on mark-green (subid=protocol-v07) | -| src/cache_commit.rs | 🟢 Green | 2026-11-03 08:00:11 | Finish the 4.7 agent contract in the Rust binary: map artifacts, deletion, update-maps flags, record-changes, dependency flags, wiki checks, legacy cache import, serve/monitor/daemon, MCP errors, project lock, and pending cleanup on mark-green (subid=protocol-v07) | -| src/jsonutil.rs | 🟡 Yellow | 2026-11-03 07:58:59 | Initial stub | -| src/legacy_cache.rs | 🟢 Green | 2026-11-03 08:00:12 | Finish the 4.7 agent contract in the Rust binary: map artifacts, deletion, update-maps flags, record-changes, dependency flags, wiki checks, legacy cache import, serve/monitor/daemon, MCP errors, project lock, and pending cleanup on mark-green (subid=protocol-v07) | -| src/lib.rs | 🟢 Green | 2026-11-03 08:00:12 | Finish the 4.7 agent contract in the Rust binary: map artifacts, deletion, update-maps flags, record-changes, dependency flags, wiki checks, legacy cache import, serve/monitor/daemon, MCP errors, project lock, and pending cleanup on mark-green (subid=protocol-v07) | -| src/main.rs | 🟢 Green | 2026-11-03 11:39:32 | Use init DIR as the project root unless --target is set (subid=cutover-verify) | -| src/maps_out.rs | 🟢 Green | 2026-11-03 08:04:45 | Write a file tree into library.md so the dashboard can show the map (subid=protocol-v07) | -| src/mcp.rs | 🟢 Green | 2026-11-03 08:00:12 | Finish the 4.7 agent contract in the Rust binary: map artifacts, deletion, update-maps flags, record-changes, dependency flags, wiki checks, legacy cache import, serve/monitor/daemon, MCP errors, project lock, and pending cleanup on mark-green (subid=protocol-v07) | -| src/parse.rs | 🟢 Green | 2026-11-03 08:00:12 | Finish the 4.7 agent contract in the Rust binary: map artifacts, deletion, update-maps flags, record-changes, dependency flags, wiki checks, legacy cache import, serve/monitor/daemon, MCP errors, project lock, and pending cleanup on mark-green (subid=protocol-v07) | +| skills/run.md | 🟢 Green | 2026-11-03 05:34:35 | Address the 3 Nov review findings (subid=review-2026-11-03) | +| src/api.rs | 🟢 Green | 2026-11-03 05:34:35 | Address the 3 Nov review findings (subid=review-2026-11-03) | +| src/artifacts.rs | 🟢 Green | 2026-11-03 05:35:24 | Wiki names the public health and journal types (subid=review-w3) | +| src/cache.rs | 🟢 Green | 2026-11-03 05:35:24 | Wiki names the cache index surface (subid=review-w3) | +| src/cache_commit.rs | 🟢 Green | 2026-11-03 05:34:35 | Address the 3 Nov review findings (subid=review-2026-11-03) | +| src/jsonutil.rs | 🟢 Green | 2026-11-03 05:34:35 | Address the 3 Nov review findings (subid=review-2026-11-03) | +| src/legacy_cache.rs | 🟢 Green | 2026-11-03 05:34:35 | Address the 3 Nov review findings (subid=review-2026-11-03) | +| src/lib.rs | 🟢 Green | 2026-11-03 05:35:24 | Wiki names the crate re-exports (subid=review-w3) | +| src/main.rs | 🟢 Green | 2026-11-03 05:34:35 | Address the 3 Nov review findings (subid=review-2026-11-03) | +| src/maps_out.rs | 🟢 Green | 2026-11-03 05:35:24 | Wiki names read_status (subid=review-w3) | +| src/mcp.rs | 🟢 Green | 2026-11-03 05:34:35 | Address the 3 Nov review findings (subid=review-2026-11-03) | +| src/parse.rs | 🟢 Green | 2026-11-03 05:34:35 | Address the 3 Nov review findings (subid=review-2026-11-03) | | src/proc.rs | 🟡 Yellow | 2026-11-03 07:58:59 | Initial stub | -| src/project_lock.rs | 🟢 Green | 2026-11-03 08:00:12 | Finish the 4.7 agent contract in the Rust binary: map artifacts, deletion, update-maps flags, record-changes, dependency flags, wiki checks, legacy cache import, serve/monitor/daemon, MCP errors, project lock, and pending cleanup on mark-green (subid=protocol-v07) | +| src/project_lock.rs | 🟢 Green | 2026-11-03 05:35:15 | Wiki names ProjectLock after Rust symbol checks (subid=review-w3) | | src/scope.rs | 🟡 Yellow | 2026-11-03 07:58:59 | Initial stub | | src/sha256.rs | 🟡 Yellow | 2026-11-03 07:58:59 | Initial stub | | src/timeutil.rs | 🟡 Yellow | 2026-11-03 07:58:59 | Initial stub | +| tests/behaviour_gaps.rs | 🟢 Green | 2026-11-03 05:34:35 | Address the 3 Nov review findings (subid=review-2026-11-03) | | tests/cache_and_artifacts.rs | 🟡 Yellow | 2026-11-03 07:58:59 | Initial stub | | tests/cli_surface.rs | 🟡 Yellow | 2026-11-03 07:58:59 | Initial stub | | tests/contract_gaps.rs | 🟡 Yellow | 2026-11-03 07:58:59 | Initial stub | -| tests/dogfood.rs | 🟡 Yellow | 2026-11-03 07:58:59 | Initial stub | -| tests/parity.rs | 🟡 Yellow | 2026-11-03 07:58:59 | Initial stub | -| tests/protocol_v07.rs | 🟢 Green | 2026-11-03 11:39:32 | Cover heal dry-run, init DIR, search-journal --file, and verify-wikis prefix (subid=cutover-verify) | +| tests/dogfood.rs | 🟢 Green | 2026-11-03 05:34:35 | Address the 3 Nov review findings (subid=review-2026-11-03) | +| tests/parity.rs | 🟢 Green | 2026-11-03 05:34:35 | Address the 3 Nov review findings (subid=review-2026-11-03) | +| tests/protocol_v07.rs | 🟢 Green | 2026-11-03 05:34:35 | Address the 3 Nov review findings (subid=review-2026-11-03) | | tests/robustness.rs | 🟡 Yellow | 2026-11-03 07:58:59 | Initial stub | | tests/skip_maps.rs | 🟡 Yellow | 2026-11-03 07:58:59 | Initial stub | | wikifier.sh | 🟢 Green | 2026-11-03 08:06:46 | Wiki describes the Rust binary launcher (subid=protocol-v07) | diff --git a/journal/2026/11/03.md b/journal/2026/11/03.md index 2df897f..422e6a3 100644 --- a/journal/2026/11/03.md +++ b/journal/2026/11/03.md @@ -158,3 +158,99 @@ **File:** tests/protocol_v07.rs **Reason:** Cover heal dry-run, init DIR, search-journal --file, and verify-wikis prefix (subid=cutover-verify) +## [2026-11-03 05:34:35] record-deletion +**File:** tests/test_gap_closure.py.wiki.md +**Reason:** Removed the wiki for a deleted Python test (subid=review-tidy) + +## [2026-11-03 05:34:35] record-change +**File:** .github/workflows/ci.yml +**Reason:** Address the 3 Nov review findings (subid=review-2026-11-03) + +## [2026-11-03 05:34:35] record-change +**File:** .github/workflows/publish.yml +**Reason:** Address the 3 Nov review findings (subid=review-2026-11-03) + +## [2026-11-03 05:34:35] record-change +**File:** AGENTS.md +**Reason:** Address the 3 Nov review findings (subid=review-2026-11-03) + +## [2026-11-03 05:34:35] record-change +**File:** CHANGELOG.md +**Reason:** Address the 3 Nov review findings (subid=review-2026-11-03) + +## [2026-11-03 05:34:35] record-change +**File:** CLAUDE.md +**Reason:** Address the 3 Nov review findings (subid=review-2026-11-03) + +## [2026-11-03 05:34:35] record-change +**File:** CONTRIBUTING.md +**Reason:** Address the 3 Nov review findings (subid=review-2026-11-03) + +## [2026-11-03 05:34:35] record-deletion +**File:** MODULARIZATION_PLAN.md +**Reason:** Address the 3 Nov review findings (subid=review-2026-11-03) + +## [2026-11-03 05:34:35] record-change +**File:** Findings/archive/MODULARIZATION_PLAN.md +**Reason:** Address the 3 Nov review findings (subid=review-2026-11-03) + +## [2026-11-03 05:34:35] record-deletion +**File:** SIMPLIFICATION_PLAN_AND_LOG.md +**Reason:** Address the 3 Nov review findings (subid=review-2026-11-03) + +## [2026-11-03 05:34:35] record-change +**File:** Findings/archive/SIMPLIFICATION_PLAN_AND_LOG.md +**Reason:** Address the 3 Nov review findings (subid=review-2026-11-03) + +## [2026-11-03 05:34:35] record-change +**File:** README.md +**Reason:** Address the 3 Nov review findings (subid=review-2026-11-03) + +## [2026-11-03 05:34:35] record-change +**File:** skills/run.md +**Reason:** Address the 3 Nov review findings (subid=review-2026-11-03) + +## [2026-11-03 05:34:35] record-change +**File:** src/api.rs +**Reason:** Address the 3 Nov review findings (subid=review-2026-11-03) + +## [2026-11-03 05:34:35] record-change +**File:** src/cache_commit.rs +**Reason:** Address the 3 Nov review findings (subid=review-2026-11-03) + +## [2026-11-03 05:34:35] record-change +**File:** src/jsonutil.rs +**Reason:** Address the 3 Nov review findings (subid=review-2026-11-03) + +## [2026-11-03 05:34:35] record-change +**File:** src/legacy_cache.rs +**Reason:** Address the 3 Nov review findings (subid=review-2026-11-03) + +## [2026-11-03 05:34:35] record-change +**File:** src/main.rs +**Reason:** Address the 3 Nov review findings (subid=review-2026-11-03) + +## [2026-11-03 05:34:35] record-change +**File:** src/mcp.rs +**Reason:** Address the 3 Nov review findings (subid=review-2026-11-03) + +## [2026-11-03 05:34:35] record-change +**File:** src/parse.rs +**Reason:** Address the 3 Nov review findings (subid=review-2026-11-03) + +## [2026-11-03 05:34:35] record-change +**File:** tests/behaviour_gaps.rs +**Reason:** Address the 3 Nov review findings (subid=review-2026-11-03) + +## [2026-11-03 05:34:35] record-change +**File:** tests/dogfood.rs +**Reason:** Address the 3 Nov review findings (subid=review-2026-11-03) + +## [2026-11-03 05:34:35] record-change +**File:** tests/parity.rs +**Reason:** Address the 3 Nov review findings (subid=review-2026-11-03) + +## [2026-11-03 05:34:35] record-change +**File:** tests/protocol_v07.rs +**Reason:** Address the 3 Nov review findings (subid=review-2026-11-03) + diff --git a/library.md b/library.md index ee9d38d..6f6eed5 100644 --- a/library.md +++ b/library.md @@ -2,7 +2,7 @@ Generated by `wikifier update-maps`. Agents orient with the root folder card and `list-paths`, not this mermaid. -Files: 24. Edges: 32. +Files: 25. Edges: 74. ## File Tree @@ -23,6 +23,7 @@ src/project_lock.rs src/scope.rs src/sha256.rs src/timeutil.rs +tests/behaviour_gaps.rs tests/cache_and_artifacts.rs tests/cli_surface.rs tests/contract_gaps.rs @@ -40,48 +41,101 @@ flowchart LR n0["src/api.rs"] --> n1["src/artifacts.rs"] n0["src/api.rs"] --> n2["src/cache.rs"] n0["src/api.rs"] --> n3["src/jsonutil.rs"] - n0["src/api.rs"] --> n4["src/scope.rs"] - n0["src/api.rs"] --> n5["src/sha256.rs"] - n0["src/api.rs"] --> n6["src/timeutil.rs"] + n0["src/api.rs"] --> n4["src/legacy_cache.rs"] + n0["src/api.rs"] --> n5["src/maps_out.rs"] + n0["src/api.rs"] --> n6["src/parse.rs"] + n0["src/api.rs"] --> n7["src/proc.rs"] + n0["src/api.rs"] --> n8["src/project_lock.rs"] + n0["src/api.rs"] --> n9["src/scope.rs"] + n0["src/api.rs"] --> n10["src/sha256.rs"] + n0["src/api.rs"] --> n11["src/timeutil.rs"] n1["src/artifacts.rs"] --> n3["src/jsonutil.rs"] - n1["src/artifacts.rs"] --> n6["src/timeutil.rs"] - n2["src/cache.rs"] --> n4["src/scope.rs"] - n7["src/cache_commit.rs"] --> n2["src/cache.rs"] - n8["src/legacy_cache.rs"] --> n2["src/cache.rs"] - n8["src/legacy_cache.rs"] --> n3["src/jsonutil.rs"] - n9["src/lib.rs"] --> n0["src/api.rs"] - n9["src/lib.rs"] --> n1["src/artifacts.rs"] - n9["src/lib.rs"] --> n2["src/cache.rs"] - n9["src/lib.rs"] --> n7["src/cache_commit.rs"] - n9["src/lib.rs"] --> n3["src/jsonutil.rs"] - n9["src/lib.rs"] --> n8["src/legacy_cache.rs"] - n9["src/lib.rs"] --> n10["src/maps_out.rs"] - n9["src/lib.rs"] --> n11["src/mcp.rs"] - n9["src/lib.rs"] --> n12["src/parse.rs"] - n9["src/lib.rs"] --> n13["src/proc.rs"] - n9["src/lib.rs"] --> n14["src/project_lock.rs"] - n9["src/lib.rs"] --> n4["src/scope.rs"] - n9["src/lib.rs"] --> n5["src/sha256.rs"] - n9["src/lib.rs"] --> n6["src/timeutil.rs"] - n10["src/maps_out.rs"] --> n2["src/cache.rs"] - n10["src/maps_out.rs"] --> n3["src/jsonutil.rs"] - n11["src/mcp.rs"] --> n0["src/api.rs"] - n11["src/mcp.rs"] --> n3["src/jsonutil.rs"] - n12["src/parse.rs"] --> n3["src/jsonutil.rs"] - n12["src/parse.rs"] --> n4["src/scope.rs"] + n1["src/artifacts.rs"] --> n11["src/timeutil.rs"] + n2["src/cache.rs"] --> n12["src/cache_commit.rs"] + n2["src/cache.rs"] --> n3["src/jsonutil.rs"] + n2["src/cache.rs"] --> n9["src/scope.rs"] + n12["src/cache_commit.rs"] --> n2["src/cache.rs"] + n12["src/cache_commit.rs"] --> n3["src/jsonutil.rs"] + n12["src/cache_commit.rs"] --> n7["src/proc.rs"] + n4["src/legacy_cache.rs"] --> n2["src/cache.rs"] + n4["src/legacy_cache.rs"] --> n3["src/jsonutil.rs"] + n4["src/legacy_cache.rs"] --> n5["src/maps_out.rs"] + n4["src/legacy_cache.rs"] --> n8["src/project_lock.rs"] + n13["src/lib.rs"] --> n0["src/api.rs"] + n13["src/lib.rs"] --> n1["src/artifacts.rs"] + n13["src/lib.rs"] --> n2["src/cache.rs"] + n13["src/lib.rs"] --> n12["src/cache_commit.rs"] + n13["src/lib.rs"] --> n3["src/jsonutil.rs"] + n13["src/lib.rs"] --> n4["src/legacy_cache.rs"] + n13["src/lib.rs"] --> n5["src/maps_out.rs"] + n13["src/lib.rs"] --> n14["src/mcp.rs"] + n13["src/lib.rs"] --> n6["src/parse.rs"] + n13["src/lib.rs"] --> n7["src/proc.rs"] + n13["src/lib.rs"] --> n8["src/project_lock.rs"] + n13["src/lib.rs"] --> n9["src/scope.rs"] + n13["src/lib.rs"] --> n10["src/sha256.rs"] + n13["src/lib.rs"] --> n11["src/timeutil.rs"] + n15["src/main.rs"] --> n0["src/api.rs"] + n15["src/main.rs"] --> n14["src/mcp.rs"] + n15["src/main.rs"] --> n11["src/timeutil.rs"] + n5["src/maps_out.rs"] --> n2["src/cache.rs"] + n5["src/maps_out.rs"] --> n3["src/jsonutil.rs"] + n14["src/mcp.rs"] --> n0["src/api.rs"] + n14["src/mcp.rs"] --> n3["src/jsonutil.rs"] + n6["src/parse.rs"] --> n3["src/jsonutil.rs"] + n6["src/parse.rs"] --> n9["src/scope.rs"] + n8["src/project_lock.rs"] --> n7["src/proc.rs"] + n16["tests/behaviour_gaps.rs"] --> n2["src/cache.rs"] + n16["tests/behaviour_gaps.rs"] --> n3["src/jsonutil.rs"] + n16["tests/behaviour_gaps.rs"] --> n13["src/lib.rs"] + n16["tests/behaviour_gaps.rs"] --> n14["src/mcp.rs"] + n17["tests/cache_and_artifacts.rs"] --> n2["src/cache.rs"] + n17["tests/cache_and_artifacts.rs"] --> n13["src/lib.rs"] + n18["tests/cli_surface.rs"] --> n0["src/api.rs"] + n18["tests/cli_surface.rs"] --> n3["src/jsonutil.rs"] + n18["tests/cli_surface.rs"] --> n13["src/lib.rs"] + n19["tests/contract_gaps.rs"] --> n0["src/api.rs"] + n19["tests/contract_gaps.rs"] --> n3["src/jsonutil.rs"] + n19["tests/contract_gaps.rs"] --> n13["src/lib.rs"] + n20["tests/dogfood.rs"] --> n3["src/jsonutil.rs"] + n20["tests/dogfood.rs"] --> n13["src/lib.rs"] + n21["tests/parity.rs"] --> n13["src/lib.rs"] + n22["tests/protocol_v07.rs"] --> n0["src/api.rs"] + n22["tests/protocol_v07.rs"] --> n2["src/cache.rs"] + n22["tests/protocol_v07.rs"] --> n3["src/jsonutil.rs"] + n22["tests/protocol_v07.rs"] --> n14["src/mcp.rs"] + n23["tests/robustness.rs"] --> n0["src/api.rs"] + n23["tests/robustness.rs"] --> n2["src/cache.rs"] + n23["tests/robustness.rs"] --> n3["src/jsonutil.rs"] + n23["tests/robustness.rs"] --> n13["src/lib.rs"] + n24["tests/skip_maps.rs"] --> n0["src/api.rs"] + n24["tests/skip_maps.rs"] --> n2["src/cache.rs"] + n24["tests/skip_maps.rs"] --> n3["src/jsonutil.rs"] + n24["tests/skip_maps.rs"] --> n13["src/lib.rs"] ``` | Source | Depends on | |--------|------------| -| src/api.rs | src/artifacts.rs, src/cache.rs, src/jsonutil.rs, src/scope.rs, src/sha256.rs, src/timeutil.rs | +| src/api.rs | src/artifacts.rs, src/cache.rs, src/jsonutil.rs, src/legacy_cache.rs, src/maps_out.rs, src/parse.rs, src/proc.rs, src/project_lock.rs, src/scope.rs, src/sha256.rs, src/timeutil.rs | | src/artifacts.rs | src/jsonutil.rs, src/timeutil.rs | -| src/cache.rs | src/scope.rs | -| src/cache_commit.rs | src/cache.rs | -| src/legacy_cache.rs | src/cache.rs, src/jsonutil.rs | +| src/cache.rs | src/cache_commit.rs, src/jsonutil.rs, src/scope.rs | +| src/cache_commit.rs | src/cache.rs, src/jsonutil.rs, src/proc.rs | +| src/legacy_cache.rs | src/cache.rs, src/jsonutil.rs, src/maps_out.rs, src/project_lock.rs | | src/lib.rs | src/api.rs, src/artifacts.rs, src/cache.rs, src/cache_commit.rs, src/jsonutil.rs, src/legacy_cache.rs, src/maps_out.rs, src/mcp.rs, src/parse.rs, src/proc.rs, src/project_lock.rs, src/scope.rs, src/sha256.rs, src/timeutil.rs | +| src/main.rs | src/api.rs, src/mcp.rs, src/timeutil.rs | | src/maps_out.rs | src/cache.rs, src/jsonutil.rs | | src/mcp.rs | src/api.rs, src/jsonutil.rs | | src/parse.rs | src/jsonutil.rs, src/scope.rs | +| src/project_lock.rs | src/proc.rs | +| tests/behaviour_gaps.rs | src/cache.rs, src/jsonutil.rs, src/lib.rs, src/mcp.rs | +| tests/cache_and_artifacts.rs | src/cache.rs, src/lib.rs | +| tests/cli_surface.rs | src/api.rs, src/jsonutil.rs, src/lib.rs | +| tests/contract_gaps.rs | src/api.rs, src/jsonutil.rs, src/lib.rs | +| tests/dogfood.rs | src/jsonutil.rs, src/lib.rs | +| tests/parity.rs | src/lib.rs | +| tests/protocol_v07.rs | src/api.rs, src/cache.rs, src/jsonutil.rs, src/mcp.rs | +| tests/robustness.rs | src/api.rs, src/cache.rs, src/jsonutil.rs, src/lib.rs | +| tests/skip_maps.rs | src/api.rs, src/cache.rs, src/jsonutil.rs, src/lib.rs | Edges with a conditional, dynamic, barrel, or lower-confidence flag: 0. Run `wikifier cycles` for a break hint on each strongly connected component. diff --git a/src/api.rs b/src/api.rs index b9ad282..9bb9e67 100644 --- a/src/api.rs +++ b/src/api.rs @@ -457,24 +457,6 @@ pub fn list_paths(root: &Path, prefix: &str) -> Json { let _ = crate::legacy_cache::import_if_empty(root); let paths = cache::list_prefix(root, prefix).unwrap_or_default(); let pref = prefix.trim_matches('/'); - let mut dirs: BTreeMap = BTreeMap::new(); - let mut files = Vec::new(); - for path in &paths { - let rest = if pref.is_empty() { path.as_str() } else if let Some(r) = path.strip_prefix(pref) { r.trim_start_matches('/') } else { continue }; - if rest.is_empty() { - files.push(path.clone()); - continue; - } - if let Some((dir, _)) = rest.split_once('/') { - *dirs.entry(dir.to_string()).or_insert(0) += 1; - } else if !pref.is_empty() && path == pref { - files.push(path.clone()); - } else if pref.is_empty() { - files.push(path.clone()); - } else { - files.push(path.clone()); - } - } // depth-1: a path directly under prefix is a file; a longer path contributes to its first segment let mut direct_files = Vec::new(); let mut dir_counts: BTreeMap = BTreeMap::new(); @@ -494,7 +476,6 @@ pub fn list_paths(root: &Path, prefix: &str) -> Json { direct_files.push(path.clone()); } } - let _ = (dirs, files); ok(vec![ ("prefix", Json::String(pref.into())), ("files", Json::Array(direct_files.into_iter().map(Json::String).collect())), @@ -1170,50 +1151,69 @@ fn cycle_pairs(root: &Path) -> BTreeSet<(String, String)> { pairs } -fn tarjan(graph: &BTreeMap>) -> Vec> { - fn strong(node: &str, graph: &BTreeMap>, index: &mut i64, stack: &mut Vec, on: &mut BTreeSet, idx: &mut BTreeMap, low: &mut BTreeMap, out: &mut Vec>) { - idx.insert(node.to_string(), *index); - low.insert(node.to_string(), *index); - *index += 1; - stack.push(node.to_string()); - on.insert(node.to_string()); - for nxt in graph.get(node).into_iter().flatten() { - if !idx.contains_key(nxt) { - strong(nxt, graph, index, stack, on, idx, low, out); - let ln = *low.get(node).unwrap_or(&0); - let lnxt = *low.get(nxt).unwrap_or(&0); - low.insert(node.to_string(), ln.min(lnxt)); - } else if on.contains(nxt) { - let ln = *low.get(node).unwrap_or(&0); - let inxt = *idx.get(nxt).unwrap_or(&0); - low.insert(node.to_string(), ln.min(inxt)); +struct Tarjan<'a> { + graph: &'a BTreeMap>, + index: i64, + stack: Vec, + on: BTreeSet, + idx: BTreeMap, + low: BTreeMap, + out: Vec>, +} + +impl Tarjan<'_> { + fn strong(&mut self, node: &str) { + self.idx.insert(node.to_string(), self.index); + self.low.insert(node.to_string(), self.index); + self.index += 1; + self.stack.push(node.to_string()); + self.on.insert(node.to_string()); + let next: Vec = self.graph.get(node).into_iter().flatten().cloned().collect(); + for nxt in next { + if !self.idx.contains_key(&nxt) { + self.strong(&nxt); + let ln = *self.low.get(node).unwrap_or(&0); + let lnxt = *self.low.get(&nxt).unwrap_or(&0); + self.low.insert(node.to_string(), ln.min(lnxt)); + } else if self.on.contains(&nxt) { + let ln = *self.low.get(node).unwrap_or(&0); + let inxt = *self.idx.get(&nxt).unwrap_or(&0); + self.low.insert(node.to_string(), ln.min(inxt)); } } - if low.get(node) == idx.get(node) { + if self.low.get(node) == self.idx.get(node) { let mut comp = Vec::new(); - while let Some(w) = stack.pop() { - on.remove(&w); + while let Some(w) = self.stack.pop() { + self.on.remove(&w); let done = w == node; comp.push(w); - if done { break; } + if done { + break; + } } comp.sort(); - out.push(comp); + self.out.push(comp); } } - let mut index = 0i64; - let mut stack = Vec::new(); - let mut on = BTreeSet::new(); - let mut idx = BTreeMap::new(); - let mut low = BTreeMap::new(); - let mut out = Vec::new(); +} + +fn tarjan(graph: &BTreeMap>) -> Vec> { + let mut search = Tarjan { + graph, + index: 0, + stack: Vec::new(), + on: BTreeSet::new(), + idx: BTreeMap::new(), + low: BTreeMap::new(), + out: Vec::new(), + }; let nodes: Vec = graph.keys().cloned().collect(); for n in nodes { - if !idx.contains_key(&n) { - strong(&n, graph, &mut index, &mut stack, &mut on, &mut idx, &mut low, &mut out); + if !search.idx.contains_key(&n) { + search.strong(&n); } } - out + search.out } pub fn verify_wikis(root: &Path) -> Json { @@ -2428,21 +2428,17 @@ fn init_project(root: &Path) -> Json { }; let src = root.join("src"); let mut seeded = Vec::new(); - if !path_list_has_entries(root, "map_paths.txt") { - if src.is_dir() { - if fs::write(root.join("map_paths.txt"), "src\n").is_err() { - return fail("InitError", "could not write map_paths.txt"); - } - seeded.push("map_paths.txt"); + if !path_list_has_entries(root, "map_paths.txt") && src.is_dir() { + if fs::write(root.join("map_paths.txt"), "src\n").is_err() { + return fail("InitError", "could not write map_paths.txt"); } + seeded.push("map_paths.txt"); } - if !path_list_has_entries(root, "monitored_paths.txt") { - if src.is_dir() { - if fs::write(root.join("monitored_paths.txt"), "src\n").is_err() { - return fail("InitError", "could not write monitored_paths.txt"); - } - seeded.push("monitored_paths.txt"); + if !path_list_has_entries(root, "monitored_paths.txt") && src.is_dir() { + if fs::write(root.join("monitored_paths.txt"), "src\n").is_err() { + return fail("InitError", "could not write monitored_paths.txt"); } + seeded.push("monitored_paths.txt"); } if !path_list_has_entries(root, "map_paths.txt") && !src.is_dir() { return fail("ScopeError", "no src/ directory and map_paths.txt has no roots; name map_paths before update-maps"); diff --git a/src/cache_commit.rs b/src/cache_commit.rs index 12610f2..a7feebb 100644 --- a/src/cache_commit.rs +++ b/src/cache_commit.rs @@ -364,7 +364,7 @@ fn load_rows(root: &Path) -> (BTreeMap, bool) { fn save_index(root: &Path, rows: &BTreeMap) -> Result<(), String> { let mut body = String::new(); let mut paths = String::new(); - for (_, row) in rows { + for row in rows.values() { body.push_str(&format!("{}\t{}\t{}\t{}\t{}\n", row.rel, row.mtime_ns, row.size, row.hash, join_dsts(&row.dsts))); paths.push_str(&row.rel); paths.push('\n'); diff --git a/src/jsonutil.rs b/src/jsonutil.rs index 671a4f9..9ee69e1 100644 --- a/src/jsonutil.rs +++ b/src/jsonutil.rs @@ -191,7 +191,7 @@ struct Parser<'a> { i: usize, } -impl<'a> Parser<'a> { +impl Parser<'_> { fn skip(&mut self) { while self.i < self.s.len() && self.s[self.i].is_ascii_whitespace() { self.i += 1; diff --git a/src/jsonutil.rs.wiki.md b/src/jsonutil.rs.wiki.md new file mode 100644 index 0000000..f301af3 --- /dev/null +++ b/src/jsonutil.rs.wiki.md @@ -0,0 +1,7 @@ +# src/jsonutil.rs + +**Role:** The small JSON value this program reads and writes. There is no serde crate. + +`parse` reads a JSON document. `parse_jsonc` and `strip_json_comments` accept a tsconfig-style document with comments. `Json` is the value: null, bool, int, float, string, array, and object. + +Public names: `Json`, `parse`, `parse_jsonc`, `strip_json_comments`. diff --git a/src/legacy_cache.rs b/src/legacy_cache.rs index 018bdd1..14782f0 100644 --- a/src/legacy_cache.rs +++ b/src/legacy_cache.rs @@ -99,7 +99,7 @@ fn import_json(root: &Path, path: &Path) -> Result { if key.starts_with('_') { continue; } - if !entry.as_object().is_some() { + if entry.as_object().is_none() { continue; } rels.insert(key.clone()); diff --git a/src/main.rs b/src/main.rs index 84cc8fa..01bf5cd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,20 +33,17 @@ fn main() -> ExitCode { let mut i = 0; while i < args.len() { let a = &args[i]; - if a == "-r" || a == "--reason" || a == "--only" { - if i + 1 < args.len() { - let key = if a == "--only" { "only" } else { "reasons" }; - let cur = flags.entry(key.into()).or_default(); - if !cur.is_empty() { - cur.push('\n'); - } - cur.push_str(&args[i + 1]); - i += 2; - continue; + if (a == "-r" || a == "--reason" || a == "--only") && i + 1 < args.len() { + let key = if a == "--only" { "only" } else { "reasons" }; + let cur = flags.entry(key.into()).or_default(); + if !cur.is_empty() { + cur.push('\n'); } + cur.push_str(&args[i + 1]); + i += 2; + continue; } - if a == "--reasons" { - if i + 1 < args.len() { + if a == "--reasons" && i + 1 < args.len() { let text = std::fs::read_to_string(&args[i + 1]).unwrap_or_default(); let cur = flags.entry("reasons".into()).or_default(); for line in text.lines() { @@ -61,7 +58,6 @@ fn main() -> ExitCode { } i += 2; continue; - } } if a == "--json" || a == "--full" || a == "--summary" || a == "--force" || a == "--stream" || a == "--quiet" || a == "--dry-run" || a == "--green" { flags.insert(a.trim_start_matches('-').to_string(), "true".into()); @@ -126,13 +122,11 @@ fn extract_target(args: &[String]) -> (Option, bool, Vec) { let mut i = 0; while i < args.len() { let a = &args[i]; - if a == "--target" || a == "--project-root" { - if i + 1 < args.len() { - root = Some(PathBuf::from(&args[i + 1])); - explicit = true; - i += 2; - continue; - } + if (a == "--target" || a == "--project-root") && i + 1 < args.len() { + root = Some(PathBuf::from(&args[i + 1])); + explicit = true; + i += 2; + continue; } if let Some(v) = a.strip_prefix("--target=").or_else(|| a.strip_prefix("--project-root=")) { root = Some(PathBuf::from(v)); diff --git a/src/mcp.rs b/src/mcp.rs index 8d085c2..524fd1d 100644 --- a/src/mcp.rs +++ b/src/mcp.rs @@ -84,9 +84,7 @@ pub fn tool_schema(name: &str) -> Json { pub fn handle_message(msg: &Json) -> Option { let id = msg.get("id").cloned(); let method = msg.get("method").and_then(|m| m.as_str()).unwrap_or(""); - if id.is_none() { - return None; - } + id.as_ref()?; let result = match method { "initialize" => Json::obj(vec![ ("protocolVersion", Json::String("2024-11-05".into())), @@ -261,7 +259,14 @@ fn resource_read(msg: &Json) -> Json { "wikifier://pending" => std::fs::read_to_string(root.join("pending_updates.md")).unwrap_or_default(), "wikifier://journal" => { let entries = api::read_journal(&root, None); - entries.into_iter().map(|e| format!("## [{}] {}\n**File:** {}\n**Reason:** {}\n", e.stamp, e.action, e.file, e.reason)).collect() + let mut text = String::new(); + for entry in entries { + let _ = std::fmt::Write::write_fmt( + &mut text, + format_args!("## [{}] {}\n**File:** {}\n**Reason:** {}\n", entry.stamp, entry.action, entry.file, entry.reason), + ); + } + text } _ => return Json::obj(vec![ ("success", Json::Bool(false)), diff --git a/src/parse.rs b/src/parse.rs index dbe8f0b..c66de0d 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -817,7 +817,7 @@ fn paren_delta(s: &str) -> i32 { fn parse_py_stmt(stmt: &str) -> Option { let stmt = stmt.trim().trim_end_matches('\\').trim(); if let Some(rest) = stmt.strip_prefix("import ") { - let names = rest.split(',').map(|s| s.trim().split_whitespace().next().unwrap_or("")).filter(|s| !s.is_empty() && *s != "__future__").collect::>().join(","); + let names = rest.split(',').map(|s| s.split_whitespace().next().unwrap_or("")).filter(|s| !s.is_empty() && *s != "__future__").collect::>().join(","); if names.is_empty() { return None; } @@ -838,7 +838,7 @@ fn parse_py_stmt(stmt: &str) -> Option { let module = left[level..].trim(); let names: Vec = right .split(',') - .map(|s| s.trim().split_whitespace().next().unwrap_or("").to_string()) + .map(|s| s.split_whitespace().next().unwrap_or("").to_string()) .filter(|s| !s.is_empty()) .collect(); if module.starts_with("__future__") { @@ -1978,12 +1978,7 @@ fn module_file(base: &Path, dotted: &str) -> Option { t.set_file_name(name); t }; - for cand in [py, init, pyi] { - if exists_exact(base, &cand) { - return Some(cand); - } - } - None + [py, init, pyi].into_iter().find(|cand| exists_exact(base, cand)) } fn exists_exact(base: &Path, candidate: &Path) -> bool { diff --git a/src/project_lock.rs.wiki.md b/src/project_lock.rs.wiki.md index 4012e2e..df0d254 100644 --- a/src/project_lock.rs.wiki.md +++ b/src/project_lock.rs.wiki.md @@ -3,3 +3,5 @@ **Role:** The project-wide advisory lock at `.wikifier_staging/.lock`. `ProjectLock::acquire` creates the pid file, waits 30 seconds, and steals the file when the recorded pid is dead. The same thread may re-enter. The outer guard is the one that deletes the file. Callers take this lock before the cache lock. + +Public names: `ProjectLock`. diff --git a/tests/behaviour_gaps.rs b/tests/behaviour_gaps.rs index c3fcaa2..8ad5234 100644 --- a/tests/behaviour_gaps.rs +++ b/tests/behaviour_gaps.rs @@ -50,7 +50,7 @@ fn mcp_call(bin: &str, deadline: Option<&str>, body: &str) -> (Json, Duration) { (parsed, elapsed) } -fn tool_body(root: &PathBuf, name: &str) -> String { +fn tool_body(root: &std::path::Path, name: &str) -> String { let root_text = root.to_str().unwrap().replace('\\', "\\\\").replace('"', "\\\""); format!( r#"{{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{{"name":"{name}","arguments":{{"project_root":"{root_text}"}}}}}}"# diff --git a/tests/test_gap_closure.py.wiki.md b/tests/test_gap_closure.py.wiki.md deleted file mode 100644 index 78867ec..0000000 --- a/tests/test_gap_closure.py.wiki.md +++ /dev/null @@ -1,5 +0,0 @@ -# tests/test_gap_closure.py - -**Role:** Gap-closure regressions — G4 ACS actionable low-conf, G5 health module vs `wikifier.health` function shadow, G7 ghosts/`record-deletion`, map-first readiness/taxonomy, pending prune hygiene. - -**Agent note:** Uses temp projects (`TempProjectTestCase`). Does not cover case-alias ghosts (`Claude.md` vs `CLAUDE.md`) — that is separate product debt.