From f93f4cc5e4ed0f899c470ea0d9fcd540f60f65a2 Mon Sep 17 00:00:00 2001 From: IronAdamant <18153828+IronAdamant@users.noreply.github.com> Date: Thu, 5 Nov 2026 15:18:12 +1100 Subject: [PATCH 01/11] Pass the exit code through wikifier.bat; pin launcher line endings. wikifier.bat read %ERRORLEVEL% inside a parenthesized block. cmd expands it when the block is parsed, before the binary runs, so every failure exited 0 (e.g. `wikifier.bat no-such-command` printed UnknownCommand and returned 0). The launcher now picks the binary first and runs it outside any block. .gitattributes keeps *.sh LF (bash on WSL/Linux rejects CRLF) and *.bat/*.ps1 CRLF (cmd misparses labels in LF-only batch files) whatever core.autocrlf says. tests/cli_surface.rs runs wikifier.bat on Windows and wikifier.sh elsewhere against the built binary and checks exit codes 0 and 1. Co-Authored-By: Claude Opus 5.5 --- .gitattributes | 4 ++++ tests/cli_surface.rs | 34 ++++++++++++++++++++++++++++++++++ wikifier.bat | 14 ++++++++++---- 3 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c825a3f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +# Launchers: bash needs LF, cmd.exe needs CRLF (labels misparse in LF-only batch files). +*.sh text eol=lf +*.bat text eol=crlf +*.ps1 text eol=crlf diff --git a/tests/cli_surface.rs b/tests/cli_surface.rs index 110cb0b..8c0dbc1 100644 --- a/tests/cli_surface.rs +++ b/tests/cli_surface.rs @@ -165,6 +165,40 @@ fn mcp_stdio_is_newline_delimited_json() { let _ = std::fs::remove_dir_all(root); } +/// The launcher must pass the binary's exit code through. `wikifier.bat` used to read +/// `%ERRORLEVEL%` inside a parenthesized block, which expands before the command runs, +/// so every failure exited 0. +#[test] +fn launcher_passes_exit_code_through() { + let here = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let launch = |arg: &str| { + let mut cmd = if cfg!(windows) { + let mut c = Command::new("cmd"); + c.arg("/c").arg(here.join("wikifier.bat")); + c + } else { + let mut c = Command::new("bash"); + c.arg(here.join("wikifier.sh")); + c + }; + cmd.arg(arg).env("WIKIFIER_BIN", bin()).output().unwrap() + }; + let ok = launch("--version"); + assert!(ok.status.success(), "status {:?}", ok.status); + assert!( + String::from_utf8_lossy(&ok.stdout).contains(env!("CARGO_PKG_VERSION")), + "{}", + String::from_utf8_lossy(&ok.stdout) + ); + let bad = launch("no-such-command"); + assert_eq!(bad.status.code(), Some(1), "status {:?}", bad.status); + assert!( + String::from_utf8_lossy(&bad.stdout).contains("UnknownCommand"), + "{}", + String::from_utf8_lossy(&bad.stdout) + ); +} + fn parse_line(line: &str) -> wikifier::Json { wikifier::jsonutil::parse(line.trim()).unwrap_or_else(|err| panic!("stdio json {err}: {line}")) } diff --git a/wikifier.bat b/wikifier.bat index 961104e..354fac9 100644 --- a/wikifier.bat +++ b/wikifier.bat @@ -1,9 +1,15 @@ @echo off -REM Launch the Rust wikifier binary. +REM Launch the Rust wikifier binary. Prefers WIKIFIER_BIN, then a local build, then cargo run. +REM No parenthesized blocks: %ERRORLEVEL% inside one expands before the command runs, +REM which used to turn every failure into exit code 0. setlocal set "HERE=%~dp0" -if not "%WIKIFIER_BIN%"=="" ("%WIKIFIER_BIN%" %* & exit /b %ERRORLEVEL%) -if exist "%HERE%target\release\wikifier.exe" ("%HERE%target\release\wikifier.exe" %* & exit /b %ERRORLEVEL%) -if exist "%HERE%target\debug\wikifier.exe" ("%HERE%target\debug\wikifier.exe" %* & exit /b %ERRORLEVEL%) +set "BIN=%WIKIFIER_BIN%" +if not defined BIN if exist "%HERE%target\release\wikifier.exe" set "BIN=%HERE%target\release\wikifier.exe" +if not defined BIN if exist "%HERE%target\debug\wikifier.exe" set "BIN=%HERE%target\debug\wikifier.exe" +if not defined BIN goto cargo +"%BIN%" %* +exit /b %ERRORLEVEL% +:cargo cargo run --quiet --manifest-path "%HERE%Cargo.toml" -- %* exit /b %ERRORLEVEL% From 16f82e96bda27c4d34b09454d3a0c488951476f7 Mon Sep 17 00:00:00 2001 From: IronAdamant <18153828+IronAdamant@users.noreply.github.com> Date: Thu, 5 Nov 2026 15:19:01 +1100 Subject: [PATCH 02/11] Hash health baselines with CRLF read as LF. file_health.json stores a sha256 of each Green file. Git for Windows checks files out with CRLF (core.autocrlf=true), so on a Windows clone every baseline hashed from LF elsewhere mismatched: check-changes on this repo flagged 51 untouched files dirty and rewrote health and pending. content_sha256_prefixed drops the CR of each CRLF pair before hashing. LF-only content hashes exactly as before, so existing baselines stay valid. update-maps uses the same hash for its cache entries. Unit test in src/sha256.rs. Co-Authored-By: Claude Opus 5.5 --- src/api.rs | 4 ++-- src/sha256.rs | 30 +++++++++++++++++++++++++++++- src/sha256.rs.wiki.md | 5 +++++ 3 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 src/sha256.rs.wiki.md diff --git a/src/api.rs b/src/api.rs index 769ea0b..7fb4807 100644 --- a/src/api.rs +++ b/src/api.rs @@ -8,7 +8,7 @@ use crate::artifacts::{self, HealthRow, JournalEntry}; use crate::cache::{self, FileUpdate, Session}; use crate::jsonutil::Json; use crate::scope::{self, Scope}; -use crate::sha256::{file_sha256_prefixed, sha256_prefixed}; +use crate::sha256::{content_sha256_prefixed, file_sha256_prefixed}; use crate::timeutil::local_stamp; pub struct MapRun { @@ -292,7 +292,7 @@ pub fn update_maps_run(root: &Path, run: &MapRun) -> Json { continue; } let bytes = fs::read(file).unwrap_or_default(); - let hash = sha256_prefixed(&bytes); + let hash = content_sha256_prefixed(&bytes); let (mtime, size) = if mtime == 0 && size == 0 { cache::stat_ns(file).unwrap_or((0, bytes.len() as u64)) } else { diff --git a/src/sha256.rs b/src/sha256.rs index 2540de1..4b79ebb 100644 --- a/src/sha256.rs +++ b/src/sha256.rs @@ -95,9 +95,28 @@ pub fn sha256_prefixed(data: &[u8]) -> String { format!("sha256:{}", sha256_hex(data)) } +/// Content identity for health baselines: CRLF counts as LF. A Windows checkout with +/// `core.autocrlf` has CRLF files whose Green baselines were hashed from LF elsewhere; +/// hashing raw bytes flipped every one of them to dirty. +pub fn content_sha256_prefixed(data: &[u8]) -> String { + const CR: u8 = 13; + const LF: u8 = 10; + if !data.windows(2).any(|pair| pair == [CR, LF]) { + return sha256_prefixed(data); + } + let mut unix = Vec::with_capacity(data.len()); + for (i, byte) in data.iter().enumerate() { + if *byte == CR && data.get(i + 1) == Some(&LF) { + continue; + } + unix.push(*byte); + } + sha256_prefixed(&unix) +} + pub fn file_sha256_prefixed(path: &std::path::Path) -> Option { let bytes = std::fs::read(path).ok()?; - Some(sha256_prefixed(&bytes)) + Some(content_sha256_prefixed(&bytes)) } #[cfg(test)] @@ -112,4 +131,13 @@ mod tests { ); assert!(sha256_prefixed(b"abc").starts_with("sha256:")); } + + #[test] + fn content_hash_treats_crlf_as_lf() { + let unix = content_sha256_prefixed(&[b'a', 10, b'b', 10]); + assert_eq!(unix, sha256_prefixed(&[b'a', 10, b'b', 10])); + assert_eq!(content_sha256_prefixed(&[b'a', 13, 10, b'b', 13, 10]), unix); + assert_eq!(content_sha256_prefixed(&[b'a', 13, 10, b'b', 10]), unix); + assert_ne!(content_sha256_prefixed(&[b'a', 13, b'b', 10]), unix); + } } diff --git a/src/sha256.rs.wiki.md b/src/sha256.rs.wiki.md new file mode 100644 index 0000000..e4e8d4d --- /dev/null +++ b/src/sha256.rs.wiki.md @@ -0,0 +1,5 @@ +# src/sha256.rs + +**Role:** Std-only SHA-256 for content identity. + +`sha256` returns the digest, `sha256_hex` the hex string, and `sha256_prefixed` the `sha256:` form stored in `file_health.json`. `content_sha256_prefixed` hashes with CRLF read as LF, so a Windows `core.autocrlf` checkout matches a baseline hashed from LF. `file_sha256_prefixed` reads a file and returns its content hash. From 64d090612c03e6b8a0dfe2592c8ea1f766ce4944 Mon Sep 17 00:00:00 2001 From: IronAdamant <18153828+IronAdamant@users.noreply.github.com> Date: Thu, 5 Nov 2026 15:19:12 +1100 Subject: [PATCH 03/11] Make file arguments and MCP replies work for agents the first time. Found by driving `wikifier mcp` the way an agent does, on copies of real repos (a 17-file crate and a 587-file Cargo workspace): - prepare_edit / get_dependencies / get_dependents / get_file_wiki with a guessed argument name (`file_path`) or an unknown path answered success:true with every field empty. MCP now returns InvalidArguments naming the missing field, the accepted fields and the received ones; the lookups return FileNotFound with a list_paths hint. - `src\a.rs` and absolute paths (which skills/run.md already promised) returned empty lookups, and record-change on `src\a.rs` created a second health row. scope::rel_arg turns every file argument into the project-relative `/` key. - MCP update_maps returned the whole edge list (~280 KB on the workspace) and health ignored its advertised `limit` (~145 KB). The edge list is now opt-in (`edges: true`, count still in edges_persisted); health honors `limit` (adds entries_total) and `summary`. The update_maps schema now lists full, max_files and edges. - init always seeded `src/`, so a workspace with crates/ hit ScopeError on the first map. It now seeds src/ when present, else the top-level directories that hold source. tests/agent_args.rs covers each case, plus a Green file surviving a CRLF checkout. skills/run.md and CHANGELOG (Unreleased) describe the changes. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 13 ++ skills/run.md | 8 +- skills/run.md.wiki.md | 2 +- src/api.rs | 92 +++++++++--- src/api.rs.wiki.md | 4 +- src/mcp.rs | 73 +++++++++- src/mcp.rs.wiki.md | 2 + src/scope.rs | 70 +++++++++ src/scope.rs.wiki.md | 7 + tests/agent_args.rs | 302 +++++++++++++++++++++++++++++++++++++++ tests/dogfood.rs | 1 + tests/dogfood.rs.wiki.md | 2 +- 12 files changed, 547 insertions(+), 29 deletions(-) create mode 100644 src/scope.rs.wiki.md create mode 100644 tests/agent_args.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e65c2e..004d9c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- Windows: `wikifier.bat` passes the binary's exit code through. It read `%ERRORLEVEL%` inside a parenthesized block, so every failure exited 0. +- Windows: health baselines hash CRLF as LF (`content_sha256_prefixed`). A `core.autocrlf` checkout no longer flips every Green file to dirty (51 files in this repo). +- A file argument written with `\` separators or as an absolute path under the root names the same row as `src/a.rs`. Before, an absolute path returned empty lookups and a backslash path created a second health row. +- `prepare_edit`, `get_dependencies`, `get_dependents`, and `get_file_wiki` return `FileNotFound` with a `list_paths` hint for a path that is neither on disk nor in the map, instead of `success: true` with empty fields. +- MCP `tools/call` with a missing required argument (for example `file_path` instead of `file`) returns `InvalidArguments` naming the accepted and received arguments. Before, the tool ran with an empty path and reported success. +- `health --limit N` (and MCP `limit`) caps the entry list and adds `entries_total`. The argument was advertised and ignored. + +### Changed +- `init` seeds `map_paths.txt` and `monitored_paths.txt` with `src/` when it exists, otherwise with each top-level directory that holds source (a Cargo workspace's `crates/`, a Python package directory, `tests/`). Before, a project without `src/` hit a scope error on the first `update-maps`. +- MCP `update_maps` omits the edge list unless `edges: true`; `edges_persisted` still gives the count. On a 587-file Cargo workspace the list was about 280 KB. The schema now lists `full`, `max_files`, and `edges`; `health` lists `limit` and `summary`. +- `.gitattributes` keeps `*.sh` LF and `*.bat` / `*.ps1` CRLF on every checkout. + ## [5.0.0] - 2026-11-03 First release of the std-only Rust rewrite. `[dependencies]` is empty: no crates.io crates and no Python runtime. The binary implements the Python 4.7 agent contract. Protocol v0.7 stays. `pip install wikifier` still installs that Python 4.7 package. diff --git a/skills/run.md b/skills/run.md index dece840..a52052c 100644 --- a/skills/run.md +++ b/skills/run.md @@ -24,7 +24,7 @@ This file is the contract for any agent operating Wikifier. Part 1 is all you ne | 5+6 for many files | `record_changes(reasons, green=True)` — reasons per path/glob/`dir/`, from git | `record-changes -r "src/=why" --green` | - Always pass `project_root=` (MCP/library) or `--target DIR` / `WIKIFIER_PROJECT_ROOT` (CLI) for any project other than the current directory. **A root that does not exist is an error** — nothing is ever written elsewhere. -- File arguments may be project-relative, `./`-prefixed or absolute (inside the project). Results return the canonical project-relative `file`. +- File arguments may be project-relative, `./`-prefixed, `\`-separated (Windows) or absolute (inside the project). Results return the canonical project-relative `file`. A lookup (`prepare_edit`, `get_dependencies`, `get_dependents`, `get_file_wiki`) on a path that is neither on disk nor in the map returns `error_type: "FileNotFound"`; `list_paths` shows the mapped files. - `record_change` and `mark_green` require the file to exist; for removed files use `record_deletion`. - `check_changes` watches every mapped file, every file ever marked Green, and `monitored_paths.txt` entries — the list only adds files (docs, scripts), it never hides mapped ones. - Green means the wiki was checked against the code (see *Wiki verification*). A failed check lists the symbols to document; update the wiki and retry. `force=True` with a reason overrides and is recorded as `forced`. @@ -128,7 +128,7 @@ Mutations take an advisory project lock (`.wikifier_staging/.lock`, a create-new | Command | Purpose | |---|---| -| `init [DIR]` | keeps an existing path list, seeds `src/` when that list is empty, copies `index.html` only when it is missing, writes `.wikifier/marker`. `[DIR]` is the project root unless `--target` is set | +| `init [DIR]` | keeps an existing path list, seeds the map roots when that list is empty (`src/` if present, else each top-level directory that holds source), copies `index.html` only when it is missing, writes `.wikifier/marker`. `[DIR]` is the project root unless `--target` is set | | `session-bootstrap`, `check-changes`, `suggest-next [--json]` | core loop | | `prepare-edit F`, `record-change F WHY`, `mark-green F [WHY] [--force]`, `record-deletion F [WHY]` | per-file loop | | `record-changes [-r PATH=WHY]… [--reasons FILE] [--default WHY] [--only P]… [--since REF] [--green] [--dry-run]` | batch loop (git) | @@ -163,9 +163,9 @@ The program is a std-only Rust binary under `src/`. `Cargo.toml` has an empty `[ - `seed-source-hashes` writes the current hash onto Green rows and does not change status. `--force` replaces a hash that is already stored. `seed-health` still adds Initial-stub rows. `heal-stubs` promotes a stub whose wiki is at least 350 characters (`--min-wiki-length` overrides). - Health stubs from `update-maps` follow `monitored_paths.txt` when that file lists real paths. If the file is absent, empty, or only a bare `.`, every mapped file is seeded. - The scan ignores comments and strings. It does not use CPython `ast`. It does read `tsconfig.json` `paths`, `package.json` `exports`, and npm `workspaces` (`*` directory globs). Named re-exports are the barrel follow. Rust reads the root `Cargo.toml` `[package].name`, `[lib].name`, and `[workspace] members` (globs included; hyphens become underscores). It resolves `crate::`, `self::`, `super::`, those crate names, and `mod` to files. `crate::` stays in the file's crate (`tests/*.rs` and `src/bin/*.rs` are their own crates, and a workspace member does not share `crate::` with another member). A package or lib name points at that member's 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. Prompts name the tools to call. Optional arguments include `format` (`text` or `json`), `timeout_s`, `directory`, `detail`, `low_confidence_only`, `unresolved_only`, `depth`, `recursive`, `max_files`, `limit`, `max_items`, and a `reasons` object. A call waits at most `timeout_s` or `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. The CLI default is JSON. `--text` prints prose. `health --format text` does the same for health. +- MCP `tools/call` sets `isError` when the payload has `success: false`. `resources/read` and `prompts/get` answer. Prompts name the tools to call. Optional arguments include `format` (`text` or `json`), `timeout_s`, `directory`, `detail`, `low_confidence_only`, `unresolved_only`, `depth`, `recursive`, `max_files`, `limit`, `max_items`, and a `reasons` object. A call waits at most `timeout_s` or `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. A call that leaves out a required argument other than `project_root` returns `error_type: "InvalidArguments"` with the accepted and received argument names. Over MCP, `update_maps` omits the edge list unless `edges: true` (`edges_persisted` is the count), and `health` takes `limit` (with `entries_total`) and `summary`. The CLI default is JSON. `--text` prints prose. `health --format text` does the same for health. - 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. When a list is missing it writes a `src/` template. It also writes `exclude_patterns.txt`, empty `file_health.md`, `pending_updates.md`, `library.md`, `.wikifier/config`, `.wikifier/marker`, and the `journal/` and `Logged_issues/` directories when those paths are absent. `--no-copy` skips `index.html`. Otherwise `index.html` is copied only when it is missing. +- `init` keeps a path list that already has a non-comment entry. When a list is missing it writes the map roots: `src/` when it exists, otherwise every top-level directory that holds a source file (for example `crates/` and `tests/` in a Cargo workspace), otherwise `src/`. It also writes `exclude_patterns.txt`, empty `file_health.md`, `pending_updates.md`, `library.md`, `.wikifier/config`, `.wikifier/marker`, and the `journal/` and `Logged_issues/` directories when those paths are absent. `--no-copy` skips `index.html`. Otherwise `index.html` is copied only when it is missing. - `serve` listens on `127.0.0.1:8787` until stopped. It answers `/`, `/__wikifier/api/{bootstrap,file,dependencies,dependents,cycles,journal,diagnostics}`, `/__wikifier/status`, and the project files the dashboard reads (`file_health.json`, `library.md`). `POST /__wikifier/run` accepts `update-maps`, `update-maps --full`, and `check-changes`. `POST /__wikifier/shutdown` stops the process. `monitor` repeats `check-changes` (default every 30 seconds). `daemon run` checks every `WIKIFIER_POLL_INTERVAL` seconds (default 30) and runs `update-maps` at start and then at most every `WIKIFIER_DAEMON_MAPS_INTERVAL` seconds (default 600). `WIKIFIER_DAEMON_MAPS=0` skips the map. State is `.wikifier_staging/wikifier.pid`, `daemon.log`, and `daemon_heartbeat.json`. `daemon logs`, `restart`, `install-service`, and `uninstall-service` are supported (systemd user unit on Linux, launchd plist on macOS, Task Scheduler on Windows). `daemon start` refuses readiness `blocked` unless `--force`. `not_ready` may start. `issues` is the same `actions[]` list as `suggest-next`. `library.md` stays the short tree, Mermaid block, and dependency table; cycle severity is on `cycles`, not in that file. ```bash diff --git a/skills/run.md.wiki.md b/skills/run.md.wiki.md index 5060820..2331443 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. Wiki verification covers Python, JS/TS, and Rust `pub` items including impl methods and trait-impl items. Rust import names include `[workspace] members`. -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 package line is 5.0.0. The live version command is `wikifier --version` (from `Cargo.toml`). +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. `init` seeds the map roots it finds. File arguments may use `\` separators. Lookups on an unknown path return `FileNotFound`; an MCP call missing a required argument returns `InvalidArguments`; MCP `update_maps` leaves out the edge list unless `edges: true`. The protocol version stays v0.7. The package line is 5.0.0. The live version command is `wikifier --version` (from `Cargo.toml`). diff --git a/src/api.rs b/src/api.rs index 7fb4807..00cd8ea 100644 --- a/src/api.rs +++ b/src/api.rs @@ -84,6 +84,27 @@ fn fail(error_type: &str, error: impl Into) -> Json { ]) } +/// A lookup on a path that is neither on disk nor in the map fails with a next step, +/// instead of a `success: true` reply whose fields are all empty. +fn unknown_file(root: &Path, rel: &str) -> Option { + if rel.is_empty() { + return Some(fail( + "ValueError", + "file is required: pass a project-relative path such as src/main.rs", + )); + } + if root.join(rel).exists() || cache::all_paths(root).iter().any(|p| p == rel) { + return None; + } + Some(fail( + "FileNotFound", + format!( + "{rel} is not on disk under {} and not in the map. Pass a path relative to the project root; list_paths shows the mapped files.", + root.display() + ), + )) +} + fn root_or_err(root: Option<&Path>) -> Result { let root = match root { Some(p) => p.to_path_buf(), @@ -631,7 +652,11 @@ pub fn load_dependencies(root: &Path, rel: &str) -> Json { pub fn dependency_report(root: &Path, rel: &str, full: bool) -> Json { let _ = crate::legacy_cache::import_if_empty(root); - let rel = rel.trim_start_matches("./"); + let rel = scope::rel_arg(root, rel); + let rel = rel.as_str(); + if let Some(err) = unknown_file(root, rel) { + return err; + } match cache::load_dependency_view(root, rel) { Ok(view) => { let cycled = if full { @@ -717,6 +742,11 @@ fn edge_record(edge: &cache::InternalEdge, full: bool, in_cycle: bool) -> Json { pub fn load_dependents(root: &Path, rel: &str) -> Json { let _ = crate::legacy_cache::import_if_empty(root); + let rel = scope::rel_arg(root, rel); + let rel = rel.as_str(); + if let Some(err) = unknown_file(root, rel) { + return err; + } match cache::load_dependents(root, rel) { Ok(srcs) => ok(vec![ ("file", Json::String(rel.into())), @@ -1202,8 +1232,12 @@ pub fn check_changes(root: &Path) -> Json { } pub fn prepare_edit(root: &Path, file: &str) -> Json { - let rel = file.trim_start_matches("./"); + let rel = scope::rel_arg(root, file); + let rel = rel.as_str(); let _ = crate::legacy_cache::import_if_empty(root); + if let Some(err) = unknown_file(root, rel) { + return err; + } let deps = cache::load_dependencies(root, rel).unwrap_or_default(); let importers = cache::load_dependents(root, rel).unwrap_or_default(); let row = artifacts::read_health(root) @@ -1244,7 +1278,8 @@ pub fn record_change(root: &Path, file: &str, reason: &str) -> Json { Ok(lock) => lock, Err(err) => return err, }; - let rel = file.trim_start_matches("./"); + let rel = scope::rel_arg(root, file); + let rel = rel.as_str(); let path = root.join(rel); if !path.is_file() { return fail("FileNotFound", format!("file does not exist: {rel}")); @@ -1283,7 +1318,8 @@ pub fn mark_green(root: &Path, file: &str, reason: &str, force: bool) -> Json { Ok(lock) => lock, Err(err) => return err, }; - let rel = file.trim_start_matches("./"); + let rel = scope::rel_arg(root, file); + let rel = rel.as_str(); let path = root.join(rel); if !path.is_file() { return fail("FileNotFound", format!("file does not exist: {rel}")); @@ -2231,7 +2267,8 @@ pub fn record_deletion(root: &Path, file: &str, reason: &str) -> Json { Ok(lock) => lock, Err(err) => return err, }; - let rel = file.trim_start_matches("./"); + let rel = scope::rel_arg(root, file); + let rel = rel.as_str(); let written = match artifacts::append_journal(root, "record-deletion", rel, reason) { Ok(written) => written, Err(e) => return fail("JournalError", e), @@ -3316,7 +3353,11 @@ pub fn dispatch( positional: &[String], flags: &BTreeMap, ) -> Json { - let file = positional.first().map(|s| s.as_str()).unwrap_or(""); + let file_arg = positional + .first() + .map(|s| scope::rel_arg(root, s)) + .unwrap_or_default(); + let file = file_arg.as_str(); let reason = if positional.len() > 1 { positional[1..].join(" ") } else { @@ -3396,7 +3437,17 @@ pub fn dispatch( } else { flags.get("format").map(|s| s.as_str()).unwrap_or("json") }; - health_view(root, flag_text(flags, "directory"), format) + let mut value = health_view(root, flag_text(flags, "directory"), format); + let limit = flags.get("limit").and_then(|s| s.parse::().ok()); + if let (Some(limit), Some(Json::Array(rows))) = (limit, value.get("entries")) { + if rows.len() > limit { + let total = rows.len(); + let kept = rows[..limit].to_vec(); + value.insert("entries", Json::Array(kept)); + value.insert("entries_total", Json::Int(total as i64)); + } + } + value } "cache-status" | "cache" => cache_status(root), "list-core-tools" | "list_core_tools" => ok(vec![ @@ -3560,6 +3611,9 @@ pub fn dispatch( Json::String(root.display().to_string()), )]), "get_file_wiki" => { + if let Some(err) = unknown_file(root, file) { + return err; + } let (path, wiki) = crate::wiki_files::read_wiki(root, file); ok(vec![ ("file", Json::String(file.into())), @@ -3816,16 +3870,16 @@ fn init_project(root: &Path, no_copy: bool) -> Json { created.push(rel.to_string()); true }; - let map_body = "\ -# map_paths.txt — import-map package roots for update-maps -# One directory per line. No bare \".\". -src/ -"; - let monitored_body = "\ -# monitored_paths.txt — wiki and health watch list -# Mapped sources are watched even when they are not listed here. -src/ -"; + let roots = scope::default_map_roots(root) + .into_iter() + .map(|dir| format!("{dir}\n")) + .collect::(); + let map_body = format!( + "# map_paths.txt — import-map package roots for update-maps\n# One directory per line. No bare \".\".\n{roots}" + ); + let monitored_body = format!( + "# monitored_paths.txt — wiki and health watch list\n# Mapped sources are watched even when they are not listed here.\n{roots}" + ); let exclude_body = "\ node_modules .git @@ -3845,12 +3899,12 @@ target "# Pending Updates\n\n(no pending items — run check-changes after making edits)\n"; let library_body = "# Library & Imports Map\n\nRun `wikifier update-maps` to populate.\n"; if !path_list_has_entries(root, "map_paths.txt") - && !write_new("map_paths.txt", map_body, &mut created) + && !write_new("map_paths.txt", &map_body, &mut created) { return fail("InitError", "could not write map_paths.txt"); } if !path_list_has_entries(root, "monitored_paths.txt") - && !write_new("monitored_paths.txt", monitored_body, &mut created) + && !write_new("monitored_paths.txt", &monitored_body, &mut created) { return fail("InitError", "could not write monitored_paths.txt"); } diff --git a/src/api.rs.wiki.md b/src/api.rs.wiki.md index b99e56e..d1ba7e2 100644 --- a/src/api.rs.wiki.md +++ b/src/api.rs.wiki.md @@ -6,6 +6,6 @@ `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`. +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`, `seed_source_hashes`, `list_paths_at`, `session_bootstrap_scoped`, `suggest_next_scoped`, `health_view`, `cycles_view`, `stale_wikis_in`, `daemon_command`. -`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. +`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. File arguments go through `scope::rel_arg`, so `src.rs`, `./src/a.rs`, and an absolute path under the root all name the `src/a.rs` row. `prepare_edit`, `dependency_report`, `load_dependents`, and `get_file_wiki` return `FileNotFound` (with a `list_paths` hint) for a path that is neither on disk nor in the map, and `ValueError` for an empty path. `health --limit N` keeps N entries and reports `entries_total`. `init` seeds `map_paths.txt` and `monitored_paths.txt` from `scope::default_map_roots`. Content hashes for health baselines use `content_sha256_prefixed`, so CRLF and LF copies of a file match. 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/mcp.rs b/src/mcp.rs index 0067809..ffb60e7 100644 --- a/src/mcp.rs +++ b/src/mcp.rs @@ -55,7 +55,11 @@ fn extra_props(name: &str) -> Vec<(&'static str, Json)> { ("max_items", integer("Cap the cycle list.")), ("analysis", boolean("Include severity and reverse-dependency ranking. Default true.")), ]), - "verify_wikis" | "health" | "suggest_next_actions" | "session_bootstrap" | "get_project_status" => { + "health" => props.extend([ + ("limit", integer("Return at most this many entries. entries_total gives the full count.")), + ("summary", boolean("Return only the green, yellow, and red counts.")), + ]), + "verify_wikis" | "suggest_next_actions" | "session_bootstrap" | "get_project_status" => { props.push(("limit", integer("Cap listed failures or stale rows."))); } "record_changes" => props.push(("reasons", Json::obj(vec![ @@ -64,6 +68,11 @@ fn extra_props(name: &str) -> Vec<(&'static str, Json)> { ]))), "heal_stubs" | "list_healable_stubs" => props.push(("min_wiki_length", integer("Minimum wiki length. Default 350."))), "mark_green" => props.push(("force", boolean("Override a failed wiki check. Requires a reason."))), + "update_maps" => props.extend([ + ("full", boolean("Rehash every file instead of skipping unchanged size and mtime.")), + ("max_files", integer("Parse at most this many changed files. Re-run while files_remaining_dirty > 0.")), + ("edges", boolean("Include the whole edge list. Off by default: it runs to hundreds of KB on a mid-size repo. Use get_dependencies for one file.")), + ]), _ => {} } props @@ -385,6 +394,9 @@ fn call_tool(msg: &Json) -> Json { .unwrap_or("") .to_string(); let args = params.and_then(|p| p.get("arguments")).cloned(); + if let Some(err) = missing_required(&name, args.as_ref()) { + return tool_result(err, false, &name); + } let root = args .as_ref() .and_then(|a| a.get("project_root")) @@ -475,7 +487,7 @@ fn call_tool(msg: &Json) -> Json { } } for key in [ - "full", "quiet", "stream", "green", "force", "dry_run", "dry-run", + "full", "quiet", "stream", "green", "force", "dry_run", "dry-run", "summary", ] { if arg_on(args.as_ref(), key) { let stored = if key == "dry_run" { "dry-run" } else { key }; @@ -524,6 +536,11 @@ fn call_tool(msg: &Json) -> Json { _ => {} } } + // Over MCP the edge list is opt-in. Its size grows with the repo and every byte lands in + // the agent's context; `edges_persisted` still reports the count. + if name == "update_maps" && !arg_on(args.as_ref(), "edges") { + flags.insert("quiet".into(), "true".into()); + } let textual = flags.get("format").map(|s| s == "text").unwrap_or(false); let tool_name = name.clone(); let from_arg = args @@ -572,6 +589,58 @@ fn call_tool(msg: &Json) -> Json { tool_result(value, textual, &tool_name) } +/// A required argument the client left out fails the call and names the accepted fields, +/// so an agent that guessed `file_path` or `path` can retry at once. Without this the tool +/// ran with an empty path and answered `success: true` with every field empty. +/// `project_root` is exempt: it falls back to WIKIFIER_PROJECT_ROOT, then the cwd. +fn missing_required(name: &str, args: Option<&Json>) -> Option { + if !MCP_TOOLS.contains(&name) { + return None; + } + let (_, required, _) = tool_spec(name); + let present = |key: &str| { + let value = args.and_then(|a| a.get(key)).or_else(|| { + (key == "query") + .then(|| args.and_then(|a| a.get("text"))) + .flatten() + }); + matches!(value, Some(Json::String(text)) if !text.trim().is_empty()) + }; + let missing: Vec<&str> = required + .iter() + .copied() + .filter(|field| *field != "project_root" && !present(field)) + .collect(); + if missing.is_empty() { + return None; + } + let names = |value: Option<&Json>| { + value + .and_then(|v| v.as_object()) + .map(|map| map.keys().cloned().collect::>().join(", ")) + .unwrap_or_default() + }; + let schema = tool_schema(name); + let accepted = names(schema.get("inputSchema").and_then(|s| s.get("properties"))); + let given = names(args); + let given = if given.is_empty() { + "none".to_string() + } else { + given + }; + Some(Json::obj(vec![ + ("success", Json::Bool(false)), + ( + "error", + Json::String(format!( + "{name} needs {} as a non-empty string. Accepted arguments: {accepted}. Received: {given}.", + missing.join(", ") + )), + ), + ("error_type", Json::String("InvalidArguments".into())), + ])) +} + fn tool_result(value: Json, textual: bool, tool_name: &str) -> Json { let is_error = value.get("success").and_then(|s| s.as_bool()) == Some(false); let text = if textual { diff --git a/src/mcp.rs.wiki.md b/src/mcp.rs.wiki.md index c6228e7..14aa755 100644 --- a/src/mcp.rs.wiki.md +++ b/src/mcp.rs.wiki.md @@ -3,3 +3,5 @@ **Role:** Stdio JSON-RPC MCP adapter over `src/api.rs`. `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`. + +A `tools/call` that leaves out a required argument other than `project_root` (or sends it empty) gets `InvalidArguments` naming the missing field, the accepted fields, and the fields received. `project_root` falls back to `WIKIFIER_PROJECT_ROOT`, then the cwd. `update_maps` omits the edge list unless `edges: true`; `edges_persisted` still gives the count. `health` takes `limit` (adds `entries_total` when it cuts) and `summary`. diff --git a/src/scope.rs b/src/scope.rs index 4bcbfcc..7d01b77 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -313,3 +313,73 @@ pub fn rel_of(root: &Path, path: &Path) -> String { .collect::>() .join("/") } + +/// Map roots for a fresh `init`: `src/` when it exists, else every top-level directory that +/// holds a source file (a Cargo workspace's `crates/`, a Python package dir, `tests/`). +/// Falls back to `src/` so the scope error still names a directory to create. +pub fn default_map_roots(root: &Path) -> Vec { + if root.join("src").is_dir() { + return vec!["src/".to_string()]; + } + let mut found: Vec = fs::read_dir(root) + .map(|entries| { + entries + .flatten() + .filter(|ent| ent.path().is_dir()) + .map(|ent| ent.file_name().to_string_lossy().into_owned()) + .filter(|name| !skip_dir(name) && has_source(&root.join(name), 6)) + .map(|name| format!("{name}/")) + .collect() + }) + .unwrap_or_default(); + found.sort(); + if found.is_empty() { + found.push("src/".to_string()); + } + found +} + +fn skip_dir(name: &str) -> bool { + name.starts_with('.') || DEFAULT_EXCLUDES.contains(&name) +} + +fn has_source(dir: &Path, depth: usize) -> bool { + let Ok(entries) = fs::read_dir(dir) else { + return false; + }; + let mut subdirs = Vec::new(); + for ent in entries.flatten() { + let path = ent.path(); + if path.is_dir() { + if depth > 0 && !skip_dir(&ent.file_name().to_string_lossy()) { + subdirs.push(path); + } + } else if is_source(&path) { + return true; + } + } + subdirs.into_iter().any(|sub| has_source(&sub, depth - 1)) +} + +/// A caller's file argument as the map and health key: project-relative and `/`-separated. +/// Accepts `./src/a.rs`, `src\a.rs`, and an absolute path under `root`, so a Windows agent +/// does not get an empty lookup or a second health row for the same file. +pub fn rel_arg(root: &Path, file: &str) -> String { + let path = Path::new(file); + if path.is_absolute() { + if let Ok(rel) = path.strip_prefix(root) { + return rel_of(Path::new(""), rel); + } + if let (Ok(real_root), Ok(real_path)) = (root.canonicalize(), path.canonicalize()) { + if let Ok(rel) = real_path.strip_prefix(&real_root) { + return rel_of(Path::new(""), rel); + } + } + } + let slashed = file.replace('\\', "/"); + let mut rel = slashed.as_str(); + while let Some(rest) = rel.strip_prefix("./") { + rel = rest; + } + rel.to_string() +} diff --git a/src/scope.rs.wiki.md b/src/scope.rs.wiki.md new file mode 100644 index 0000000..65ed0d9 --- /dev/null +++ b/src/scope.rs.wiki.md @@ -0,0 +1,7 @@ +# src/scope.rs + +**Role:** Which files a run may touch, and how a caller's path becomes a map key. + +`detect` reads `map_paths.txt` and `monitored_paths.txt` into a `Scope`. A missing configured directory is a scope error, and three or more child projects make the root a container; `Scope::allows_walk` is false in both cases. `collect` walks the allowed prefixes with `excludes` (defaults plus `exclude_patterns.txt`) and keeps files that pass `is_source` (extensions in `SOURCE_EXTS`). `read_path_list` reads one path list. + +`rel_of` turns a path under the root into the `/`-separated key the cache and health use. `rel_arg` does the same for a caller's argument: `src\a.rs`, `./src/a.rs`, and an absolute path under the root all give `src/a.rs`. `default_map_roots` picks the map roots for `init`: `src/` when it exists, else each top-level directory holding a source file, else `src/`. diff --git a/tests/agent_args.rs b/tests/agent_args.rs new file mode 100644 index 0000000..a8639d9 --- /dev/null +++ b/tests/agent_args.rs @@ -0,0 +1,302 @@ +//! Arguments as agents actually send them: Windows separators, absolute paths, a guessed +//! argument name, a file that is not there. Each must hit the right row or fail with a next step. + +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use wikifier::api; +use wikifier::{load_dependents, prepare_edit, read_health, record_change, update_maps, Json}; + +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-args-{name}-{n}-{}", std::process::id())); + let _ = fs::remove_dir_all(&dir); + fs::create_dir_all(&dir).unwrap(); + dir +} + +fn mapped_project(name: &str) -> PathBuf { + let root = scratch(name); + fs::create_dir_all(root.join("src")).unwrap(); + fs::write(root.join("map_paths.txt"), "src\n").unwrap(); + fs::write(root.join("src/a.py"), "def hello():\n return 1\n").unwrap(); + fs::write(root.join("src/b.py"), "from a import hello\n").unwrap(); + let built = update_maps(&root); + assert_eq!( + built.get("success").and_then(|v| v.as_bool()), + Some(true), + "{built}" + ); + root +} + +fn field<'a>(value: &'a Json, key: &str) -> &'a str { + value.get(key).and_then(|v| v.as_str()).unwrap_or("") +} + +fn mcp_call(root: &Path, name: &str, mut args: Vec<(&str, Json)>) -> Json { + args.push(( + "project_root", + Json::String(root.to_string_lossy().into_owned()), + )); + let msg = Json::obj(vec![ + ("jsonrpc", Json::String("2.0".into())), + ("id", Json::Int(7)), + ("method", Json::String("tools/call".into())), + ( + "params", + Json::obj(vec![ + ("name", Json::String(name.into())), + ("arguments", Json::obj(args)), + ]), + ), + ]); + let reply = wikifier::mcp::handle_message(&msg).expect("tools/call has an id"); + reply + .get("result") + .and_then(|r| r.get("structuredContent")) + .cloned() + .unwrap_or_else(|| panic!("no structuredContent: {reply}")) +} + +#[test] +fn backslash_and_absolute_file_args_hit_the_mapped_row() { + let root = mapped_project("sep"); + let recorded = record_change(&root, "src\\a.py", "windows separator (args-1)"); + assert_eq!(field(&recorded, "file"), "src/a.py", "{recorded}"); + let rows: Vec<_> = read_health(&root) + .into_iter() + .filter(|row| row.file.ends_with("a.py")) + .collect(); + assert_eq!(rows.len(), 1, "a second health row appeared: {rows:?}"); + assert_eq!(rows[0].file, "src/a.py"); + assert_eq!(rows[0].reason, "windows separator (args-1)"); + + let absolute = root.join("src").join("a.py"); + for arg in [absolute.to_string_lossy().into_owned(), "./src/a.py".into()] { + let report = prepare_edit(&root, &arg); + assert_eq!(field(&report, "file"), "src/a.py", "{arg}: {report}"); + assert!(report.to_string().contains("src/b.py"), "{arg}: {report}"); + } + let importers = load_dependents(&root, "src\\a.py"); + assert!(importers.to_string().contains("src/b.py"), "{importers}"); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn lookups_on_a_missing_file_fail_with_a_next_step() { + let root = mapped_project("missing"); + for value in [ + prepare_edit(&root, "src/nope.py"), + load_dependents(&root, "src/nope.py"), + api::dependency_report(&root, "src/nope.py", false), + api::dispatch( + "get_file_wiki", + &root, + &["src/nope.py".into()], + &BTreeMap::new(), + ), + ] { + assert_eq!( + value.get("success").and_then(|v| v.as_bool()), + Some(false), + "{value}" + ); + assert_eq!(field(&value, "error_type"), "FileNotFound", "{value}"); + assert!(field(&value, "error").contains("list_paths"), "{value}"); + } + let empty = api::dependency_report(&root, "", false); + assert_eq!(field(&empty, "error_type"), "ValueError", "{empty}"); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn mcp_names_the_argument_an_agent_left_out() { + let root = mapped_project("mcpargs"); + let guessed = mcp_call( + &root, + "prepare_edit", + vec![("file_path", Json::String("src/a.py".into()))], + ); + assert_eq!( + field(&guessed, "error_type"), + "InvalidArguments", + "{guessed}" + ); + let text = field(&guessed, "error"); + assert!(text.contains("needs file"), "{text}"); + assert!(text.contains("Received: file_path, project_root"), "{text}"); + + let no_reason = mcp_call( + &root, + "record_change", + vec![("file", Json::String("src/a.py".into()))], + ); + assert!( + field(&no_reason, "error").contains("needs reason"), + "{no_reason}" + ); + assert!( + wikifier::read_journal(&root, None).is_empty(), + "a rejected call still wrote the journal" + ); + + let alias = mcp_call( + &root, + "search_journal", + vec![("text", Json::String("anything".into()))], + ); + assert_eq!( + alias.get("success").and_then(|v| v.as_bool()), + Some(true), + "{alias}" + ); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn mcp_update_maps_leaves_out_the_edge_list_unless_asked() { + let root = mapped_project("edges"); + let lean = mcp_call(&root, "update_maps", vec![("full", Json::Bool(true))]); + assert_eq!( + lean.get("success").and_then(|v| v.as_bool()), + Some(true), + "{lean}" + ); + assert_eq!( + lean.get("edges_omitted").and_then(|v| v.as_bool()), + Some(true), + "{lean}" + ); + assert_eq!(lean.get("edges_persisted"), Some(&Json::Int(1)), "{lean}"); + assert_eq!( + lean.get("edges") + .and_then(|v| v.as_array()) + .map(|a| a.len()), + Some(0) + ); + + let full = mcp_call( + &root, + "update_maps", + vec![("full", Json::Bool(true)), ("edges", Json::Bool(true))], + ); + assert_eq!( + full.get("edges") + .and_then(|v| v.as_array()) + .map(|a| a.len()), + Some(1), + "{full}" + ); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn init_maps_the_directories_that_hold_source() { + let init = |root: &Path| { + let value = api::dispatch("init", root, &[], &BTreeMap::new()); + assert_eq!( + value.get("success").and_then(|v| v.as_bool()), + Some(true), + "{value}" + ); + let text = fs::read_to_string(root.join("map_paths.txt")).unwrap(); + text.lines() + .filter(|line| !line.starts_with('#') && !line.trim().is_empty()) + .map(|line| line.to_string()) + .collect::>() + }; + + let workspace = scratch("workspace"); + fs::create_dir_all(workspace.join("crates/core/src")).unwrap(); + fs::write(workspace.join("crates/core/src/lib.rs"), "pub fn f() {}\n").unwrap(); + fs::create_dir_all(workspace.join("tests")).unwrap(); + fs::write(workspace.join("tests/it.rs"), "#[test]\nfn t() {}\n").unwrap(); + fs::create_dir_all(workspace.join("docs")).unwrap(); + fs::write(workspace.join("docs/guide.md"), "# guide\n").unwrap(); + fs::create_dir_all(workspace.join("target/debug")).unwrap(); + fs::write(workspace.join("target/debug/build.rs"), "fn main() {}\n").unwrap(); + assert_eq!(init(&workspace), ["crates/", "tests/"]); + let built = update_maps(&workspace); + assert_eq!( + built.get("success").and_then(|v| v.as_bool()), + Some(true), + "{built}" + ); + let _ = fs::remove_dir_all(workspace); + + let classic = scratch("classic"); + fs::create_dir_all(classic.join("src")).unwrap(); + fs::create_dir_all(classic.join("lib")).unwrap(); + fs::write(classic.join("lib/x.py"), "x = 1\n").unwrap(); + assert_eq!(init(&classic), ["src/"]); + let _ = fs::remove_dir_all(classic); + + let empty = scratch("empty"); + assert_eq!(init(&empty), ["src/"]); + let _ = fs::remove_dir_all(empty); +} + +#[test] +fn mcp_health_honors_limit_and_summary() { + let root = mapped_project("health"); + let capped = mcp_call(&root, "health", vec![("limit", Json::Int(1))]); + assert_eq!( + capped + .get("entries") + .and_then(|v| v.as_array()) + .map(|a| a.len()), + Some(1), + "{capped}" + ); + assert_eq!(capped.get("entries_total"), Some(&Json::Int(2)), "{capped}"); + + let counts = mcp_call(&root, "health", vec![("summary", Json::Bool(true))]); + assert!(counts.get("entries").is_none(), "{counts}"); + assert!(counts.get("health_summary").is_some(), "{counts}"); + let _ = fs::remove_dir_all(root); +} + +/// A Windows checkout with `core.autocrlf` turns LF into CRLF. That is not an edit, so a +/// Green file must stay clean. +#[test] +fn crlf_checkout_of_a_green_file_is_not_a_change() { + let root = mapped_project("crlf"); + fs::write( + root.join("src/a.py.wiki.md"), + "# a.py\n\n- `hello()` returns 1.\n", + ) + .unwrap(); + let green = wikifier::mark_green(&root, "src/a.py", "wiki written (args-2)", false); + assert_eq!( + green.get("success").and_then(|v| v.as_bool()), + Some(true), + "{green}" + ); + let lf = fs::read(root.join("src/a.py")).unwrap(); + let mut crlf = Vec::new(); + for byte in lf { + if byte == b'\n' { + crlf.push(b'\r'); + } + crlf.push(byte); + } + fs::write(root.join("src/a.py"), crlf).unwrap(); + let checked = wikifier::check_changes(&root); + assert_eq!( + checked.get("changes_detected"), + Some(&Json::Int(0)), + "{checked}" + ); + let row = read_health(&root) + .into_iter() + .find(|row| row.file == "src/a.py") + .unwrap(); + assert!(row.status.contains("Green"), "{row:?}"); + let _ = fs::remove_dir_all(root); +} diff --git a/tests/dogfood.rs b/tests/dogfood.rs index 72d83af..57b0e03 100644 --- a/tests/dogfood.rs +++ b/tests/dogfood.rs @@ -282,6 +282,7 @@ fn prepare_edit_on_this_repo_lists_api_dependents() { "src/lib.rs", "src/main.rs", "src/mcp.rs", + "tests/agent_args.rs", "tests/cli_surface.rs", "tests/contract_gaps.rs", "tests/parity_fixes.rs", diff --git a/tests/dogfood.rs.wiki.md b/tests/dogfood.rs.wiki.md index ad425a4..58c87c0 100644 --- a/tests/dogfood.rs.wiki.md +++ b/tests/dogfood.rs.wiki.md @@ -2,4 +2,4 @@ **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`). +`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` (`agent_args`, `cli_surface`, `contract_gaps`, `parity_fixes`, `protocol_v07`, `robustness`, `skip_maps`). From 0fe756debaccc89e541e7b7b40340f9c2125363e Mon Sep 17 00:00:00 2001 From: IronAdamant <18153828+IronAdamant@users.noreply.github.com> Date: Thu, 5 Nov 2026 15:19:17 +1100 Subject: [PATCH 04/11] Rewrite README for newcomers. What it is, install, connecting an agent over MCP (Claude Code command and .mcp.json), a short quick start of the edit loop, and how it works (pipeline, artifacts, language table). The Performance and Benchmarks sections, tables and measurement notes are unchanged byte for byte. Detail moves to links: skills/run.md, AGENTS.md, CONTRIBUTING.md, docs/spec.md. README.md.wiki.md describes the new layout. Co-Authored-By: Claude Opus 5.5 --- README.md | 198 +++++++++++++++------------------------------- README.md.wiki.md | 2 +- 2 files changed, 63 insertions(+), 137 deletions(-) diff --git a/README.md b/README.md index 2848465..e456666 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,13 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![GitHub Stars](https://img.shields.io/github/stars/IronAdamant/wikifier?style=social)](https://github.com/IronAdamant/wikifier/stargazers) +**A zero-dependency codebase wiki for AI coding agents.** Wikifier keeps a small, queryable map of a project: a dependency graph, a health matrix that says which file notes are current, short per-file wiki notes, and a journal of why each file changed. An agent asks "who imports this file?" or "what changed since the last session, and why?" and gets a few hundred bytes of JSON instead of re-reading sources. + +It is one std-only Rust binary (empty `[dependencies]`) with a CLI and an MCP server (`wikifier mcp`). Agents keep the map current as they work. A small HTML dashboard exists for humans, but the product is the agent loop, not a docs site. + ## Install -Download a binary from the [GitHub releases](https://github.com/IronAdamant/wikifier/releases). A `v*.*.*` tag publishes these archives and a `SHA256SUMS` file: +Download a binary from the [GitHub releases](https://github.com/IronAdamant/wikifier/releases). Each `v*.*.*` release has these archives and a `SHA256SUMS` file: | Platform | Archive | |---|---| @@ -14,7 +18,7 @@ Download a binary from the [GitHub releases](https://github.com/IronAdamant/wiki | macOS Intel | `wikifier--x86_64-apple-darwin.tar.gz` | | Windows x86_64 | `wikifier--x86_64-pc-windows-msvc.zip` | -Check `SHA256SUMS`, unpack, and put `wikifier` (`wikifier.exe` on Windows) on your `PATH`. +Check `SHA256SUMS`, unpack, and put `wikifier` (`wikifier.exe` on Windows) on your `PATH`: ```bash # Linux x86_64, 5.0.0 @@ -25,7 +29,7 @@ tar -xzf wikifier-5.0.0-x86_64-unknown-linux-gnu.tar.gz ./wikifier --version ``` -Or build from source. `[dependencies]` is empty: +Or build from source (Rust 1.83 or newer): ```bash git clone https://github.com/IronAdamant/wikifier @@ -34,106 +38,69 @@ cargo build --release ./target/release/wikifier --version ``` -`pip install wikifier` still installs the archived Python 4.7 package. - -**A zero-dependency codebase wiki for AI agents.** Token-efficient maps so LLMs look things up instead of re-reading full sources. - -Wikifier is an **agent-to-agent** tool: it builds a living map of a project (health matrix, dependency graph, short file summaries) and agents keep that map current as they work. Humans can peek via a small dashboard; the product is the agent loop, not a general docs site or IDE. - -Agents working in this repository start at [`AGENTS.md`](AGENTS.md). The day-to-day contract is [`skills/run.md`](skills/run.md). - -Works from small scripts to large monorepos. **Deep import/include maps** (zero-dependency parsers): - -| Language | Extensions | Notes | -|----------|------------|--------| -| 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` | `mod`, `use`, `crate::`, `self::`, `super::`, and package or lib names from the root `Cargo.toml` and from `[workspace] members` (globs included). `-` becomes `_`. A member name points at that member's library. `crate::` stays inside the crate that owns the file, 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 | -| Java | `.java` | `import` / `import static` | +`wikifier.sh`, `wikifier.ps1`, and `wikifier.bat` run that build from a checkout (`WIKIFIER_BIN` overrides the path). `pip install wikifier` installs the archived Python 4.7 package, not this program. -JavaScript and TypeScript resolution also reads `tsconfig.json` `paths`, `package.json` `exports`, and npm `workspaces`. Health/journal still work for any monitored path. The non-Python parsers are pragmatic regex (not full cargo/`go.mod`/classpath/`-I` resolution). On huge monorepos, split scope deliberately: +## Connect an agent (MCP) -| File | Surface | -|------|---------| -| **`map_paths.txt`** | Package roots for **import maps** (`update-maps` walk). Prefer package dirs (`src/`, `packages/foo/`) — not a wiki-only file list. | -| **`monitored_paths.txt`** | **Wiki / health** watch list (can be individual `.md` files). Does **not** define the map. | - -Or pass `--directory=pkg/` / `--max-files=N` per run. Raise dirty cap with `WIKIFIER_CHECK_CHANGES_MAX` (default 2000) only when needed. Never set `project_root` to a multi-repo parent of clones. - -## Why - -Context windows are finite. Re-reading a large file to answer “what is this and who depends on it?” wastes tokens. - -| Artifact | Role | -|----------|------| -| `file_health.md` | 🟢 / 🟡 / 🔴 matrix — what to trust, what to fix first | -| `library.md` | File tree, Mermaid dependency map, import tables, cycles + confidence | -| `*.wiki.md` | Short per-file “what this is for” notes (**agent-maintained** prose) | -| `journal/` + `pending_updates.md` | Semantic *why* trail + work queue (audit, **not** a full issue tracker) | - -**Map first, wiki depth second:** `update-maps` builds the structural map automatically. Rich per-file wiki text is filled by agents as they work — not a free full-repo “understand everything” pass on init. - -## First run (bootstrap the map) +The server speaks newline-delimited JSON-RPC on stdio. With Claude Code: ```bash -cargo build --release # std-only Rust binary — no crates.io dependencies -./target/release/wikifier --version - -cd /path/to/your/project -./path/to/wikifier init # seeds lean path-list templates -# Edit monitored_paths.txt + map_paths.txt to package roots (not bare ".") on real trees -wikifier update-maps # full structural map → library.md + import cache -wikifier health --summary # matrix counts -wikifier suggest-next # or MCP suggest_next_actions — 🔴/🟡 only +claude mcp add wikifier -- wikifier mcp ``` -Always set an explicit root for external trees: `WIKIFIER_PROJECT_ROOT=/abs/path wikifier …` +Or add it to a project's `.mcp.json`: -**MCP `session_bootstrap` → `readiness: blocked`?** That means lean scope and/or the map are missing (often bare `.` monitor + never ran `update-maps`) — not a broken install. Fix: write lean `monitored_paths.txt` / `map_paths.txt`, then `update-maps`. Agent contract: **`skills/run.md`** § *Readiness*. +```json +{ + "mcpServers": { + "wikifier": { "command": "wikifier", "args": ["mcp"] } + } +} +``` -## Steady state (only touch what needs it) +Every tool takes `project_root` (an absolute path). Without it the server uses `WIKIFIER_PROJECT_ROOT`, then its working directory. A call with a missing argument or an unknown file fails with a message that names the fix, and a failed call sets `isError`. Then give the agent the short prompt in [`skills/run.md`](skills/run.md#paste-into-new-sessions). Any agent that can run shell commands can use the CLI instead: the daily tools have twins such as `wikifier session-bootstrap`, `prepare-edit`, and `record-change` that print the same JSON (the mapping is in `skills/run.md`). -Full protocol: [`skills/run.md`](skills/run.md) (Agent Protocol v0.7 — package **5.0.0**). +## Quick start ```bash -wikifier session-bootstrap # one-shot: root, health, attention, actions[], names-only map_index -wikifier check-changes # content-honest dirty; red ghosts (missing paths) -# prioritize 🔴 then *actionable* 🟡 — do NOT re-wiki 🟢 Green files -wikifier prepare-edit path/file.py # wiki + status + deps/dependents preflight -# ... edit only those sources ... -wikifier record-change "path/file.py" "why this changed" # required -# ... refresh that file’s wiki summary only ... -wikifier mark-green "path/file.py" # refused if the wiki misses public symbols you added/removed -# many files at once (reasons per path, glob or dir/; git supplies the file list): -wikifier record-changes -r "src/api/=retry on 429" -r "tests/=cover retries" --only src/ --only tests/ --green -wikifier update-maps # only if imports/structure changed (warm 0-dirty is cheap) -# removals: -wikifier record-deletion "path/gone.py" "why removed" +cd /path/to/project +wikifier init # path lists (src/, or the top-level dirs that hold source), empty health and journal +wikifier update-maps # parse imports, write the map and library.md +wikifier session-bootstrap # readiness, health counts, next actions[] + +wikifier prepare-edit src/api.rs # before an edit: status, wiki, dependencies, dependents +# ... edit src/api.rs ... +wikifier record-change src/api.rs "retry on 429 (task-12)" # the why goes to the journal +# ... update src/api.rs.wiki.md ... +wikifier mark-green src/api.rs # refused while the wiki misses a public symbol ``` -**Core 6** (prefer every session — MCP or library/CLI): -`session_bootstrap` → `check_changes` → `prepare_edit` → `suggest_next_actions` (json `actions[]`) → `record_change` → `mark_green`. +A second agent that starts cold runs `session-bootstrap`: files left Yellow come back as `actions[]` with the first agent's reason, and `search-journal` / `why-file` return the recorded reasons for any file. The full loop, JSON shapes, and every command are in [`skills/run.md`](skills/run.md). -Advanced intel as needed: `list_paths` (names-only folder expand), `get_dependencies`, `get_dependents`, `get_cycles`, barrels/diagnostics. Always pass `project_root=` / `WIKIFIER_PROJECT_ROOT` for external trees. **Never** point `project_root` at a multi-repo parent folder (e.g. a directory of clones). +## How it works -**Map split:** `update-maps` writes two views from the same import cache — human `library.md` (File Tree + mermaid, dashboard only) and sharded agent folder cards (`.wikifier_staging/maps/`, depth-1). Bootstrap returns the root card; `list_paths` expands one folder; `prepare_edit` follows file-to-file links. Do **not** read `library.md` mermaid for orientation. +`update-maps` runs **scan → parse → resolve → cache → artifacts**. It walks the directories in `map_paths.txt`, parses imports with comment-masked scanners, resolves them to project files, and stores edges in a text index under `.wikifier_staging/`. Unchanged files are skipped by size and mtime, so a warm run is fast. + +| Artifact | What it holds | +|---|---| +| `file_health.md` / `.json` | 🟢 Green (wiki checked against the code), 🟡 Yellow (changed, or no wiki yet), 🔴 Red (deleted) per file | +| `*.wiki.md` | Short agent-written note beside each source file | +| `journal/` + `pending_updates.md` | Why each file changed, and the work queue | +| `library.md` + `.wikifier_staging/maps/` | File tree and dependency tables; folder cards that `list-paths` returns | -## What you get +`check-changes` compares files with their Green baselines. `mark-green` checks that the wiki names the file's public Python, JS/TS, or Rust symbols. Other languages are mapped and recorded as `unchecked`. `map_paths.txt` lists the import-map roots; `monitored_paths.txt` adds extra files to watch, such as docs. -- **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, 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 unrestricted `pub` items, including methods and associated items on a public type and the items written in a trait impl for that type. `pub(crate)`, `pub(super)`, `pub(self)`, and `pub(in ...)` stay out of 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`. -- **Scale** — dependents and dependencies read one edge index. Unchanged files are skipped by size and mtime. `--full` rehashes. -- **MCP** — `wikifier mcp` in the same binary. A failed tool call sets `isError`. Calls stop at `WIKIFIER_MCP_DEADLINE_S` (default 60 seconds). -- **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. +| Language | Extensions | Import scan | +|---|---|---| +| Python | `.py` | relative and same-tree absolute imports; an indented import is flagged conditional | +| JavaScript / TypeScript | `.js` `.ts` `.jsx` `.tsx` `.mjs` `.cjs` `.mts` `.cts` | named re-exports followed to depth 8; `import()` flagged dynamic; reads `tsconfig.json` `paths`, `package.json` `exports`, npm `workspaces` | +| Rust | `.rs` | `mod`, `crate::`, `self::`, `super::`, and crate names from `Cargo.toml` and `[workspace] members` | +| Go | `.go` | `import` and import blocks | +| C / C++ | `.c` `.h` `.cpp` `.cc` `.cxx` `.hpp` `.hh` | `#include` | +| C# | `.cs` | `using` | +| Java | `.java` | `import`, `import static` | + +The scanners are pragmatic regex, not compilers. On a very large tree, list package roots in `map_paths.txt`, or pass `--directory` / `--max-files` (a budgeted run returns `map_complete=false` and the next run resumes). Never point the project root at a folder that holds several repositories. ## Performance (measured) @@ -189,59 +156,18 @@ Small is three modules under `src/pkg`. Medium is `src/` and `tests/`. Large is Tests: `cargo test` (std-only Rust; accuracy fixtures, cache lookups, and the agent-loop commands). -## Commands - -| Command | Purpose | -|---------|---------| -| `wikifier init [--target DIR]` | Bootstrap project + human `index.html` | -| `wikifier session-bootstrap` | Session start: health, attention, `actions[]`, names-only `map_index` | -| `wikifier check-changes` | Content-honest scan → health / pending | -| `wikifier prepare-edit ` | Preflight: status, wiki snippet, deps, dependents | -| `wikifier list-paths [prefix]` | Depth-1 folder card (names only). `--recursive` / `--depth=0` for a subtree. Then `prepare-edit` for file links. | -| `wikifier record-change "reason"` | Log *why* (required after edits) | -| `wikifier mark-green [--force]` | Check the wiki against the code, then mark it current (`--force` + reason overrides) | -| `wikifier record-changes [-r PATH=WHY]… [--only P]… [--green] [--dry-run]` | Record every file git reports as changed, reasons per path/glob/`dir/`; nothing written if any reason is missing | -| `wikifier verify-wikis [dir]` | Re-check every Green wiki against its code (exit 1 on failures) | -| `wikifier record-deletion "reason"` | Mark removed paths 🔴, drop them from the graph, prune barrel refs | -| `wikifier dependencies [--full]` / `dependents ` | What a file imports (compact; `--full` for per-edge confidence records) / who imports it (JSON) | -| `wikifier suggest-next` | Next actions (JSON `actions[]`; `--text` for prose). Includes low-confidence edges when any exist | -| `wikifier update-maps [--directory=src/] [--max-files=N]` | Rebuild graph + human `library.md` + agent folder maps (warm 0-dirty is fast; honors `map_paths.txt`) | -| `wikifier cache-status` | `wk_cache` backend and mapped file count | -| `wikifier health [--summary\|--format text]` | Health matrix. Default is JSON. `--summary` is counts only. `--text` or `--format text` is prose | -| `wikifier validate` | Missing wiki rows + ghost paths | -| `wikifier cycles [--no-analysis] [--max-items N]` | Circular deps, break hints, severity, and who is most depended on | -| `wikifier heal-stubs [--dry-run]` | Promote Initial stubs whose wiki is at least 350 characters | -| `wikifier monitor` / `daemon` | Background maintenance (`WIKIFIER_DAEMON_MAPS=0` for check-only) | -| `wikifier serve` | Localhost dashboard with Run/Stop | - -The same operations live in `src/api.rs`. Agents call the CLI or `wikifier mcp`. `wikifier.sh` / `wikifier.ps1` / `wikifier.bat` launch the Rust binary (`WIKIFIER_BIN` overrides the path). `pip install wikifier` still installs the archived Python 4.7 package. - -## MCP - -```bash -WIKIFIER_PROJECT_ROOT=/abs/path/to/project wikifier mcp -``` - -This repo's `.mcp.json` launches `wikifier` with argument `mcp`. Tool names and the daily loop are in [`skills/run.md`](skills/run.md). - -## Human dashboard (secondary) +## Human dashboard ![Wikifier dashboard — file tree, health pills, local Run/Stop](https://raw.githubusercontent.com/IronAdamant/wikifier/main/screenshot/front_page_review.png) -`wikifier init` drops a single `index.html`. Prefer **`wikifier serve`** (e.g. http://localhost:8787/index.html) — `file://` can’t load project files. The markdown artifacts and CLI/MCP tools stay the source of truth; the UI is a read-only window. - -`wikifier serve` also exposes a read-only JSON API (`/__wikifier/api/file?path=…`, `dependencies`, `dependents`, `cycles`, `journal`, `bootstrap`, `diagnostics`) that the dashboard uses for per-file dependencies and history. This repo also contains a proposed redesign, `index.v2.html` and `diagnostics.v2.html`, to compare side by side with the current pages (`http://localhost:8787/index.v2.html`); they are not deployed by `init`. - -## Scope - -**In:** agent-maintained codebase wiki, dependency intelligence, token-saving lookup for LLMs and coding agents. -**Out:** general human documentation systems, IDE plugins, “docs for everyone” product growth. +`wikifier init` copies one `index.html`. Run `wikifier serve` and open http://localhost:8787/index.html. It is a read-only view of the same artifacts. -**Agent navigability:** Prefer the protocol ([`skills/run.md`](skills/run.md)) and the MCP Core 6 over reading the parsers or the cache. Tests live in `tests/*.rs`. +## Documentation -## Links +- [`skills/run.md`](skills/run.md): the agent protocol (v0.7): the daily loop, readiness, JSON shapes, and every command +- [`AGENTS.md`](AGENTS.md): start here when working on this repository +- [`CONTRIBUTING.md`](CONTRIBUTING.md): adding a command or a language +- [`docs/spec.md`](docs/spec.md): original requirements and core principles +- [`CHANGELOG.md`](CHANGELOG.md) · [Releases](https://github.com/IronAdamant/wikifier/releases) -- [Releases](https://github.com/IronAdamant/wikifier/releases) · [PyPI](https://pypi.org/project/wikifier/) · [GitHub](https://github.com/IronAdamant/wikifier) -- Agent protocol: [`skills/run.md`](skills/run.md) -- Changelog: [`CHANGELOG.md`](CHANGELOG.md) -- Dogfood notes: `Findings/` (historical plans and research in `Findings/archive/`) +Scope: an agent-maintained codebase wiki for token-efficient lookup. General human documentation and IDE plugins are out of scope. diff --git a/README.md.wiki.md b/README.md.wiki.md index cd1e259..31239a4 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. -Install is a release archive (`wikifier--`, plus `SHA256SUMS`) or `cargo build --release`. `pip install wikifier` still installs Python 4.7. Agents start in this repo at `AGENTS.md` and follow `skills/run.md` (protocol v0.7, package 5.0.0). 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 `pub` items, impl methods, and trait-impl items, as well as Python and JS/TS. Rust also reads `[workspace] members`. MCP is `wikifier mcp`. +Order: what it is, install (release archive `wikifier--` plus `SHA256SUMS`, or `cargo build --release`), connecting an agent over MCP (`claude mcp add wikifier -- wikifier mcp` or `.mcp.json`, `project_root` on every call), a quick start of the edit loop, how it works (pipeline, artifacts, language table), then the measured Performance and Benchmarks tables (unchanged; the Performance table says its numbers are from the Python package), the dashboard, and links. Detail lives in `skills/run.md` (protocol v0.7, package 5.0.0) and `AGENTS.md`. `pip install wikifier` still installs Python 4.7. From 82532ee59db61522f1d4048fe9de3bb0cb04c6c9 Mon Sep 17 00:00:00 2001 From: IronAdamant <18153828+IronAdamant@users.noreply.github.com> Date: Thu, 5 Nov 2026 15:19:26 +1100 Subject: [PATCH 05/11] Dogfood: record this branch's changes in the repo's own wiki state. record-change / mark-green for the edited sources, tests and docs; update-maps refreshed library.md (it listed 25 of the 34 mapped files). Generated by the wikifier binary, not edited by hand. Co-Authored-By: Claude Opus 5.5 --- file_health.json | 2 +- file_health.md | 19 ++--- journal/2026/11/05.md | 64 +++++++++++++++ library.md | 177 ++++++++++++++++++++++++------------------ pending_updates.md | 6 +- 5 files changed, 176 insertions(+), 92 deletions(-) create mode 100644 journal/2026/11/05.md diff --git a/file_health.json b/file_health.json index 4a75859..16f6974 100644 --- a/file_health.json +++ b/file_health.json @@ -1 +1 @@ -{"entries":{".github/workflows/publish.yml":{"last_updated":"2026-11-03 09:27:04","reason":"Multi-target release archives and SHA256SUMS (subid=release-5.0.0)","source_content_hash":"sha256:930170e1eb57afbe2850a0e9c2ac179eb2b5947bf2f74d4292bcd2b0099ab8de","source_mtime_ns":"1793697915725124882","status":"🟢 Green","verification":"no_wiki"},"AGENTS.md":{"last_updated":"2026-11-03 09:27:04","reason":"Package line is 5.0.0; pip remains Python 4.7 (subid=release-5.0.0)","source_content_hash":"sha256:f2db15364d5b8b309d03a37fa123c55eb73103316632d2d0b3418205fea5625e","source_mtime_ns":"1793697832705476376","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:d82ebf68153d48b828a60ddafa7e0f4ed00095141048f3014da7e520d994cde0"},"CHANGELOG.md":{"last_updated":"2026-11-03 09:27:04","reason":"5.0.0 Rust release notes and benchmark link (subid=release-5.0.0)","source_content_hash":"sha256:799b5a8acb15bd3ddcd35d9812d7014bab833bbab5dd11b51a580c7da107e729","source_mtime_ns":"1793697832121478133","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:757601288624e346279cef86383a88cdf7c1c467e7c6538f40c05d475e243de8"},"CLAUDE.md":{"last_updated":"2026-11-03 09:27:04","reason":"Package line is 5.0.0 (subid=release-5.0.0)","source_content_hash":"sha256:2734c85af757bf0b873c3cc5ced86347f9042e37a90976023fbdbe43a6dbeda3","source_mtime_ns":"1793697832749476242","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:8991f81c78306a55befa7ef13876879fc5c22cd58ee6013567db4133afa14d34"},"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"},"Cargo.lock":{"last_updated":"2026-11-03 09:27:04","reason":"Lockfile version 5.0.0 (subid=release-5.0.0)","source_content_hash":"sha256:eb5e5f51e2a4fd584bd48af18328f7eaaae1efc4ee9fb23743d85a34faa8b435","source_mtime_ns":"1793697831949478649","status":"🟢 Green","verification":"no_wiki"},"Cargo.toml":{"last_updated":"2026-11-03 09:27:04","reason":"Version 5.0.0 (subid=release-5.0.0)","source_content_hash":"sha256:d16350a4d796d62eac12d64651ff9e9ae843db94489bfa4fb67c4a657b31b288","source_mtime_ns":"1793697832013478456","status":"🟢 Green","verification":"no_wiki"},"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 09:27:04","reason":"Install leads with release archives; package 5.0.0 (subid=release-5.0.0)","source_content_hash":"sha256:040de72ecff9502785073970d2d04dc8fa07da8d440b9e9dc6d8f0253655bd8e","source_mtime_ns":"1793697834025472402","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:db9ac3864b0bfcee45ebc1fc34622dabaa5ba34d49baa171626997c3e03c677f"},"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"},"scripts/benchmark_parity.sh":{"last_updated":"2026-11-03 08:56:15","reason":"Benchmark script and measured results.","source_content_hash":"sha256:b8f72e3245e77b9df17b303293d8cbe4a93cc36023c41b89a794b77b01daef87","status":"🟡 Yellow"},"scripts/benchmark_results.md":{"last_updated":"2026-11-03 08:56:15","reason":"Benchmark script and measured results.","source_content_hash":"sha256:8bf9342fce85f126a37f812b03b6f6997f1fe8e7c5176d30301c1a4a15aeee29","status":"🟡 Yellow"},"skills/run.md":{"last_updated":"2026-11-03 09:27:04","reason":"Package line is 5.0.0; protocol stays v0.7 (subid=release-5.0.0)","source_content_hash":"sha256:2ab1c890e39e1b499ebcfd4dd4a83733450369af6ef922ddf4fb4469b63b6b54","source_mtime_ns":"1793697833221474822","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:3956ae27f074aa9f8a94c643d2c8fb9ab56c38eb75e47a9bd727ad5de38ea4c7"},"src/api.rs":{"last_updated":"2026-11-03 08:56:14","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":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:8b23c099bfab315e96c055c0bcaf7d7edceb9e40420adc2d21a8ba930abbf1ef","source_mtime_ns":"1793684003575439733","status":"🟡 Yellow","wiki_hash":"sha256:69ac1101639d23ac6893adf37e501a820e277ace0ddeaf3c2c80afba28557e54"},"src/artifacts.rs":{"last_updated":"2026-11-03 08:56:14","public_symbols":"HealthRow\nread_health\nwrite_health\nupsert_health_row\nJournalEntry\njournal_path\nread_journal\nparse_journal\nJournalWrite\nappend_journal\nread_pending\nfolder_card\nnew\nkeep_baseline_from","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:6a7ca10120cbb4a37de62999330a92b181f3ebd7210f92209b280ba5282d8544","source_mtime_ns":"1793678007374624809","status":"🟡 Yellow","wiki_hash":"sha256:75bf0f72cc4bd57b6178dc6a0a58b8bf97bc3e7265485a28102093b967f20181"},"src/cache.rs":{"last_updated":"2026-11-03 08:56:14","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\nconfidence\nacs_reason","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:fb8a06997639e0f1556cc64ce5792f1525b0cb3c4afab5f9cbfcaeb9c809adf4","source_mtime_ns":"1793678007370625780","status":"🟡 Yellow","wiki_hash":"sha256:afb7493ff8742682f5f3c743da8919408502e6a6e75bba9998eb59cbbaf1c02a"},"src/cache_commit.rs":{"last_updated":"2026-11-03 08:56:14","public_symbols":"Consistency\nSession\ncheck_consistency\nrebuild\nopen\nknown\ndsts_of\nnote_stat\nrels\nupsert\nremove\nstage_barrel\ncommit","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:c987ccdccdd0c980c0af64a2dcb349cd3b4ed1de7070a481dba146782f40a745","source_mtime_ns":"1793684429116993727","status":"🟡 Yellow","wiki_hash":"sha256:c6f3fbdcd6aafe62de119c302e09e11ae0ea208681c52166cfcf79de80bb5f6c"},"src/cycle_report.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:db35b610d9a9f9917805da9383e61872e66e640482f57fc9e17ae91daf77fe3d","status":"🟡 Yellow"},"src/daemon_svc.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:0887baeb3ad7f2fca851fccc0d9335dafd4b42f3e54e236175a13d39c5bcd063","status":"🟡 Yellow"},"src/jsonutil.rs":{"last_updated":"2026-11-03 08:56:14","public_symbols":"Json\nparse\nparse_jsonc\nstrip_json_comments\nobj\nfrom_pairs\nget\nas_str\nas_bool\nas_array\nas_object\ninsert\nfmt","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:cb71e00e352df010928a42323d6d87bbe67348834a209348fc174b9d00489676","source_mtime_ns":"1793684429116993727","status":"🟡 Yellow","wiki_hash":"sha256:fb4cffba8b14a179dd90f201b61df2608ca78a9775e9646e7f011f1b9fa07c63"},"src/legacy_cache.rs":{"last_updated":"2026-11-03 08:56:14","public_symbols":"import_if_empty","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:a127d0b1742d69b736194ca98579f5fc0611fc51a291d691aef6fe586fd68ba3","source_mtime_ns":"1793683981803718735","status":"🟡 Yellow","wiki_hash":"sha256:a6fe6c79185a46cc4b30d215475bab28f517f6e5a6fd7bce6423c2792814aac8"},"src/lib.rs":{"last_updated":"2026-11-03 08:56:14","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":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:9c27c83b58ff7f1acf62d5d19ee31e2070daa06499e27141cc7071fae44e26b8","source_mtime_ns":"1793678007370625780","status":"🟡 Yellow","wiki_hash":"sha256:d7e771a3ff63ab55a7967195e3b403dd75d5ae020a383adc56f0e5c09d7b03a0"},"src/low_conf.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:3ade5fc70ce585a48b9acf6a2b48753e58e80f63281a31c8b8453816700b375e","status":"🟡 Yellow"},"src/main.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:2b3cebdafca3ca3da7831101a63b36f72260b3b2879871e5364b427428e782e0","source_mtime_ns":"1793683990351610198","status":"🟡 Yellow","wiki_hash":"sha256:db6e1cd5beeb90e5a65b3e00a37ee2c73ea4c53750771151e655459a1b823ff5"},"src/maps_out.rs":{"last_updated":"2026-11-03 08:56:14","public_symbols":"write\nwrite_status\nread_status","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:c018687f1e511e23bc92a3f771a1f2bf5e693d513d8e4bcad958d9908f0af09c","source_mtime_ns":"1793678007370625780","status":"🟡 Yellow","wiki_hash":"sha256:d441c395fe565828a87c54d5545e4c89e4ec4cfc49308a08525e6dcf3c744e75"},"src/mcp.rs":{"last_updated":"2026-11-03 08:56:14","public_symbols":"tool_schema\nhandle_message\nserve_stdio","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:22c88894fc72f3fad68a31c1582a01772a8409483d3a3127730eccce63067119","source_mtime_ns":"1793683982335712018","status":"🟡 Yellow","wiki_hash":"sha256:8414b1757bf3acc39a66a52443e9625cb027173b2783d996e39e9f707a9a1933"},"src/parse.rs":{"last_updated":"2026-11-03 08:56:14","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":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:5607acdac0b08854951700a9d2187a9f849d2fdb4febc2959fe065bcf778963a","source_mtime_ns":"1793684824322776381","status":"🟡 Yellow","wiki_hash":"sha256:c70af52c81a4dfffb364cba13ca21873491e83e1c9c5cee31ab2bab2bc379fbb"},"src/pending_line.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:c941813474dbb2cfc1ffed8c7bc9791d08eb718d92d336e1f42d1c1b0d67bcdc","status":"🟡 Yellow"},"src/proc.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:8f13798a8b9289d78913becc626056c8b016838570e356e09bfa3333e800351c","status":"🟡 Yellow"},"src/project_lock.rs":{"last_updated":"2026-11-03 08:56:14","public_symbols":"ProjectLock\ndrop\nacquire","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:194e329f35ff59b11fc734b980a1ee33f8e464bcdb5eae899486e487daef3a25","source_mtime_ns":"1793678007370625780","status":"🟡 Yellow","wiki_hash":"sha256:fccc8d547f30262ff64d506d842472623e828ebe39fde71532a683c6956329d2"},"src/scope.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:6dec4152e1ae1657661bd9f504877c02dd70bdb97c39173a952bea2851bedb4c","status":"🟡 Yellow"},"src/sha256.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:d16c7732fbdd20b3697b78129e8052f676c42bc862065f2bfe2b16fae1b7df30","status":"🟡 Yellow"},"src/symbols.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:4a150ba04e9ea26020c874c5e256a9ec5acc3ef4d621fb6ff03b21935c3f8046","status":"🟡 Yellow"},"src/text_out.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:8e0edc1141a2c1745c0e6fdd7a7645c7332365fac532d52fb3e188f4ef06148e","status":"🟡 Yellow"},"src/timeutil.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:9921127144d9d40619e260753e700e9c8a47135ed95b2ad8791afb7fe78afd75","status":"🟡 Yellow"},"src/wiki_files.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:2612539ce9092a3d126d80ce2947f6b4555ddb0bb335769b1619ec67d08a5e84","status":"🟡 Yellow"},"tests/behaviour_gaps.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Tests for those parity fixes.","source_content_hash":"sha256:8c8d669dae2dbe39b88c2223d20fb2fca51ac1605c62edd108d6ef224223402b","source_mtime_ns":"1793684893298602299","status":"🟡 Yellow","wiki_hash":"sha256:b9d20a310ca48ffd858e6d0d4e49b5e0568ecb57dc5ad981eb54398562864691"},"tests/cache_and_artifacts.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Tests for those parity fixes.","source_content_hash":"sha256:b9483666c1e19f90d4db1af718b08beebadfbcba66152497b7694d8e757a5ec1","status":"🟡 Yellow"},"tests/cli_surface.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Tests for those parity fixes.","source_content_hash":"sha256:c6d2ec49ab47aac39a17e6d39fb7eae08d4b27935a9d68bc02a238000646c739","status":"🟡 Yellow"},"tests/contract_gaps.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Tests for those parity fixes.","source_content_hash":"sha256:dc91a239bbc615524b4ac2c58d06515183dfc4981309906003f551e6721d82ae","status":"🟡 Yellow"},"tests/dogfood.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Tests for those parity fixes.","source_content_hash":"sha256:b596d9aa2a19c0e030e8d0c02613d1ced0ddf026c976bfcb3c563758bce57fb6","source_mtime_ns":"1793683337392806763","status":"🟡 Yellow","wiki_hash":"sha256:9546cc2062f19584c348419322f07d25d353c7f8ba0257e992372f63d798c020"},"tests/parity.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Tests for those parity fixes.","source_content_hash":"sha256:0246038a1cc01bf66da2fd186ede61f2dcd2c98da9c0c90dd7f3b2cbab1036db","source_mtime_ns":"1793683314773282057","status":"🟡 Yellow","wiki_hash":"sha256:32753c244642df6e08f8578c6203f77225363783c15bc57c33a84f03989f2378"},"tests/parity_fixes.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Tests for those parity fixes.","source_content_hash":"sha256:ac0fa6064323885f6e7bd767557fe3721723c8d20d60e36c7cc5265b85747e31","status":"🟡 Yellow"},"tests/protocol_v07.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Tests for those parity fixes.","source_content_hash":"sha256:bb7cffecabc3fb51ebcc77c64f7fd78f5cd15c445d7e13493ed491227280f37d","source_mtime_ns":"1793684781738827745","status":"🟡 Yellow","wiki_hash":"sha256:b38f4a13e316cefd56070f822980b4f83d9cdacb850ab1978de48ed35c630ec8"},"tests/robustness.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Tests for those parity fixes.","source_content_hash":"sha256:fad304193a47baa8cce21cff8786b35974c7c7f6f71b3318edeb68bd60121681","status":"🟡 Yellow"},"tests/skip_maps.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Tests for those parity fixes.","source_content_hash":"sha256:2eed0c6bc97d91a4be3f2b6ab08c78c62a3a05725a503496e3df91abad6b2130","status":"🟡 Yellow"},"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":{".github/workflows/publish.yml":{"last_updated":"2026-11-03 09:27:04","reason":"Multi-target release archives and SHA256SUMS (subid=release-5.0.0)","source_content_hash":"sha256:930170e1eb57afbe2850a0e9c2ac179eb2b5947bf2f74d4292bcd2b0099ab8de","source_mtime_ns":"1793697915725124882","status":"🟢 Green","verification":"no_wiki"},"AGENTS.md":{"last_updated":"2026-11-03 09:27:04","reason":"Package line is 5.0.0; pip remains Python 4.7 (subid=release-5.0.0)","source_content_hash":"sha256:f2db15364d5b8b309d03a37fa123c55eb73103316632d2d0b3418205fea5625e","source_mtime_ns":"1793697832705476376","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:d82ebf68153d48b828a60ddafa7e0f4ed00095141048f3014da7e520d994cde0"},"CHANGELOG.md":{"last_updated":"2026-11-05 04:16:31","reason":"wiki updated (claude/windows-verify)","source_content_hash":"sha256:f1ded74a2ac5495dcf0d5caaa5c8022d4c913192e5348febd96c519103dbcfe1","source_mtime_ns":"1793852120420150200","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:757601288624e346279cef86383a88cdf7c1c467e7c6538f40c05d475e243de8"},"CLAUDE.md":{"last_updated":"2026-11-03 09:27:04","reason":"Package line is 5.0.0 (subid=release-5.0.0)","source_content_hash":"sha256:2734c85af757bf0b873c3cc5ced86347f9042e37a90976023fbdbe43a6dbeda3","source_mtime_ns":"1793697832749476242","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:8991f81c78306a55befa7ef13876879fc5c22cd58ee6013567db4133afa14d34"},"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"},"Cargo.lock":{"last_updated":"2026-11-03 09:27:04","reason":"Lockfile version 5.0.0 (subid=release-5.0.0)","source_content_hash":"sha256:eb5e5f51e2a4fd584bd48af18328f7eaaae1efc4ee9fb23743d85a34faa8b435","source_mtime_ns":"1793697831949478649","status":"🟢 Green","verification":"no_wiki"},"Cargo.toml":{"last_updated":"2026-11-03 09:27:04","reason":"Version 5.0.0 (subid=release-5.0.0)","source_content_hash":"sha256:d16350a4d796d62eac12d64651ff9e9ae843db94489bfa4fb67c4a657b31b288","source_mtime_ns":"1793697832013478456","status":"🟢 Green","verification":"no_wiki"},"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-05 04:16:31","reason":"wiki updated (claude/windows-verify)","source_content_hash":"sha256:3d3678c13234664aeaf1d923d79f5082bb57da407c22dfa82a38f5bed4799e78","source_mtime_ns":"1793852178767718000","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:45e2581fe9d69ba807dfce7e982c015e23c2702a6c765b83eb9513ef809418da"},"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"},"scripts/benchmark_parity.sh":{"last_updated":"2026-11-03 08:56:15","reason":"Benchmark script and measured results.","source_content_hash":"sha256:b8f72e3245e77b9df17b303293d8cbe4a93cc36023c41b89a794b77b01daef87","status":"🟡 Yellow"},"scripts/benchmark_results.md":{"last_updated":"2026-11-03 08:56:15","reason":"Benchmark script and measured results.","source_content_hash":"sha256:8bf9342fce85f126a37f812b03b6f6997f1fe8e7c5176d30301c1a4a15aeee29","status":"🟡 Yellow"},"skills/run.md":{"last_updated":"2026-11-05 04:16:31","reason":"wiki updated (claude/windows-verify)","source_content_hash":"sha256:ae3a88efc0247857f9c5b9b6ae6985084bdad780e89186e35933ba3a9f93f625","source_mtime_ns":"1793852105332278800","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:d1f7fb759d196fd6a4b468148f0078915537d78aff1906017b9d855eca5b7200"},"src/api.rs":{"last_updated":"2026-11-05 04:14:15","public_symbols":"MapRun\nVERSION\nMCP_TOOLS\nupdate_maps\nupdate_maps_opts\nupdate_maps_run\nseed_source_hashes\nmapped_edges\nload_dependencies\ndependency_report\nload_dependents\nlist_paths\nlist_paths_at\nsession_bootstrap\nsession_bootstrap_scoped\nsuggest_next_actions\nsuggest_next_scoped\ncheck_changes\nprepare_edit\nrecord_change\nmark_green\nhealth\nhealth_view\nread_health\nread_journal\ncache_status\nvalidate\ncycles\ncycles_view\nverify_wikis\nrecord_changes\nrecord_changes_opts\nrecord_deletion\nheal_stubs\nprune_health\nprune_health_opts\nprune_pending\nstale_wikis\nstale_wikis_in\nprune_barrels\nmetrics_snapshot\nhealing_stats\nresolution_diagnostics\nbarrel_reports\nincremental_status\nserve_once\nserve_loop\nmonitor_loop\ndaemon_ctl\ndaemon_command\ndispatch\nCLI_COMMANDS\nensure_root","reason":"wiki updated (claude/windows-verify)","source_content_hash":"sha256:ab6cc6173ad4a8cdcb0d1bcd4561f52761e59824e491fb36477081d5434a7262","source_mtime_ns":"1793851923248387900","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:c4a22ccbc36099bc875de935d34358b654491670143864fb11fd8fba54543a48"},"src/artifacts.rs":{"last_updated":"2026-11-03 08:56:14","public_symbols":"HealthRow\nread_health\nwrite_health\nupsert_health_row\nJournalEntry\njournal_path\nread_journal\nparse_journal\nJournalWrite\nappend_journal\nread_pending\nfolder_card\nnew\nkeep_baseline_from","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:6a7ca10120cbb4a37de62999330a92b181f3ebd7210f92209b280ba5282d8544","source_mtime_ns":"1793678007374624809","status":"🟡 Yellow","wiki_hash":"sha256:75bf0f72cc4bd57b6178dc6a0a58b8bf97bc3e7265485a28102093b967f20181"},"src/cache.rs":{"last_updated":"2026-11-03 08:56:14","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\nconfidence\nacs_reason","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:fb8a06997639e0f1556cc64ce5792f1525b0cb3c4afab5f9cbfcaeb9c809adf4","source_mtime_ns":"1793678007370625780","status":"🟡 Yellow","wiki_hash":"sha256:afb7493ff8742682f5f3c743da8919408502e6a6e75bba9998eb59cbbaf1c02a"},"src/cache_commit.rs":{"last_updated":"2026-11-03 08:56:14","public_symbols":"Consistency\nSession\ncheck_consistency\nrebuild\nopen\nknown\ndsts_of\nnote_stat\nrels\nupsert\nremove\nstage_barrel\ncommit","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:c987ccdccdd0c980c0af64a2dcb349cd3b4ed1de7070a481dba146782f40a745","source_mtime_ns":"1793684429116993727","status":"🟡 Yellow","wiki_hash":"sha256:c6f3fbdcd6aafe62de119c302e09e11ae0ea208681c52166cfcf79de80bb5f6c"},"src/cycle_report.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:db35b610d9a9f9917805da9383e61872e66e640482f57fc9e17ae91daf77fe3d","status":"🟡 Yellow"},"src/daemon_svc.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:0887baeb3ad7f2fca851fccc0d9335dafd4b42f3e54e236175a13d39c5bcd063","status":"🟡 Yellow"},"src/jsonutil.rs":{"last_updated":"2026-11-03 08:56:14","public_symbols":"Json\nparse\nparse_jsonc\nstrip_json_comments\nobj\nfrom_pairs\nget\nas_str\nas_bool\nas_array\nas_object\ninsert\nfmt","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:cb71e00e352df010928a42323d6d87bbe67348834a209348fc174b9d00489676","source_mtime_ns":"1793684429116993727","status":"🟡 Yellow","wiki_hash":"sha256:fb4cffba8b14a179dd90f201b61df2608ca78a9775e9646e7f011f1b9fa07c63"},"src/legacy_cache.rs":{"last_updated":"2026-11-03 08:56:14","public_symbols":"import_if_empty","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:a127d0b1742d69b736194ca98579f5fc0611fc51a291d691aef6fe586fd68ba3","source_mtime_ns":"1793683981803718735","status":"🟡 Yellow","wiki_hash":"sha256:a6fe6c79185a46cc4b30d215475bab28f517f6e5a6fd7bce6423c2792814aac8"},"src/lib.rs":{"last_updated":"2026-11-03 08:56:14","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":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:9c27c83b58ff7f1acf62d5d19ee31e2070daa06499e27141cc7071fae44e26b8","source_mtime_ns":"1793678007370625780","status":"🟡 Yellow","wiki_hash":"sha256:d7e771a3ff63ab55a7967195e3b403dd75d5ae020a383adc56f0e5c09d7b03a0"},"src/low_conf.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:3ade5fc70ce585a48b9acf6a2b48753e58e80f63281a31c8b8453816700b375e","status":"🟡 Yellow"},"src/main.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:2b3cebdafca3ca3da7831101a63b36f72260b3b2879871e5364b427428e782e0","source_mtime_ns":"1793683990351610198","status":"🟡 Yellow","wiki_hash":"sha256:db6e1cd5beeb90e5a65b3e00a37ee2c73ea4c53750771151e655459a1b823ff5"},"src/maps_out.rs":{"last_updated":"2026-11-03 08:56:14","public_symbols":"write\nwrite_status\nread_status","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:c018687f1e511e23bc92a3f771a1f2bf5e693d513d8e4bcad958d9908f0af09c","source_mtime_ns":"1793678007370625780","status":"🟡 Yellow","wiki_hash":"sha256:d441c395fe565828a87c54d5545e4c89e4ec4cfc49308a08525e6dcf3c744e75"},"src/mcp.rs":{"last_updated":"2026-11-05 04:14:09","public_symbols":"tool_schema\nhandle_message\nserve_stdio","reason":"wiki updated (claude/windows-verify)","source_content_hash":"sha256:f6e3f44b0ca8f058cffa63ef2c42f0864eae0c99fb82ee68cc2b8fd3cdac3d4a","source_mtime_ns":"1793851788815286600","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:a54f210674d40bfe390a7271ecdb1111ecca199a217c49764f9b1a616d835486"},"src/parse.rs":{"last_updated":"2026-11-03 08:56:14","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":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:5607acdac0b08854951700a9d2187a9f849d2fdb4febc2959fe065bcf778963a","source_mtime_ns":"1793684824322776381","status":"🟡 Yellow","wiki_hash":"sha256:c70af52c81a4dfffb364cba13ca21873491e83e1c9c5cee31ab2bab2bc379fbb"},"src/pending_line.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:c941813474dbb2cfc1ffed8c7bc9791d08eb718d92d336e1f42d1c1b0d67bcdc","status":"🟡 Yellow"},"src/proc.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:8f13798a8b9289d78913becc626056c8b016838570e356e09bfa3333e800351c","status":"🟡 Yellow"},"src/project_lock.rs":{"last_updated":"2026-11-03 08:56:14","public_symbols":"ProjectLock\ndrop\nacquire","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:194e329f35ff59b11fc734b980a1ee33f8e464bcdb5eae899486e487daef3a25","source_mtime_ns":"1793678007370625780","status":"🟡 Yellow","wiki_hash":"sha256:fccc8d547f30262ff64d506d842472623e828ebe39fde71532a683c6956329d2"},"src/scope.rs":{"last_updated":"2026-11-05 04:14:08","public_symbols":"SOURCE_EXTS\nScope\ndetect\nread_path_list\nexcludes\nis_source\ncollect\nrel_of\ndefault_map_roots\nrel_arg\nallows_walk","reason":"wiki updated (claude/windows-verify)","source_content_hash":"sha256:ad1e6cbb7ab6f5d5f9767b788b2f150174d9b752763dd577b22d752fe267cb08","source_mtime_ns":"1793851508989498600","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:574d4cc8bd261700b1e192628435926d8f1e533f7f1967e27eb6c797e59e6c0e"},"src/sha256.rs":{"last_updated":"2026-11-05 04:14:08","public_symbols":"sha256\nsha256_hex\nsha256_prefixed\ncontent_sha256_prefixed\nfile_sha256_prefixed","reason":"wiki updated (claude/windows-verify)","source_content_hash":"sha256:2f4f8a8ccb9f9d4748fc0678e6bac63dc69ade57e9ab4b3c6eb2a1c43fcf5c14","source_mtime_ns":"1793851995838527400","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:0d1d67e61bed7a20c6a8413a352a55fb87f05bda6e39760015727d128a67ae33"},"src/symbols.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:4a150ba04e9ea26020c874c5e256a9ec5acc3ef4d621fb6ff03b21935c3f8046","status":"🟡 Yellow"},"src/text_out.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:8e0edc1141a2c1745c0e6fdd7a7645c7332365fac532d52fb3e188f4ef06148e","status":"🟡 Yellow"},"src/timeutil.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:9921127144d9d40619e260753e700e9c8a47135ed95b2ad8791afb7fe78afd75","status":"🟡 Yellow"},"src/wiki_files.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:2612539ce9092a3d126d80ce2947f6b4555ddb0bb335769b1619ec67d08a5e84","status":"🟡 Yellow"},"tests/agent_args.rs":{"last_updated":"2026-11-05 04:14:08","reason":"new: agent-style file args, missing args, lean MCP outputs, init roots, CRLF (claude/windows-verify)","source_content_hash":"sha256:ae25dcc233b1d40c48dbc7199e350032717412316089084751b9280970feb66d","status":"🟡 Yellow"},"tests/behaviour_gaps.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Tests for those parity fixes.","source_content_hash":"sha256:8c8d669dae2dbe39b88c2223d20fb2fca51ac1605c62edd108d6ef224223402b","source_mtime_ns":"1793684893298602299","status":"🟡 Yellow","wiki_hash":"sha256:b9d20a310ca48ffd858e6d0d4e49b5e0568ecb57dc5ad981eb54398562864691"},"tests/cache_and_artifacts.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Tests for those parity fixes.","source_content_hash":"sha256:b9483666c1e19f90d4db1af718b08beebadfbcba66152497b7694d8e757a5ec1","status":"🟡 Yellow"},"tests/cli_surface.rs":{"last_updated":"2026-11-05 04:14:08","reason":"launcher exit-code test for wikifier.bat / wikifier.sh (claude/windows-verify)","source_content_hash":"sha256:085faae9e15ace6df3f133f2426ab642f809fe2f1fbc305ae252dcf8a04f57b5","status":"🟡 Yellow"},"tests/contract_gaps.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Tests for those parity fixes.","source_content_hash":"sha256:dc91a239bbc615524b4ac2c58d06515183dfc4981309906003f551e6721d82ae","status":"🟡 Yellow"},"tests/dogfood.rs":{"last_updated":"2026-11-05 04:14:09","reason":"wiki updated (claude/windows-verify)","source_content_hash":"sha256:ca91d83da8aeb3cc2b7619e48258d0895e668d22c3f2980c89e2ab33f265fb88","source_mtime_ns":"1793851508995537100","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:0bc5f3d78d1779ba6302ecc406dc0b25d8beda25f1681a26d57e97210df19906"},"tests/parity.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Tests for those parity fixes.","source_content_hash":"sha256:0246038a1cc01bf66da2fd186ede61f2dcd2c98da9c0c90dd7f3b2cbab1036db","source_mtime_ns":"1793683314773282057","status":"🟡 Yellow","wiki_hash":"sha256:32753c244642df6e08f8578c6203f77225363783c15bc57c33a84f03989f2378"},"tests/parity_fixes.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Tests for those parity fixes.","source_content_hash":"sha256:ac0fa6064323885f6e7bd767557fe3721723c8d20d60e36c7cc5265b85747e31","status":"🟡 Yellow"},"tests/protocol_v07.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Tests for those parity fixes.","source_content_hash":"sha256:bb7cffecabc3fb51ebcc77c64f7fd78f5cd15c445d7e13493ed491227280f37d","source_mtime_ns":"1793684781738827745","status":"🟡 Yellow","wiki_hash":"sha256:b38f4a13e316cefd56070f822980b4f83d9cdacb850ab1978de48ed35c630ec8"},"tests/robustness.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Tests for those parity fixes.","source_content_hash":"sha256:fad304193a47baa8cce21cff8786b35974c7c7f6f71b3318edeb68bd60121681","status":"🟡 Yellow"},"tests/skip_maps.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Tests for those parity fixes.","source_content_hash":"sha256:2eed0c6bc97d91a4be3f2b6ab08c78c62a3a05725a503496e3df91abad6b2130","status":"🟡 Yellow"},"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 e7c22a2..382ee70 100644 --- a/file_health.md +++ b/file_health.md @@ -4,7 +4,7 @@ |------|--------|--------------|-----------------| | .github/workflows/publish.yml | 🟢 Green | 2026-11-03 09:27:04 | Multi-target release archives and SHA256SUMS (subid=release-5.0.0) | | AGENTS.md | 🟢 Green | 2026-11-03 09:27:04 | Package line is 5.0.0; pip remains Python 4.7 (subid=release-5.0.0) | -| CHANGELOG.md | 🟢 Green | 2026-11-03 09:27:04 | 5.0.0 Rust release notes and benchmark link (subid=release-5.0.0) | +| CHANGELOG.md | 🟢 Green | 2026-11-05 04:16:31 | wiki updated (claude/windows-verify) | | CLAUDE.md | 🟢 Green | 2026-11-03 09:27:04 | Package line is 5.0.0 (subid=release-5.0.0) | | CONTRIBUTING.md | 🟢 Green | 2026-11-03 05:34:35 | Address the 3 Nov review findings (subid=review-2026-11-03) | | Cargo.lock | 🟢 Green | 2026-11-03 09:27:04 | Lockfile version 5.0.0 (subid=release-5.0.0) | @@ -14,12 +14,12 @@ | 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 09:27:04 | Install leads with release archives; package 5.0.0 (subid=release-5.0.0) | +| README.md | 🟢 Green | 2026-11-05 04:16:31 | wiki updated (claude/windows-verify) | | 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) | | scripts/benchmark_parity.sh | 🟡 Yellow | 2026-11-03 08:56:15 | Benchmark script and measured results. | | scripts/benchmark_results.md | 🟡 Yellow | 2026-11-03 08:56:15 | Benchmark script and measured results. | -| skills/run.md | 🟢 Green | 2026-11-03 09:27:04 | Package line is 5.0.0; protocol stays v0.7 (subid=release-5.0.0) | -| src/api.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | +| skills/run.md | 🟢 Green | 2026-11-05 04:16:31 | wiki updated (claude/windows-verify) | +| src/api.rs | 🟢 Green | 2026-11-05 04:14:15 | wiki updated (claude/windows-verify) | | src/artifacts.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | | src/cache.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | | src/cache_commit.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | @@ -31,22 +31,23 @@ | src/low_conf.rs | 🟡 Yellow | 2026-11-03 08:56:15 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | | src/main.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | | src/maps_out.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | -| src/mcp.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | +| src/mcp.rs | 🟢 Green | 2026-11-05 04:14:09 | wiki updated (claude/windows-verify) | | src/parse.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | | src/pending_line.rs | 🟡 Yellow | 2026-11-03 08:56:15 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | | src/proc.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | | src/project_lock.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | -| src/scope.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | -| src/sha256.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | +| src/scope.rs | 🟢 Green | 2026-11-05 04:14:08 | wiki updated (claude/windows-verify) | +| src/sha256.rs | 🟢 Green | 2026-11-05 04:14:08 | wiki updated (claude/windows-verify) | | src/symbols.rs | 🟡 Yellow | 2026-11-03 08:56:15 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | | src/text_out.rs | 🟡 Yellow | 2026-11-03 08:56:15 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | | src/timeutil.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | | src/wiki_files.rs | 🟡 Yellow | 2026-11-03 08:56:15 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | +| tests/agent_args.rs | 🟡 Yellow | 2026-11-05 04:14:08 | new: agent-style file args, missing args, lean MCP outputs, init roots, CRLF (claude/windows-verify) | | tests/behaviour_gaps.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Tests for those parity fixes. | | tests/cache_and_artifacts.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Tests for those parity fixes. | -| tests/cli_surface.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Tests for those parity fixes. | +| tests/cli_surface.rs | 🟡 Yellow | 2026-11-05 04:14:08 | launcher exit-code test for wikifier.bat / wikifier.sh (claude/windows-verify) | | tests/contract_gaps.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Tests for those parity fixes. | -| tests/dogfood.rs | 🟡 Yellow | 2026-11-03 08:56:15 | Tests for those parity fixes. | +| tests/dogfood.rs | 🟢 Green | 2026-11-05 04:14:09 | wiki updated (claude/windows-verify) | | tests/parity.rs | 🟡 Yellow | 2026-11-03 08:56:15 | Tests for those parity fixes. | | tests/parity_fixes.rs | 🟡 Yellow | 2026-11-03 08:56:15 | Tests for those parity fixes. | | tests/protocol_v07.rs | 🟡 Yellow | 2026-11-03 08:56:15 | Tests for those parity fixes. | diff --git a/journal/2026/11/05.md b/journal/2026/11/05.md new file mode 100644 index 0000000..8796dbe --- /dev/null +++ b/journal/2026/11/05.md @@ -0,0 +1,64 @@ +## [2026-11-05 04:11:53] check-changes +**File:** src/api.rs +**Reason:** content changed since last mark-green (check-changes) + +## [2026-11-05 04:11:53] check-changes +**File:** src/mcp.rs +**Reason:** content changed since last mark-green (check-changes) + +## [2026-11-05 04:11:53] check-changes +**File:** src/scope.rs +**Reason:** content changed since last mark-green (check-changes) + +## [2026-11-05 04:11:53] check-changes +**File:** src/sha256.rs +**Reason:** content changed since last mark-green (check-changes) + +## [2026-11-05 04:11:53] check-changes +**File:** tests/cli_surface.rs +**Reason:** content changed since last mark-green (check-changes) + +## [2026-11-05 04:11:53] check-changes +**File:** tests/dogfood.rs +**Reason:** content changed since last mark-green (check-changes) + +## [2026-11-05 04:14:08] record-change +**File:** src/scope.rs +**Reason:** rel_arg normalizes Windows/absolute file args; default_map_roots for init (claude/windows-verify) + +## [2026-11-05 04:14:08] record-change +**File:** src/sha256.rs +**Reason:** content_sha256_prefixed: CRLF hashes as LF so autocrlf checkouts stay Green (claude/windows-verify) + +## [2026-11-05 04:14:08] record-change +**File:** src/api.rs +**Reason:** file args via rel_arg, FileNotFound for unknown lookups, health limit, init map roots, CRLF-neutral hashes (claude/windows-verify) + +## [2026-11-05 04:14:08] record-change +**File:** src/mcp.rs +**Reason:** InvalidArguments for missing required args; update_maps edges opt-in; health limit/summary schema (claude/windows-verify) + +## [2026-11-05 04:14:08] record-change +**File:** tests/cli_surface.rs +**Reason:** launcher exit-code test for wikifier.bat / wikifier.sh (claude/windows-verify) + +## [2026-11-05 04:14:08] record-change +**File:** tests/dogfood.rs +**Reason:** agent_args.rs is a new api dependent (claude/windows-verify) + +## [2026-11-05 04:14:08] record-change +**File:** tests/agent_args.rs +**Reason:** new: agent-style file args, missing args, lean MCP outputs, init roots, CRLF (claude/windows-verify) + +## [2026-11-05 04:16:31] record-change +**File:** README.md +**Reason:** rewrite for newcomers: what, install, MCP setup, quick start, how it works; benchmarks unchanged (claude/windows-verify) + +## [2026-11-05 04:16:31] record-change +**File:** skills/run.md +**Reason:** document backslash/absolute file args, FileNotFound lookups, InvalidArguments, MCP update_maps edges opt-in, health limit, init map roots (claude/windows-verify) + +## [2026-11-05 04:16:31] record-change +**File:** CHANGELOG.md +**Reason:** Unreleased: Windows fixes, agent-arg errors, lean MCP outputs, init map roots (claude/windows-verify) + diff --git a/library.md b/library.md index 6f6eed5..99616dd 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: 25. Edges: 74. +Files: 34. Edges: 101. ## File Tree @@ -11,24 +11,33 @@ src/api.rs src/artifacts.rs src/cache.rs src/cache_commit.rs +src/cycle_report.rs +src/daemon_svc.rs src/jsonutil.rs src/legacy_cache.rs src/lib.rs +src/low_conf.rs src/main.rs src/maps_out.rs src/mcp.rs src/parse.rs +src/pending_line.rs src/proc.rs src/project_lock.rs src/scope.rs src/sha256.rs +src/symbols.rs +src/text_out.rs src/timeutil.rs +src/wiki_files.rs +tests/agent_args.rs tests/behaviour_gaps.rs tests/cache_and_artifacts.rs tests/cli_surface.rs tests/contract_gaps.rs tests/dogfood.rs tests/parity.rs +tests/parity_fixes.rs tests/protocol_v07.rs tests/robustness.rs tests/skip_maps.rs @@ -40,99 +49,113 @@ tests/skip_maps.rs 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/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"] --> 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"] + n0["src/api.rs"] --> n3["src/cycle_report.rs"] + n0["src/api.rs"] --> n4["src/daemon_svc.rs"] + n0["src/api.rs"] --> n5["src/jsonutil.rs"] + n0["src/api.rs"] --> n6["src/legacy_cache.rs"] + n0["src/api.rs"] --> n7["src/low_conf.rs"] + n0["src/api.rs"] --> n8["src/maps_out.rs"] + n0["src/api.rs"] --> n9["src/parse.rs"] + n0["src/api.rs"] --> n10["src/pending_line.rs"] + n0["src/api.rs"] --> n11["src/proc.rs"] + n0["src/api.rs"] --> n12["src/project_lock.rs"] + n0["src/api.rs"] --> n13["src/scope.rs"] + n0["src/api.rs"] --> n14["src/sha256.rs"] + n0["src/api.rs"] --> n15["src/symbols.rs"] + n0["src/api.rs"] --> n16["src/timeutil.rs"] + n0["src/api.rs"] --> n17["src/wiki_files.rs"] + n1["src/artifacts.rs"] --> n5["src/jsonutil.rs"] + n1["src/artifacts.rs"] --> n16["src/timeutil.rs"] + n2["src/cache.rs"] --> n18["src/cache_commit.rs"] + n2["src/cache.rs"] --> n5["src/jsonutil.rs"] + n2["src/cache.rs"] --> n13["src/scope.rs"] + n18["src/cache_commit.rs"] --> n2["src/cache.rs"] + n18["src/cache_commit.rs"] --> n5["src/jsonutil.rs"] + n18["src/cache_commit.rs"] --> n11["src/proc.rs"] + n3["src/cycle_report.rs"] --> n5["src/jsonutil.rs"] + n4["src/daemon_svc.rs"] --> n5["src/jsonutil.rs"] + n4["src/daemon_svc.rs"] --> n16["src/timeutil.rs"] + n6["src/legacy_cache.rs"] --> n2["src/cache.rs"] + n6["src/legacy_cache.rs"] --> n5["src/jsonutil.rs"] + n6["src/legacy_cache.rs"] --> n8["src/maps_out.rs"] + n6["src/legacy_cache.rs"] --> n12["src/project_lock.rs"] + n19["src/lib.rs"] --> n0["src/api.rs"] + n19["src/lib.rs"] --> n1["src/artifacts.rs"] + n19["src/lib.rs"] --> n2["src/cache.rs"] + n19["src/lib.rs"] --> n18["src/cache_commit.rs"] + n19["src/lib.rs"] --> n3["src/cycle_report.rs"] + n19["src/lib.rs"] --> n4["src/daemon_svc.rs"] + n19["src/lib.rs"] --> n5["src/jsonutil.rs"] + n19["src/lib.rs"] --> n6["src/legacy_cache.rs"] + n19["src/lib.rs"] --> n7["src/low_conf.rs"] + n19["src/lib.rs"] --> n8["src/maps_out.rs"] + n19["src/lib.rs"] --> n20["src/mcp.rs"] + n19["src/lib.rs"] --> n9["src/parse.rs"] + n19["src/lib.rs"] --> n10["src/pending_line.rs"] + n19["src/lib.rs"] --> n11["src/proc.rs"] + n19["src/lib.rs"] --> n12["src/project_lock.rs"] + n19["src/lib.rs"] --> n13["src/scope.rs"] + n19["src/lib.rs"] --> n14["src/sha256.rs"] + n19["src/lib.rs"] --> n15["src/symbols.rs"] + n19["src/lib.rs"] --> n21["src/text_out.rs"] + n19["src/lib.rs"] --> n16["src/timeutil.rs"] + n19["src/lib.rs"] --> n17["src/wiki_files.rs"] + n7["src/low_conf.rs"] --> n2["src/cache.rs"] + n22["src/main.rs"] --> n0["src/api.rs"] + n22["src/main.rs"] --> n5["src/jsonutil.rs"] + n22["src/main.rs"] --> n20["src/mcp.rs"] + n22["src/main.rs"] --> n21["src/text_out.rs"] + n22["src/main.rs"] --> n16["src/timeutil.rs"] + n8["src/maps_out.rs"] --> n2["src/cache.rs"] + n8["src/maps_out.rs"] --> n5["src/jsonutil.rs"] + n20["src/mcp.rs"] --> n0["src/api.rs"] + n20["src/mcp.rs"] --> n5["src/jsonutil.rs"] + n20["src/mcp.rs"] --> n21["src/text_out.rs"] + n9["src/parse.rs"] --> n5["src/jsonutil.rs"] + n9["src/parse.rs"] --> n13["src/scope.rs"] + n12["src/project_lock.rs"] --> n11["src/proc.rs"] + n21["src/text_out.rs"] --> n5["src/jsonutil.rs"] + n23["tests/agent_args.rs"] --> n0["src/api.rs"] + n23["tests/agent_args.rs"] --> n19["src/lib.rs"] + n23["tests/agent_args.rs"] --> n20["src/mcp.rs"] + n24["tests/behaviour_gaps.rs"] --> n2["src/cache.rs"] + n24["tests/behaviour_gaps.rs"] --> n5["src/jsonutil.rs"] + n24["tests/behaviour_gaps.rs"] --> n19["src/lib.rs"] + n24["tests/behaviour_gaps.rs"] --> n20["src/mcp.rs"] + n25["tests/cache_and_artifacts.rs"] --> n2["src/cache.rs"] + n25["tests/cache_and_artifacts.rs"] --> n19["src/lib.rs"] + n26["tests/cli_surface.rs"] --> n0["src/api.rs"] + n26["tests/cli_surface.rs"] --> n5["src/jsonutil.rs"] + n26["tests/cli_surface.rs"] --> n19["src/lib.rs"] ``` +Mermaid shows the first 80 edges of 101. + | Source | Depends on | |--------|------------| -| 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/api.rs | src/artifacts.rs, src/cache.rs, src/cycle_report.rs, src/daemon_svc.rs, src/jsonutil.rs, src/legacy_cache.rs, src/low_conf.rs, src/maps_out.rs, src/parse.rs, src/pending_line.rs, src/proc.rs, src/project_lock.rs, src/scope.rs, src/sha256.rs, src/symbols.rs, src/timeutil.rs, src/wiki_files.rs | | src/artifacts.rs | src/jsonutil.rs, src/timeutil.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/cycle_report.rs | src/jsonutil.rs | +| src/daemon_svc.rs | src/jsonutil.rs, src/timeutil.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/lib.rs | src/api.rs, src/artifacts.rs, src/cache.rs, src/cache_commit.rs, src/cycle_report.rs, src/daemon_svc.rs, src/jsonutil.rs, src/legacy_cache.rs, src/low_conf.rs, src/maps_out.rs, src/mcp.rs, src/parse.rs, src/pending_line.rs, src/proc.rs, src/project_lock.rs, src/scope.rs, src/sha256.rs, src/symbols.rs, src/text_out.rs, src/timeutil.rs, src/wiki_files.rs | +| src/low_conf.rs | src/cache.rs | +| src/main.rs | src/api.rs, src/jsonutil.rs, src/mcp.rs, src/text_out.rs, src/timeutil.rs | | src/maps_out.rs | src/cache.rs, src/jsonutil.rs | -| src/mcp.rs | src/api.rs, src/jsonutil.rs | +| src/mcp.rs | src/api.rs, src/jsonutil.rs, src/text_out.rs | | src/parse.rs | src/jsonutil.rs, src/scope.rs | | src/project_lock.rs | src/proc.rs | +| src/text_out.rs | src/jsonutil.rs | +| tests/agent_args.rs | src/api.rs, src/lib.rs, src/mcp.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/parity_fixes.rs | src/api.rs, src/jsonutil.rs, src/mcp.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 | diff --git a/pending_updates.md b/pending_updates.md index 8253624..ce18444 100644 --- a/pending_updates.md +++ b/pending_updates.md @@ -1,7 +1,6 @@ # Pending Updates (no pending items — run check-changes after making edits) -- src/api.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. - src/artifacts.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. - src/cache.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. - src/cache_commit.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. @@ -10,18 +9,14 @@ - src/lib.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. - src/main.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. - src/maps_out.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. -- src/mcp.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. - src/parse.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. - src/proc.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. - src/project_lock.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. -- src/scope.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. -- src/sha256.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. - src/timeutil.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. - tests/behaviour_gaps.rs: LLM/agent edit — Tests for those parity fixes. - tests/cache_and_artifacts.rs: LLM/agent edit — Tests for those parity fixes. - tests/cli_surface.rs: LLM/agent edit — Tests for those parity fixes. - tests/contract_gaps.rs: LLM/agent edit — Tests for those parity fixes. -- tests/dogfood.rs: LLM/agent edit — Tests for those parity fixes. - tests/parity.rs: LLM/agent edit — Tests for those parity fixes. - tests/protocol_v07.rs: LLM/agent edit — Tests for those parity fixes. - tests/robustness.rs: LLM/agent edit — Tests for those parity fixes. @@ -36,3 +31,4 @@ - src/text_out.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. - src/wiki_files.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. - tests/parity_fixes.rs: LLM/agent edit — Tests for those parity fixes. +- tests/agent_args.rs: LLM/agent edit — new: agent-style file args, missing args, lean MCP outputs, init roots, CRLF (claude/windows-verify) From 09ecec6468b88e0773caba6df25b2d9060825e72 Mon Sep 17 00:00:00 2001 From: IronAdamant <18153828+IronAdamant@users.noreply.github.com> Date: Thu, 5 Nov 2026 16:17:51 +1100 Subject: [PATCH 06/11] Speed up cold and warm maps, check-changes, cycles, and bootstrap. Measured on a 587-file Cargo workspace (Specula) on Windows 11, same copy before and after. Cache files, library.md, folder cards, health hashes and the update-maps / cycles / dependencies --full / dependents / check-changes / session-bootstrap output are byte-identical (timestamps aside). Warm MCP calls, median of 5: check_changes 353 -> 53 ms, update_maps 708 -> 50 ms, get_cycles 317 -> 39 ms, session_bootstrap 33 -> 16 ms. Cold update-maps, interleaved runs: 18.7 s -> 6.4 s. - Cache commits write files on several threads and flush each directory once instead of once per file. Every file is still synced and renamed into place; forward files still land before by_dst. - A full map scans each file once (the second resolution pass reuses the scans). Import lines come from one substring search per distinct spec, not a per-line loop per import. JS/TS barrel follow-through builds its sorted scan table once, not once per imported name. - cycles and dependencies --full read edges from index.tsv instead of one by_src file per mapped file; cycles loads edge flags only for files in a cycle. library.md no longer reads the notes files. - update-maps keeps library.md and the folder cards when the cache commit changed nothing. - check-changes and health seeding use a mapped file's cache hash when size and mtime match (the rule update-maps already skips by) and read the file otherwise, or when that hash is not the baseline. - The walk takes file types from the directory entry. - session-bootstrap --limit caps `files` (files_total gives the count); over MCP the default cap is 100 (21.7 KB -> 4.7 KB here). tests/speed_paths.rs checks each fast path against the slow one; unit tests cover line_of and the batched writer. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CMYkogM3QWVjMLzWKGyjuJ --- CHANGELOG.md | 10 ++ skills/run.md | 6 +- src/api.rs | 86 ++++++++---- src/api.rs.wiki.md | 2 +- src/cache.rs | 199 +++++++++++++++++++++++----- src/cache.rs.wiki.md | 4 +- src/cache_commit.rs | 81 ++++++++---- src/cache_commit.rs.wiki.md | 4 +- src/maps_out.rs | 17 ++- src/maps_out.rs.wiki.md | 2 +- src/mcp.rs | 16 ++- src/mcp.rs.wiki.md | 2 +- src/parse.rs | 102 +++++++++++--- src/parse.rs.wiki.md | 2 +- src/scope.rs | 9 +- tests/dogfood.rs | 1 + tests/speed_paths.rs | 256 ++++++++++++++++++++++++++++++++++++ 17 files changed, 688 insertions(+), 111 deletions(-) create mode 100644 tests/speed_paths.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 004d9c8..4e1bea2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,10 +17,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - MCP `tools/call` with a missing required argument (for example `file_path` instead of `file`) returns `InvalidArguments` naming the accepted and received arguments. Before, the tool ran with an empty path and reported success. - `health --limit N` (and MCP `limit`) caps the entry list and adds `entries_total`. The argument was advertised and ignored. +### Performance +Same outputs: on a 587-file Cargo workspace the cache files, `library.md`, folder cards, health hashes, and the `update-maps`, `cycles`, `dependencies --full`, `dependents`, `check-changes`, and `session-bootstrap` output were byte-identical before and after (CLI, timestamps aside). Warm MCP calls there, median of 5 on Windows 11: `check_changes` 353 → 53 ms, `update_maps` 708 → 50 ms, `get_cycles` 317 → 39 ms, `session_bootstrap` 33 → 16 ms and 21.7 → 4.7 KB. Cold `update-maps` (CLI, runs interleaved on a loaded machine, median of 3) went from 18.7 s to 6.4 s. +- A cache commit writes its files on several threads and flushes each directory once, not once per file. Every file is still synced and renamed into place, and forward files still land before `by_dst`. +- A full map scans each file once; the second JS/TS/Rust resolution pass reuses those scans. Import line numbers come from one substring search per distinct spec instead of a line-by-line scan per import. JS/TS barrel follow-through builds its sorted scan table once instead of once per imported name. +- `cycles` and `dependencies --full` read edges from `index.tsv` instead of one `by_src` file per mapped file, and `cycles` loads edge flags only for files inside a cycle. `library.md` reads `by_src` records without the notes files. +- `update-maps` keeps `library.md` and the folder cards when the cache commit changed nothing. +- `check-changes` and health seeding take a mapped file's content hash from its cache row when size and mtime match (the rule `update-maps` already used), and read the file otherwise or when that hash is not the baseline. Health rows are found by path through a map, not a scan per file. +- The file walk takes directory and file types from the directory entry instead of two `stat` calls per entry. + ### Changed - `init` seeds `map_paths.txt` and `monitored_paths.txt` with `src/` when it exists, otherwise with each top-level directory that holds source (a Cargo workspace's `crates/`, a Python package directory, `tests/`). Before, a project without `src/` hit a scope error on the first `update-maps`. - MCP `update_maps` omits the edge list unless `edges: true`; `edges_persisted` still gives the count. On a 587-file Cargo workspace the list was about 280 KB. The schema now lists `full`, `max_files`, and `edges`; `health` lists `limit` and `summary`. - `.gitattributes` keeps `*.sh` LF and `*.bat` / `*.ps1` CRLF on every checkout. +- `session-bootstrap --limit N` (MCP `limit`) caps the `files` list and adds `files_total`. Over MCP the default cap is 100; `limit: 0` lists every file. `map_index` and `list_paths` are the way to orient on a large tree. ## [5.0.0] - 2026-11-03 diff --git a/skills/run.md b/skills/run.md index a52052c..21e7060 100644 --- a/skills/run.md +++ b/skills/run.md @@ -26,7 +26,7 @@ This file is the contract for any agent operating Wikifier. Part 1 is all you ne - Always pass `project_root=` (MCP/library) or `--target DIR` / `WIKIFIER_PROJECT_ROOT` (CLI) for any project other than the current directory. **A root that does not exist is an error** — nothing is ever written elsewhere. - File arguments may be project-relative, `./`-prefixed, `\`-separated (Windows) or absolute (inside the project). Results return the canonical project-relative `file`. A lookup (`prepare_edit`, `get_dependencies`, `get_dependents`, `get_file_wiki`) on a path that is neither on disk nor in the map returns `error_type: "FileNotFound"`; `list_paths` shows the mapped files. - `record_change` and `mark_green` require the file to exist; for removed files use `record_deletion`. -- `check_changes` watches every mapped file, every file ever marked Green, and `monitored_paths.txt` entries — the list only adds files (docs, scripts), it never hides mapped ones. +- `check_changes` watches every mapped file, every file ever marked Green, and `monitored_paths.txt` entries — the list only adds files (docs, scripts), it never hides mapped ones. A mapped file whose size and mtime still match its cache row is compared by the hash stored there (the rule `update_maps` skips by); any other file is read and hashed. - Green means the wiki was checked against the code (see *Wiki verification*). A failed check lists the symbols to document; update the wiki and retry. `force=True` with a reason overrides and is recorded as `forced`. - Orient with the bootstrap **root folder card** → `list_paths(prefix)` for one folder → `prepare_edit(file)` for file-to-file links. Do not read `library.md` mermaid to orient. - Wiki convention: `.wiki.md` next to the source (e.g. `src/api.rs.wiki.md`). @@ -151,13 +151,13 @@ There is no Python library. Agents call the CLI or `wikifier mcp`. The same func The program is a std-only Rust binary under `src/`. `Cargo.toml` has an empty `[dependencies]` list. Launchers exec that binary. MCP is `wikifier mcp`. -- `update-maps` writes `library.md` (a file tree, a Mermaid block capped at 80 edges, and the dependency table) and `.wikifier_staging/maps/` (`_root.json` plus one `_index.json` per directory). `--directory`, `--max-files`, and `--stream` apply. A file budget returns `map_complete=false` and `files_remaining_dirty`. The next run stat-skips files already parsed, so the same command resumes. `--stream` prints JSON events on stderr and still persists the map. The result field `parser` is `regex+reexport`. +- `update-maps` writes `library.md` (a file tree, a Mermaid block capped at 80 edges, and the dependency table) and `.wikifier_staging/maps/` (`_root.json` plus one `_index.json` per directory). `--directory`, `--max-files`, and `--stream` apply. A file budget returns `map_complete=false` and `files_remaining_dirty`. The next run stat-skips files already parsed, so the same command resumes. `--stream` prints JSON events on stderr and still persists the map. The result field `parser` is `regex+reexport`. A run that changes nothing in the cache keeps `library.md` and the folder cards as they are. - The cache is `.wikifier_staging/wk_cache`, a text index (payload, `by_src`, `by_dst`, notes, barrels). When that index is empty, an existing `import_cache.sqlite` is imported with the `sqlite3` CLI, and `import_cache.json` is the other fallback. Rows imported this way use hash `sha256:legacy` and mtime 0, so the next `update-maps` re-parses them. If the sqlite file is present and cannot be read, the result includes the warning `sqlite_present_unread`. - A resolved edge line is `dst`, `line`, `names`, then optional flag letters `c` (conditional), `d` (dynamic), and `b` (via barrel). Confidence is assigned when the edge is read: dynamic 0.4, conditional 0.55, via barrel 0.7, static 0.9. ACS reason codes are `dynamic_literal`, `conditional_guard`, `via_barrel`, and `resolved_static`. Unresolved notes use `unresolved_relative` (the specifier starts with `.`), `dynamic_literal`, or `unresolved`. - `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 `?`). `--reasons` accepts a JSON object (`{"path": "why"}`) or `PATH=REASON` lines (`-` reads stdin). `--only`, `--green`, `--dry-run`, and deletions are applied. `--green` lists verification failures and leaves those files Yellow. MCP `reasons` may be that object. -- `mark_green` extracts Python public names with a tokenizer (multi-line `__all__` lists, tuples, and `+=`; comments and strings ignored; top-level `def`, `async def`, and `class`, including decorated ones; `.py` and `.pyi`), JS/TS `export` names after comments and strings are masked, and Rust unrestricted `pub` items (`fn`, `struct`, `enum`, `trait`, `mod`, `type`, `const`, `static`, and `pub use` names), plus `pub` methods and associated items on a public type and the items written in a trait impl for that type. Each current name must appear in the wiki. `pub(crate)`, `pub(super)`, `pub(self)`, and `pub(in ...)` 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`. The wiki is `.wiki.md`, then `.md`, the stem beside the source, then the same names under `docs/wiki`, `docs`, `wiki`, `documentation`, and `.wiki`. `health --summary` returns `health_summary` (including `green_breakdown`) without the entry list. `health --format healing-stats` is the stub counts. `--directory` limits health, bootstrap, and suggest-next. A successful mark removes that file's pending line. Pending bullets are `- path: message` (`LLM/agent edit — …` after `record-change`). +- `mark_green` extracts Python public names with a tokenizer (multi-line `__all__` lists, tuples, and `+=`; comments and strings ignored; top-level `def`, `async def`, and `class`, including decorated ones; `.py` and `.pyi`), JS/TS `export` names after comments and strings are masked, and Rust unrestricted `pub` items (`fn`, `struct`, `enum`, `trait`, `mod`, `type`, `const`, `static`, and `pub use` names), plus `pub` methods and associated items on a public type and the items written in a trait impl for that type. Each current name must appear in the wiki. `pub(crate)`, `pub(super)`, `pub(self)`, and `pub(in ...)` 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`. The wiki is `.wiki.md`, then `.md`, the stem beside the source, then the same names under `docs/wiki`, `docs`, `wiki`, `documentation`, and `.wiki`. `health --summary` returns `health_summary` (including `green_breakdown`) without the entry list. `health --format healing-stats` is the stub counts. `--directory` limits health, bootstrap, and suggest-next. A successful mark removes that file's pending line. Pending bullets are `- path: message` (`LLM/agent edit — …` after `record-change`). `session_bootstrap --limit N` caps `files` and adds `files_total`; over MCP the default cap is 100 (`limit: 0` lists every file). - `suggest-next` adds `review_low_confidence_edges` when a mapped file has a resolved, non-dynamic edge below confidence 0.65. The count is stored while parsing (`low_conf.tsv`) so a warm `update-maps` does not rescan it. - `list-paths --depth 0` or `--recursive` lists every file under the prefix. `--max-files` caps that list. `verify-wikis --limit` and `stale-wikis --limit` cap the rows returned. `cycles` adds `severity`, `score`, `external_blast_radius`, `recommendation`, and `reverse_dependencies` (top 20 by inbound edges). `--no-analysis` omits those. `--max-items` caps the cycle list. - `seed-source-hashes` writes the current hash onto Green rows and does not change status. `--force` replaces a hash that is already stored. `seed-health` still adds Initial-stub rows. `heal-stubs` promotes a stub whose wiki is at least 350 characters (`--min-wiki-length` overrides). diff --git a/src/api.rs b/src/api.rs index 00cd8ea..858ff74 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,6 +1,6 @@ //! One function per agent-facing operation. CLI and MCP both call these. -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::fs; use std::path::{Path, PathBuf}; @@ -105,6 +105,24 @@ fn unknown_file(root: &Path, rel: &str) -> Option { )) } +/// `limit` caps the bootstrap `files` list and adds `files_total`. `0` or no limit keeps +/// every file. `map_index` and `list_paths` are the way to orient on a large tree. +fn cap_files(mut value: Json, flags: &BTreeMap) -> Json { + let limit = flags + .get("limit") + .and_then(|s| s.parse::().ok()) + .filter(|n| *n > 0); + if let (Some(limit), Some(Json::Array(files))) = (limit, value.get("files")) { + if files.len() > limit { + let total = files.len(); + let kept = files[..limit].to_vec(); + value.insert("files", Json::Array(kept)); + value.insert("files_total", Json::Int(total as i64)); + } + } + value +} + fn root_or_err(root: Option<&Path>) -> Result { let root = match root { Some(p) => p.to_path_buf(), @@ -451,6 +469,7 @@ pub fn update_maps_run(root: &Path, run: &MapRun) -> Json { } else { edge_pairs.len() as i64 }; + let cache_changed = session.has_changes(); if let Err(err) = session.commit() { let kind = if err.contains("locked") { "CacheLocked" @@ -460,8 +479,10 @@ pub fn update_maps_run(root: &Path, run: &MapRun) -> Json { return fail_warnings(kind, err, &warnings); } let _ = seed_health_from_map(root); - if let Err(err) = crate::maps_out::write(root) { - warnings.push(format!("map artifacts: {err}")); + if cache_changed || !crate::maps_out::current(root) { + if let Err(err) = crate::maps_out::write(root) { + warnings.push(format!("map artifacts: {err}")); + } } let map_complete = files_remaining_dirty == 0; let status = Json::obj(vec![ @@ -545,6 +566,7 @@ fn seed_health_from_map(root: &Path) -> Result<(), String> { let stamp = local_stamp(); let mut changed = false; let scoped = monitored_seed_prefixes(root); + let indexed = cache::index_by_path(root); for rel in cache::all_paths(root) { if let Some(prefixes) = &scoped { if !prefixes.iter().any(|prefix| under_prefix(prefix, &rel)) { @@ -554,7 +576,9 @@ fn seed_health_from_map(root: &Path) -> Result<(), String> { if known.contains(&rel) { continue; } - let hash = file_sha256_prefixed(&root.join(&rel)).unwrap_or_default(); + let path = root.join(&rel); + let hash = cache::hash_if_unchanged(&path, indexed.get(&rel)) + .unwrap_or_else(|| file_sha256_prefixed(&path).unwrap_or_default()); rows.push(HealthRow::new( rel, "🟡 Yellow", @@ -1119,9 +1143,15 @@ pub fn check_changes(root: &Path) -> Json { let reason = "content changed since last mark-green (check-changes)"; let mut dirty = Vec::new(); let mut changed_rows = false; + let indexed = cache::index_by_path(root); + let mut row_at: HashMap = rows + .iter() + .enumerate() + .map(|(i, row)| (row.file.clone(), i)) + .collect(); for rel in watched { let path = root.join(&rel); - let idx = rows.iter().position(|r| r.file == rel); + let idx = row_at.get(&rel).copied(); if !path.is_file() { dirty.push(rel.clone()); let already = idx.map(|i| is_red(&rows[i].status)).unwrap_or(false); @@ -1141,6 +1171,7 @@ pub fn check_changes(root: &Path) -> Json { if let Some(i) = idx { rows[i] = row; } else { + row_at.insert(rel.clone(), rows.len()); rows.push(row); } let _ = artifacts::append_journal(root, "check-changes", &rel, "missing on disk"); @@ -1148,7 +1179,6 @@ pub fn check_changes(root: &Path) -> Json { } continue; } - let live = file_sha256_prefixed(&path).unwrap_or_default(); let baseline = idx.and_then(|i| { let h = rows[i].source_hash.clone(); if h.starts_with("sha256:") { @@ -1157,13 +1187,16 @@ pub fn check_changes(root: &Path) -> Json { None } }); + let index_row = indexed.get(&rel); + // Stat first: a file whose size and mtime still match its cache row has the content + // hash recorded there (the rule update-maps skips by). Read and hash it only when + // the stat moved or that hash is not the baseline being checked. + let live = cache::hash_if_unchanged(&path, index_row) + .filter(|hash| baseline.as_ref().is_none_or(|base| base == hash)) + .unwrap_or_else(|| file_sha256_prefixed(&path).unwrap_or_default()); let drifted = match &baseline { Some(base) => live != *base, - None => cache::indexed_rows(root) - .into_iter() - .find(|r| r.0 == rel) - .map(|r| r.3 != live) - .unwrap_or(false), + None => index_row.map(|r| r.2 != live).unwrap_or(false), }; if !drifted { continue; @@ -1196,6 +1229,7 @@ pub fn check_changes(root: &Path) -> Json { if let Some(i) = idx { rows[i] = row; } else { + row_at.insert(rel.clone(), rows.len()); rows.push(row); } let _ = artifacts::append_journal(root, "check-changes", &rel, reason); @@ -1665,7 +1699,6 @@ pub fn cycles(root: &Path) -> Json { pub fn cycles_view(root: &Path, analysis: bool, max_items: Option) -> Json { let _ = crate::legacy_cache::import_if_empty(root); let edges = cache::edges(root); - let meta = edge_meta(root); let mut graph: BTreeMap> = BTreeMap::new(); for (src, dst) in &edges { graph.entry(src.clone()).or_default().push(dst.clone()); @@ -1673,6 +1706,7 @@ pub fn cycles_view(root: &Path, analysis: bool, max_items: Option) -> Jso } let sccs = tarjan(&graph); let cyclic: Vec> = sccs.into_iter().filter(|c| c.len() > 1).collect(); + let meta = edge_meta(root, cyclic.iter().flatten()); let value = ok(vec![ ("count", Json::Int(cyclic.len() as i64)), ( @@ -1746,13 +1780,17 @@ pub fn cycles_view(root: &Path, analysis: bool, max_items: Option) -> Jso value } -fn edge_meta(root: &Path) -> BTreeMap<(String, String), cache::InternalEdge> { +/// Edge flags for the given source files only. `cycles` needs them for files inside a +/// cycle, so it no longer opens every mapped file's records. +fn edge_meta<'a>( + root: &Path, + files: impl Iterator, +) -> BTreeMap<(String, String), cache::InternalEdge> { let mut out = BTreeMap::new(); - for rel in cache::all_paths(root) { - if let Ok(view) = cache::load_dependency_view(root, &rel) { - for edge in view.internal { - out.insert((rel.clone(), edge.path.clone()), edge); - } + let files: BTreeSet<&String> = files.collect(); + for rel in files { + for edge in cache::load_internal_edges(root, rel) { + out.insert((rel.clone(), edge.path.clone()), edge); } } out @@ -3373,9 +3411,10 @@ pub fn dispatch( flags.get("force").map(|s| s == "true").unwrap_or(false), ), "record-deletion" | "record_deletion" => record_deletion(root, file, &reason), - "session-bootstrap" | "bootstrap" | "session-start" | "session_bootstrap" => { - session_bootstrap_scoped(root, flag_text(flags, "directory")) - } + "session-bootstrap" | "bootstrap" | "session-start" | "session_bootstrap" => cap_files( + session_bootstrap_scoped(root, flag_text(flags, "directory")), + flags, + ), "suggest-next" | "suggest" | "suggest-next-actions" | "suggest_next_actions" => { suggest_next_scoped(root, flag_text(flags, "directory")) } @@ -3605,7 +3644,10 @@ pub fn dispatch( ]) } "init" => init_project(root, flag_on(flags, "no-copy")), - "get_project_status" => session_bootstrap_scoped(root, flag_text(flags, "directory")), + "get_project_status" => cap_files( + session_bootstrap_scoped(root, flag_text(flags, "directory")), + flags, + ), "get_current_project_root" => ok(vec![( "project_root", Json::String(root.display().to_string()), diff --git a/src/api.rs.wiki.md b/src/api.rs.wiki.md index d1ba7e2..e4922e7 100644 --- a/src/api.rs.wiki.md +++ b/src/api.rs.wiki.md @@ -8,4 +8,4 @@ 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`, `seed_source_hashes`, `list_paths_at`, `session_bootstrap_scoped`, `suggest_next_scoped`, `health_view`, `cycles_view`, `stale_wikis_in`, `daemon_command`. -`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. File arguments go through `scope::rel_arg`, so `src.rs`, `./src/a.rs`, and an absolute path under the root all name the `src/a.rs` row. `prepare_edit`, `dependency_report`, `load_dependents`, and `get_file_wiki` return `FileNotFound` (with a `list_paths` hint) for a path that is neither on disk nor in the map, and `ValueError` for an empty path. `health --limit N` keeps N entries and reports `entries_total`. `init` seeds `map_paths.txt` and `monitored_paths.txt` from `scope::default_map_roots`. Content hashes for health baselines use `content_sha256_prefixed`, so CRLF and LF copies of a file match. 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. +`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. File arguments go through `scope::rel_arg`, so `src.rs`, `./src/a.rs`, and an absolute path under the root all name the `src/a.rs` row. `prepare_edit`, `dependency_report`, `load_dependents`, and `get_file_wiki` return `FileNotFound` (with a `list_paths` hint) for a path that is neither on disk nor in the map, and `ValueError` for an empty path. `health --limit N` keeps N entries and reports `entries_total`. `init` seeds `map_paths.txt` and `monitored_paths.txt` from `scope::default_map_roots`. Content hashes for health baselines use `content_sha256_prefixed`, so CRLF and LF copies of a file match. `check_changes` and health seeding reuse a mapped file's cache hash when size and mtime match (`cache::hash_if_unchanged`); `cycles` loads edge flags only for files in a cycle; `update_maps_run` skips `maps_out::write` when the commit changed nothing and the artifacts are current; `session-bootstrap --limit` caps `files` and adds `files_total`. 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/cache.rs b/src/cache.rs index 91e8876..e1066d3 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -103,6 +103,15 @@ pub(crate) fn names_marker(root: &Path) -> PathBuf { pub(crate) fn atomic_write(path: &Path, bytes: &[u8]) -> Result<(), String> { let parent = path.parent().unwrap_or(Path::new(".")); fs::create_dir_all(parent).map_err(|e| e.to_string())?; + write_replace(path, bytes)?; + fsync_dir(parent); + Ok(()) +} + +/// Write a temp file beside `path`, sync it, and rename it into place. +/// The parent directory must exist; the caller flushes it. +fn write_replace(path: &Path, bytes: &[u8]) -> Result<(), String> { + let parent = path.parent().unwrap_or(Path::new(".")); let seq = TEMP_SEQ.fetch_add(1, Ordering::Relaxed); let file_name = path.file_name().and_then(|s| s.to_str()).unwrap_or("cache"); let tmp = parent.join(format!(".{file_name}.{}.{seq}.tmp", std::process::id())); @@ -124,17 +133,60 @@ pub(crate) fn atomic_write(path: &Path, bytes: &[u8]) -> Result<(), String> { let _ = fs::remove_file(&tmp); return Err(err); } - fsync_dir(parent); Ok(()) } -pub(crate) fn write_labeled(path: &Path, rel: &str, body: &str) -> Result<(), String> { +/// Write many cache files with the guarantees of [`atomic_write`]: each file is synced and +/// renamed into place, and every parent directory is flushed before this returns. The +/// per-file syncs run on several threads and each directory is flushed once, not once per +/// file. Serial flushes were most of a cold `update-maps` (about 2,100 files on a +/// 587-file workspace). +pub(crate) fn atomic_write_all(entries: &[(PathBuf, Vec)]) -> Result<(), String> { + if entries.is_empty() { + return Ok(()); + } + let parents: BTreeSet = entries + .iter() + .map(|(path, _)| path.parent().unwrap_or(Path::new(".")).to_path_buf()) + .collect(); + for dir in &parents { + fs::create_dir_all(dir).map_err(|e| e.to_string())?; + } + let threads = std::thread::available_parallelism() + .map(|n| n.get()) + .unwrap_or(1) + .clamp(1, 16); + let chunk = entries.len().div_ceil(threads).max(1); + std::thread::scope(|scope| { + let workers: Vec<_> = entries + .chunks(chunk) + .map(|part| { + scope.spawn(move || { + part.iter() + .try_for_each(|(path, bytes)| write_replace(path, bytes)) + }) + }) + .collect(); + workers.into_iter().try_for_each(|worker| { + worker + .join() + .unwrap_or_else(|_| Err("cache writer thread panicked".to_string())) + }) + })?; + for dir in &parents { + fsync_dir(dir); + } + Ok(()) +} + +/// The bytes of an edge or notes file: an `@` header line, then the body. +pub(crate) fn labeled(rel: &str, body: &str) -> Vec { let mut text = format!("@\t{rel}\n"); text.push_str(body); if !text.ends_with('\n') { text.push('\n'); } - atomic_write(path, text.as_bytes()) + text.into_bytes() } fn replace_file(from: &Path, to: &Path) -> Result<(), String> { @@ -356,31 +408,7 @@ fn notes_path(root: &Path, rel: &str) -> PathBuf { } pub fn load_dependency_view(root: &Path, rel: &str) -> Result { - let mut internal = Vec::new(); - for line in content_lines(&by_src_path(root, rel)) { - let mut parts = line.split('\t'); - let path = parts.next().unwrap_or("").trim().to_string(); - if path.is_empty() { - continue; - } - let line_no = parts.next().unwrap_or("0").parse().unwrap_or(0); - let names = parts - .next() - .unwrap_or("") - .split(',') - .filter(|s| !s.is_empty()) - .map(|s| s.to_string()) - .collect(); - let (conditional, dynamic, via_barrel) = parse_flags(parts.next().unwrap_or("")); - internal.push(InternalEdge { - path, - line: line_no, - names, - conditional, - dynamic, - via_barrel, - }); - } + let internal = load_internal_edges(root, rel); let mut external = Vec::new(); let mut unresolved = Vec::new(); for line in read_lines(¬es_path(root, rel)) { @@ -417,6 +445,36 @@ pub fn load_dependency_view(root: &Path, rel: &str) -> Result Vec { + let mut internal = Vec::new(); + for line in content_lines(&by_src_path(root, rel)) { + let mut parts = line.split('\t'); + let path = parts.next().unwrap_or("").trim().to_string(); + if path.is_empty() { + continue; + } + let line_no = parts.next().unwrap_or("0").parse().unwrap_or(0); + let names = parts + .next() + .unwrap_or("") + .split(',') + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + .collect(); + let (conditional, dynamic, via_barrel) = parse_flags(parts.next().unwrap_or("")); + internal.push(InternalEdge { + path, + line: line_no, + names, + conditional, + dynamic, + via_barrel, + }); + } + internal +} + pub fn load_dependents(root: &Path, rel: &str) -> Result, String> { let mut srcs = content_lines(&read_file(root, "by_dst", rel, "edges")); srcs.sort(); @@ -481,6 +539,22 @@ pub fn indexed_rows(root: &Path) -> Vec<(String, u128, u64, String)> { .collect() } +/// Index rows by path: `(mtime_ns, size, content hash)`. +pub fn index_by_path(root: &Path) -> std::collections::HashMap { + indexed_rows(root) + .into_iter() + .map(|(rel, mtime, size, hash)| (rel, (mtime, size, hash))) + .collect() +} + +/// The content hash stored for `path` when its size and mtime still match the index row, +/// the same rule `update-maps` uses to skip a file. `None` means read and hash the file. +pub fn hash_if_unchanged(path: &Path, row: Option<&(u128, u64, String)>) -> Option { + let (mtime, size, hash) = row?; + (*mtime != 0 && hash.starts_with("sha256:") && stat_ns(path) == Some((*mtime, *size))) + .then(|| hash.clone()) +} + pub fn payload_bytes(root: &Path, rel: &str) -> Option> { fs::read(payload_path(root, rel)).ok() } @@ -496,14 +570,22 @@ pub fn stat_ns(path: &Path) -> Option<(u128, u64)> { Some((mtime, meta.len())) } +/// Every mapped edge, self-imports left out. Read from `index.tsv`, which carries each +/// file's resolved targets, instead of opening one `by_src` file per mapped file. An index +/// row written before targets were recorded falls back to that file's `by_src` record. pub fn edges(root: &Path) -> BTreeSet<(String, String)> { let mut out = BTreeSet::new(); - for rel in all_paths(root) { - if let Ok(dsts) = load_dependencies(root, &rel) { - for dst in dsts { - if dst != rel { - out.insert((rel.clone(), dst)); - } + for (rel, dsts) in crate::cache_commit::index_targets(root) { + let dsts = match dsts { + Some(dsts) => dsts, + None => load_dependencies(root, &rel) + .unwrap_or_default() + .into_iter() + .collect(), + }; + for dst in dsts { + if dst != rel { + out.insert((rel.clone(), dst)); } } } @@ -586,3 +668,52 @@ pub fn prune_barrel_files(root: &Path, max_age_days: f64, dry_run: bool) -> Resu } Ok(pruned) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn atomic_write_all_publishes_every_file_and_leaves_no_temp() { + let dir = std::env::temp_dir().join(format!( + "wk-batch-{}-{}", + std::process::id(), + TEMP_SEQ.fetch_add(1, Ordering::Relaxed) + )); + let _ = fs::remove_dir_all(&dir); + let mut entries: Vec<(PathBuf, Vec)> = (0..300) + .map(|i| { + let sub = ["by_src", "by_dst", "payload"][i % 3]; + ( + dir.join(sub).join(format!("{i}.edges")), + format!("body {i}").into_bytes(), + ) + }) + .collect(); + atomic_write_all(&entries).unwrap(); + // A second batch replaces existing files in place. + for (i, entry) in entries.iter_mut().enumerate() { + entry.1 = format!("second {i}").into_bytes(); + } + atomic_write_all(&entries).unwrap(); + for (i, (path, _)) in entries.iter().enumerate() { + assert_eq!(fs::read_to_string(path).unwrap(), format!("second {i}")); + } + for sub in ["by_src", "by_dst", "payload"] { + let leftovers: Vec<_> = fs::read_dir(dir.join(sub)) + .unwrap() + .flatten() + .filter(|ent| ent.file_name().to_string_lossy().ends_with(".tmp")) + .collect(); + assert!(leftovers.is_empty(), "{sub}: {leftovers:?}"); + } + assert!(atomic_write_all(&[]).is_ok()); + let _ = fs::remove_dir_all(dir); + } + + #[test] + fn labeled_adds_the_header_and_a_final_newline() { + assert_eq!(labeled("src/a.rs", "x"), b"@\tsrc/a.rs\nx\n".to_vec()); + assert_eq!(labeled("src/a.rs", ""), b"@\tsrc/a.rs\n".to_vec()); + } +} diff --git a/src/cache.rs.wiki.md b/src/cache.rs.wiki.md index f24e539..5046703 100644 --- a/src/cache.rs.wiki.md +++ b/src/cache.rs.wiki.md @@ -4,6 +4,8 @@ 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`. +`atomic_write` writes a temp file, syncs it, renames it into place, and flushes the directory. `atomic_write_all` does the same for a batch on several threads and flushes each directory once; a commit uses it for the forward files, then for `by_dst`. `edges` reads every file's targets from `index.tsv` (one file), falling back to `by_src` for an index row without targets. `load_internal_edges` reads one `by_src` record without the notes. `index_by_path` and `hash_if_unchanged` give `check-changes` and health seeding the stored content hash when a file's size and mtime still match its index row. + 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`, `acs_reason`, `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`. +Public names: `check_consistency`, `rebuild`, `Consistency`, `Session`, `payload_path`, `by_src_path`, `commit_pending_path`, `load_dependencies`, `InternalEdge`, `confidence`, `acs_reason`, `confidence_of`, `format_edge_line`, `DependencyView`, `load_dependency_view`, `load_internal_edges`, `load_dependents`, `list_prefix`, `all_paths`, `FileUpdate`, `upsert_file`, `remove_file`, `indexed_rows`, `index_by_path`, `hash_if_unchanged`, `payload_bytes`, `stat_ns`, `edges`, `rel_string`, `write_barrel_record`, `prune_barrel_files`. diff --git a/src/cache_commit.rs b/src/cache_commit.rs index 385346e..c0f908b 100644 --- a/src/cache_commit.rs +++ b/src/cache_commit.rs @@ -298,8 +298,13 @@ impl Session { self.barrels.push((rel.to_string(), dsts.to_vec())); } + /// True when [`Session::commit`] would write anything. + pub fn has_changes(&self) -> bool { + self.rewrite_index || !self.barrels.is_empty() + } + pub fn commit(self) -> Result<(), String> { - if !self.rewrite_index && self.barrels.is_empty() { + if !self.has_changes() { return Ok(()); } let gen = read_generation(&self.root).saturating_add(1); @@ -320,6 +325,9 @@ impl Session { let _ = fs::remove_file(cache::legacy_file(&self.root, sub, rel, ext)); } } + // Forward files (by_src, payload, notes, barrels) are written and flushed as one + // batch before any by_dst file, so a crash between them is repaired from by_src. + let mut forward: Vec<(PathBuf, Vec)> = Vec::new(); for (rel, lines) in &self.dirty_src { if self.removed.contains(rel) { continue; @@ -329,28 +337,30 @@ impl Session { } else { format!("{}\n", lines.join("\n")) }; - cache::write_labeled( - &cache::store_file(&self.root, "by_src", rel, "edges"), - rel, - &body, - )?; + forward.push(( + cache::store_file(&self.root, "by_src", rel, "edges"), + cache::labeled(rel, &body), + )); } for (rel, payload) in &self.payloads { - cache::atomic_write( - &cache::store_file(&self.root, "payload", rel, "json"), - payload.as_bytes(), - )?; + forward.push(( + cache::store_file(&self.root, "payload", rel, "json"), + payload.as_bytes().to_vec(), + )); } for (rel, notes) in &self.notes { let path = cache::store_file(&self.root, "notes", rel, "txt"); match notes { - Some(body) => cache::write_labeled(&path, rel, body)?, + Some(body) => forward.push((path, cache::labeled(rel, body))), None => { let _ = fs::remove_file(&path); } } } - for (rel, dsts) in &self.barrels { + // A file staged twice keeps its last record, as the serial writes did. + let barrels: BTreeMap<&String, &Vec> = + self.barrels.iter().map(|(rel, dsts)| (rel, dsts)).collect(); + for (rel, dsts) in barrels { let body = crate::jsonutil::Json::obj(vec![ ("src", crate::jsonutil::Json::String(rel.clone())), ( @@ -363,12 +373,13 @@ impl Session { ), ), ]); - cache::atomic_write( - &cache::store_file(&self.root, "barrels", rel, "json"), - body.to_string().as_bytes(), - )?; + forward.push(( + cache::store_file(&self.root, "barrels", rel, "json"), + body.to_string().into_bytes(), + )); } - // Reverse edges after forward edges. A crash here is repaired from by_src. + cache::atomic_write_all(&forward)?; + let mut reverse: Vec<(PathBuf, Vec)> = Vec::new(); for dst in &self.dirty_dst { let path = cache::store_file(&self.root, "by_dst", dst, "edges"); let srcs = self.rev.get(dst).cloned().unwrap_or_default(); @@ -377,9 +388,10 @@ impl Session { } else { let lines: Vec = srcs.iter().cloned().collect(); let body = format!("{}\n", lines.join("\n")); - cache::write_labeled(&path, dst, &body)?; + reverse.push((path, cache::labeled(dst, &body))); } } + cache::atomic_write_all(&reverse)?; save_index(&self.root, &self.rows)?; cache::atomic_write( &cache::generation_path(&self.root), @@ -425,6 +437,22 @@ fn index_record_lines(root: &Path) -> Vec { .collect() } +/// Each indexed file with its resolved targets, or `None` for a row from before targets +/// were stored in the index. +pub(crate) fn index_targets(root: &Path) -> Vec<(String, Option>)> { + index_record_lines(root) + .into_iter() + .filter_map(|line| { + let mut parts = line.split('\t'); + let rel = parts.next().unwrap_or("").to_string(); + if rel.is_empty() { + return None; + } + Some((rel, parts.nth(3).map(parse_dsts))) + }) + .collect() +} + fn load_rows(root: &Path) -> (BTreeMap, bool) { let mut map = BTreeMap::new(); let mut upgrade = false; @@ -699,11 +727,18 @@ fn rebuild_locked(root: &Path) -> Result<(), String> { rev.entry(dst.clone()).or_default().insert(src.clone()); } } - for (dst, srcs) in &rev { - let lines: Vec = srcs.iter().cloned().collect(); - let body = format!("{}\n", lines.join("\n")); - cache::write_labeled(&cache::store_file(root, "by_dst", dst, "edges"), dst, &body)?; - } + let reverse: Vec<(PathBuf, Vec)> = rev + .iter() + .map(|(dst, srcs)| { + let lines: Vec = srcs.iter().cloned().collect(); + let body = format!("{}\n", lines.join("\n")); + ( + cache::store_file(root, "by_dst", dst, "edges"), + cache::labeled(dst, &body), + ) + }) + .collect(); + cache::atomic_write_all(&reverse)?; let mut rows = BTreeMap::new(); for (rel, dsts) in &forward { let prev = old.get(rel); diff --git a/src/cache_commit.rs.wiki.md b/src/cache_commit.rs.wiki.md index 6916418..e7cc7ba 100644 --- a/src/cache_commit.rs.wiki.md +++ b/src/cache_commit.rs.wiki.md @@ -4,4 +4,6 @@ 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. -`Session` methods: `open`, `known`, `dsts_of`, `note_stat`, `rels`, `upsert`, `remove`, `stage_barrel`, `commit`. +Forward files are written as one parallel batch before the `by_dst` batch (`cache::atomic_write_all`). `index_targets` lists each index row's targets for `cache::edges`. + +`Session` methods: `open`, `known`, `dsts_of`, `note_stat`, `rels`, `upsert`, `remove`, `stage_barrel`, `has_changes`, `commit`. `has_changes` is true when `commit` would write. diff --git a/src/maps_out.rs b/src/maps_out.rs index b0c3302..bbc01cc 100644 --- a/src/maps_out.rs +++ b/src/maps_out.rs @@ -9,13 +9,26 @@ use crate::jsonutil::Json; const MERMAID_CAP: usize = 80; +/// True when `library.md` and the root folder card were written by [`write`]. Both are +/// pure functions of the cache, so a run that commits nothing can keep them. +pub fn current(root: &Path) -> bool { + let library = fs::read_to_string(root.join("library.md")).unwrap_or_default(); + // A checkout with core.autocrlf turns the LF header into CRLF. + let head: String = library.chars().take(80).filter(|c| *c != '\r').collect(); + head.starts_with("# Library\n\nGenerated by `wikifier update-maps`") + && root + .join(".wikifier_staging") + .join("maps") + .join("_root.json") + .is_file() +} + pub fn write(root: &Path) -> Result<(), String> { let paths = cache::all_paths(root); let mut edges: Vec<(String, String)> = Vec::new(); let mut flagged = 0i64; for src in &paths { - let view = cache::load_dependency_view(root, src).map_err(|e| e.to_string())?; - for edge in view.internal { + for edge in cache::load_internal_edges(root, src) { if edge.conditional || edge.dynamic || edge.via_barrel || edge.confidence() < 0.8 { flagged += 1; } diff --git a/src/maps_out.rs.wiki.md b/src/maps_out.rs.wiki.md index 1374e6f..e8bb881 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`. `read_status` reads that file back. +`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` reads each file's `by_src` record only. `current` is true when `library.md` (LF or CRLF) and `_root.json` were written by `write`; `update-maps` skips `write` when the cache commit changed nothing and `current` holds. `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 b/src/mcp.rs index ffb60e7..6c31903 100644 --- a/src/mcp.rs +++ b/src/mcp.rs @@ -59,7 +59,11 @@ fn extra_props(name: &str) -> Vec<(&'static str, Json)> { ("limit", integer("Return at most this many entries. entries_total gives the full count.")), ("summary", boolean("Return only the green, yellow, and red counts.")), ]), - "verify_wikis" | "suggest_next_actions" | "session_bootstrap" | "get_project_status" => { + "session_bootstrap" | "get_project_status" => props.push(( + "limit", + integer("Cap the files list. Default 100 over MCP; 0 lists every file. files_total gives the count. Orient with map_index and list_paths."), + )), + "verify_wikis" | "suggest_next_actions" => { props.push(("limit", integer("Cap listed failures or stale rows."))); } "record_changes" => props.push(("reasons", Json::obj(vec![ @@ -78,6 +82,9 @@ fn extra_props(name: &str) -> Vec<(&'static str, Json)> { props } +/// Files listed by an MCP `session_bootstrap` that passes no `limit`. +const MCP_BOOTSTRAP_FILES: usize = 100; + fn string_prop(description: &str) -> Json { Json::obj(vec![ ("type", Json::String("string".into())), @@ -541,6 +548,13 @@ fn call_tool(msg: &Json) -> Json { if name == "update_maps" && !arg_on(args.as_ref(), "edges") { flags.insert("quiet".into(), "true".into()); } + // The bootstrap file list grows with the repo (about 21 KB at 587 files). Over MCP it + // is capped unless the caller asks; map_index already gives the folder counts. + if matches!(name.as_str(), "session_bootstrap" | "get_project_status") + && !flags.contains_key("limit") + { + flags.insert("limit".into(), MCP_BOOTSTRAP_FILES.to_string()); + } let textual = flags.get("format").map(|s| s == "text").unwrap_or(false); let tool_name = name.clone(); let from_arg = args diff --git a/src/mcp.rs.wiki.md b/src/mcp.rs.wiki.md index 14aa755..0abd0ac 100644 --- a/src/mcp.rs.wiki.md +++ b/src/mcp.rs.wiki.md @@ -4,4 +4,4 @@ `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`. -A `tools/call` that leaves out a required argument other than `project_root` (or sends it empty) gets `InvalidArguments` naming the missing field, the accepted fields, and the fields received. `project_root` falls back to `WIKIFIER_PROJECT_ROOT`, then the cwd. `update_maps` omits the edge list unless `edges: true`; `edges_persisted` still gives the count. `health` takes `limit` (adds `entries_total` when it cuts) and `summary`. +A `tools/call` that leaves out a required argument other than `project_root` (or sends it empty) gets `InvalidArguments` naming the missing field, the accepted fields, and the fields received. `project_root` falls back to `WIKIFIER_PROJECT_ROOT`, then the cwd. `update_maps` omits the edge list unless `edges: true`; `edges_persisted` still gives the count. `health` takes `limit` (adds `entries_total` when it cuts) and `summary`. `session_bootstrap` and `get_project_status` cap `files` at `MCP_BOOTSTRAP_FILES` (100) unless the call passes `limit` (`0` lists all). diff --git a/src/parse.rs b/src/parse.rs index e36c2f0..28428b0 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -136,9 +136,19 @@ pub fn resolve_project(root: &Path, files: &[PathBuf]) -> BTreeMap, +) -> BTreeMap> { + let resolver = Resolver::build(root, rels); let mut edges: BTreeMap> = BTreeMap::new(); - for rel in &rels { + for rel in rels { let scan = scans.get(rel).cloned().unwrap_or_default(); let mut dsts = BTreeSet::new(); let lang = language(Path::new(rel)).unwrap_or(""); @@ -152,7 +162,7 @@ pub fn resolve_project(root: &Path, files: &[PathBuf]) -> BTreeMap bool { #[cfg(test)] mod dbg_scan { + #[test] + fn line_of_matches_the_line_by_line_scan() { + let texts = [ + "use a::b;@N@fn f() { a::b::c(); }@N@", + "x@R@@N@use crate::m::{n, o};@R@@N@m::n();@R@@N@", + "", + "one line, no newline", + ]; + let needles: [&[&str]; 6] = [ + &["a::b"], + &["crate::m::n"], + &["m::n", "use"], + &["missing"], + &["", "fn"], + &["line"], + ]; + for text in texts { + let text = text.replace("@N@", "\n").replace("@R@", "\r"); + for set in needles { + let set: Vec = set.iter().map(|s| s.to_string()).collect(); + assert_eq!( + super::line_of(&text, &set), + super::line_of_by_lines(&text, &set), + "{text:?} {set:?}" + ); + } + } + } + #[test] fn dynamic_import_spec() { let src = "import { add } from '@acme/core';\nimport { log } from '@app/util';\nimport type { Config } from './types';\nconst lazy = () => import('./lazy');\nimport React from 'react';\nlog(add(1, 2));\n"; @@ -2961,7 +3000,32 @@ pub struct Analysis { pub warnings: Vec, } +/// 1-based number of the first line that contains any needle, else 1. A needle without a +/// line break matches inside one line, so the earliest byte match gives the same line as a +/// line-by-line scan, without the per-line loop (most of a cold map on a large crate). fn line_of(text: &str, needles: &[String]) -> i64 { + let mut first: Option = None; + for needle in needles.iter().filter(|n| !n.is_empty()) { + if needle.contains(['\n', '\r']) { + return line_of_by_lines(text, needles); + } + if let Some(at) = text.find(needle.as_str()) { + first = Some(first.map_or(at, |seen| seen.min(at))); + } + } + match first { + Some(at) => { + text.as_bytes()[..at] + .iter() + .filter(|b| **b == b'\n') + .count() as i64 + + 1 + } + None => 1, + } +} + +fn line_of_by_lines(text: &str, needles: &[String]) -> i64 { for (i, line) in text.lines().enumerate() { if needles .iter() @@ -3044,11 +3108,18 @@ fn analyze_files_filtered( // The full-tree path keeps the second resolution pass. A partial update // resolves changed files against the whole universe below. let js_edges = if only.is_none() { - resolve_project(root, &other_files) + let other_rels: Vec = other_files.iter().map(|f| rel_of(root, f)).collect(); + let other_scans: BTreeMap = other_rels + .iter() + .map(|rel| (rel.clone(), scans.get(rel).cloned().unwrap_or_default())) + .collect(); + resolve_scanned(root, &other_rels, &other_scans) } else { BTreeMap::new() }; let resolver = Resolver::build(root, &rels); + // Barrel follow-through wants the scans sorted. Build that copy once, not per name. + let mut sorted_scans: Option> = None; let mut out = BTreeMap::new(); for rel in &rels { if only.as_ref().is_some_and(|set| !set.contains(rel)) { @@ -3098,9 +3169,13 @@ fn analyze_files_filtered( } } } else if !lang.is_empty() { + // A spec repeats across imports (every `a::b` path in a Rust file); find its line once. + let mut lines_seen: HashMap<&str, i64> = HashMap::new(); for imp in &scan.imports { let target = resolver.resolve(rel, &imp.spec, lang, imp.inline_depth); - let line = line_of(&text, &[imp.spec.clone()]); + let line = *lines_seen + .entry(imp.spec.as_str()) + .or_insert_with(|| line_of(&text, std::slice::from_ref(&imp.spec))); let dynamic = imp.kind == Kind::Dynamic; match target { Some(dst) if dst != *rel => { @@ -3117,19 +3192,10 @@ fn analyze_files_filtered( && (imp.kind == Kind::Import || imp.kind == Kind::ExportFrom) { for name in &imp.names { - if let Some(def) = defining_file( - &resolver, - &{ - let mut m = BTreeMap::new(); - for (k, v) in &scans { - m.insert(k.clone(), v.clone()); - } - m - }, - &dst, - name, - 0, - ) { + let sorted = sorted_scans.get_or_insert_with(|| { + scans.iter().map(|(k, v)| (k.clone(), v.clone())).collect() + }); + if let Some(def) = defining_file(&resolver, sorted, &dst, name, 0) { if def != *rel && def != dst && !facts.dsts.contains(&def) { add_edge( &mut facts, diff --git a/src/parse.rs.wiki.md b/src/parse.rs.wiki.md index 38533ac..aa358e1 100644 --- a/src/parse.rs.wiki.md +++ b/src/parse.rs.wiki.md @@ -6,6 +6,6 @@ Unknown suffixes emit no edges. Named JS/TS re-exports are followed to depth 8 a 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. +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. A full map scans each file once: `resolve_project` and the internal `resolve_scanned` share those scans. `line_of` finds an import's line by its earliest byte match (a needle with a line break uses the line-by-line scan), and repeated specs in one file are looked up once. 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 unrestricted `pub` items, including `pub` methods and associated items on a public type, and the items written in a trait impl for a public type. `pub(crate)`, `pub(super)`, `pub(self)`, and `pub(in ...)` stay out. A root `Cargo.toml` `[workspace] members` list, including globs, supplies each member crate name. `crate::` stays inside the member that owns the file. diff --git a/src/scope.rs b/src/scope.rs index 7d01b77..489652b 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -278,9 +278,14 @@ fn walk(root: &Path, dir: &Path, ex_names: &[String], ex_globs: &[String], out: if ex_names.iter().any(|n| n == &name) || ex_globs.iter().any(|g| fnmatch(g, &name)) { continue; } - if path.is_dir() { + // The directory entry already knows its type; a symlink still follows its target. + let (is_dir, is_file) = match ent.file_type() { + Ok(kind) if !kind.is_symlink() => (kind.is_dir(), kind.is_file()), + _ => (path.is_dir(), path.is_file()), + }; + if is_dir { walk(root, &path, ex_names, ex_globs, out); - } else if path.is_file() && is_source(&path) && path.starts_with(root) { + } else if is_file && is_source(&path) && path.starts_with(root) { out.push(path); } } diff --git a/tests/dogfood.rs b/tests/dogfood.rs index 57b0e03..a5bdc40 100644 --- a/tests/dogfood.rs +++ b/tests/dogfood.rs @@ -289,6 +289,7 @@ fn prepare_edit_on_this_repo_lists_api_dependents() { "tests/protocol_v07.rs", "tests/robustness.rs", "tests/skip_maps.rs", + "tests/speed_paths.rs", ]; assert_eq!(dependents, expected, "prepare-edit src/api.rs dependents"); diff --git a/tests/speed_paths.rs b/tests/speed_paths.rs new file mode 100644 index 0000000..b4ed8f1 --- /dev/null +++ b/tests/speed_paths.rs @@ -0,0 +1,256 @@ +//! The fast paths must give the answers the slow paths gave: edges from the index, +//! map artifacts kept when the cache did not change, stat-first change checks, and a +//! capped bootstrap file list. + +use std::collections::{BTreeMap, BTreeSet}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use wikifier::api; +use wikifier::cache; +use wikifier::{check_changes, mark_green, read_health, update_maps, Json}; + +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-speed-{name}-{n}-{}", std::process::id())); + let _ = fs::remove_dir_all(&dir); + fs::create_dir_all(dir.join("src/pkg")).unwrap(); + fs::write(dir.join("map_paths.txt"), "src\n").unwrap(); + fs::write(dir.join("monitored_paths.txt"), "src\n").unwrap(); + dir +} + +/// `a` and `b` import each other, `c` imports `a` and itself. +fn cyclic_project(name: &str) -> PathBuf { + let root = scratch(name); + fs::write(root.join("src/pkg/__init__.py"), "").unwrap(); + fs::write(root.join("src/pkg/a.py"), "from . import b\n").unwrap(); + fs::write(root.join("src/pkg/b.py"), "from . import a\n").unwrap(); + fs::write( + root.join("src/pkg/c.py"), + "from . import a\nfrom . import c\n", + ) + .unwrap(); + let built = update_maps(&root); + assert_eq!(ok(&built), Some(true), "{built}"); + root +} + +fn ok(value: &Json) -> Option { + value.get("success").and_then(|v| v.as_bool()) +} + +fn by_src_edges(root: &Path) -> BTreeSet<(String, String)> { + let mut out = BTreeSet::new(); + for rel in cache::all_paths(root) { + for dst in cache::load_dependencies(root, &rel).unwrap() { + if dst != rel { + out.insert((rel.clone(), dst)); + } + } + } + out +} + +#[test] +fn edges_from_the_index_match_the_by_src_records() { + let root = cyclic_project("edges"); + let expected = by_src_edges(&root); + assert!(expected.contains(&("src/pkg/a.py".into(), "src/pkg/b.py".into()))); + assert_eq!(cache::edges(&root), expected); + + // An index written before targets were stored still yields the same edges. + let index = root.join(".wikifier_staging/wk_cache/index.tsv"); + let old_rows: String = fs::read_to_string(&index) + .unwrap() + .lines() + .map(|line| line.split('\t').take(4).collect::>().join("\t") + "\n") + .collect(); + fs::write(&index, old_rows).unwrap(); + assert_eq!(cache::edges(&root), expected); + + let cycles = api::cycles(&root); + assert_eq!(cycles.get("count"), Some(&Json::Int(1)), "{cycles}"); + assert!(cycles.to_string().contains("break_hint"), "{cycles}"); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn a_warm_run_keeps_map_artifacts_until_the_cache_changes() { + let root = cyclic_project("warm"); + let library = root.join("library.md"); + let marked = format!("{}\n", fs::read_to_string(&library).unwrap()); + fs::write(&library, &marked).unwrap(); + let warm = update_maps(&root); + assert_eq!(warm.get("files_parsed"), Some(&Json::Int(0)), "{warm}"); + assert_eq!(fs::read_to_string(&library).unwrap(), marked); + + // CRLF from a Windows checkout is still recognised as the generated file. + fs::write(&library, marked.replace('\n', "\r\n")).unwrap(); + update_maps(&root); + assert!(fs::read_to_string(&library) + .unwrap() + .contains("")); + + fs::write(root.join("src/pkg/d.py"), "from . import c\n").unwrap(); + update_maps(&root); + let text = fs::read_to_string(&library).unwrap(); + assert!(!text.contains(""), "{text}"); + assert!(text.contains("src/pkg/d.py"), "{text}"); + + // The init placeholder or a missing folder card is rebuilt even with no cache change. + fs::write( + &library, + "# Library & Imports Map\n\nRun `wikifier update-maps` to populate.\n", + ) + .unwrap(); + fs::remove_dir_all(root.join(".wikifier_staging/maps")).unwrap(); + update_maps(&root); + assert!(fs::read_to_string(&library) + .unwrap() + .contains("src/pkg/d.py")); + assert!(root.join(".wikifier_staging/maps/_root.json").is_file()); + let _ = fs::remove_dir_all(root); +} + +fn green(root: &Path, rel: &str) { + fs::write(root.join(format!("{rel}.wiki.md")), "# a\n\nImports `b`.\n").unwrap(); + let value = mark_green(root, rel, "wiki (speed-1)", false); + assert_eq!(ok(&value), Some(true), "{value}"); +} + +fn changes(root: &Path) -> i64 { + match check_changes(root).get("changes_detected") { + Some(Json::Int(n)) => *n, + other => panic!("{other:?}"), + } +} + +#[test] +fn check_changes_trusts_an_unchanged_stat_and_reads_everything_else() { + let root = cyclic_project("stat"); + let rel = "src/pkg/a.py"; + let path = root.join(rel); + green(&root, rel); + assert_eq!(changes(&root), 0); + + // Same size, mtime put back: the stat rule that update-maps also uses says unchanged. + let mtime = fs::metadata(&path).unwrap().modified().unwrap(); + fs::write(&path, "from . import c\n").unwrap(); + fs::File::options() + .write(true) + .open(&path) + .unwrap() + .set_modified(mtime) + .unwrap(); + assert_eq!(changes(&root), 0); + + // A real edit moves the mtime and is caught. + fs::write(&path, "from . import b\nX = 2\n").unwrap(); + fs::File::options() + .write(true) + .open(&path) + .unwrap() + .set_modified(mtime + Duration::from_secs(5)) + .unwrap(); + assert_eq!(changes(&root), 1); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn check_changes_reads_the_file_when_the_index_hash_is_not_the_baseline() { + let root = cyclic_project("baseline"); + let rel = "src/pkg/b.py"; + green(&root, rel); + // An index hash from an older hashing rule, with the stat still matching. + let index = root.join(".wikifier_staging/wk_cache/index.tsv"); + let rewritten: String = fs::read_to_string(&index) + .unwrap() + .lines() + .map(|line| { + let mut cols: Vec = line.split('\t').map(|c| c.to_string()).collect(); + if cols[0] == rel { + cols[3] = "sha256:0000".into(); + } + cols.join("\t") + "\n" + }) + .collect(); + fs::write(&index, rewritten).unwrap(); + assert_eq!(changes(&root), 0); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn seeded_rows_carry_the_file_content_hash() { + let root = cyclic_project("seed"); + for row in read_health(&root) { + let bytes = fs::read(root.join(&row.file)).unwrap(); + assert_eq!( + row.source_hash, + wikifier::sha256::content_sha256_prefixed(&bytes), + "{}", + row.file + ); + } + let _ = fs::remove_dir_all(root); +} + +#[test] +fn bootstrap_caps_its_file_list() { + let root = scratch("bootstrap"); + for i in 0..105 { + fs::write(root.join(format!("src/m{i:03}.py")), "X = 1\n").unwrap(); + } + update_maps(&root); + let mut flags = BTreeMap::new(); + let full = api::dispatch("session-bootstrap", &root, &[], &flags); + assert_eq!(files(&full), 105); + assert!(full.get("files_total").is_none()); + flags.insert("limit".to_string(), "3".to_string()); + let capped = api::dispatch("session-bootstrap", &root, &[], &flags); + assert_eq!(files(&capped), 3); + assert_eq!(capped.get("files_total"), Some(&Json::Int(105))); + + let mcp = |args: Vec<(&str, Json)>| { + let mut args = args; + args.push(( + "project_root", + Json::String(root.to_string_lossy().into_owned()), + )); + let msg = Json::obj(vec![ + ("jsonrpc", Json::String("2.0".into())), + ("id", Json::Int(1)), + ("method", Json::String("tools/call".into())), + ( + "params", + Json::obj(vec![ + ("name", Json::String("session_bootstrap".into())), + ("arguments", Json::obj(args)), + ]), + ), + ]); + wikifier::mcp::handle_message(&msg) + .unwrap() + .get("result") + .and_then(|r| r.get("structuredContent")) + .cloned() + .unwrap() + }; + let default = mcp(vec![]); + assert_eq!(files(&default), 100, "{default}"); + assert_eq!(default.get("files_total"), Some(&Json::Int(105))); + assert_eq!(files(&mcp(vec![("limit", Json::Int(0))])), 105); + let _ = fs::remove_dir_all(root); +} + +fn files(value: &Json) -> usize { + value + .get("files") + .and_then(|v| v.as_array()) + .map(|a| a.len()) + .unwrap_or_else(|| panic!("{value}")) +} From 15e424b76f89f123861e1e6761a8151cfb62f1f0 Mon Sep 17 00:00:00 2001 From: IronAdamant <18153828+IronAdamant@users.noreply.github.com> Date: Thu, 5 Nov 2026 16:19:32 +1100 Subject: [PATCH 07/11] Pending queue: drop the empty-queue note with the first item; say what closes an item. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pending_updates.md kept "(no pending items — run check-changes after making edits)" above the items once any were added (this repo's own file showed it). The note now goes when the first item arrives and comes back when mark-green removes the last one. A record-change item read `LLM/agent edit — `, wording carried over from the Python package. It now reads ` — refresh the wiki, then mark-green`, the step that clears it. tests/parity_fixes.rs covers both. CHANGELOG and skills/run.md updated. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CMYkogM3QWVjMLzWKGyjuJ --- CHANGELOG.md | 2 ++ skills/run.md | 2 +- src/api.rs | 21 ++++++++------------- src/pending_line.rs | 33 +++++++++++++++++++++++++++++---- src/pending_line.rs.wiki.md | 5 +++++ tests/parity_fixes.rs | 17 +++++++++++++++-- 6 files changed, 60 insertions(+), 20 deletions(-) create mode 100644 src/pending_line.rs.wiki.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e1bea2..db0ae50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `prepare_edit`, `get_dependencies`, `get_dependents`, and `get_file_wiki` return `FileNotFound` with a `list_paths` hint for a path that is neither on disk nor in the map, instead of `success: true` with empty fields. - MCP `tools/call` with a missing required argument (for example `file_path` instead of `file`) returns `InvalidArguments` naming the accepted and received arguments. Before, the tool ran with an empty path and reported success. - `health --limit N` (and MCP `limit`) caps the entry list and adds `entries_total`. The argument was advertised and ignored. +- `pending_updates.md` no longer says "(no pending items …)" above a list of items. The note is dropped when the first item arrives and comes back when the last one is removed. This was carried over from the Python package. +- A `record-change` pending item reads `- path: — refresh the wiki, then mark-green` instead of the Python wording `LLM/agent edit — `. ### Performance Same outputs: on a 587-file Cargo workspace the cache files, `library.md`, folder cards, health hashes, and the `update-maps`, `cycles`, `dependencies --full`, `dependents`, `check-changes`, and `session-bootstrap` output were byte-identical before and after (CLI, timestamps aside). Warm MCP calls there, median of 5 on Windows 11: `check_changes` 353 → 53 ms, `update_maps` 708 → 50 ms, `get_cycles` 317 → 39 ms, `session_bootstrap` 33 → 16 ms and 21.7 → 4.7 KB. Cold `update-maps` (CLI, runs interleaved on a loaded machine, median of 3) went from 18.7 s to 6.4 s. diff --git a/skills/run.md b/skills/run.md index 21e7060..25bdee0 100644 --- a/skills/run.md +++ b/skills/run.md @@ -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 `?`). `--reasons` accepts a JSON object (`{"path": "why"}`) or `PATH=REASON` lines (`-` reads stdin). `--only`, `--green`, `--dry-run`, and deletions are applied. `--green` lists verification failures and leaves those files Yellow. MCP `reasons` may be that object. -- `mark_green` extracts Python public names with a tokenizer (multi-line `__all__` lists, tuples, and `+=`; comments and strings ignored; top-level `def`, `async def`, and `class`, including decorated ones; `.py` and `.pyi`), JS/TS `export` names after comments and strings are masked, and Rust unrestricted `pub` items (`fn`, `struct`, `enum`, `trait`, `mod`, `type`, `const`, `static`, and `pub use` names), plus `pub` methods and associated items on a public type and the items written in a trait impl for that type. Each current name must appear in the wiki. `pub(crate)`, `pub(super)`, `pub(self)`, and `pub(in ...)` 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`. The wiki is `.wiki.md`, then `.md`, the stem beside the source, then the same names under `docs/wiki`, `docs`, `wiki`, `documentation`, and `.wiki`. `health --summary` returns `health_summary` (including `green_breakdown`) without the entry list. `health --format healing-stats` is the stub counts. `--directory` limits health, bootstrap, and suggest-next. A successful mark removes that file's pending line. Pending bullets are `- path: message` (`LLM/agent edit — …` after `record-change`). `session_bootstrap --limit N` caps `files` and adds `files_total`; over MCP the default cap is 100 (`limit: 0` lists every file). +- `mark_green` extracts Python public names with a tokenizer (multi-line `__all__` lists, tuples, and `+=`; comments and strings ignored; top-level `def`, `async def`, and `class`, including decorated ones; `.py` and `.pyi`), JS/TS `export` names after comments and strings are masked, and Rust unrestricted `pub` items (`fn`, `struct`, `enum`, `trait`, `mod`, `type`, `const`, `static`, and `pub use` names), plus `pub` methods and associated items on a public type and the items written in a trait impl for that type. Each current name must appear in the wiki. `pub(crate)`, `pub(super)`, `pub(self)`, and `pub(in ...)` 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`. The wiki is `.wiki.md`, then `.md`, the stem beside the source, then the same names under `docs/wiki`, `docs`, `wiki`, `documentation`, and `.wiki`. `health --summary` returns `health_summary` (including `green_breakdown`) without the entry list. `health --format healing-stats` is the stub counts. `--directory` limits health, bootstrap, and suggest-next. A successful mark removes that file's pending line. Pending bullets are `- path: message` (` — refresh the wiki, then mark-green` after `record-change`). The `(no pending items …)` line shows only while the queue is empty. `session_bootstrap --limit N` caps `files` and adds `files_total`; over MCP the default cap is 100 (`limit: 0` lists every file). - `suggest-next` adds `review_low_confidence_edges` when a mapped file has a resolved, non-dynamic edge below confidence 0.65. The count is stored while parsing (`low_conf.tsv`) so a warm `update-maps` does not rescan it. - `list-paths --depth 0` or `--recursive` lists every file under the prefix. `--max-files` caps that list. `verify-wikis --limit` and `stale-wikis --limit` cap the rows returned. `cycles` adds `severity`, `score`, `external_blast_radius`, `recommendation`, and `reverse_dependencies` (top 20 by inbound edges). `--no-analysis` omits those. `--max-items` caps the cycle list. - `seed-source-hashes` writes the current hash onto Green rows and does not change status. `--force` replaces a hash that is already stored. `seed-health` still adds Initial-stub rows. `heal-stubs` promotes a stub whose wiki is at least 350 characters (`--min-wiki-length` overrides). diff --git a/src/api.rs b/src/api.rs index 858ff74..6aa4e2d 100644 --- a/src/api.rs +++ b/src/api.rs @@ -2349,18 +2349,14 @@ pub fn record_deletion(root: &Path, file: &str, reason: &str) -> Json { fn drop_pending_line(root: &Path, rel: &str) { let path = root.join("pending_updates.md"); let text = fs::read_to_string(&path).unwrap_or_default(); - if text.is_empty() { + if text.is_empty() + || !text + .lines() + .any(|line| crate::pending_line::file_of(line) == Some(rel)) + { return; } - let mut out = String::new(); - for line in text.lines() { - let hit = crate::pending_line::file_of(line) == Some(rel); - if !hit { - out.push_str(line); - out.push('\n'); - } - } - let _ = fs::write(path, out); + let _ = fs::write(path, crate::pending_line::remove_item(&text, rel)); } pub fn heal_stubs(root: &Path, min_len: usize, dry_run: bool) -> Json { @@ -3937,8 +3933,7 @@ target | File | Status | Last Updated | Reason / Intent | |------|--------|--------------|-----------------| "; - let pending_body = - "# Pending Updates\n\n(no pending items — run check-changes after making edits)\n"; + let pending_body = format!("# Pending Updates\n\n{}\n", crate::pending_line::EMPTY_NOTE); let library_body = "# Library & Imports Map\n\nRun `wikifier update-maps` to populate.\n"; if !path_list_has_entries(root, "map_paths.txt") && !write_new("map_paths.txt", &map_body, &mut created) @@ -3956,7 +3951,7 @@ target if !write_new("file_health.md", health_body, &mut created) { return fail("InitError", "could not write file_health.md"); } - if !write_new("pending_updates.md", pending_body, &mut created) { + if !write_new("pending_updates.md", &pending_body, &mut created) { return fail("InitError", "could not write pending_updates.md"); } if !write_new("library.md", library_body, &mut created) { diff --git a/src/pending_line.rs b/src/pending_line.rs index fe3a6b5..2bcd580 100644 --- a/src/pending_line.rs +++ b/src/pending_line.rs @@ -24,6 +24,10 @@ pub fn format_item(rel: &str, message: &str) -> String { format!("- {rel}: {message}\n") } +/// The line an empty queue shows. It goes when the first item arrives and comes back when +/// the last one leaves, so the file never says "no pending items" above a list of items. +pub const EMPTY_NOTE: &str = "(no pending items — run check-changes after making edits)"; + /// Append `message` for `rel` when that file is not already listed. pub fn append_item(text: &str, rel: &str, message: &str) -> String { if text.lines().any(|line| file_of(line) == Some(rel)) { @@ -32,17 +36,38 @@ pub fn append_item(text: &str, rel: &str, message: &str) -> String { let mut out = if text.trim().is_empty() { "# Pending Updates\n\n".to_string() } else { - text.to_string() + text.lines() + .filter(|line| line.trim() != EMPTY_NOTE) + .map(|line| format!("{line}\n")) + .collect() }; - if !out.is_empty() && !out.ends_with('\n') { + out.push_str(&format_item(rel, message)); + out +} + +/// `text` without the item for `rel`. The empty-queue note returns with the last item. +pub fn remove_item(text: &str, rel: &str) -> String { + let mut out = String::new(); + for line in text.lines() { + if file_of(line) != Some(rel) { + out.push_str(line); + out.push('\n'); + } + } + let empty = out.lines().all(|line| file_of(line).is_none()); + if empty && !out.contains(EMPTY_NOTE) { + if !out.ends_with("\n\n") { + out.push('\n'); + } + out.push_str(EMPTY_NOTE); out.push('\n'); } - out.push_str(&format_item(rel, message)); out } +/// A `record-change` item: the agent's reason, then what closes the item. pub fn record_message(reason: &str) -> String { - format!("LLM/agent edit — {reason}") + format!("{reason} — refresh the wiki, then mark-green") } pub const CHECK_MESSAGE: &str = "Content change detected — review, refresh wiki, then mark-green"; diff --git a/src/pending_line.rs.wiki.md b/src/pending_line.rs.wiki.md new file mode 100644 index 0000000..073b81e --- /dev/null +++ b/src/pending_line.rs.wiki.md @@ -0,0 +1,5 @@ +# src/pending_line.rs + +**Role:** Items in `pending_updates.md`. + +An item is `- path: message`; `file_of` also reads the older `- path — message` shape. `format_item` builds one line. `append_item` adds an item when the file is not listed yet and drops the `EMPTY_NOTE` line; `remove_item` drops a file's item and puts `EMPTY_NOTE` back when no item is left. `record_message` is the reason followed by "refresh the wiki, then mark-green". `CHECK_MESSAGE` is the text `check-changes` writes. diff --git a/tests/parity_fixes.rs b/tests/parity_fixes.rs index 674187a..0457348 100644 --- a/tests/parity_fixes.rs +++ b/tests/parity_fixes.rs @@ -104,9 +104,11 @@ fn js_comment_exports_are_not_public() { } #[test] -fn pending_line_uses_the_python_shape() { +fn pending_line_names_the_reason_and_the_next_step() { let root = scratch("pend"); fs::write(root.join("src/a.py"), "x = 1\n").unwrap(); + let empty = "# Pending Updates\n\n(no pending items — run check-changes after making edits)\n"; + fs::write(root.join("pending_updates.md"), empty).unwrap(); assert_eq!( api::record_change(&root, "src/a.py", "first reason") .get("success") @@ -115,9 +117,20 @@ fn pending_line_uses_the_python_shape() { ); let text = fs::read_to_string(root.join("pending_updates.md")).unwrap(); assert!( - text.contains("- src/a.py: LLM/agent edit — first reason"), + text.contains("- src/a.py: first reason — refresh the wiki, then mark-green"), "{text}" ); + assert!(!text.contains("no pending items"), "{text}"); + + fs::write(root.join("src/a.py.wiki.md"), "# a\n\nSets `x`.\n").unwrap(); + let marked = api::mark_green(&root, "src/a.py", "wiki (pend-1)", false); + assert_eq!( + marked.get("success").and_then(|v| v.as_bool()), + Some(true), + "{marked}" + ); + let text = fs::read_to_string(root.join("pending_updates.md")).unwrap(); + assert_eq!(text, empty); let _ = fs::remove_dir_all(&root); } From 6805d018bb68a7d66bc8cb58ef247222f3175ba3 Mon Sep 17 00:00:00 2001 From: IronAdamant <18153828+IronAdamant@users.noreply.github.com> Date: Thu, 5 Nov 2026 16:19:39 +1100 Subject: [PATCH 08/11] Dogfood: record the speed and pending changes in the repo's wiki state. record-change / mark-green for the edited files, then update-maps. Generated by the wikifier binary, not edited by hand. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CMYkogM3QWVjMLzWKGyjuJ --- file_health.json | 2 +- file_health.md | 25 +++++----- journal/2026/11/05.md | 104 ++++++++++++++++++++++++++++++++++++++++++ library.md | 6 ++- pending_updates.md | 7 +-- 5 files changed, 123 insertions(+), 21 deletions(-) diff --git a/file_health.json b/file_health.json index 16f6974..984b965 100644 --- a/file_health.json +++ b/file_health.json @@ -1 +1 @@ -{"entries":{".github/workflows/publish.yml":{"last_updated":"2026-11-03 09:27:04","reason":"Multi-target release archives and SHA256SUMS (subid=release-5.0.0)","source_content_hash":"sha256:930170e1eb57afbe2850a0e9c2ac179eb2b5947bf2f74d4292bcd2b0099ab8de","source_mtime_ns":"1793697915725124882","status":"🟢 Green","verification":"no_wiki"},"AGENTS.md":{"last_updated":"2026-11-03 09:27:04","reason":"Package line is 5.0.0; pip remains Python 4.7 (subid=release-5.0.0)","source_content_hash":"sha256:f2db15364d5b8b309d03a37fa123c55eb73103316632d2d0b3418205fea5625e","source_mtime_ns":"1793697832705476376","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:d82ebf68153d48b828a60ddafa7e0f4ed00095141048f3014da7e520d994cde0"},"CHANGELOG.md":{"last_updated":"2026-11-05 04:16:31","reason":"wiki updated (claude/windows-verify)","source_content_hash":"sha256:f1ded74a2ac5495dcf0d5caaa5c8022d4c913192e5348febd96c519103dbcfe1","source_mtime_ns":"1793852120420150200","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:757601288624e346279cef86383a88cdf7c1c467e7c6538f40c05d475e243de8"},"CLAUDE.md":{"last_updated":"2026-11-03 09:27:04","reason":"Package line is 5.0.0 (subid=release-5.0.0)","source_content_hash":"sha256:2734c85af757bf0b873c3cc5ced86347f9042e37a90976023fbdbe43a6dbeda3","source_mtime_ns":"1793697832749476242","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:8991f81c78306a55befa7ef13876879fc5c22cd58ee6013567db4133afa14d34"},"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"},"Cargo.lock":{"last_updated":"2026-11-03 09:27:04","reason":"Lockfile version 5.0.0 (subid=release-5.0.0)","source_content_hash":"sha256:eb5e5f51e2a4fd584bd48af18328f7eaaae1efc4ee9fb23743d85a34faa8b435","source_mtime_ns":"1793697831949478649","status":"🟢 Green","verification":"no_wiki"},"Cargo.toml":{"last_updated":"2026-11-03 09:27:04","reason":"Version 5.0.0 (subid=release-5.0.0)","source_content_hash":"sha256:d16350a4d796d62eac12d64651ff9e9ae843db94489bfa4fb67c4a657b31b288","source_mtime_ns":"1793697832013478456","status":"🟢 Green","verification":"no_wiki"},"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-05 04:16:31","reason":"wiki updated (claude/windows-verify)","source_content_hash":"sha256:3d3678c13234664aeaf1d923d79f5082bb57da407c22dfa82a38f5bed4799e78","source_mtime_ns":"1793852178767718000","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:45e2581fe9d69ba807dfce7e982c015e23c2702a6c765b83eb9513ef809418da"},"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"},"scripts/benchmark_parity.sh":{"last_updated":"2026-11-03 08:56:15","reason":"Benchmark script and measured results.","source_content_hash":"sha256:b8f72e3245e77b9df17b303293d8cbe4a93cc36023c41b89a794b77b01daef87","status":"🟡 Yellow"},"scripts/benchmark_results.md":{"last_updated":"2026-11-03 08:56:15","reason":"Benchmark script and measured results.","source_content_hash":"sha256:8bf9342fce85f126a37f812b03b6f6997f1fe8e7c5176d30301c1a4a15aeee29","status":"🟡 Yellow"},"skills/run.md":{"last_updated":"2026-11-05 04:16:31","reason":"wiki updated (claude/windows-verify)","source_content_hash":"sha256:ae3a88efc0247857f9c5b9b6ae6985084bdad780e89186e35933ba3a9f93f625","source_mtime_ns":"1793852105332278800","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:d1f7fb759d196fd6a4b468148f0078915537d78aff1906017b9d855eca5b7200"},"src/api.rs":{"last_updated":"2026-11-05 04:14:15","public_symbols":"MapRun\nVERSION\nMCP_TOOLS\nupdate_maps\nupdate_maps_opts\nupdate_maps_run\nseed_source_hashes\nmapped_edges\nload_dependencies\ndependency_report\nload_dependents\nlist_paths\nlist_paths_at\nsession_bootstrap\nsession_bootstrap_scoped\nsuggest_next_actions\nsuggest_next_scoped\ncheck_changes\nprepare_edit\nrecord_change\nmark_green\nhealth\nhealth_view\nread_health\nread_journal\ncache_status\nvalidate\ncycles\ncycles_view\nverify_wikis\nrecord_changes\nrecord_changes_opts\nrecord_deletion\nheal_stubs\nprune_health\nprune_health_opts\nprune_pending\nstale_wikis\nstale_wikis_in\nprune_barrels\nmetrics_snapshot\nhealing_stats\nresolution_diagnostics\nbarrel_reports\nincremental_status\nserve_once\nserve_loop\nmonitor_loop\ndaemon_ctl\ndaemon_command\ndispatch\nCLI_COMMANDS\nensure_root","reason":"wiki updated (claude/windows-verify)","source_content_hash":"sha256:ab6cc6173ad4a8cdcb0d1bcd4561f52761e59824e491fb36477081d5434a7262","source_mtime_ns":"1793851923248387900","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:c4a22ccbc36099bc875de935d34358b654491670143864fb11fd8fba54543a48"},"src/artifacts.rs":{"last_updated":"2026-11-03 08:56:14","public_symbols":"HealthRow\nread_health\nwrite_health\nupsert_health_row\nJournalEntry\njournal_path\nread_journal\nparse_journal\nJournalWrite\nappend_journal\nread_pending\nfolder_card\nnew\nkeep_baseline_from","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:6a7ca10120cbb4a37de62999330a92b181f3ebd7210f92209b280ba5282d8544","source_mtime_ns":"1793678007374624809","status":"🟡 Yellow","wiki_hash":"sha256:75bf0f72cc4bd57b6178dc6a0a58b8bf97bc3e7265485a28102093b967f20181"},"src/cache.rs":{"last_updated":"2026-11-03 08:56:14","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\nconfidence\nacs_reason","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:fb8a06997639e0f1556cc64ce5792f1525b0cb3c4afab5f9cbfcaeb9c809adf4","source_mtime_ns":"1793678007370625780","status":"🟡 Yellow","wiki_hash":"sha256:afb7493ff8742682f5f3c743da8919408502e6a6e75bba9998eb59cbbaf1c02a"},"src/cache_commit.rs":{"last_updated":"2026-11-03 08:56:14","public_symbols":"Consistency\nSession\ncheck_consistency\nrebuild\nopen\nknown\ndsts_of\nnote_stat\nrels\nupsert\nremove\nstage_barrel\ncommit","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:c987ccdccdd0c980c0af64a2dcb349cd3b4ed1de7070a481dba146782f40a745","source_mtime_ns":"1793684429116993727","status":"🟡 Yellow","wiki_hash":"sha256:c6f3fbdcd6aafe62de119c302e09e11ae0ea208681c52166cfcf79de80bb5f6c"},"src/cycle_report.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:db35b610d9a9f9917805da9383e61872e66e640482f57fc9e17ae91daf77fe3d","status":"🟡 Yellow"},"src/daemon_svc.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:0887baeb3ad7f2fca851fccc0d9335dafd4b42f3e54e236175a13d39c5bcd063","status":"🟡 Yellow"},"src/jsonutil.rs":{"last_updated":"2026-11-03 08:56:14","public_symbols":"Json\nparse\nparse_jsonc\nstrip_json_comments\nobj\nfrom_pairs\nget\nas_str\nas_bool\nas_array\nas_object\ninsert\nfmt","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:cb71e00e352df010928a42323d6d87bbe67348834a209348fc174b9d00489676","source_mtime_ns":"1793684429116993727","status":"🟡 Yellow","wiki_hash":"sha256:fb4cffba8b14a179dd90f201b61df2608ca78a9775e9646e7f011f1b9fa07c63"},"src/legacy_cache.rs":{"last_updated":"2026-11-03 08:56:14","public_symbols":"import_if_empty","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:a127d0b1742d69b736194ca98579f5fc0611fc51a291d691aef6fe586fd68ba3","source_mtime_ns":"1793683981803718735","status":"🟡 Yellow","wiki_hash":"sha256:a6fe6c79185a46cc4b30d215475bab28f517f6e5a6fd7bce6423c2792814aac8"},"src/lib.rs":{"last_updated":"2026-11-03 08:56:14","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":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:9c27c83b58ff7f1acf62d5d19ee31e2070daa06499e27141cc7071fae44e26b8","source_mtime_ns":"1793678007370625780","status":"🟡 Yellow","wiki_hash":"sha256:d7e771a3ff63ab55a7967195e3b403dd75d5ae020a383adc56f0e5c09d7b03a0"},"src/low_conf.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:3ade5fc70ce585a48b9acf6a2b48753e58e80f63281a31c8b8453816700b375e","status":"🟡 Yellow"},"src/main.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:2b3cebdafca3ca3da7831101a63b36f72260b3b2879871e5364b427428e782e0","source_mtime_ns":"1793683990351610198","status":"🟡 Yellow","wiki_hash":"sha256:db6e1cd5beeb90e5a65b3e00a37ee2c73ea4c53750771151e655459a1b823ff5"},"src/maps_out.rs":{"last_updated":"2026-11-03 08:56:14","public_symbols":"write\nwrite_status\nread_status","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:c018687f1e511e23bc92a3f771a1f2bf5e693d513d8e4bcad958d9908f0af09c","source_mtime_ns":"1793678007370625780","status":"🟡 Yellow","wiki_hash":"sha256:d441c395fe565828a87c54d5545e4c89e4ec4cfc49308a08525e6dcf3c744e75"},"src/mcp.rs":{"last_updated":"2026-11-05 04:14:09","public_symbols":"tool_schema\nhandle_message\nserve_stdio","reason":"wiki updated (claude/windows-verify)","source_content_hash":"sha256:f6e3f44b0ca8f058cffa63ef2c42f0864eae0c99fb82ee68cc2b8fd3cdac3d4a","source_mtime_ns":"1793851788815286600","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:a54f210674d40bfe390a7271ecdb1111ecca199a217c49764f9b1a616d835486"},"src/parse.rs":{"last_updated":"2026-11-03 08:56:14","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":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:5607acdac0b08854951700a9d2187a9f849d2fdb4febc2959fe065bcf778963a","source_mtime_ns":"1793684824322776381","status":"🟡 Yellow","wiki_hash":"sha256:c70af52c81a4dfffb364cba13ca21873491e83e1c9c5cee31ab2bab2bc379fbb"},"src/pending_line.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:c941813474dbb2cfc1ffed8c7bc9791d08eb718d92d336e1f42d1c1b0d67bcdc","status":"🟡 Yellow"},"src/proc.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:8f13798a8b9289d78913becc626056c8b016838570e356e09bfa3333e800351c","status":"🟡 Yellow"},"src/project_lock.rs":{"last_updated":"2026-11-03 08:56:14","public_symbols":"ProjectLock\ndrop\nacquire","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:194e329f35ff59b11fc734b980a1ee33f8e464bcdb5eae899486e487daef3a25","source_mtime_ns":"1793678007370625780","status":"🟡 Yellow","wiki_hash":"sha256:fccc8d547f30262ff64d506d842472623e828ebe39fde71532a683c6956329d2"},"src/scope.rs":{"last_updated":"2026-11-05 04:14:08","public_symbols":"SOURCE_EXTS\nScope\ndetect\nread_path_list\nexcludes\nis_source\ncollect\nrel_of\ndefault_map_roots\nrel_arg\nallows_walk","reason":"wiki updated (claude/windows-verify)","source_content_hash":"sha256:ad1e6cbb7ab6f5d5f9767b788b2f150174d9b752763dd577b22d752fe267cb08","source_mtime_ns":"1793851508989498600","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:574d4cc8bd261700b1e192628435926d8f1e533f7f1967e27eb6c797e59e6c0e"},"src/sha256.rs":{"last_updated":"2026-11-05 04:14:08","public_symbols":"sha256\nsha256_hex\nsha256_prefixed\ncontent_sha256_prefixed\nfile_sha256_prefixed","reason":"wiki updated (claude/windows-verify)","source_content_hash":"sha256:2f4f8a8ccb9f9d4748fc0678e6bac63dc69ade57e9ab4b3c6eb2a1c43fcf5c14","source_mtime_ns":"1793851995838527400","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:0d1d67e61bed7a20c6a8413a352a55fb87f05bda6e39760015727d128a67ae33"},"src/symbols.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:4a150ba04e9ea26020c874c5e256a9ec5acc3ef4d621fb6ff03b21935c3f8046","status":"🟡 Yellow"},"src/text_out.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:8e0edc1141a2c1745c0e6fdd7a7645c7332365fac532d52fb3e188f4ef06148e","status":"🟡 Yellow"},"src/timeutil.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:9921127144d9d40619e260753e700e9c8a47135ed95b2ad8791afb7fe78afd75","status":"🟡 Yellow"},"src/wiki_files.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:2612539ce9092a3d126d80ce2947f6b4555ddb0bb335769b1619ec67d08a5e84","status":"🟡 Yellow"},"tests/agent_args.rs":{"last_updated":"2026-11-05 04:14:08","reason":"new: agent-style file args, missing args, lean MCP outputs, init roots, CRLF (claude/windows-verify)","source_content_hash":"sha256:ae25dcc233b1d40c48dbc7199e350032717412316089084751b9280970feb66d","status":"🟡 Yellow"},"tests/behaviour_gaps.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Tests for those parity fixes.","source_content_hash":"sha256:8c8d669dae2dbe39b88c2223d20fb2fca51ac1605c62edd108d6ef224223402b","source_mtime_ns":"1793684893298602299","status":"🟡 Yellow","wiki_hash":"sha256:b9d20a310ca48ffd858e6d0d4e49b5e0568ecb57dc5ad981eb54398562864691"},"tests/cache_and_artifacts.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Tests for those parity fixes.","source_content_hash":"sha256:b9483666c1e19f90d4db1af718b08beebadfbcba66152497b7694d8e757a5ec1","status":"🟡 Yellow"},"tests/cli_surface.rs":{"last_updated":"2026-11-05 04:14:08","reason":"launcher exit-code test for wikifier.bat / wikifier.sh (claude/windows-verify)","source_content_hash":"sha256:085faae9e15ace6df3f133f2426ab642f809fe2f1fbc305ae252dcf8a04f57b5","status":"🟡 Yellow"},"tests/contract_gaps.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Tests for those parity fixes.","source_content_hash":"sha256:dc91a239bbc615524b4ac2c58d06515183dfc4981309906003f551e6721d82ae","status":"🟡 Yellow"},"tests/dogfood.rs":{"last_updated":"2026-11-05 04:14:09","reason":"wiki updated (claude/windows-verify)","source_content_hash":"sha256:ca91d83da8aeb3cc2b7619e48258d0895e668d22c3f2980c89e2ab33f265fb88","source_mtime_ns":"1793851508995537100","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:0bc5f3d78d1779ba6302ecc406dc0b25d8beda25f1681a26d57e97210df19906"},"tests/parity.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Tests for those parity fixes.","source_content_hash":"sha256:0246038a1cc01bf66da2fd186ede61f2dcd2c98da9c0c90dd7f3b2cbab1036db","source_mtime_ns":"1793683314773282057","status":"🟡 Yellow","wiki_hash":"sha256:32753c244642df6e08f8578c6203f77225363783c15bc57c33a84f03989f2378"},"tests/parity_fixes.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Tests for those parity fixes.","source_content_hash":"sha256:ac0fa6064323885f6e7bd767557fe3721723c8d20d60e36c7cc5265b85747e31","status":"🟡 Yellow"},"tests/protocol_v07.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Tests for those parity fixes.","source_content_hash":"sha256:bb7cffecabc3fb51ebcc77c64f7fd78f5cd15c445d7e13493ed491227280f37d","source_mtime_ns":"1793684781738827745","status":"🟡 Yellow","wiki_hash":"sha256:b38f4a13e316cefd56070f822980b4f83d9cdacb850ab1978de48ed35c630ec8"},"tests/robustness.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Tests for those parity fixes.","source_content_hash":"sha256:fad304193a47baa8cce21cff8786b35974c7c7f6f71b3318edeb68bd60121681","status":"🟡 Yellow"},"tests/skip_maps.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Tests for those parity fixes.","source_content_hash":"sha256:2eed0c6bc97d91a4be3f2b6ab08c78c62a3a05725a503496e3df91abad6b2130","status":"🟡 Yellow"},"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":{".github/workflows/publish.yml":{"last_updated":"2026-11-03 09:27:04","reason":"Multi-target release archives and SHA256SUMS (subid=release-5.0.0)","source_content_hash":"sha256:930170e1eb57afbe2850a0e9c2ac179eb2b5947bf2f74d4292bcd2b0099ab8de","source_mtime_ns":"1793697915725124882","status":"🟢 Green","verification":"no_wiki"},"AGENTS.md":{"last_updated":"2026-11-03 09:27:04","reason":"Package line is 5.0.0; pip remains Python 4.7 (subid=release-5.0.0)","source_content_hash":"sha256:f2db15364d5b8b309d03a37fa123c55eb73103316632d2d0b3418205fea5625e","source_mtime_ns":"1793697832705476376","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:d82ebf68153d48b828a60ddafa7e0f4ed00095141048f3014da7e520d994cde0"},"CHANGELOG.md":{"last_updated":"2026-11-05 05:15:34","reason":"wiki updated (claude/windows-verify speed)","source_content_hash":"sha256:b098df3fdbacefc56f8b59e5b5731b20edabe69e3ebd5a96130b0582bff184cc","source_mtime_ns":"1793855706689418600","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:757601288624e346279cef86383a88cdf7c1c467e7c6538f40c05d475e243de8"},"CLAUDE.md":{"last_updated":"2026-11-03 09:27:04","reason":"Package line is 5.0.0 (subid=release-5.0.0)","source_content_hash":"sha256:2734c85af757bf0b873c3cc5ced86347f9042e37a90976023fbdbe43a6dbeda3","source_mtime_ns":"1793697832749476242","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:8991f81c78306a55befa7ef13876879fc5c22cd58ee6013567db4133afa14d34"},"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"},"Cargo.lock":{"last_updated":"2026-11-03 09:27:04","reason":"Lockfile version 5.0.0 (subid=release-5.0.0)","source_content_hash":"sha256:eb5e5f51e2a4fd584bd48af18328f7eaaae1efc4ee9fb23743d85a34faa8b435","source_mtime_ns":"1793697831949478649","status":"🟢 Green","verification":"no_wiki"},"Cargo.toml":{"last_updated":"2026-11-03 09:27:04","reason":"Version 5.0.0 (subid=release-5.0.0)","source_content_hash":"sha256:d16350a4d796d62eac12d64651ff9e9ae843db94489bfa4fb67c4a657b31b288","source_mtime_ns":"1793697832013478456","status":"🟢 Green","verification":"no_wiki"},"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-05 04:16:31","reason":"wiki updated (claude/windows-verify)","source_content_hash":"sha256:3d3678c13234664aeaf1d923d79f5082bb57da407c22dfa82a38f5bed4799e78","source_mtime_ns":"1793852178767718000","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:45e2581fe9d69ba807dfce7e982c015e23c2702a6c765b83eb9513ef809418da"},"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"},"scripts/benchmark_parity.sh":{"last_updated":"2026-11-03 08:56:15","reason":"Benchmark script and measured results.","source_content_hash":"sha256:b8f72e3245e77b9df17b303293d8cbe4a93cc36023c41b89a794b77b01daef87","status":"🟡 Yellow"},"scripts/benchmark_results.md":{"last_updated":"2026-11-03 08:56:15","reason":"Benchmark script and measured results.","source_content_hash":"sha256:8bf9342fce85f126a37f812b03b6f6997f1fe8e7c5176d30301c1a4a15aeee29","status":"🟡 Yellow"},"skills/run.md":{"last_updated":"2026-11-05 05:15:34","reason":"wiki updated (claude/windows-verify speed)","source_content_hash":"sha256:44e948cd0a958cf3ed76ae3abdf97782c5e965f32714925cf506c79abd452ef8","source_mtime_ns":"1793855566520057200","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:d1f7fb759d196fd6a4b468148f0078915537d78aff1906017b9d855eca5b7200"},"src/api.rs":{"last_updated":"2026-11-05 05:19:31","public_symbols":"MapRun\nVERSION\nMCP_TOOLS\nupdate_maps\nupdate_maps_opts\nupdate_maps_run\nseed_source_hashes\nmapped_edges\nload_dependencies\ndependency_report\nload_dependents\nlist_paths\nlist_paths_at\nsession_bootstrap\nsession_bootstrap_scoped\nsuggest_next_actions\nsuggest_next_scoped\ncheck_changes\nprepare_edit\nrecord_change\nmark_green\nhealth\nhealth_view\nread_health\nread_journal\ncache_status\nvalidate\ncycles\ncycles_view\nverify_wikis\nrecord_changes\nrecord_changes_opts\nrecord_deletion\nheal_stubs\nprune_health\nprune_health_opts\nprune_pending\nstale_wikis\nstale_wikis_in\nprune_barrels\nmetrics_snapshot\nhealing_stats\nresolution_diagnostics\nbarrel_reports\nincremental_status\nserve_once\nserve_loop\nmonitor_loop\ndaemon_ctl\ndaemon_command\ndispatch\nCLI_COMMANDS\nensure_root","reason":"wiki current (claude/windows-verify pending)","source_content_hash":"sha256:9efaf60e86ecdf6e35d02d12248c1e86a74a628b6fba376169ba1e8c52c3ec0a","source_mtime_ns":"1793855932520145100","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:1959bc30bd0758cbcbef5c2c1da7bf2599b044f8f954f8d9043ca7e794590a7a"},"src/artifacts.rs":{"last_updated":"2026-11-03 08:56:14","public_symbols":"HealthRow\nread_health\nwrite_health\nupsert_health_row\nJournalEntry\njournal_path\nread_journal\nparse_journal\nJournalWrite\nappend_journal\nread_pending\nfolder_card\nnew\nkeep_baseline_from","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:6a7ca10120cbb4a37de62999330a92b181f3ebd7210f92209b280ba5282d8544","source_mtime_ns":"1793678007374624809","status":"🟡 Yellow","wiki_hash":"sha256:75bf0f72cc4bd57b6178dc6a0a58b8bf97bc3e7265485a28102093b967f20181"},"src/cache.rs":{"last_updated":"2026-11-05 05:15:33","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_internal_edges\nload_dependents\nlist_prefix\nall_paths\nFileUpdate\nupsert_file\nremove_file\nindexed_rows\nindex_by_path\nhash_if_unchanged\npayload_bytes\nstat_ns\nedges\nrel_string\nwrite_barrel_record\nprune_barrel_files\nconfidence\nacs_reason","reason":"wiki updated (claude/windows-verify speed)","source_content_hash":"sha256:7bb46530191f358739283873979d56ad30b432343001ddf9c9703ef35fe53a9b","source_mtime_ns":"1793855097333896800","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:b33377afaddb8232e6d4f3fab3628416f8cc7fb5cd6953be940deb89f4cddca6"},"src/cache_commit.rs":{"last_updated":"2026-11-05 05:15:33","public_symbols":"Consistency\nSession\ncheck_consistency\nrebuild\nopen\nknown\ndsts_of\nnote_stat\nrels\nupsert\nremove\nstage_barrel\nhas_changes\ncommit","reason":"wiki updated (claude/windows-verify speed)","source_content_hash":"sha256:f8928344f031e861ee86f07dd981833edb293ac8e010439e9557b25cf0a26f14","source_mtime_ns":"1793853982550113800","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:7ae37e2390ff978ac42640c05cc885d9c86d7aaa879144ed1ef86cc57e9cb3e3"},"src/cycle_report.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:db35b610d9a9f9917805da9383e61872e66e640482f57fc9e17ae91daf77fe3d","status":"🟡 Yellow"},"src/daemon_svc.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:0887baeb3ad7f2fca851fccc0d9335dafd4b42f3e54e236175a13d39c5bcd063","status":"🟡 Yellow"},"src/jsonutil.rs":{"last_updated":"2026-11-03 08:56:14","public_symbols":"Json\nparse\nparse_jsonc\nstrip_json_comments\nobj\nfrom_pairs\nget\nas_str\nas_bool\nas_array\nas_object\ninsert\nfmt","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:cb71e00e352df010928a42323d6d87bbe67348834a209348fc174b9d00489676","source_mtime_ns":"1793684429116993727","status":"🟡 Yellow","wiki_hash":"sha256:fb4cffba8b14a179dd90f201b61df2608ca78a9775e9646e7f011f1b9fa07c63"},"src/legacy_cache.rs":{"last_updated":"2026-11-03 08:56:14","public_symbols":"import_if_empty","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:a127d0b1742d69b736194ca98579f5fc0611fc51a291d691aef6fe586fd68ba3","source_mtime_ns":"1793683981803718735","status":"🟡 Yellow","wiki_hash":"sha256:a6fe6c79185a46cc4b30d215475bab28f517f6e5a6fd7bce6423c2792814aac8"},"src/lib.rs":{"last_updated":"2026-11-03 08:56:14","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":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:9c27c83b58ff7f1acf62d5d19ee31e2070daa06499e27141cc7071fae44e26b8","source_mtime_ns":"1793678007370625780","status":"🟡 Yellow","wiki_hash":"sha256:d7e771a3ff63ab55a7967195e3b403dd75d5ae020a383adc56f0e5c09d7b03a0"},"src/low_conf.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:3ade5fc70ce585a48b9acf6a2b48753e58e80f63281a31c8b8453816700b375e","status":"🟡 Yellow"},"src/main.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:2b3cebdafca3ca3da7831101a63b36f72260b3b2879871e5364b427428e782e0","source_mtime_ns":"1793683990351610198","status":"🟡 Yellow","wiki_hash":"sha256:db6e1cd5beeb90e5a65b3e00a37ee2c73ea4c53750771151e655459a1b823ff5"},"src/maps_out.rs":{"last_updated":"2026-11-05 05:15:33","public_symbols":"current\nwrite\nwrite_status\nread_status","reason":"wiki updated (claude/windows-verify speed)","source_content_hash":"sha256:4223756e3ddd7a7317d364f5de9bbffe1fe59e820b0b8187164bd30449039432","source_mtime_ns":"1793854043821084000","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:5218571835e011f1f33b83b5a57723204910bb8209a5f88a1a2d3a06ee82e30a"},"src/mcp.rs":{"last_updated":"2026-11-05 05:15:47","public_symbols":"tool_schema\nhandle_message\nserve_stdio","reason":"wiki updated (claude/windows-verify speed)","source_content_hash":"sha256:e2c23ee73b46d74c10ede239e9e84b6d23780d3327243fb13a8ab81598fa2206","source_mtime_ns":"1793854176936293700","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:fa2b29df474b116fd21d53019c113eb9171681362d072b6e6db4a2f15b049cf4"},"src/parse.rs":{"last_updated":"2026-11-05 05:15:33","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":"wiki updated (claude/windows-verify speed)","source_content_hash":"sha256:4122cf1ec797e2af0a410dcf13d6cfb3f087410db5dcae27cff7c79cbdd88228","source_mtime_ns":"1793855453252350400","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:3ea6562f6fbb69c3510cba443a994caa2dbab3fae30e290847ec1cd43913e047"},"src/pending_line.rs":{"last_updated":"2026-11-05 05:19:31","public_symbols":"file_of\nformat_item\nEMPTY_NOTE\nappend_item\nremove_item\nrecord_message\nCHECK_MESSAGE","reason":"wiki (claude/windows-verify pending)","source_content_hash":"sha256:f1ad66b1c330ce490b36a72d58cb2a341b0f44b0b5ec2166cd943a83cf694d05","source_mtime_ns":"1793855871596115600","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:3bac5aead2f3a0b020e056fe388d7670f9a3b45d79be75d197f5b71966620f56"},"src/proc.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:8f13798a8b9289d78913becc626056c8b016838570e356e09bfa3333e800351c","status":"🟡 Yellow"},"src/project_lock.rs":{"last_updated":"2026-11-03 08:56:14","public_symbols":"ProjectLock\ndrop\nacquire","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:194e329f35ff59b11fc734b980a1ee33f8e464bcdb5eae899486e487daef3a25","source_mtime_ns":"1793678007370625780","status":"🟡 Yellow","wiki_hash":"sha256:fccc8d547f30262ff64d506d842472623e828ebe39fde71532a683c6956329d2"},"src/scope.rs":{"last_updated":"2026-11-05 05:15:34","public_symbols":"SOURCE_EXTS\nScope\ndetect\nread_path_list\nexcludes\nis_source\ncollect\nrel_of\ndefault_map_roots\nrel_arg\nallows_walk","reason":"wiki updated (claude/windows-verify speed)","source_content_hash":"sha256:0843deef3d82162edf7bac2c53c9a9f716735b4e639451f7fb36620de2dcd059","source_mtime_ns":"1793854073257790700","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:574d4cc8bd261700b1e192628435926d8f1e533f7f1967e27eb6c797e59e6c0e"},"src/sha256.rs":{"last_updated":"2026-11-05 04:14:08","public_symbols":"sha256\nsha256_hex\nsha256_prefixed\ncontent_sha256_prefixed\nfile_sha256_prefixed","reason":"wiki updated (claude/windows-verify)","source_content_hash":"sha256:2f4f8a8ccb9f9d4748fc0678e6bac63dc69ade57e9ab4b3c6eb2a1c43fcf5c14","source_mtime_ns":"1793851995838527400","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:0d1d67e61bed7a20c6a8413a352a55fb87f05bda6e39760015727d128a67ae33"},"src/symbols.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:4a150ba04e9ea26020c874c5e256a9ec5acc3ef4d621fb6ff03b21935c3f8046","status":"🟡 Yellow"},"src/text_out.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:8e0edc1141a2c1745c0e6fdd7a7645c7332365fac532d52fb3e188f4ef06148e","status":"🟡 Yellow"},"src/timeutil.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:9921127144d9d40619e260753e700e9c8a47135ed95b2ad8791afb7fe78afd75","status":"🟡 Yellow"},"src/wiki_files.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:2612539ce9092a3d126d80ce2947f6b4555ddb0bb335769b1619ec67d08a5e84","status":"🟡 Yellow"},"tests/agent_args.rs":{"last_updated":"2026-11-05 04:14:08","reason":"new: agent-style file args, missing args, lean MCP outputs, init roots, CRLF (claude/windows-verify)","source_content_hash":"sha256:ae25dcc233b1d40c48dbc7199e350032717412316089084751b9280970feb66d","status":"🟡 Yellow"},"tests/behaviour_gaps.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Tests for those parity fixes.","source_content_hash":"sha256:8c8d669dae2dbe39b88c2223d20fb2fca51ac1605c62edd108d6ef224223402b","source_mtime_ns":"1793684893298602299","status":"🟡 Yellow","wiki_hash":"sha256:b9d20a310ca48ffd858e6d0d4e49b5e0568ecb57dc5ad981eb54398562864691"},"tests/cache_and_artifacts.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Tests for those parity fixes.","source_content_hash":"sha256:b9483666c1e19f90d4db1af718b08beebadfbcba66152497b7694d8e757a5ec1","status":"🟡 Yellow"},"tests/cli_surface.rs":{"last_updated":"2026-11-05 04:14:08","reason":"launcher exit-code test for wikifier.bat / wikifier.sh (claude/windows-verify)","source_content_hash":"sha256:085faae9e15ace6df3f133f2426ab642f809fe2f1fbc305ae252dcf8a04f57b5","status":"🟡 Yellow"},"tests/contract_gaps.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Tests for those parity fixes.","source_content_hash":"sha256:dc91a239bbc615524b4ac2c58d06515183dfc4981309906003f551e6721d82ae","status":"🟡 Yellow"},"tests/dogfood.rs":{"last_updated":"2026-11-05 05:15:34","reason":"wiki updated (claude/windows-verify speed)","source_content_hash":"sha256:3d0509564036285a918a0a8d720df5ec9fa0d31490c8cf16b49c16570f31f6c5","source_mtime_ns":"1793855180442138300","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:0bc5f3d78d1779ba6302ecc406dc0b25d8beda25f1681a26d57e97210df19906"},"tests/parity.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Tests for those parity fixes.","source_content_hash":"sha256:0246038a1cc01bf66da2fd186ede61f2dcd2c98da9c0c90dd7f3b2cbab1036db","source_mtime_ns":"1793683314773282057","status":"🟡 Yellow","wiki_hash":"sha256:32753c244642df6e08f8578c6203f77225363783c15bc57c33a84f03989f2378"},"tests/parity_fixes.rs":{"last_updated":"2026-11-05 05:15:33","reason":"pending item wording and empty-queue note (claude/windows-verify speed)","source_content_hash":"sha256:b74f37dffb2a5bbcc8724298885a677a9d4476f846791d31770798e0a6caa7a3","status":"🟡 Yellow"},"tests/protocol_v07.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Tests for those parity fixes.","source_content_hash":"sha256:bb7cffecabc3fb51ebcc77c64f7fd78f5cd15c445d7e13493ed491227280f37d","source_mtime_ns":"1793684781738827745","status":"🟡 Yellow","wiki_hash":"sha256:b38f4a13e316cefd56070f822980b4f83d9cdacb850ab1978de48ed35c630ec8"},"tests/robustness.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Tests for those parity fixes.","source_content_hash":"sha256:fad304193a47baa8cce21cff8786b35974c7c7f6f71b3318edeb68bd60121681","status":"🟡 Yellow"},"tests/skip_maps.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Tests for those parity fixes.","source_content_hash":"sha256:2eed0c6bc97d91a4be3f2b6ab08c78c62a3a05725a503496e3df91abad6b2130","status":"🟡 Yellow"},"tests/speed_paths.rs":{"last_updated":"2026-11-05 05:15:33","reason":"new: fast paths give the slow paths' answers (claude/windows-verify speed)","source_content_hash":"sha256:192eff47f96030640416741176835f8020c122ba30723531eec2cde86ea7901a","status":"🟡 Yellow"},"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 382ee70..43990d1 100644 --- a/file_health.md +++ b/file_health.md @@ -4,7 +4,7 @@ |------|--------|--------------|-----------------| | .github/workflows/publish.yml | 🟢 Green | 2026-11-03 09:27:04 | Multi-target release archives and SHA256SUMS (subid=release-5.0.0) | | AGENTS.md | 🟢 Green | 2026-11-03 09:27:04 | Package line is 5.0.0; pip remains Python 4.7 (subid=release-5.0.0) | -| CHANGELOG.md | 🟢 Green | 2026-11-05 04:16:31 | wiki updated (claude/windows-verify) | +| CHANGELOG.md | 🟢 Green | 2026-11-05 05:15:34 | wiki updated (claude/windows-verify speed) | | CLAUDE.md | 🟢 Green | 2026-11-03 09:27:04 | Package line is 5.0.0 (subid=release-5.0.0) | | CONTRIBUTING.md | 🟢 Green | 2026-11-03 05:34:35 | Address the 3 Nov review findings (subid=review-2026-11-03) | | Cargo.lock | 🟢 Green | 2026-11-03 09:27:04 | Lockfile version 5.0.0 (subid=release-5.0.0) | @@ -18,11 +18,11 @@ | 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) | | scripts/benchmark_parity.sh | 🟡 Yellow | 2026-11-03 08:56:15 | Benchmark script and measured results. | | scripts/benchmark_results.md | 🟡 Yellow | 2026-11-03 08:56:15 | Benchmark script and measured results. | -| skills/run.md | 🟢 Green | 2026-11-05 04:16:31 | wiki updated (claude/windows-verify) | -| src/api.rs | 🟢 Green | 2026-11-05 04:14:15 | wiki updated (claude/windows-verify) | +| skills/run.md | 🟢 Green | 2026-11-05 05:15:34 | wiki updated (claude/windows-verify speed) | +| src/api.rs | 🟢 Green | 2026-11-05 05:19:31 | wiki current (claude/windows-verify pending) | | src/artifacts.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | -| src/cache.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | -| src/cache_commit.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | +| src/cache.rs | 🟢 Green | 2026-11-05 05:15:33 | wiki updated (claude/windows-verify speed) | +| src/cache_commit.rs | 🟢 Green | 2026-11-05 05:15:33 | wiki updated (claude/windows-verify speed) | | src/cycle_report.rs | 🟡 Yellow | 2026-11-03 08:56:15 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | | src/daemon_svc.rs | 🟡 Yellow | 2026-11-03 08:56:15 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | | src/jsonutil.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | @@ -30,13 +30,13 @@ | src/lib.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | | src/low_conf.rs | 🟡 Yellow | 2026-11-03 08:56:15 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | | src/main.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | -| src/maps_out.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | -| src/mcp.rs | 🟢 Green | 2026-11-05 04:14:09 | wiki updated (claude/windows-verify) | -| src/parse.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | -| src/pending_line.rs | 🟡 Yellow | 2026-11-03 08:56:15 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | +| src/maps_out.rs | 🟢 Green | 2026-11-05 05:15:33 | wiki updated (claude/windows-verify speed) | +| src/mcp.rs | 🟢 Green | 2026-11-05 05:15:47 | wiki updated (claude/windows-verify speed) | +| src/parse.rs | 🟢 Green | 2026-11-05 05:15:33 | wiki updated (claude/windows-verify speed) | +| src/pending_line.rs | 🟢 Green | 2026-11-05 05:19:31 | wiki (claude/windows-verify pending) | | src/proc.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | | src/project_lock.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | -| src/scope.rs | 🟢 Green | 2026-11-05 04:14:08 | wiki updated (claude/windows-verify) | +| src/scope.rs | 🟢 Green | 2026-11-05 05:15:34 | wiki updated (claude/windows-verify speed) | | src/sha256.rs | 🟢 Green | 2026-11-05 04:14:08 | wiki updated (claude/windows-verify) | | src/symbols.rs | 🟡 Yellow | 2026-11-03 08:56:15 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | | src/text_out.rs | 🟡 Yellow | 2026-11-03 08:56:15 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | @@ -47,10 +47,11 @@ | tests/cache_and_artifacts.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Tests for those parity fixes. | | tests/cli_surface.rs | 🟡 Yellow | 2026-11-05 04:14:08 | launcher exit-code test for wikifier.bat / wikifier.sh (claude/windows-verify) | | tests/contract_gaps.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Tests for those parity fixes. | -| tests/dogfood.rs | 🟢 Green | 2026-11-05 04:14:09 | wiki updated (claude/windows-verify) | +| tests/dogfood.rs | 🟢 Green | 2026-11-05 05:15:34 | wiki updated (claude/windows-verify speed) | | tests/parity.rs | 🟡 Yellow | 2026-11-03 08:56:15 | Tests for those parity fixes. | -| tests/parity_fixes.rs | 🟡 Yellow | 2026-11-03 08:56:15 | Tests for those parity fixes. | +| tests/parity_fixes.rs | 🟡 Yellow | 2026-11-05 05:15:33 | pending item wording and empty-queue note (claude/windows-verify speed) | | tests/protocol_v07.rs | 🟡 Yellow | 2026-11-03 08:56:15 | Tests for those parity fixes. | | tests/robustness.rs | 🟡 Yellow | 2026-11-03 08:56:15 | Tests for those parity fixes. | | tests/skip_maps.rs | 🟡 Yellow | 2026-11-03 08:56:15 | Tests for those parity fixes. | +| tests/speed_paths.rs | 🟡 Yellow | 2026-11-05 05:15:33 | new: fast paths give the slow paths' answers (claude/windows-verify speed) | | wikifier.sh | 🟢 Green | 2026-11-03 08:06:46 | Wiki describes the Rust binary launcher (subid=protocol-v07) | diff --git a/journal/2026/11/05.md b/journal/2026/11/05.md index 8796dbe..0807fb4 100644 --- a/journal/2026/11/05.md +++ b/journal/2026/11/05.md @@ -62,3 +62,107 @@ **File:** CHANGELOG.md **Reason:** Unreleased: Windows fixes, agent-arg errors, lean MCP outputs, init map roots (claude/windows-verify) +## [2026-11-05 05:15:32] check-changes +**File:** CHANGELOG.md +**Reason:** content changed since last mark-green (check-changes) + +## [2026-11-05 05:15:32] check-changes +**File:** skills/run.md +**Reason:** content changed since last mark-green (check-changes) + +## [2026-11-05 05:15:32] check-changes +**File:** src/api.rs +**Reason:** content changed since last mark-green (check-changes) + +## [2026-11-05 05:15:32] check-changes +**File:** src/cache.rs +**Reason:** content changed since last mark-green (check-changes) + +## [2026-11-05 05:15:32] check-changes +**File:** src/cache_commit.rs +**Reason:** content changed since last mark-green (check-changes) + +## [2026-11-05 05:15:32] check-changes +**File:** src/maps_out.rs +**Reason:** content changed since last mark-green (check-changes) + +## [2026-11-05 05:15:32] check-changes +**File:** src/mcp.rs +**Reason:** content changed since last mark-green (check-changes) + +## [2026-11-05 05:15:32] check-changes +**File:** src/parse.rs +**Reason:** content changed since last mark-green (check-changes) + +## [2026-11-05 05:15:32] check-changes +**File:** src/pending_line.rs +**Reason:** content changed since last mark-green (check-changes) + +## [2026-11-05 05:15:32] check-changes +**File:** src/scope.rs +**Reason:** content changed since last mark-green (check-changes) + +## [2026-11-05 05:15:32] check-changes +**File:** tests/dogfood.rs +**Reason:** content changed since last mark-green (check-changes) + +## [2026-11-05 05:15:32] check-changes +**File:** tests/parity_fixes.rs +**Reason:** content changed since last mark-green (check-changes) + +## [2026-11-05 05:15:33] record-change +**File:** src/cache.rs +**Reason:** parallel batched atomic writes; edges from index.tsv; load_internal_edges; stat-first hash helpers (claude/windows-verify speed) + +## [2026-11-05 05:15:33] record-change +**File:** src/cache_commit.rs +**Reason:** commit and rebuild write forward and by_dst files as two parallel batches; has_changes; index_targets (claude/windows-verify speed) + +## [2026-11-05 05:15:33] record-change +**File:** src/maps_out.rs +**Reason:** library from by_src only; current() lets update-maps skip unchanged artifacts (claude/windows-verify speed) + +## [2026-11-05 05:15:33] record-change +**File:** src/parse.rs +**Reason:** reuse scans in the second resolution pass; substring line_of with per-spec memo; sorted scans built once (claude/windows-verify speed) + +## [2026-11-05 05:15:33] record-change +**File:** src/api.rs +**Reason:** stat-first check_changes and seeding, lazy cycle edge flags, skip map artifacts when cache unchanged, bootstrap limit, pending removal via pending_line (claude/windows-verify speed) + +## [2026-11-05 05:15:33] record-change +**File:** src/mcp.rs +**Reason:** session_bootstrap default files cap 100 over MCP; limit schema text (claude/windows-verify speed) + +## [2026-11-05 05:15:33] record-change +**File:** src/scope.rs +**Reason:** walk uses directory entry file types (claude/windows-verify speed) + +## [2026-11-05 05:15:33] record-change +**File:** src/pending_line.rs +**Reason:** EMPTY_NOTE dropped/restored with items; record message names the next step (claude/windows-verify speed) + +## [2026-11-05 05:15:33] record-change +**File:** tests/speed_paths.rs +**Reason:** new: fast paths give the slow paths' answers (claude/windows-verify speed) + +## [2026-11-05 05:15:33] record-change +**File:** tests/parity_fixes.rs +**Reason:** pending item wording and empty-queue note (claude/windows-verify speed) + +## [2026-11-05 05:15:33] record-change +**File:** tests/dogfood.rs +**Reason:** speed_paths.rs is a new api dependent (claude/windows-verify speed) + +## [2026-11-05 05:15:33] record-change +**File:** skills/run.md +**Reason:** document stat-first check, unchanged-map artifacts, pending wording, bootstrap limit (claude/windows-verify speed) + +## [2026-11-05 05:15:33] record-change +**File:** CHANGELOG.md +**Reason:** Unreleased: performance section with before/after timings, pending fixes (claude/windows-verify speed) + +## [2026-11-05 05:19:31] record-change +**File:** src/api.rs +**Reason:** pending removal via pending_line::remove_item; init uses EMPTY_NOTE (claude/windows-verify pending) + diff --git a/library.md b/library.md index 99616dd..f7af883 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: 34. Edges: 101. +Files: 35. Edges: 106. ## File Tree @@ -41,6 +41,7 @@ tests/parity_fixes.rs tests/protocol_v07.rs tests/robustness.rs tests/skip_maps.rs +tests/speed_paths.rs ``` ## Dependencies @@ -129,7 +130,7 @@ flowchart LR n26["tests/cli_surface.rs"] --> n19["src/lib.rs"] ``` -Mermaid shows the first 80 edges of 101. +Mermaid shows the first 80 edges of 106. | Source | Depends on | |--------|------------| @@ -159,6 +160,7 @@ Mermaid shows the first 80 edges of 101. | 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 | +| tests/speed_paths.rs | src/api.rs, src/cache.rs, src/lib.rs, src/mcp.rs, src/sha256.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/pending_updates.md b/pending_updates.md index ce18444..3f713ac 100644 --- a/pending_updates.md +++ b/pending_updates.md @@ -1,15 +1,10 @@ # Pending Updates -(no pending items — run check-changes after making edits) - src/artifacts.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. -- src/cache.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. -- src/cache_commit.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. - src/jsonutil.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. - src/legacy_cache.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. - src/lib.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. - src/main.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. -- src/maps_out.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. -- src/parse.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. - src/proc.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. - src/project_lock.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. - src/timeutil.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. @@ -26,9 +21,9 @@ - src/cycle_report.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. - src/daemon_svc.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. - src/low_conf.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. -- src/pending_line.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. - src/symbols.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. - src/text_out.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. - src/wiki_files.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. - tests/parity_fixes.rs: LLM/agent edit — Tests for those parity fixes. - tests/agent_args.rs: LLM/agent edit — new: agent-style file args, missing args, lean MCP outputs, init roots, CRLF (claude/windows-verify) +- tests/speed_paths.rs: new: fast paths give the slow paths' answers (claude/windows-verify speed) — refresh the wiki, then mark-green From 371644b9205873eeafcdb9f77158ebb3a27ce733 Mon Sep 17 00:00:00 2001 From: IronAdamant <18153828+IronAdamant@users.noreply.github.com> Date: Thu, 5 Nov 2026 16:37:37 +1100 Subject: [PATCH 09/11] Stop syncing derived cache files; heal bad ones; sync user state. The wk_cache files are derived and can be rebuilt from the sources, so by_src, by_dst, payload, notes and barrel records are now written through a temp file and a rename (a reader never sees a torn file) but not synced. To make that safe, every commit records each file's length in wk_cache/sizes.tsv. Session::open compares those lengths with one directory listing per kind and reads a file only when no length is recorded (an older cache, or a lost sizes.tsv). A missing, resized or malformed file is a miss: update-maps parses that source again, or rewrites the by_dst record from the index. Lookups that meet a bad record answer from index.tsv in the meantime. Health (json and md), the journal and pending_updates.md were plain overwrites with no sync; they now go through a synced temp file and a rename (artifacts::write_durable). Cold update-maps on the same Specula copy, interleaved runs, machine at ~80% load: median 11.9 s with batched syncs, 12.5 s without. No measurable change on this Windows box, where creating ~2,000 small files (antivirus on create) dominates; the sync cost matters more on Linux and macOS. tests/speed_paths.rs corrupts and truncates cache records, checks the lookups still answer, then checks the next update-maps rebuilds every cache file byte for byte (and catches same-size damage once sizes.tsv is gone). Another test covers the durable user-state writes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CMYkogM3QWVjMLzWKGyjuJ --- src/api.rs | 17 ++-- src/artifacts.rs | 16 ++- src/artifacts.rs.wiki.md | 4 +- src/cache.rs | 128 ++++++++++++++++------- src/cache.rs.wiki.md | 2 +- src/cache_commit.rs | 197 ++++++++++++++++++++++++++++++++++-- src/cache_commit.rs.wiki.md | 4 +- tests/speed_paths.rs | 132 ++++++++++++++++++++++++ 8 files changed, 439 insertions(+), 61 deletions(-) diff --git a/src/api.rs b/src/api.rs index 6aa4e2d..e890350 100644 --- a/src/api.rs +++ b/src/api.rs @@ -317,7 +317,9 @@ pub fn update_maps_run(root: &Path, run: &MapRun) -> Json { seen.insert(rel.clone()); let (mtime, size) = cache::stat_ns(file).unwrap_or((0, 0)); let known = session.known(&rel); + let reparse = session.needs_parse(&rel); let stat_hit = !full + && !reparse && known .as_ref() .is_some_and(|(old_mtime, old_size, old_hash)| { @@ -339,7 +341,7 @@ pub fn update_maps_run(root: &Path, run: &MapRun) -> Json { }; hashed += 1; if let Some((_, _, old_hash)) = &known { - if old_hash == &hash && old_hash.starts_with("sha256:") { + if !reparse && old_hash == &hash && old_hash.starts_with("sha256:") { session.note_stat(&rel, mtime, size); continue; } @@ -1237,7 +1239,7 @@ pub fn check_changes(root: &Path) -> Json { let text = fs::read_to_string(&pending).unwrap_or_default(); let text = crate::pending_line::append_item(&text, &rel, crate::pending_line::CHECK_MESSAGE); - let _ = fs::write(&pending, text); + let _ = artifacts::write_durable(&pending, text.as_bytes()); changed_rows = true; } } @@ -1337,7 +1339,7 @@ pub fn record_change(root: &Path, file: &str, reason: &str) -> Json { let text = fs::read_to_string(&pending).unwrap_or_default(); let text = crate::pending_line::append_item(&text, rel, &crate::pending_line::record_message(reason)); - if fs::write(&pending, text).is_err() { + if artifacts::write_durable(&pending, text.as_bytes()).is_err() { return fail("PendingError", "could not write pending_updates.md"); } ok(vec![ @@ -2356,7 +2358,10 @@ fn drop_pending_line(root: &Path, rel: &str) { { return; } - let _ = fs::write(path, crate::pending_line::remove_item(&text, rel)); + let _ = artifacts::write_durable( + &path, + crate::pending_line::remove_item(&text, rel).as_bytes(), + ); } pub fn heal_stubs(root: &Path, min_len: usize, dry_run: bool) -> Json { @@ -2432,7 +2437,7 @@ pub fn prune_health_opts(root: &Path, deleted_missing: bool) -> Json { out.push('\n'); } } - let _ = fs::write(pending, out); + let _ = artifacts::write_durable(&pending, out.as_bytes()); } ok(vec![ ("changed", Json::Int(changed as i64)), @@ -2467,7 +2472,7 @@ pub fn prune_pending(root: &Path) -> Json { out.push('\n'); } } - let _ = fs::write(path, out); + let _ = artifacts::write_durable(&path, out.as_bytes()); ok(vec![("changed", Json::Int(changed))]) } diff --git a/src/artifacts.rs b/src/artifacts.rs index 0356a6a..6972aa0 100644 --- a/src/artifacts.rs +++ b/src/artifacts.rs @@ -54,6 +54,13 @@ impl HealthRow { } } +/// Replace a user-facing state file (health, journal, pending) atomically and durably: temp +/// file, sync, rename, directory flush. Unlike the derived cache, these hold the agents' +/// recorded reasons and baselines and cannot be rebuilt from the sources. +pub fn write_durable(path: &Path, bytes: &[u8]) -> Result<(), String> { + crate::cache::atomic_write(path, bytes) +} + pub fn read_health(root: &Path) -> Vec { let json_path = root.join("file_health.json"); if json_path.is_file() { @@ -205,7 +212,10 @@ pub fn write_health(root: &Path, rows: &[HealthRow]) -> Result<(), String> { ), ), ]); - fs::write(root.join("file_health.json"), stable.to_string()).map_err(|e| e.to_string())?; + write_durable( + &root.join("file_health.json"), + stable.to_string().as_bytes(), + )?; let mut md = String::from("# Documentation Health Matrix\n\n| File | Status | Last Updated | Reason / Intent |\n|------|--------|--------------|-----------------|\n"); let mut ordered = rows.to_vec(); ordered.sort_by(|a, b| a.file.cmp(&b.file)); @@ -218,7 +228,7 @@ pub fn write_health(root: &Path, rows: &[HealthRow]) -> Result<(), String> { row.reason.replace('|', "/") )); } - fs::write(root.join("file_health.md"), md).map_err(|e| e.to_string())?; + write_durable(&root.join("file_health.md"), md.as_bytes())?; Ok(()) } @@ -332,7 +342,7 @@ pub fn append_journal( let block = format!("## [{stamp}] {action}\n**File:** {file}\n**Reason:** {reason}\n\n"); let mut body = existing; body.push_str(&block); - fs::write(&path, body).map_err(|e| e.to_string())?; + write_durable(&path, body.as_bytes())?; Ok(JournalWrite { stamp, journaled: true, diff --git a/src/artifacts.rs.wiki.md b/src/artifacts.rs.wiki.md index 3439dd5..2614942 100644 --- a/src/artifacts.rs.wiki.md +++ b/src/artifacts.rs.wiki.md @@ -4,6 +4,6 @@ `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`. +`append_journal` skips an identical action, file, and reason inside 120 seconds. `write_durable` replaces health, journal, and pending files through a synced temp file and a rename; `write_health` and `append_journal` use it. `folder_card` reads `.wikifier_staging/maps/_root.json` or a directory `_index.json`. -Public names: `HealthRow`, `new`, `keep_baseline_from`, `read_health`, `write_health`, `upsert_health_row`, `JournalEntry`, `journal_path`, `read_journal`, `parse_journal`, `JournalWrite`, `append_journal`, `read_pending`, `folder_card`. +Public names: `HealthRow`, `new`, `keep_baseline_from`, `read_health`, `write_health`, `upsert_health_row`, `JournalEntry`, `journal_path`, `read_journal`, `parse_journal`, `JournalWrite`, `append_journal`, `read_pending`, `folder_card`, `write_durable`. diff --git a/src/cache.rs b/src/cache.rs index e1066d3..1afdd1a 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -103,14 +103,15 @@ pub(crate) fn names_marker(root: &Path) -> PathBuf { pub(crate) fn atomic_write(path: &Path, bytes: &[u8]) -> Result<(), String> { let parent = path.parent().unwrap_or(Path::new(".")); fs::create_dir_all(parent).map_err(|e| e.to_string())?; - write_replace(path, bytes)?; + write_replace(path, bytes, true)?; fsync_dir(parent); Ok(()) } -/// Write a temp file beside `path`, sync it, and rename it into place. -/// The parent directory must exist; the caller flushes it. -fn write_replace(path: &Path, bytes: &[u8]) -> Result<(), String> { +/// Write a temp file beside `path` and rename it into place, so a reader never sees a +/// torn file. `durable` also syncs the data before the rename and writes the rename +/// through; the parent directory must exist and the caller flushes it. +fn write_replace(path: &Path, bytes: &[u8], durable: bool) -> Result<(), String> { let parent = path.parent().unwrap_or(Path::new(".")); let seq = TEMP_SEQ.fetch_add(1, Ordering::Relaxed); let file_name = path.file_name().and_then(|s| s.to_str()).unwrap_or("cache"); @@ -122,26 +123,27 @@ fn write_replace(path: &Path, bytes: &[u8]) -> Result<(), String> { .open(&tmp) .map_err(|e| e.to_string())?; file.write_all(bytes).map_err(|e| e.to_string())?; - file.sync_all().map_err(|e| e.to_string())?; + if durable { + file.sync_all().map_err(|e| e.to_string())?; + } Ok(()) })(); if let Err(err) = write_result { let _ = fs::remove_file(&tmp); return Err(err); } - if let Err(err) = replace_file(&tmp, path) { + if let Err(err) = replace_file(&tmp, path, durable) { let _ = fs::remove_file(&tmp); return Err(err); } Ok(()) } -/// Write many cache files with the guarantees of [`atomic_write`]: each file is synced and -/// renamed into place, and every parent directory is flushed before this returns. The -/// per-file syncs run on several threads and each directory is flushed once, not once per -/// file. Serial flushes were most of a cold `update-maps` (about 2,100 files on a -/// 587-file workspace). -pub(crate) fn atomic_write_all(entries: &[(PathBuf, Vec)]) -> Result<(), String> { +/// Write many derived cache files: each goes to a temp file and is renamed into place, so a +/// reader never sees a torn file, but nothing is synced. The cache can be rebuilt from the +/// sources, and `Session::open` treats a file whose size or content does not check out as +/// a miss and parses that source again. Writes run on several threads. +pub(crate) fn write_derived_all(entries: &[(PathBuf, Vec)]) -> Result<(), String> { if entries.is_empty() { return Ok(()); } @@ -163,7 +165,7 @@ pub(crate) fn atomic_write_all(entries: &[(PathBuf, Vec)]) -> Result<(), Str .map(|part| { scope.spawn(move || { part.iter() - .try_for_each(|(path, bytes)| write_replace(path, bytes)) + .try_for_each(|(path, bytes)| write_replace(path, bytes, false)) }) }) .collect(); @@ -172,11 +174,7 @@ pub(crate) fn atomic_write_all(entries: &[(PathBuf, Vec)]) -> Result<(), Str .join() .unwrap_or_else(|_| Err("cache writer thread panicked".to_string())) }) - })?; - for dir in &parents { - fsync_dir(dir); - } - Ok(()) + }) } /// The bytes of an edge or notes file: an `@` header line, then the body. @@ -189,20 +187,46 @@ pub(crate) fn labeled(rel: &str, body: &str) -> Vec { text.into_bytes() } -fn replace_file(from: &Path, to: &Path) -> Result<(), String> { +/// Body lines of a labeled cache file written for `rel`: valid UTF-8, no NUL, an +/// `@` header and a final newline. `None` for a torn or foreign file. +pub(crate) fn labeled_lines(bytes: &[u8], rel: &str) -> Option> { + let text = std::str::from_utf8(bytes).ok()?; + let body = text.strip_prefix(&format!("@\t{rel}\n"))?; + if text.contains('\0') || !text.ends_with('\n') { + return None; + } + Some( + body.lines() + .map(|line| line.trim().to_string()) + .filter(|line| !line.is_empty()) + .collect(), + ) +} + +/// Body lines of `rel`'s record of one kind: `Some(empty)` when there is no file, `None` +/// when the file is there but does not check out. +fn record_lines(root: &Path, sub: &str, rel: &str, ext: &str) -> Option> { + match fs::read(read_file(root, sub, rel, ext)) { + Ok(bytes) => labeled_lines(&bytes, rel), + Err(_) => Some(Vec::new()), + } +} + +fn replace_file(from: &Path, to: &Path, durable: bool) -> Result<(), String> { #[cfg(windows)] { - replace_file_windows(from, to) + replace_file_windows(from, to, durable) } #[cfg(not(windows))] { + let _ = durable; fs::rename(from, to) .map_err(|e| format!("rename {} -> {}: {e}", from.display(), to.display())) } } #[cfg(windows)] -fn replace_file_windows(from: &Path, to: &Path) -> Result<(), String> { +fn replace_file_windows(from: &Path, to: &Path, durable: bool) -> Result<(), String> { use std::os::windows::ffi::OsStrExt; const MOVEFILE_REPLACE_EXISTING: u32 = 0x1; const MOVEFILE_WRITE_THROUGH: u32 = 0x8; @@ -221,7 +245,11 @@ fn replace_file_windows(from: &Path, to: &Path) -> Result<(), String> { MoveFileExW( src.as_ptr(), dst.as_ptr(), - MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH, + if durable { + MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH + } else { + MOVEFILE_REPLACE_EXISTING + }, ) }; if ok == 0 { @@ -312,10 +340,9 @@ pub(crate) fn dst_of(line: &str) -> String { } pub fn load_dependencies(root: &Path, rel: &str) -> Result, String> { - let mut dsts: Vec = content_lines(&by_src_path(root, rel)) + let mut dsts: Vec = load_internal_edges(root, rel) .into_iter() - .map(|line| dst_of(&line)) - .filter(|s| !s.is_empty()) + .map(|edge| edge.path) .collect(); dsts.sort(); dsts.dedup(); @@ -403,15 +430,11 @@ pub struct DependencyView { pub unresolved: Vec<(String, i64, String)>, } -fn notes_path(root: &Path, rel: &str) -> PathBuf { - read_file(root, "notes", rel, "txt") -} - pub fn load_dependency_view(root: &Path, rel: &str) -> Result { let internal = load_internal_edges(root, rel); let mut external = Vec::new(); let mut unresolved = Vec::new(); - for line in read_lines(¬es_path(root, rel)) { + for line in record_lines(root, "notes", rel, "txt").unwrap_or_default() { let mut parts = line.split('\t'); match parts.next().unwrap_or("") { "E" => { @@ -446,9 +469,24 @@ pub fn load_dependency_view(root: &Path, rel: &str) -> Result Vec { + let Some(lines) = record_lines(root, "by_src", rel, "edges") else { + return index_dsts(root, rel) + .into_iter() + .map(|path| InternalEdge { + path, + line: 0, + names: Vec::new(), + conditional: false, + dynamic: false, + via_barrel: false, + }) + .collect(); + }; let mut internal = Vec::new(); - for line in content_lines(&by_src_path(root, rel)) { + for line in lines { let mut parts = line.split('\t'); let path = parts.next().unwrap_or("").trim().to_string(); if path.is_empty() { @@ -475,8 +513,26 @@ pub fn load_internal_edges(root: &Path, rel: &str) -> Vec { internal } +/// `rel`'s targets as `index.tsv` records them. +fn index_dsts(root: &Path, rel: &str) -> BTreeSet { + crate::cache_commit::index_targets(root) + .into_iter() + .find(|(src, _)| src == rel) + .and_then(|(_, dsts)| dsts) + .unwrap_or_default() +} + +/// Files that import `rel`. A `by_dst` record that does not check out is a miss: the +/// importers come from the targets in `index.tsv`. pub fn load_dependents(root: &Path, rel: &str) -> Result, String> { - let mut srcs = content_lines(&read_file(root, "by_dst", rel, "edges")); + let mut srcs = match record_lines(root, "by_dst", rel, "edges") { + Some(lines) => lines, + None => crate::cache_commit::index_targets(root) + .into_iter() + .filter(|(_, dsts)| dsts.as_ref().is_some_and(|d| d.contains(rel))) + .map(|(src, _)| src) + .collect(), + }; srcs.sort(); srcs.dedup(); Ok(srcs) @@ -674,7 +730,7 @@ mod tests { use super::*; #[test] - fn atomic_write_all_publishes_every_file_and_leaves_no_temp() { + fn write_derived_all_publishes_every_file_and_leaves_no_temp() { let dir = std::env::temp_dir().join(format!( "wk-batch-{}-{}", std::process::id(), @@ -690,12 +746,12 @@ mod tests { ) }) .collect(); - atomic_write_all(&entries).unwrap(); + write_derived_all(&entries).unwrap(); // A second batch replaces existing files in place. for (i, entry) in entries.iter_mut().enumerate() { entry.1 = format!("second {i}").into_bytes(); } - atomic_write_all(&entries).unwrap(); + write_derived_all(&entries).unwrap(); for (i, (path, _)) in entries.iter().enumerate() { assert_eq!(fs::read_to_string(path).unwrap(), format!("second {i}")); } @@ -707,7 +763,7 @@ mod tests { .collect(); assert!(leftovers.is_empty(), "{sub}: {leftovers:?}"); } - assert!(atomic_write_all(&[]).is_ok()); + assert!(write_derived_all(&[]).is_ok()); let _ = fs::remove_dir_all(dir); } diff --git a/src/cache.rs.wiki.md b/src/cache.rs.wiki.md index 5046703..3c0a2d9 100644 --- a/src/cache.rs.wiki.md +++ b/src/cache.rs.wiki.md @@ -4,7 +4,7 @@ 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`. -`atomic_write` writes a temp file, syncs it, renames it into place, and flushes the directory. `atomic_write_all` does the same for a batch on several threads and flushes each directory once; a commit uses it for the forward files, then for `by_dst`. `edges` reads every file's targets from `index.tsv` (one file), falling back to `by_src` for an index row without targets. `load_internal_edges` reads one `by_src` record without the notes. `index_by_path` and `hash_if_unchanged` give `check-changes` and health seeding the stored content hash when a file's size and mtime still match its index row. +`atomic_write` writes a temp file, syncs it, renames it into place, and flushes the directory (index, generation, `sizes.tsv`, and user state through `artifacts::write_durable`). `write_derived_all` writes a batch of derived files on several threads through a temp file and a rename, without syncing; a commit uses it for the forward files, then for `by_dst`. `labeled_lines` checks a labeled record (UTF-8, no NUL, `@` header, final newline); `load_internal_edges`, `load_dependents`, and the notes read treat a record that fails it as a miss and answer from `index.tsv`. `edges` reads every file's targets from `index.tsv` (one file), falling back to `by_src` for an index row without targets. `load_internal_edges` reads one `by_src` record without the notes. `index_by_path` and `hash_if_unchanged` give `check-changes` and health seeding the stored content hash when a file's size and mtime still match its index row. 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`. diff --git a/src/cache_commit.rs b/src/cache_commit.rs index c0f908b..34eef85 100644 --- a/src/cache_commit.rs +++ b/src/cache_commit.rs @@ -114,6 +114,8 @@ pub struct Session { barrels: Vec<(String, Vec)>, removed: BTreeSet, rewrite_index: bool, + sizes: Sizes, + reparse: BTreeSet, _lock: LockGuard, } @@ -131,17 +133,24 @@ impl Session { rev.entry(dst.clone()).or_default().insert(rel.clone()); } } + let check = check_entries(root, &rows, &rev); + let rewrite_index = rewrite_index + || check.rewrite + || !check.reparse.is_empty() + || !check.stale_dsts.is_empty(); Ok(Session { root: root.to_path_buf(), rows, rev, dirty_src: BTreeMap::new(), - dirty_dst: BTreeSet::new(), + dirty_dst: check.stale_dsts, payloads: BTreeMap::new(), notes: BTreeMap::new(), barrels: Vec::new(), removed: BTreeSet::new(), rewrite_index, + sizes: check.sizes, + reparse: check.reparse, _lock: lock, }) } @@ -303,10 +312,17 @@ impl Session { self.rewrite_index || !self.barrels.is_empty() } - pub fn commit(self) -> Result<(), String> { + /// True when `rel`'s cache files failed the open check, so it must be parsed again + /// even though its size and mtime are unchanged. + pub fn needs_parse(&self, rel: &str) -> bool { + self.reparse.contains(rel) + } + + pub fn commit(mut self) -> Result<(), String> { if !self.has_changes() { return Ok(()); } + let mut sizes = std::mem::take(&mut self.sizes); let gen = read_generation(&self.root).saturating_add(1); cache::atomic_write( &cache::commit_pending_path(&self.root), @@ -315,6 +331,9 @@ impl Session { // by_dst is keyed by the importer's target. Dropping a file removes its // forward edges above; the reverse file for this path stays until importers change. for rel in &self.removed { + for kind in ["by_src", "payload", "notes"] { + sizes.remove(&(kind.to_string(), rel.clone())); + } for (sub, ext) in [ ("payload", "json"), ("by_src", "edges"), @@ -337,12 +356,12 @@ impl Session { } else { format!("{}\n", lines.join("\n")) }; - forward.push(( - cache::store_file(&self.root, "by_src", rel, "edges"), - cache::labeled(rel, &body), - )); + let bytes = cache::labeled(rel, &body); + sizes.insert(("by_src".into(), rel.clone()), bytes.len() as u64); + forward.push((cache::store_file(&self.root, "by_src", rel, "edges"), bytes)); } for (rel, payload) in &self.payloads { + sizes.insert(("payload".into(), rel.clone()), payload.len() as u64); forward.push(( cache::store_file(&self.root, "payload", rel, "json"), payload.as_bytes().to_vec(), @@ -351,8 +370,13 @@ impl Session { for (rel, notes) in &self.notes { let path = cache::store_file(&self.root, "notes", rel, "txt"); match notes { - Some(body) => forward.push((path, cache::labeled(rel, body))), + Some(body) => { + let bytes = cache::labeled(rel, body); + sizes.insert(("notes".into(), rel.clone()), bytes.len() as u64); + forward.push((path, bytes)); + } None => { + sizes.remove(&("notes".to_string(), rel.clone())); let _ = fs::remove_file(&path); } } @@ -378,21 +402,25 @@ impl Session { body.to_string().into_bytes(), )); } - cache::atomic_write_all(&forward)?; + cache::write_derived_all(&forward)?; let mut reverse: Vec<(PathBuf, Vec)> = Vec::new(); for dst in &self.dirty_dst { let path = cache::store_file(&self.root, "by_dst", dst, "edges"); let srcs = self.rev.get(dst).cloned().unwrap_or_default(); if srcs.is_empty() { + sizes.remove(&("by_dst".to_string(), dst.clone())); let _ = fs::remove_file(&path); } else { let lines: Vec = srcs.iter().cloned().collect(); let body = format!("{}\n", lines.join("\n")); - reverse.push((path, cache::labeled(dst, &body))); + let bytes = cache::labeled(dst, &body); + sizes.insert(("by_dst".into(), dst.clone()), bytes.len() as u64); + reverse.push((path, bytes)); } } - cache::atomic_write_all(&reverse)?; + cache::write_derived_all(&reverse)?; save_index(&self.root, &self.rows)?; + save_sizes(&self.root, &sizes)?; cache::atomic_write( &cache::generation_path(&self.root), format!("{gen}\n").as_bytes(), @@ -403,6 +431,151 @@ impl Session { } } +/// Kinds of cache file `Session::open` checks before trusting a row: sub-directory and +/// extension. Barrel records are auxiliary and are not checked. +const CHECKED: &[(&str, &str)] = &[ + ("by_src", "edges"), + ("payload", "json"), + ("notes", "txt"), + ("by_dst", "edges"), +]; + +/// Byte length of each checked cache file, keyed by kind and path. +type Sizes = BTreeMap<(String, String), u64>; + +fn sizes_path(root: &Path) -> PathBuf { + cache::cache_root(root).join("sizes.tsv") +} + +/// The recorded sizes, or `None` when the file is missing or any line is malformed. +fn read_sizes(root: &Path) -> Option { + let text = fs::read_to_string(sizes_path(root)).ok()?; + let mut out = Sizes::new(); + for line in text.lines() { + let mut parts = line.splitn(3, '\t'); + let (Some(kind), Some(len), Some(rel)) = (parts.next(), parts.next(), parts.next()) else { + return None; + }; + if rel.is_empty() || !CHECKED.iter().any(|(k, _)| *k == kind) { + return None; + } + out.insert((kind.to_string(), rel.to_string()), len.parse().ok()?); + } + Some(out) +} + +fn save_sizes(root: &Path, sizes: &Sizes) -> Result<(), String> { + let mut body = String::new(); + for ((kind, rel), len) in sizes { + body.push_str(&format!("{kind}\t{len}\t{rel}\n")); + } + cache::atomic_write(&sizes_path(root), body.as_bytes()) +} + +/// File name to byte length for one cache directory, from a single listing. +fn listed_sizes(dir: &Path) -> HashMap { + let Ok(entries) = fs::read_dir(dir) else { + return HashMap::new(); + }; + entries + .flatten() + .filter_map(|ent| { + let len = ent.metadata().ok()?.len(); + Some((ent.file_name().to_string_lossy().into_owned(), len)) + }) + .collect() +} + +/// What `Session::open` found wrong with the cache files. +struct EntryCheck { + sizes: Sizes, + reparse: BTreeSet, + stale_dsts: BTreeSet, + rewrite: bool, +} + +/// Compare every checked cache file with its recorded size (one directory listing per +/// kind). A file with no recorded size is read and its content checked once. A missing, +/// resized, or malformed file is a miss: its source is parsed again, or its `by_dst` +/// record is rewritten from the index. This is what lets the cache skip fsync. +fn check_entries( + root: &Path, + rows: &BTreeMap, + rev: &BTreeMap>, +) -> EntryCheck { + let recorded = read_sizes(root); + let mut check = EntryCheck { + sizes: Sizes::new(), + reparse: BTreeSet::new(), + stale_dsts: BTreeSet::new(), + rewrite: recorded.is_none(), + }; + let recorded = recorded.unwrap_or_default(); + let listed: HashMap<&str, HashMap> = CHECKED + .iter() + .map(|(kind, _)| (*kind, listed_sizes(&cache::cache_root(root).join(kind)))) + .collect(); + let mut verify = |kind: &str, ext: &str, rel: &str, required: bool| -> bool { + let name = format!("{}.{ext}", cache::enc(rel)); + let actual = listed.get(kind).and_then(|dir| dir.get(&name)).copied(); + let key = (kind.to_string(), rel.to_string()); + if let Some(len) = recorded.get(&key) { + if actual == Some(*len) { + check.sizes.insert(key, *len); + return true; + } + return false; + } + let Some(len) = actual else { + return !required; + }; + let path = cache::store_file(root, kind, rel, ext); + let bytes = fs::read(&path).unwrap_or_default(); + let ok = match kind { + "by_src" => cache::labeled_lines(&bytes, rel).is_some_and(|lines| { + let dsts: BTreeSet = lines.iter().map(|l| cache::dst_of(l)).collect(); + rows.get(rel).is_some_and(|row| row.dsts == dsts) + }), + "notes" => cache::labeled_lines(&bytes, rel).is_some_and(|lines| { + lines + .iter() + .all(|l| l.starts_with("E\t") || l.starts_with("U\t")) + }), + "payload" => std::str::from_utf8(&bytes) + .ok() + .and_then(|text| crate::jsonutil::parse(text).ok()) + .is_some_and(|json| json.get("rel").and_then(|v| v.as_str()) == Some(rel)), + _ => cache::labeled_lines(&bytes, rel).is_some_and(|lines| { + let srcs: BTreeSet = lines.into_iter().collect(); + rev.get(rel).is_some_and(|want| *want == srcs) + }), + }; + if ok { + check.sizes.insert(key, len); + check.rewrite = true; + } + ok + }; + let mut reparse = BTreeSet::new(); + for rel in rows.keys() { + let src_ok = verify("by_src", "edges", rel, true); + let payload_ok = verify("payload", "json", rel, false); + let notes_ok = verify("notes", "txt", rel, false); + if !(src_ok && payload_ok && notes_ok) { + reparse.insert(rel.clone()); + } + } + let mut stale = BTreeSet::new(); + for (dst, srcs) in rev { + if !srcs.is_empty() && !verify("by_dst", "edges", dst, true) { + stale.insert(dst.clone()); + } + } + check.reparse = reparse; + check.stale_dsts = stale; + check +} + fn read_generation(root: &Path) -> u64 { fs::read_to_string(cache::generation_path(root)) .ok() @@ -702,6 +875,8 @@ pub fn rebuild(root: &Path) -> Result<(), String> { } fn rebuild_locked(root: &Path) -> Result<(), String> { + // by_dst is rewritten from scratch; the next open re-reads every file for its size. + let _ = fs::remove_file(sizes_path(root)); let (old, _) = load_rows(root); let names = hash_to_rel(root); let mut orphans = Vec::new(); @@ -738,7 +913,7 @@ fn rebuild_locked(root: &Path) -> Result<(), String> { ) }) .collect(); - cache::atomic_write_all(&reverse)?; + cache::write_derived_all(&reverse)?; let mut rows = BTreeMap::new(); for (rel, dsts) in &forward { let prev = old.get(rel); diff --git a/src/cache_commit.rs.wiki.md b/src/cache_commit.rs.wiki.md index e7cc7ba..22d5810 100644 --- a/src/cache_commit.rs.wiki.md +++ b/src/cache_commit.rs.wiki.md @@ -4,6 +4,6 @@ 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. -Forward files are written as one parallel batch before the `by_dst` batch (`cache::atomic_write_all`). `index_targets` lists each index row's targets for `cache::edges`. +Forward files are written as one parallel batch before the `by_dst` batch (`cache::atomic_write_all`). `index_targets` lists each index row's targets for `cache::edges`. Every commit records each checked file's length in `sizes.tsv`; `Session::open` compares those lengths with one directory listing per kind (`by_src`, `payload`, `notes`, `by_dst`) and reads a file only when no length is recorded. A bad forward file marks its source for re-parsing (`needs_parse`); a bad `by_dst` file is rewritten from the index. `rebuild` deletes `sizes.tsv`. -`Session` methods: `open`, `known`, `dsts_of`, `note_stat`, `rels`, `upsert`, `remove`, `stage_barrel`, `has_changes`, `commit`. `has_changes` is true when `commit` would write. +`Session` methods: `open`, `known`, `dsts_of`, `note_stat`, `rels`, `upsert`, `remove`, `stage_barrel`, `has_changes`, `needs_parse`, `commit`. `has_changes` is true when `commit` would write. diff --git a/tests/speed_paths.rs b/tests/speed_paths.rs index b4ed8f1..a5c5f86 100644 --- a/tests/speed_paths.rs +++ b/tests/speed_paths.rs @@ -254,3 +254,135 @@ fn files(value: &Json) -> usize { .map(|a| a.len()) .unwrap_or_else(|| panic!("{value}")) } + +/// Every cache file except the generation counter, by path under `wk_cache`. +fn cache_snapshot(root: &Path) -> BTreeMap> { + fn walk(dir: &Path, base: &Path, out: &mut BTreeMap>) { + for ent in fs::read_dir(dir).unwrap().flatten() { + let path = ent.path(); + if path.is_dir() { + walk(&path, base, out); + } else { + let rel = path + .strip_prefix(base) + .unwrap() + .to_string_lossy() + .replace('\\', "/"); + if rel != "generation" { + out.insert(rel, fs::read(&path).unwrap()); + } + } + } + } + let base = root.join(".wikifier_staging/wk_cache"); + let mut out = BTreeMap::new(); + walk(&base, &base, &mut out); + out +} + +fn answers(root: &Path) -> Vec { + ["src/pkg/a.py", "src/pkg/b.py", "src/pkg/c.py"] + .iter() + .flat_map(|rel| { + [ + api::dependency_report(root, rel, true).to_string(), + wikifier::load_dependents(root, rel).to_string(), + ] + }) + .chain([api::cycles(root).to_string()]) + .collect() +} + +fn record_of(root: &Path, kind: &str, contains: &str) -> PathBuf { + let dir = root.join(".wikifier_staging/wk_cache").join(kind); + fs::read_dir(&dir) + .unwrap() + .flatten() + .map(|ent| ent.path()) + .find(|path| String::from_utf8_lossy(&fs::read(path).unwrap()).contains(contains)) + .unwrap_or_else(|| panic!("no {kind} record mentioning {contains}")) +} + +#[test] +fn corrupt_and_truncated_cache_files_are_rebuilt_identically() { + let root = cyclic_project("heal"); + assert!(root.join(".wikifier_staging/wk_cache/sizes.tsv").is_file()); + let before = cache_snapshot(&root); + let answered = answers(&root); + + // Garbage of another length in a forward record, and a truncated reverse record. + let by_src = record_of(&root, "by_src", "@\tsrc/pkg/a.py\n"); + fs::write(&by_src, b"\0\0garbage").unwrap(); + let by_dst = record_of(&root, "by_dst", "@\tsrc/pkg/a.py\n"); + let bytes = fs::read(&by_dst).unwrap(); + fs::write(&by_dst, &bytes[..bytes.len() / 2]).unwrap(); + + // Lookups treat a bad record as a miss and answer from index.tsv. + assert_eq!( + cache::load_dependencies(&root, "src/pkg/a.py").unwrap(), + vec!["src/pkg/b.py".to_string()] + ); + let importers = cache::load_dependents(&root, "src/pkg/a.py").unwrap(); + assert_eq!( + importers, + vec!["src/pkg/b.py".to_string(), "src/pkg/c.py".to_string()] + ); + + let healed = update_maps(&root); + assert_eq!(healed.get("files_parsed"), Some(&Json::Int(1)), "{healed}"); + assert_eq!(cache_snapshot(&root), before); + assert_eq!(answers(&root), answered); + + // Same-size damage is caught by content once the recorded sizes are gone too. + let notes_free = record_of(&root, "by_src", "@\tsrc/pkg/b.py\n"); + let len = fs::read(¬es_free).unwrap().len(); + fs::write(¬es_free, vec![0u8; len]).unwrap(); + fs::remove_file(root.join(".wikifier_staging/wk_cache/sizes.tsv")).unwrap(); + update_maps(&root); + assert_eq!(cache_snapshot(&root), before); + assert_eq!(answers(&root), answered); + let _ = fs::remove_dir_all(root); +} + +/// Health, journal, and pending stay durable: each write replaces the whole file through a +/// synced temp file, and no temp file is left beside it. +#[test] +fn user_state_is_replaced_whole_and_leaves_no_temp_files() { + let root = cyclic_project("durable"); + let recorded = wikifier::record_change(&root, "src/pkg/a.py", "durable write (speed-2)"); + assert_eq!(ok(&recorded), Some(true), "{recorded}"); + let pending = fs::read_to_string(root.join("pending_updates.md")).unwrap(); + assert!( + pending.contains("- src/pkg/a.py: durable write (speed-2)"), + "{pending}" + ); + assert!(read_health(&root) + .iter() + .any(|row| row.file == "src/pkg/a.py" && row.reason == "durable write (speed-2)")); + let journal = wikifier::read_journal(&root, None); + assert!(journal + .iter() + .any(|e| e.reason == "durable write (speed-2)")); + let mut dirs = vec![root.clone()]; + while let Some(dir) = dirs.pop() { + for ent in fs::read_dir(&dir).unwrap().flatten() { + let path = ent.path(); + if path.is_dir() { + dirs.push(path); + } else { + assert!( + !path.to_string_lossy().ends_with(".tmp"), + "left behind: {}", + path.display() + ); + } + } + } + wikifier::artifacts::write_durable(&root.join("pending_updates.md"), b"# Pending Updates\n") + .unwrap(); + assert_eq!( + fs::read_to_string(root.join("pending_updates.md")).unwrap(), + "# Pending Updates\n" + ); + let _ = fs::remove_dir_all(root); +} From 5648fe079c5a50b552934455c3e5ab757c7cced0 Mon Sep 17 00:00:00 2001 From: IronAdamant <18153828+IronAdamant@users.noreply.github.com> Date: Thu, 5 Nov 2026 16:37:37 +1100 Subject: [PATCH 10/11] Release 5.1.0. Bump Cargo.toml/Cargo.lock to 5.1.0 and the docs that name the package version (AGENTS.md, CLAUDE.md, skills/run.md, README install snippet). The README benchmark section keeps its 5.0.0 measurement note. CHANGELOG: Unreleased becomes "5.1.0 - 2026-11-05", with a note for MCP clients that session_bootstrap now caps `files` at 100 unless `limit` is passed and update_maps leaves out the edge list unless `edges: true`. README says the same. skills/run.md and CLAUDE.md describe the unsynced derived cache and the durable user state. tests/cli_surface.rs checks those docs name CARGO_PKG_VERSION, so the next bump cannot leave one behind. The binary, tests and release workflow already read the version from Cargo.toml. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CMYkogM3QWVjMLzWKGyjuJ --- AGENTS.md | 2 +- AGENTS.md.wiki.md | 2 +- CHANGELOG.md | 9 ++++++++- CLAUDE.md | 6 +++--- CLAUDE.md.wiki.md | 2 +- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 10 +++++----- skills/run.md | 8 ++++---- skills/run.md.wiki.md | 2 +- tests/cli_surface.rs | 18 ++++++++++++++++++ 11 files changed, 44 insertions(+), 19 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index af87734..6800eb9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # AGENTS.md -Entry point for any coding agent working in this repository, or using Wikifier on another tree. The day-to-day contract is `skills/run.md` (Agent Protocol v0.7, package 5.0.0). Read that file before substantive work. This file says what is live, what is history, and which rules are easy to miss. +Entry point for any coding agent working in this repository, or using Wikifier on another tree. The day-to-day contract is `skills/run.md` (Agent Protocol v0.7, package 5.1.0). Read that file before substantive work. This file says what is live, what is history, and which rules are easy to miss. ## What this program is diff --git a/AGENTS.md.wiki.md b/AGENTS.md.wiki.md index 6a603af..f300bcd 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, package 5.0.0) and names the live Rust binary, the daily loop, and the Python-era records kept under `Findings/archive/`. `pip install wikifier` is the archived Python 4.7 package. Map roots stay `src/` and `tests/`. The import scan stays comment-masked regex plus named re-exports. Rust also resolves `crate::`, `self::`, `super::`, root crate names, and `[workspace] members`, so `src/main.rs` and `tests/*.rs` are dependents of the library modules they import. `mark-green` checks Rust `pub` names, impl methods, and trait-impl items. +It points at `skills/run.md` (protocol v0.7, package 5.1.0) and names the live Rust binary, the daily loop, and the Python-era records kept under `Findings/archive/`. `pip install wikifier` is the archived Python 4.7 package. Map roots stay `src/` and `tests/`. The import scan stays comment-masked regex plus named re-exports. Rust also resolves `crate::`, `self::`, `super::`, root crate names, and `[workspace] members`, so `src/main.rs` and `tests/*.rs` are dependents of the library modules they import. `mark-green` checks Rust `pub` names, impl methods, and trait-impl items. diff --git a/CHANGELOG.md b/CHANGELOG.md index db0ae50..5d35231 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [5.1.0] - 2026-11-05 + +Agent-facing fixes found by driving the MCP server like an agent on real repositories, Windows fixes, and a speed pass. Protocol stays v0.7. + +**MCP clients, note:** `session_bootstrap` and `get_project_status` now list at most 100 files unless the call passes `limit` (`files_total` gives the full count, `limit: 0` lists every file), and `update_maps` leaves out the edge list unless `edges: true`. Orient with `map_index` and `list_paths`. The CLI defaults for both commands are unchanged. + ### Fixed - Windows: `wikifier.bat` passes the binary's exit code through. It read `%ERRORLEVEL%` inside a parenthesized block, so every failure exited 0. - Windows: health baselines hash CRLF as LF (`content_sha256_prefixed`). A `core.autocrlf` checkout no longer flips every Green file to dirty (51 files in this repo). @@ -21,7 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Performance Same outputs: on a 587-file Cargo workspace the cache files, `library.md`, folder cards, health hashes, and the `update-maps`, `cycles`, `dependencies --full`, `dependents`, `check-changes`, and `session-bootstrap` output were byte-identical before and after (CLI, timestamps aside). Warm MCP calls there, median of 5 on Windows 11: `check_changes` 353 → 53 ms, `update_maps` 708 → 50 ms, `get_cycles` 317 → 39 ms, `session_bootstrap` 33 → 16 ms and 21.7 → 4.7 KB. Cold `update-maps` (CLI, runs interleaved on a loaded machine, median of 3) went from 18.7 s to 6.4 s. -- A cache commit writes its files on several threads and flushes each directory once, not once per file. Every file is still synced and renamed into place, and forward files still land before `by_dst`. +- Derived cache files (`by_src`, `by_dst`, `payload`, `notes`, barrel records) are written on several threads through a temp file and a rename, so a reader never sees a torn file, and are no longer synced. `wk_cache/sizes.tsv` records each file's length; `update-maps` compares it with one directory listing per kind and treats a missing, resized, or unreadable file as a miss: the source is parsed again or the `by_dst` record is rewritten from the index. Without `sizes.tsv` every file is read and checked once. Lookups that meet a bad record answer from `index.tsv`. On this Windows machine the sync change made no measurable difference to a cold map (median 11.9 s with batched syncs, 12.5 s without, same copy, interleaved, machine at about 80% load): the remaining cost is creating about 2,000 small files. +- `file_health.json`, `file_health.md`, the journal, and `pending_updates.md` are now replaced through a synced temp file and a rename. They were plain overwrites with no sync. - A full map scans each file once; the second JS/TS/Rust resolution pass reuses those scans. Import line numbers come from one substring search per distinct spec instead of a line-by-line scan per import. JS/TS barrel follow-through builds its sorted scan table once instead of once per imported name. - `cycles` and `dependencies --full` read edges from `index.tsv` instead of one `by_src` file per mapped file, and `cycles` loads edge flags only for files inside a cycle. `library.md` reads `by_src` records without the notes files. - `update-maps` keeps `library.md` and the folder cards when the cache commit changed nothing. diff --git a/CLAUDE.md b/CLAUDE.md index 10137f5..4d2c9d3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Wikifier is a **zero-dependency, agent-to-agent codebase wiki**: it gives AI agents a token-efficient, queryable map of a codebase (health matrix, dependency graph, per-file wiki summaries) so they can look things up instead of re-reading full sources. The human layer (`index.html` dashboard) is strictly secondary — a read-only viewer of the same artifacts. It is deliberately NOT a general human documentation tool. -**This repo dogfoods itself.** Wikifier runs on its own source, and you are expected to follow its agent protocol when working here (see below). `AGENTS.md` is the entry point for any coding agent. The authoritative contract is `skills/run.md` (Agent Protocol v0.7, package 5.0.0). Read it before substantive work. +**This repo dogfoods itself.** Wikifier runs on its own source, and you are expected to follow its agent protocol when working here (see below). `AGENTS.md` is the entry point for any coding agent. The authoritative contract is `skills/run.md` (Agent Protocol v0.7, package 5.1.0). Read it before substantive work. **USE WIKIFIER FIRST (non-negotiable in this repo, preferred on any managed target):** At session start call `session_bootstrap` (MCP or `wikifier session-bootstrap`). Prefer Core daily tools (`list_core_tools`): bootstrap, `check_changes`, `prepare_edit`, `suggest_next_actions` (json `actions[]`), `record_change`, `mark_green`. Look up via map/wiki/deps before re-reading full sources. Selective work only (🔴 / actionable 🟡). Never skip `record_change` after edits. @@ -56,12 +56,12 @@ wikifier mcp # MCP server on stdio ## Architecture -Pipeline: **scan → parse → resolve → cache → health → artifacts**, implemented in Rust under `src/` (version in `Cargo.toml`, currently **5.0.0**). The Python package is no longer in this repository. `pip install wikifier` still installs Python 4.7. CLI (`src/main.rs`) and MCP (`src/mcp.rs`) both call `src/api.rs`. Every function returns a JSON object with `success`. Operational failures return `{"success": false, "error", "error_type"}`. +Pipeline: **scan → parse → resolve → cache → health → artifacts**, implemented in Rust under `src/` (version in `Cargo.toml`, currently **5.1.0**). The Python package is no longer in this repository. `pip install wikifier` still installs Python 4.7. CLI (`src/main.rs`) and MCP (`src/mcp.rs`) both call `src/api.rs`. Every function returns a JSON object with `success`. Operational failures return `{"success": false, "error", "error_type"}`. - `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`. JS/TS resolution reads `tsconfig.json` `paths`, `package.json` `exports`, and npm `workspaces`. Rust resolves `mod`, `crate::`, `self::`, `super::`, and package or lib names from the root `Cargo.toml` and `[workspace] members` (`-` to `_`, globs included). `crate::` stays in the file's crate; a member name points at that member's library. This is not a CPython `ast` parser. 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/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. Cache files are derived: they are renamed into place but not synced, `wk_cache/sizes.tsv` records their lengths, and a missing, resized, or malformed file is a miss that the next `update-maps` re-parses. Health, journal, and pending writes are synced. `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`. - `src/project_lock.rs` — advisory lock `.wikifier_staging/.lock`, taken before the cache lock, re-entrant per thread, 30 second wait, stolen when the recorded pid is dead. diff --git a/CLAUDE.md.wiki.md b/CLAUDE.md.wiki.md index d9a3c6b..de3bfb6 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. The Python-era plan logs live under `Findings/archive/`. It points agents at `AGENTS.md` and `skills/run.md` (protocol v0.7, package 5.0.0). 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::`, Cargo.toml crate names, `[workspace] members`, 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` (protocol v0.7, package 5.1.0). 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::`, Cargo.toml crate names, `[workspace] members`, and `rust_public_symbols` for wiki checks. JSON failures use `success: false`. diff --git a/Cargo.lock b/Cargo.lock index 6c8c4a1..f00ad76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,4 +4,4 @@ version = 4 [[package]] name = "wikifier" -version = "5.0.0" +version = "5.1.0" diff --git a/Cargo.toml b/Cargo.toml index 93878da..5c8fc61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wikifier" -version = "5.0.0" +version = "5.1.0" edition = "2021" description = "Wikifier — zero-dependency agent-to-agent codebase wiki." license = "MIT" diff --git a/README.md b/README.md index e456666..46b94a4 100644 --- a/README.md +++ b/README.md @@ -21,11 +21,11 @@ Download a binary from the [GitHub releases](https://github.com/IronAdamant/wiki Check `SHA256SUMS`, unpack, and put `wikifier` (`wikifier.exe` on Windows) on your `PATH`: ```bash -# Linux x86_64, 5.0.0 -curl -fsSL -O https://github.com/IronAdamant/wikifier/releases/download/v5.0.0/SHA256SUMS -curl -fsSL -O https://github.com/IronAdamant/wikifier/releases/download/v5.0.0/wikifier-5.0.0-x86_64-unknown-linux-gnu.tar.gz +# Linux x86_64, 5.1.0 +curl -fsSL -O https://github.com/IronAdamant/wikifier/releases/download/v5.1.0/SHA256SUMS +curl -fsSL -O https://github.com/IronAdamant/wikifier/releases/download/v5.1.0/wikifier-5.1.0-x86_64-unknown-linux-gnu.tar.gz sha256sum --check --ignore-missing SHA256SUMS -tar -xzf wikifier-5.0.0-x86_64-unknown-linux-gnu.tar.gz +tar -xzf wikifier-5.1.0-x86_64-unknown-linux-gnu.tar.gz ./wikifier --version ``` @@ -58,7 +58,7 @@ Or add it to a project's `.mcp.json`: } ``` -Every tool takes `project_root` (an absolute path). Without it the server uses `WIKIFIER_PROJECT_ROOT`, then its working directory. A call with a missing argument or an unknown file fails with a message that names the fix, and a failed call sets `isError`. Then give the agent the short prompt in [`skills/run.md`](skills/run.md#paste-into-new-sessions). Any agent that can run shell commands can use the CLI instead: the daily tools have twins such as `wikifier session-bootstrap`, `prepare-edit`, and `record-change` that print the same JSON (the mapping is in `skills/run.md`). +Every tool takes `project_root` (an absolute path). Without it the server uses `WIKIFIER_PROJECT_ROOT`, then its working directory. A call with a missing argument or an unknown file fails with a message that names the fix, and a failed call sets `isError`. To keep replies small, MCP `session_bootstrap` lists at most 100 files unless you pass `limit` (`files_total` gives the full count, `limit: 0` lists every file), and `update_maps` leaves out the edge list unless you pass `edges: true`. Then give the agent the short prompt in [`skills/run.md`](skills/run.md#paste-into-new-sessions). Any agent that can run shell commands can use the CLI instead: the daily tools have twins such as `wikifier session-bootstrap`, `prepare-edit`, and `record-change` that print the same JSON (the mapping is in `skills/run.md`). ## Quick start diff --git a/skills/run.md b/skills/run.md index 25bdee0..215e100 100644 --- a/skills/run.md +++ b/skills/run.md @@ -1,6 +1,6 @@ # Wikifier Agent Protocol v0.7 -**Package 5.0.0** (live version: `wikifier --version`, from `Cargo.toml`) · **Status:** active, supersedes v0.6 · **Scope:** agent-to-agent codebase wiki for token-efficient lookup. Not a human docs product, not an issue tracker. +**Package 5.1.0** (live version: `wikifier --version`, from `Cargo.toml`) · **Status:** active, supersedes v0.6 · **Scope:** agent-to-agent codebase wiki for token-efficient lookup. Not a human docs product, not an issue tracker. This file is the contract for any agent operating Wikifier. Part 1 is all you need day to day; Part 2 is reference. In the Wikifier repository, `AGENTS.md` is the entry point and points here. @@ -42,7 +42,7 @@ This file is the contract for any agent operating Wikifier. Part 1 is all you ne ### Paste into new sessions ``` -You are operating inside a Wikifier-managed codebase (Agent Protocol v0.7, package 5.0.0). +You are operating inside a Wikifier-managed codebase (Agent Protocol v0.7, package 5.1.0). It is an agent-to-agent wiki: look things up (health, file wikis, deps) instead of re-reading sources. SELECTIVE WORK: only update 🔴 Red and actionable 🟡 Yellow files; never re-wiki 🟢 Green files; @@ -147,11 +147,11 @@ The implementation is a std-only Rust binary. `wikifier.sh` / `.ps1` / `.bat` la There is no Python library. Agents call the CLI or `wikifier mcp`. The same functions live in the Rust crate (`src/api.rs`) and both fronts call them. -### Implementation (package 5.0.0, protocol stays v0.7) +### Implementation (package 5.1.0, protocol stays v0.7) The program is a std-only Rust binary under `src/`. `Cargo.toml` has an empty `[dependencies]` list. Launchers exec that binary. MCP is `wikifier mcp`. -- `update-maps` writes `library.md` (a file tree, a Mermaid block capped at 80 edges, and the dependency table) and `.wikifier_staging/maps/` (`_root.json` plus one `_index.json` per directory). `--directory`, `--max-files`, and `--stream` apply. A file budget returns `map_complete=false` and `files_remaining_dirty`. The next run stat-skips files already parsed, so the same command resumes. `--stream` prints JSON events on stderr and still persists the map. The result field `parser` is `regex+reexport`. A run that changes nothing in the cache keeps `library.md` and the folder cards as they are. +- `update-maps` writes `library.md` (a file tree, a Mermaid block capped at 80 edges, and the dependency table) and `.wikifier_staging/maps/` (`_root.json` plus one `_index.json` per directory). `--directory`, `--max-files`, and `--stream` apply. A file budget returns `map_complete=false` and `files_remaining_dirty`. The next run stat-skips files already parsed, so the same command resumes. `--stream` prints JSON events on stderr and still persists the map. The result field `parser` is `regex+reexport`. A run that changes nothing in the cache keeps `library.md` and the folder cards as they are. Cache files are derived and are not synced; a missing, resized, or malformed cache file is a miss, and `update_maps` parses that source again (lookups answer from `index.tsv` meanwhile). Health, journal, and pending files are written through a synced temp file. - The cache is `.wikifier_staging/wk_cache`, a text index (payload, `by_src`, `by_dst`, notes, barrels). When that index is empty, an existing `import_cache.sqlite` is imported with the `sqlite3` CLI, and `import_cache.json` is the other fallback. Rows imported this way use hash `sha256:legacy` and mtime 0, so the next `update-maps` re-parses them. If the sqlite file is present and cannot be read, the result includes the warning `sqlite_present_unread`. - A resolved edge line is `dst`, `line`, `names`, then optional flag letters `c` (conditional), `d` (dynamic), and `b` (via barrel). Confidence is assigned when the edge is read: dynamic 0.4, conditional 0.55, via barrel 0.7, static 0.9. ACS reason codes are `dynamic_literal`, `conditional_guard`, `via_barrel`, and `resolved_static`. Unresolved notes use `unresolved_relative` (the specifier starts with `.`), `dynamic_literal`, or `unresolved`. - `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`. diff --git a/skills/run.md.wiki.md b/skills/run.md.wiki.md index 2331443..84b8bd5 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. Wiki verification covers Python, JS/TS, and Rust `pub` items including impl methods and trait-impl items. Rust import names include `[workspace] members`. -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. `init` seeds the map roots it finds. File arguments may use `\` separators. Lookups on an unknown path return `FileNotFound`; an MCP call missing a required argument returns `InvalidArguments`; MCP `update_maps` leaves out the edge list unless `edges: true`. The protocol version stays v0.7. The package line is 5.0.0. The live version command is `wikifier --version` (from `Cargo.toml`). +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. `init` seeds the map roots it finds. File arguments may use `\` separators. Lookups on an unknown path return `FileNotFound`; an MCP call missing a required argument returns `InvalidArguments`; MCP `update_maps` leaves out the edge list unless `edges: true`. The protocol version stays v0.7. The package line is 5.1.0. The live version command is `wikifier --version` (from `Cargo.toml`). diff --git a/tests/cli_surface.rs b/tests/cli_surface.rs index 8c0dbc1..4a30b6f 100644 --- a/tests/cli_surface.rs +++ b/tests/cli_surface.rs @@ -202,3 +202,21 @@ fn launcher_passes_exit_code_through() { fn parse_line(line: &str) -> wikifier::Json { wikifier::jsonutil::parse(line.trim()).unwrap_or_else(|err| panic!("stdio json {err}: {line}")) } + +/// The docs that name a package version name this one. Cargo.toml is the source. +#[test] +fn docs_name_the_cargo_version() { + let here = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let version = env!("CARGO_PKG_VERSION"); + let read = |rel: &str| std::fs::read_to_string(here.join(rel)).unwrap(); + for (rel, needle) in [ + ("skills/run.md", format!("**Package {version}**")), + ("skills/run.md", format!("package {version})")), + ("AGENTS.md", format!("package {version})")), + ("CLAUDE.md", format!("currently **{version}**")), + ("README.md", format!("releases/download/v{version}/")), + ("CHANGELOG.md", format!("## [{version}]")), + ] { + assert!(read(rel).contains(&needle), "{rel} does not say {needle}"); + } +} From ac373e1277af68309277139cc90a632e90582cdd Mon Sep 17 00:00:00 2001 From: IronAdamant <18153828+IronAdamant@users.noreply.github.com> Date: Thu, 5 Nov 2026 16:37:38 +1100 Subject: [PATCH 11/11] Dogfood: record the cache durability change and the 5.1.0 release. Generated by the wikifier binary (record-change, mark-green, update-maps), not edited by hand. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CMYkogM3QWVjMLzWKGyjuJ --- file_health.json | 2 +- file_health.md | 26 +++++------ journal/2026/11/05.md | 104 ++++++++++++++++++++++++++++++++++++++++++ library.md | 10 ++-- pending_updates.md | 1 - 5 files changed, 123 insertions(+), 20 deletions(-) diff --git a/file_health.json b/file_health.json index 984b965..893bbe2 100644 --- a/file_health.json +++ b/file_health.json @@ -1 +1 @@ -{"entries":{".github/workflows/publish.yml":{"last_updated":"2026-11-03 09:27:04","reason":"Multi-target release archives and SHA256SUMS (subid=release-5.0.0)","source_content_hash":"sha256:930170e1eb57afbe2850a0e9c2ac179eb2b5947bf2f74d4292bcd2b0099ab8de","source_mtime_ns":"1793697915725124882","status":"🟢 Green","verification":"no_wiki"},"AGENTS.md":{"last_updated":"2026-11-03 09:27:04","reason":"Package line is 5.0.0; pip remains Python 4.7 (subid=release-5.0.0)","source_content_hash":"sha256:f2db15364d5b8b309d03a37fa123c55eb73103316632d2d0b3418205fea5625e","source_mtime_ns":"1793697832705476376","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:d82ebf68153d48b828a60ddafa7e0f4ed00095141048f3014da7e520d994cde0"},"CHANGELOG.md":{"last_updated":"2026-11-05 05:15:34","reason":"wiki updated (claude/windows-verify speed)","source_content_hash":"sha256:b098df3fdbacefc56f8b59e5b5731b20edabe69e3ebd5a96130b0582bff184cc","source_mtime_ns":"1793855706689418600","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:757601288624e346279cef86383a88cdf7c1c467e7c6538f40c05d475e243de8"},"CLAUDE.md":{"last_updated":"2026-11-03 09:27:04","reason":"Package line is 5.0.0 (subid=release-5.0.0)","source_content_hash":"sha256:2734c85af757bf0b873c3cc5ced86347f9042e37a90976023fbdbe43a6dbeda3","source_mtime_ns":"1793697832749476242","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:8991f81c78306a55befa7ef13876879fc5c22cd58ee6013567db4133afa14d34"},"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"},"Cargo.lock":{"last_updated":"2026-11-03 09:27:04","reason":"Lockfile version 5.0.0 (subid=release-5.0.0)","source_content_hash":"sha256:eb5e5f51e2a4fd584bd48af18328f7eaaae1efc4ee9fb23743d85a34faa8b435","source_mtime_ns":"1793697831949478649","status":"🟢 Green","verification":"no_wiki"},"Cargo.toml":{"last_updated":"2026-11-03 09:27:04","reason":"Version 5.0.0 (subid=release-5.0.0)","source_content_hash":"sha256:d16350a4d796d62eac12d64651ff9e9ae843db94489bfa4fb67c4a657b31b288","source_mtime_ns":"1793697832013478456","status":"🟢 Green","verification":"no_wiki"},"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-05 04:16:31","reason":"wiki updated (claude/windows-verify)","source_content_hash":"sha256:3d3678c13234664aeaf1d923d79f5082bb57da407c22dfa82a38f5bed4799e78","source_mtime_ns":"1793852178767718000","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:45e2581fe9d69ba807dfce7e982c015e23c2702a6c765b83eb9513ef809418da"},"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"},"scripts/benchmark_parity.sh":{"last_updated":"2026-11-03 08:56:15","reason":"Benchmark script and measured results.","source_content_hash":"sha256:b8f72e3245e77b9df17b303293d8cbe4a93cc36023c41b89a794b77b01daef87","status":"🟡 Yellow"},"scripts/benchmark_results.md":{"last_updated":"2026-11-03 08:56:15","reason":"Benchmark script and measured results.","source_content_hash":"sha256:8bf9342fce85f126a37f812b03b6f6997f1fe8e7c5176d30301c1a4a15aeee29","status":"🟡 Yellow"},"skills/run.md":{"last_updated":"2026-11-05 05:15:34","reason":"wiki updated (claude/windows-verify speed)","source_content_hash":"sha256:44e948cd0a958cf3ed76ae3abdf97782c5e965f32714925cf506c79abd452ef8","source_mtime_ns":"1793855566520057200","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:d1f7fb759d196fd6a4b468148f0078915537d78aff1906017b9d855eca5b7200"},"src/api.rs":{"last_updated":"2026-11-05 05:19:31","public_symbols":"MapRun\nVERSION\nMCP_TOOLS\nupdate_maps\nupdate_maps_opts\nupdate_maps_run\nseed_source_hashes\nmapped_edges\nload_dependencies\ndependency_report\nload_dependents\nlist_paths\nlist_paths_at\nsession_bootstrap\nsession_bootstrap_scoped\nsuggest_next_actions\nsuggest_next_scoped\ncheck_changes\nprepare_edit\nrecord_change\nmark_green\nhealth\nhealth_view\nread_health\nread_journal\ncache_status\nvalidate\ncycles\ncycles_view\nverify_wikis\nrecord_changes\nrecord_changes_opts\nrecord_deletion\nheal_stubs\nprune_health\nprune_health_opts\nprune_pending\nstale_wikis\nstale_wikis_in\nprune_barrels\nmetrics_snapshot\nhealing_stats\nresolution_diagnostics\nbarrel_reports\nincremental_status\nserve_once\nserve_loop\nmonitor_loop\ndaemon_ctl\ndaemon_command\ndispatch\nCLI_COMMANDS\nensure_root","reason":"wiki current (claude/windows-verify pending)","source_content_hash":"sha256:9efaf60e86ecdf6e35d02d12248c1e86a74a628b6fba376169ba1e8c52c3ec0a","source_mtime_ns":"1793855932520145100","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:1959bc30bd0758cbcbef5c2c1da7bf2599b044f8f954f8d9043ca7e794590a7a"},"src/artifacts.rs":{"last_updated":"2026-11-03 08:56:14","public_symbols":"HealthRow\nread_health\nwrite_health\nupsert_health_row\nJournalEntry\njournal_path\nread_journal\nparse_journal\nJournalWrite\nappend_journal\nread_pending\nfolder_card\nnew\nkeep_baseline_from","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:6a7ca10120cbb4a37de62999330a92b181f3ebd7210f92209b280ba5282d8544","source_mtime_ns":"1793678007374624809","status":"🟡 Yellow","wiki_hash":"sha256:75bf0f72cc4bd57b6178dc6a0a58b8bf97bc3e7265485a28102093b967f20181"},"src/cache.rs":{"last_updated":"2026-11-05 05:15:33","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_internal_edges\nload_dependents\nlist_prefix\nall_paths\nFileUpdate\nupsert_file\nremove_file\nindexed_rows\nindex_by_path\nhash_if_unchanged\npayload_bytes\nstat_ns\nedges\nrel_string\nwrite_barrel_record\nprune_barrel_files\nconfidence\nacs_reason","reason":"wiki updated (claude/windows-verify speed)","source_content_hash":"sha256:7bb46530191f358739283873979d56ad30b432343001ddf9c9703ef35fe53a9b","source_mtime_ns":"1793855097333896800","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:b33377afaddb8232e6d4f3fab3628416f8cc7fb5cd6953be940deb89f4cddca6"},"src/cache_commit.rs":{"last_updated":"2026-11-05 05:15:33","public_symbols":"Consistency\nSession\ncheck_consistency\nrebuild\nopen\nknown\ndsts_of\nnote_stat\nrels\nupsert\nremove\nstage_barrel\nhas_changes\ncommit","reason":"wiki updated (claude/windows-verify speed)","source_content_hash":"sha256:f8928344f031e861ee86f07dd981833edb293ac8e010439e9557b25cf0a26f14","source_mtime_ns":"1793853982550113800","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:7ae37e2390ff978ac42640c05cc885d9c86d7aaa879144ed1ef86cc57e9cb3e3"},"src/cycle_report.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:db35b610d9a9f9917805da9383e61872e66e640482f57fc9e17ae91daf77fe3d","status":"🟡 Yellow"},"src/daemon_svc.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:0887baeb3ad7f2fca851fccc0d9335dafd4b42f3e54e236175a13d39c5bcd063","status":"🟡 Yellow"},"src/jsonutil.rs":{"last_updated":"2026-11-03 08:56:14","public_symbols":"Json\nparse\nparse_jsonc\nstrip_json_comments\nobj\nfrom_pairs\nget\nas_str\nas_bool\nas_array\nas_object\ninsert\nfmt","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:cb71e00e352df010928a42323d6d87bbe67348834a209348fc174b9d00489676","source_mtime_ns":"1793684429116993727","status":"🟡 Yellow","wiki_hash":"sha256:fb4cffba8b14a179dd90f201b61df2608ca78a9775e9646e7f011f1b9fa07c63"},"src/legacy_cache.rs":{"last_updated":"2026-11-03 08:56:14","public_symbols":"import_if_empty","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:a127d0b1742d69b736194ca98579f5fc0611fc51a291d691aef6fe586fd68ba3","source_mtime_ns":"1793683981803718735","status":"🟡 Yellow","wiki_hash":"sha256:a6fe6c79185a46cc4b30d215475bab28f517f6e5a6fd7bce6423c2792814aac8"},"src/lib.rs":{"last_updated":"2026-11-03 08:56:14","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":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:9c27c83b58ff7f1acf62d5d19ee31e2070daa06499e27141cc7071fae44e26b8","source_mtime_ns":"1793678007370625780","status":"🟡 Yellow","wiki_hash":"sha256:d7e771a3ff63ab55a7967195e3b403dd75d5ae020a383adc56f0e5c09d7b03a0"},"src/low_conf.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:3ade5fc70ce585a48b9acf6a2b48753e58e80f63281a31c8b8453816700b375e","status":"🟡 Yellow"},"src/main.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:2b3cebdafca3ca3da7831101a63b36f72260b3b2879871e5364b427428e782e0","source_mtime_ns":"1793683990351610198","status":"🟡 Yellow","wiki_hash":"sha256:db6e1cd5beeb90e5a65b3e00a37ee2c73ea4c53750771151e655459a1b823ff5"},"src/maps_out.rs":{"last_updated":"2026-11-05 05:15:33","public_symbols":"current\nwrite\nwrite_status\nread_status","reason":"wiki updated (claude/windows-verify speed)","source_content_hash":"sha256:4223756e3ddd7a7317d364f5de9bbffe1fe59e820b0b8187164bd30449039432","source_mtime_ns":"1793854043821084000","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:5218571835e011f1f33b83b5a57723204910bb8209a5f88a1a2d3a06ee82e30a"},"src/mcp.rs":{"last_updated":"2026-11-05 05:15:47","public_symbols":"tool_schema\nhandle_message\nserve_stdio","reason":"wiki updated (claude/windows-verify speed)","source_content_hash":"sha256:e2c23ee73b46d74c10ede239e9e84b6d23780d3327243fb13a8ab81598fa2206","source_mtime_ns":"1793854176936293700","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:fa2b29df474b116fd21d53019c113eb9171681362d072b6e6db4a2f15b049cf4"},"src/parse.rs":{"last_updated":"2026-11-05 05:15:33","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":"wiki updated (claude/windows-verify speed)","source_content_hash":"sha256:4122cf1ec797e2af0a410dcf13d6cfb3f087410db5dcae27cff7c79cbdd88228","source_mtime_ns":"1793855453252350400","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:3ea6562f6fbb69c3510cba443a994caa2dbab3fae30e290847ec1cd43913e047"},"src/pending_line.rs":{"last_updated":"2026-11-05 05:19:31","public_symbols":"file_of\nformat_item\nEMPTY_NOTE\nappend_item\nremove_item\nrecord_message\nCHECK_MESSAGE","reason":"wiki (claude/windows-verify pending)","source_content_hash":"sha256:f1ad66b1c330ce490b36a72d58cb2a341b0f44b0b5ec2166cd943a83cf694d05","source_mtime_ns":"1793855871596115600","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:3bac5aead2f3a0b020e056fe388d7670f9a3b45d79be75d197f5b71966620f56"},"src/proc.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:8f13798a8b9289d78913becc626056c8b016838570e356e09bfa3333e800351c","status":"🟡 Yellow"},"src/project_lock.rs":{"last_updated":"2026-11-03 08:56:14","public_symbols":"ProjectLock\ndrop\nacquire","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:194e329f35ff59b11fc734b980a1ee33f8e464bcdb5eae899486e487daef3a25","source_mtime_ns":"1793678007370625780","status":"🟡 Yellow","wiki_hash":"sha256:fccc8d547f30262ff64d506d842472623e828ebe39fde71532a683c6956329d2"},"src/scope.rs":{"last_updated":"2026-11-05 05:15:34","public_symbols":"SOURCE_EXTS\nScope\ndetect\nread_path_list\nexcludes\nis_source\ncollect\nrel_of\ndefault_map_roots\nrel_arg\nallows_walk","reason":"wiki updated (claude/windows-verify speed)","source_content_hash":"sha256:0843deef3d82162edf7bac2c53c9a9f716735b4e639451f7fb36620de2dcd059","source_mtime_ns":"1793854073257790700","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:574d4cc8bd261700b1e192628435926d8f1e533f7f1967e27eb6c797e59e6c0e"},"src/sha256.rs":{"last_updated":"2026-11-05 04:14:08","public_symbols":"sha256\nsha256_hex\nsha256_prefixed\ncontent_sha256_prefixed\nfile_sha256_prefixed","reason":"wiki updated (claude/windows-verify)","source_content_hash":"sha256:2f4f8a8ccb9f9d4748fc0678e6bac63dc69ade57e9ab4b3c6eb2a1c43fcf5c14","source_mtime_ns":"1793851995838527400","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:0d1d67e61bed7a20c6a8413a352a55fb87f05bda6e39760015727d128a67ae33"},"src/symbols.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:4a150ba04e9ea26020c874c5e256a9ec5acc3ef4d621fb6ff03b21935c3f8046","status":"🟡 Yellow"},"src/text_out.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:8e0edc1141a2c1745c0e6fdd7a7645c7332365fac532d52fb3e188f4ef06148e","status":"🟡 Yellow"},"src/timeutil.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:9921127144d9d40619e260753e700e9c8a47135ed95b2ad8791afb7fe78afd75","status":"🟡 Yellow"},"src/wiki_files.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:2612539ce9092a3d126d80ce2947f6b4555ddb0bb335769b1619ec67d08a5e84","status":"🟡 Yellow"},"tests/agent_args.rs":{"last_updated":"2026-11-05 04:14:08","reason":"new: agent-style file args, missing args, lean MCP outputs, init roots, CRLF (claude/windows-verify)","source_content_hash":"sha256:ae25dcc233b1d40c48dbc7199e350032717412316089084751b9280970feb66d","status":"🟡 Yellow"},"tests/behaviour_gaps.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Tests for those parity fixes.","source_content_hash":"sha256:8c8d669dae2dbe39b88c2223d20fb2fca51ac1605c62edd108d6ef224223402b","source_mtime_ns":"1793684893298602299","status":"🟡 Yellow","wiki_hash":"sha256:b9d20a310ca48ffd858e6d0d4e49b5e0568ecb57dc5ad981eb54398562864691"},"tests/cache_and_artifacts.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Tests for those parity fixes.","source_content_hash":"sha256:b9483666c1e19f90d4db1af718b08beebadfbcba66152497b7694d8e757a5ec1","status":"🟡 Yellow"},"tests/cli_surface.rs":{"last_updated":"2026-11-05 04:14:08","reason":"launcher exit-code test for wikifier.bat / wikifier.sh (claude/windows-verify)","source_content_hash":"sha256:085faae9e15ace6df3f133f2426ab642f809fe2f1fbc305ae252dcf8a04f57b5","status":"🟡 Yellow"},"tests/contract_gaps.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Tests for those parity fixes.","source_content_hash":"sha256:dc91a239bbc615524b4ac2c58d06515183dfc4981309906003f551e6721d82ae","status":"🟡 Yellow"},"tests/dogfood.rs":{"last_updated":"2026-11-05 05:15:34","reason":"wiki updated (claude/windows-verify speed)","source_content_hash":"sha256:3d0509564036285a918a0a8d720df5ec9fa0d31490c8cf16b49c16570f31f6c5","source_mtime_ns":"1793855180442138300","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:0bc5f3d78d1779ba6302ecc406dc0b25d8beda25f1681a26d57e97210df19906"},"tests/parity.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Tests for those parity fixes.","source_content_hash":"sha256:0246038a1cc01bf66da2fd186ede61f2dcd2c98da9c0c90dd7f3b2cbab1036db","source_mtime_ns":"1793683314773282057","status":"🟡 Yellow","wiki_hash":"sha256:32753c244642df6e08f8578c6203f77225363783c15bc57c33a84f03989f2378"},"tests/parity_fixes.rs":{"last_updated":"2026-11-05 05:15:33","reason":"pending item wording and empty-queue note (claude/windows-verify speed)","source_content_hash":"sha256:b74f37dffb2a5bbcc8724298885a677a9d4476f846791d31770798e0a6caa7a3","status":"🟡 Yellow"},"tests/protocol_v07.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Tests for those parity fixes.","source_content_hash":"sha256:bb7cffecabc3fb51ebcc77c64f7fd78f5cd15c445d7e13493ed491227280f37d","source_mtime_ns":"1793684781738827745","status":"🟡 Yellow","wiki_hash":"sha256:b38f4a13e316cefd56070f822980b4f83d9cdacb850ab1978de48ed35c630ec8"},"tests/robustness.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Tests for those parity fixes.","source_content_hash":"sha256:fad304193a47baa8cce21cff8786b35974c7c7f6f71b3318edeb68bd60121681","status":"🟡 Yellow"},"tests/skip_maps.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Tests for those parity fixes.","source_content_hash":"sha256:2eed0c6bc97d91a4be3f2b6ab08c78c62a3a05725a503496e3df91abad6b2130","status":"🟡 Yellow"},"tests/speed_paths.rs":{"last_updated":"2026-11-05 05:15:33","reason":"new: fast paths give the slow paths' answers (claude/windows-verify speed)","source_content_hash":"sha256:192eff47f96030640416741176835f8020c122ba30723531eec2cde86ea7901a","status":"🟡 Yellow"},"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":{".github/workflows/publish.yml":{"last_updated":"2026-11-03 09:27:04","reason":"Multi-target release archives and SHA256SUMS (subid=release-5.0.0)","source_content_hash":"sha256:930170e1eb57afbe2850a0e9c2ac179eb2b5947bf2f74d4292bcd2b0099ab8de","source_mtime_ns":"1793697915725124882","status":"🟢 Green","verification":"no_wiki"},"AGENTS.md":{"last_updated":"2026-11-05 05:37:13","reason":"wiki updated (claude/windows-verify release-5.1.0)","source_content_hash":"sha256:061e89b5c878a724b70961af111a00463bf7825d89ffa36199f670854c492d0b","source_mtime_ns":"1793856741183515200","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:566772a56a5d2658deaf7433304d296a587dacd1974ccac0e51f5311fbfa0eb2"},"CHANGELOG.md":{"last_updated":"2026-11-05 05:37:12","reason":"wiki updated (claude/windows-verify release-5.1.0)","source_content_hash":"sha256:e934ea90eda41f79960d168deb7ea605cef9499f0b00a30ac3acc51ea7ddd1dc","source_mtime_ns":"1793856916335755100","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:757601288624e346279cef86383a88cdf7c1c467e7c6538f40c05d475e243de8"},"CLAUDE.md":{"last_updated":"2026-11-05 05:37:13","reason":"wiki updated (claude/windows-verify release-5.1.0)","source_content_hash":"sha256:cf5400147486bb6de80ce55aa1e40019eaa135d384900fe32d9f4327bbd42a7c","source_mtime_ns":"1793856929650897400","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:b1361a7b9b66eee9ebcc41b71c3535ab4deda7ac10495a352c93f9ceb0b96708"},"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"},"Cargo.lock":{"last_updated":"2026-11-05 05:37:21","reason":"version 5.1.0 (claude/windows-verify release-5.1.0)","source_content_hash":"sha256:268450875f2c013867053718dcf4691bc6bdb6e87986978c66c9c30dd5b57851","source_mtime_ns":"1793856741182515400","status":"🟢 Green","verification":"no_wiki"},"Cargo.toml":{"last_updated":"2026-11-05 05:37:20","reason":"version 5.1.0 (claude/windows-verify release-5.1.0)","source_content_hash":"sha256:696d54c0d35dbe7d4d2bfe8243808ab3e1ca4682decd39714a2b76ae11c2405b","source_mtime_ns":"1793856741181514900","status":"🟢 Green","verification":"no_wiki"},"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-05 05:37:12","reason":"wiki updated (claude/windows-verify release-5.1.0)","source_content_hash":"sha256:842e32d2b29147c076979247ecd481341d592b59c96887e889c00d3a5d9ad270","source_mtime_ns":"1793856929649896000","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:45e2581fe9d69ba807dfce7e982c015e23c2702a6c765b83eb9513ef809418da"},"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"},"scripts/benchmark_parity.sh":{"last_updated":"2026-11-03 08:56:15","reason":"Benchmark script and measured results.","source_content_hash":"sha256:b8f72e3245e77b9df17b303293d8cbe4a93cc36023c41b89a794b77b01daef87","status":"🟡 Yellow"},"scripts/benchmark_results.md":{"last_updated":"2026-11-03 08:56:15","reason":"Benchmark script and measured results.","source_content_hash":"sha256:8bf9342fce85f126a37f812b03b6f6997f1fe8e7c5176d30301c1a4a15aeee29","status":"🟡 Yellow"},"skills/run.md":{"last_updated":"2026-11-05 05:37:13","reason":"wiki updated (claude/windows-verify release-5.1.0)","source_content_hash":"sha256:0b9b5a13be93850438c7fc0e4f5a89c59d7aabcb1984d95e1fc320467ca6f3c5","source_mtime_ns":"1793856929676891500","status":"🟢 Green","verification":"unchecked","wiki_hash":"sha256:32979845d60e04b005050ff669cda3dcaf90a8cb0d2c774bda517b1e11b70a0b"},"src/api.rs":{"last_updated":"2026-11-05 05:37:12","public_symbols":"MapRun\nVERSION\nMCP_TOOLS\nupdate_maps\nupdate_maps_opts\nupdate_maps_run\nseed_source_hashes\nmapped_edges\nload_dependencies\ndependency_report\nload_dependents\nlist_paths\nlist_paths_at\nsession_bootstrap\nsession_bootstrap_scoped\nsuggest_next_actions\nsuggest_next_scoped\ncheck_changes\nprepare_edit\nrecord_change\nmark_green\nhealth\nhealth_view\nread_health\nread_journal\ncache_status\nvalidate\ncycles\ncycles_view\nverify_wikis\nrecord_changes\nrecord_changes_opts\nrecord_deletion\nheal_stubs\nprune_health\nprune_health_opts\nprune_pending\nstale_wikis\nstale_wikis_in\nprune_barrels\nmetrics_snapshot\nhealing_stats\nresolution_diagnostics\nbarrel_reports\nincremental_status\nserve_once\nserve_loop\nmonitor_loop\ndaemon_ctl\ndaemon_command\ndispatch\nCLI_COMMANDS\nensure_root","reason":"wiki updated (claude/windows-verify release-5.1.0)","source_content_hash":"sha256:f5575d808fab6a3334acd6af46c1f926b15156993828a21be86e7335487baab2","source_mtime_ns":"1793856867181743100","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:1959bc30bd0758cbcbef5c2c1da7bf2599b044f8f954f8d9043ca7e794590a7a"},"src/artifacts.rs":{"last_updated":"2026-11-05 05:37:12","public_symbols":"HealthRow\nwrite_durable\nread_health\nwrite_health\nupsert_health_row\nJournalEntry\njournal_path\nread_journal\nparse_journal\nJournalWrite\nappend_journal\nread_pending\nfolder_card\nnew\nkeep_baseline_from","reason":"wiki updated (claude/windows-verify release-5.1.0)","source_content_hash":"sha256:c5989a5fe22edf99afab4b4721fda9f45a12faff19d9f1a97277fcf1003d1ffd","source_mtime_ns":"1793856655049237400","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:751e2374f151dc19c0d20ff447db2ce26b908af28067a7e74ef3a0502ce51bc2"},"src/cache.rs":{"last_updated":"2026-11-05 05:37:12","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_internal_edges\nload_dependents\nlist_prefix\nall_paths\nFileUpdate\nupsert_file\nremove_file\nindexed_rows\nindex_by_path\nhash_if_unchanged\npayload_bytes\nstat_ns\nedges\nrel_string\nwrite_barrel_record\nprune_barrel_files\nconfidence\nacs_reason","reason":"wiki updated (claude/windows-verify release-5.1.0)","source_content_hash":"sha256:d0e30e9031d1259333ebeb7799ad78c1836135fa762197d3512f7193640ed6af","source_mtime_ns":"1793856524124108400","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:a48d3652a05cb7e208651c63045e714c62ba5ea1fd7e19fac3b9df84c1af31de"},"src/cache_commit.rs":{"last_updated":"2026-11-05 05:37:12","public_symbols":"Consistency\nSession\ncheck_consistency\nrebuild\nopen\nknown\ndsts_of\nnote_stat\nrels\nupsert\nremove\nstage_barrel\nhas_changes\nneeds_parse\ncommit","reason":"wiki updated (claude/windows-verify release-5.1.0)","source_content_hash":"sha256:fb1f90a3d27558ed141eba2d8fb408bc76a47b2c6715096dba5e75819809513f","source_mtime_ns":"1793856524408983600","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:60c2ba29a68f5feb2a5dd69b9a99d550c6d288386f2a822811eb1530742bf336"},"src/cycle_report.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:db35b610d9a9f9917805da9383e61872e66e640482f57fc9e17ae91daf77fe3d","status":"🟡 Yellow"},"src/daemon_svc.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:0887baeb3ad7f2fca851fccc0d9335dafd4b42f3e54e236175a13d39c5bcd063","status":"🟡 Yellow"},"src/jsonutil.rs":{"last_updated":"2026-11-03 08:56:14","public_symbols":"Json\nparse\nparse_jsonc\nstrip_json_comments\nobj\nfrom_pairs\nget\nas_str\nas_bool\nas_array\nas_object\ninsert\nfmt","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:cb71e00e352df010928a42323d6d87bbe67348834a209348fc174b9d00489676","source_mtime_ns":"1793684429116993727","status":"🟡 Yellow","wiki_hash":"sha256:fb4cffba8b14a179dd90f201b61df2608ca78a9775e9646e7f011f1b9fa07c63"},"src/legacy_cache.rs":{"last_updated":"2026-11-03 08:56:14","public_symbols":"import_if_empty","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:a127d0b1742d69b736194ca98579f5fc0611fc51a291d691aef6fe586fd68ba3","source_mtime_ns":"1793683981803718735","status":"🟡 Yellow","wiki_hash":"sha256:a6fe6c79185a46cc4b30d215475bab28f517f6e5a6fd7bce6423c2792814aac8"},"src/lib.rs":{"last_updated":"2026-11-03 08:56:14","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":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:9c27c83b58ff7f1acf62d5d19ee31e2070daa06499e27141cc7071fae44e26b8","source_mtime_ns":"1793678007370625780","status":"🟡 Yellow","wiki_hash":"sha256:d7e771a3ff63ab55a7967195e3b403dd75d5ae020a383adc56f0e5c09d7b03a0"},"src/low_conf.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:3ade5fc70ce585a48b9acf6a2b48753e58e80f63281a31c8b8453816700b375e","status":"🟡 Yellow"},"src/main.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:2b3cebdafca3ca3da7831101a63b36f72260b3b2879871e5364b427428e782e0","source_mtime_ns":"1793683990351610198","status":"🟡 Yellow","wiki_hash":"sha256:db6e1cd5beeb90e5a65b3e00a37ee2c73ea4c53750771151e655459a1b823ff5"},"src/maps_out.rs":{"last_updated":"2026-11-05 05:15:33","public_symbols":"current\nwrite\nwrite_status\nread_status","reason":"wiki updated (claude/windows-verify speed)","source_content_hash":"sha256:4223756e3ddd7a7317d364f5de9bbffe1fe59e820b0b8187164bd30449039432","source_mtime_ns":"1793854043821084000","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:5218571835e011f1f33b83b5a57723204910bb8209a5f88a1a2d3a06ee82e30a"},"src/mcp.rs":{"last_updated":"2026-11-05 05:15:47","public_symbols":"tool_schema\nhandle_message\nserve_stdio","reason":"wiki updated (claude/windows-verify speed)","source_content_hash":"sha256:e2c23ee73b46d74c10ede239e9e84b6d23780d3327243fb13a8ab81598fa2206","source_mtime_ns":"1793854176936293700","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:fa2b29df474b116fd21d53019c113eb9171681362d072b6e6db4a2f15b049cf4"},"src/parse.rs":{"last_updated":"2026-11-05 05:15:33","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":"wiki updated (claude/windows-verify speed)","source_content_hash":"sha256:4122cf1ec797e2af0a410dcf13d6cfb3f087410db5dcae27cff7c79cbdd88228","source_mtime_ns":"1793855453252350400","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:3ea6562f6fbb69c3510cba443a994caa2dbab3fae30e290847ec1cd43913e047"},"src/pending_line.rs":{"last_updated":"2026-11-05 05:19:31","public_symbols":"file_of\nformat_item\nEMPTY_NOTE\nappend_item\nremove_item\nrecord_message\nCHECK_MESSAGE","reason":"wiki (claude/windows-verify pending)","source_content_hash":"sha256:f1ad66b1c330ce490b36a72d58cb2a341b0f44b0b5ec2166cd943a83cf694d05","source_mtime_ns":"1793855871596115600","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:3bac5aead2f3a0b020e056fe388d7670f9a3b45d79be75d197f5b71966620f56"},"src/proc.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:8f13798a8b9289d78913becc626056c8b016838570e356e09bfa3333e800351c","status":"🟡 Yellow"},"src/project_lock.rs":{"last_updated":"2026-11-03 08:56:14","public_symbols":"ProjectLock\ndrop\nacquire","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:194e329f35ff59b11fc734b980a1ee33f8e464bcdb5eae899486e487daef3a25","source_mtime_ns":"1793678007370625780","status":"🟡 Yellow","wiki_hash":"sha256:fccc8d547f30262ff64d506d842472623e828ebe39fde71532a683c6956329d2"},"src/scope.rs":{"last_updated":"2026-11-05 05:15:34","public_symbols":"SOURCE_EXTS\nScope\ndetect\nread_path_list\nexcludes\nis_source\ncollect\nrel_of\ndefault_map_roots\nrel_arg\nallows_walk","reason":"wiki updated (claude/windows-verify speed)","source_content_hash":"sha256:0843deef3d82162edf7bac2c53c9a9f716735b4e639451f7fb36620de2dcd059","source_mtime_ns":"1793854073257790700","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:574d4cc8bd261700b1e192628435926d8f1e533f7f1967e27eb6c797e59e6c0e"},"src/sha256.rs":{"last_updated":"2026-11-05 04:14:08","public_symbols":"sha256\nsha256_hex\nsha256_prefixed\ncontent_sha256_prefixed\nfile_sha256_prefixed","reason":"wiki updated (claude/windows-verify)","source_content_hash":"sha256:2f4f8a8ccb9f9d4748fc0678e6bac63dc69ade57e9ab4b3c6eb2a1c43fcf5c14","source_mtime_ns":"1793851995838527400","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:0d1d67e61bed7a20c6a8413a352a55fb87f05bda6e39760015727d128a67ae33"},"src/symbols.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:4a150ba04e9ea26020c874c5e256a9ec5acc3ef4d621fb6ff03b21935c3f8046","status":"🟡 Yellow"},"src/text_out.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:8e0edc1141a2c1745c0e6fdd7a7645c7332365fac532d52fb3e188f4ef06148e","status":"🟡 Yellow"},"src/timeutil.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:9921127144d9d40619e260753e700e9c8a47135ed95b2ad8791afb7fe78afd75","status":"🟡 Yellow"},"src/wiki_files.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.","source_content_hash":"sha256:2612539ce9092a3d126d80ce2947f6b4555ddb0bb335769b1619ec67d08a5e84","status":"🟡 Yellow"},"tests/agent_args.rs":{"last_updated":"2026-11-05 04:14:08","reason":"new: agent-style file args, missing args, lean MCP outputs, init roots, CRLF (claude/windows-verify)","source_content_hash":"sha256:ae25dcc233b1d40c48dbc7199e350032717412316089084751b9280970feb66d","status":"🟡 Yellow"},"tests/behaviour_gaps.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Tests for those parity fixes.","source_content_hash":"sha256:8c8d669dae2dbe39b88c2223d20fb2fca51ac1605c62edd108d6ef224223402b","source_mtime_ns":"1793684893298602299","status":"🟡 Yellow","wiki_hash":"sha256:b9d20a310ca48ffd858e6d0d4e49b5e0568ecb57dc5ad981eb54398562864691"},"tests/cache_and_artifacts.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Tests for those parity fixes.","source_content_hash":"sha256:b9483666c1e19f90d4db1af718b08beebadfbcba66152497b7694d8e757a5ec1","status":"🟡 Yellow"},"tests/cli_surface.rs":{"last_updated":"2026-11-05 05:37:11","reason":"docs name the Cargo version (claude/windows-verify release-5.1.0)","source_content_hash":"sha256:0aef6e3256a524e014971637bad8a928deba528b734995728bfb72ea606730f5","status":"🟡 Yellow"},"tests/contract_gaps.rs":{"last_updated":"2026-11-03 08:56:14","reason":"Tests for those parity fixes.","source_content_hash":"sha256:dc91a239bbc615524b4ac2c58d06515183dfc4981309906003f551e6721d82ae","status":"🟡 Yellow"},"tests/dogfood.rs":{"last_updated":"2026-11-05 05:15:34","reason":"wiki updated (claude/windows-verify speed)","source_content_hash":"sha256:3d0509564036285a918a0a8d720df5ec9fa0d31490c8cf16b49c16570f31f6c5","source_mtime_ns":"1793855180442138300","status":"🟢 Green","verification":"verified","wiki_hash":"sha256:0bc5f3d78d1779ba6302ecc406dc0b25d8beda25f1681a26d57e97210df19906"},"tests/parity.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Tests for those parity fixes.","source_content_hash":"sha256:0246038a1cc01bf66da2fd186ede61f2dcd2c98da9c0c90dd7f3b2cbab1036db","source_mtime_ns":"1793683314773282057","status":"🟡 Yellow","wiki_hash":"sha256:32753c244642df6e08f8578c6203f77225363783c15bc57c33a84f03989f2378"},"tests/parity_fixes.rs":{"last_updated":"2026-11-05 05:15:33","reason":"pending item wording and empty-queue note (claude/windows-verify speed)","source_content_hash":"sha256:b74f37dffb2a5bbcc8724298885a677a9d4476f846791d31770798e0a6caa7a3","status":"🟡 Yellow"},"tests/protocol_v07.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Tests for those parity fixes.","source_content_hash":"sha256:bb7cffecabc3fb51ebcc77c64f7fd78f5cd15c445d7e13493ed491227280f37d","source_mtime_ns":"1793684781738827745","status":"🟡 Yellow","wiki_hash":"sha256:b38f4a13e316cefd56070f822980b4f83d9cdacb850ab1978de48ed35c630ec8"},"tests/robustness.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Tests for those parity fixes.","source_content_hash":"sha256:fad304193a47baa8cce21cff8786b35974c7c7f6f71b3318edeb68bd60121681","status":"🟡 Yellow"},"tests/skip_maps.rs":{"last_updated":"2026-11-03 08:56:15","reason":"Tests for those parity fixes.","source_content_hash":"sha256:2eed0c6bc97d91a4be3f2b6ab08c78c62a3a05725a503496e3df91abad6b2130","status":"🟡 Yellow"},"tests/speed_paths.rs":{"last_updated":"2026-11-05 05:37:11","reason":"self-heal and durable user-state tests (claude/windows-verify release-5.1.0)","source_content_hash":"sha256:a9348e032c3ffd9f6960710b364470cbcbf935f366af604dd2ad62ac4f2a7a9e","status":"🟡 Yellow"},"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 43990d1..0f2c0c1 100644 --- a/file_health.md +++ b/file_health.md @@ -3,26 +3,26 @@ | File | Status | Last Updated | Reason / Intent | |------|--------|--------------|-----------------| | .github/workflows/publish.yml | 🟢 Green | 2026-11-03 09:27:04 | Multi-target release archives and SHA256SUMS (subid=release-5.0.0) | -| AGENTS.md | 🟢 Green | 2026-11-03 09:27:04 | Package line is 5.0.0; pip remains Python 4.7 (subid=release-5.0.0) | -| CHANGELOG.md | 🟢 Green | 2026-11-05 05:15:34 | wiki updated (claude/windows-verify speed) | -| CLAUDE.md | 🟢 Green | 2026-11-03 09:27:04 | Package line is 5.0.0 (subid=release-5.0.0) | +| AGENTS.md | 🟢 Green | 2026-11-05 05:37:13 | wiki updated (claude/windows-verify release-5.1.0) | +| CHANGELOG.md | 🟢 Green | 2026-11-05 05:37:12 | wiki updated (claude/windows-verify release-5.1.0) | +| CLAUDE.md | 🟢 Green | 2026-11-05 05:37:13 | wiki updated (claude/windows-verify release-5.1.0) | | CONTRIBUTING.md | 🟢 Green | 2026-11-03 05:34:35 | Address the 3 Nov review findings (subid=review-2026-11-03) | -| Cargo.lock | 🟢 Green | 2026-11-03 09:27:04 | Lockfile version 5.0.0 (subid=release-5.0.0) | -| Cargo.toml | 🟢 Green | 2026-11-03 09:27:04 | Version 5.0.0 (subid=release-5.0.0) | +| Cargo.lock | 🟢 Green | 2026-11-05 05:37:21 | version 5.1.0 (claude/windows-verify release-5.1.0) | +| Cargo.toml | 🟢 Green | 2026-11-05 05:37:20 | version 5.1.0 (claude/windows-verify release-5.1.0) | | 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-05 04:16:31 | wiki updated (claude/windows-verify) | +| README.md | 🟢 Green | 2026-11-05 05:37:12 | wiki updated (claude/windows-verify release-5.1.0) | | 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) | | scripts/benchmark_parity.sh | 🟡 Yellow | 2026-11-03 08:56:15 | Benchmark script and measured results. | | scripts/benchmark_results.md | 🟡 Yellow | 2026-11-03 08:56:15 | Benchmark script and measured results. | -| skills/run.md | 🟢 Green | 2026-11-05 05:15:34 | wiki updated (claude/windows-verify speed) | -| src/api.rs | 🟢 Green | 2026-11-05 05:19:31 | wiki current (claude/windows-verify pending) | -| src/artifacts.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | -| src/cache.rs | 🟢 Green | 2026-11-05 05:15:33 | wiki updated (claude/windows-verify speed) | -| src/cache_commit.rs | 🟢 Green | 2026-11-05 05:15:33 | wiki updated (claude/windows-verify speed) | +| skills/run.md | 🟢 Green | 2026-11-05 05:37:13 | wiki updated (claude/windows-verify release-5.1.0) | +| src/api.rs | 🟢 Green | 2026-11-05 05:37:12 | wiki updated (claude/windows-verify release-5.1.0) | +| src/artifacts.rs | 🟢 Green | 2026-11-05 05:37:12 | wiki updated (claude/windows-verify release-5.1.0) | +| src/cache.rs | 🟢 Green | 2026-11-05 05:37:12 | wiki updated (claude/windows-verify release-5.1.0) | +| src/cache_commit.rs | 🟢 Green | 2026-11-05 05:37:12 | wiki updated (claude/windows-verify release-5.1.0) | | src/cycle_report.rs | 🟡 Yellow | 2026-11-03 08:56:15 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | | src/daemon_svc.rs | 🟡 Yellow | 2026-11-03 08:56:15 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | | src/jsonutil.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. | @@ -45,7 +45,7 @@ | tests/agent_args.rs | 🟡 Yellow | 2026-11-05 04:14:08 | new: agent-style file args, missing args, lean MCP outputs, init roots, CRLF (claude/windows-verify) | | tests/behaviour_gaps.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Tests for those parity fixes. | | tests/cache_and_artifacts.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Tests for those parity fixes. | -| tests/cli_surface.rs | 🟡 Yellow | 2026-11-05 04:14:08 | launcher exit-code test for wikifier.bat / wikifier.sh (claude/windows-verify) | +| tests/cli_surface.rs | 🟡 Yellow | 2026-11-05 05:37:11 | docs name the Cargo version (claude/windows-verify release-5.1.0) | | tests/contract_gaps.rs | 🟡 Yellow | 2026-11-03 08:56:14 | Tests for those parity fixes. | | tests/dogfood.rs | 🟢 Green | 2026-11-05 05:15:34 | wiki updated (claude/windows-verify speed) | | tests/parity.rs | 🟡 Yellow | 2026-11-03 08:56:15 | Tests for those parity fixes. | @@ -53,5 +53,5 @@ | tests/protocol_v07.rs | 🟡 Yellow | 2026-11-03 08:56:15 | Tests for those parity fixes. | | tests/robustness.rs | 🟡 Yellow | 2026-11-03 08:56:15 | Tests for those parity fixes. | | tests/skip_maps.rs | 🟡 Yellow | 2026-11-03 08:56:15 | Tests for those parity fixes. | -| tests/speed_paths.rs | 🟡 Yellow | 2026-11-05 05:15:33 | new: fast paths give the slow paths' answers (claude/windows-verify speed) | +| tests/speed_paths.rs | 🟡 Yellow | 2026-11-05 05:37:11 | self-heal and durable user-state tests (claude/windows-verify release-5.1.0) | | wikifier.sh | 🟢 Green | 2026-11-03 08:06:46 | Wiki describes the Rust binary launcher (subid=protocol-v07) | diff --git a/journal/2026/11/05.md b/journal/2026/11/05.md index 0807fb4..7634120 100644 --- a/journal/2026/11/05.md +++ b/journal/2026/11/05.md @@ -166,3 +166,107 @@ **File:** src/api.rs **Reason:** pending removal via pending_line::remove_item; init uses EMPTY_NOTE (claude/windows-verify pending) +## [2026-11-05 05:37:10] check-changes +**File:** AGENTS.md +**Reason:** content changed since last mark-green (check-changes) + +## [2026-11-05 05:37:10] check-changes +**File:** CHANGELOG.md +**Reason:** content changed since last mark-green (check-changes) + +## [2026-11-05 05:37:10] check-changes +**File:** CLAUDE.md +**Reason:** content changed since last mark-green (check-changes) + +## [2026-11-05 05:37:10] check-changes +**File:** Cargo.lock +**Reason:** content changed since last mark-green (check-changes) + +## [2026-11-05 05:37:10] check-changes +**File:** Cargo.toml +**Reason:** content changed since last mark-green (check-changes) + +## [2026-11-05 05:37:10] check-changes +**File:** README.md +**Reason:** content changed since last mark-green (check-changes) + +## [2026-11-05 05:37:10] check-changes +**File:** skills/run.md +**Reason:** content changed since last mark-green (check-changes) + +## [2026-11-05 05:37:10] check-changes +**File:** src/api.rs +**Reason:** content changed since last mark-green (check-changes) + +## [2026-11-05 05:37:10] check-changes +**File:** src/artifacts.rs +**Reason:** content changed since last mark-green (check-changes) + +## [2026-11-05 05:37:10] check-changes +**File:** src/cache.rs +**Reason:** content changed since last mark-green (check-changes) + +## [2026-11-05 05:37:10] check-changes +**File:** src/cache_commit.rs +**Reason:** content changed since last mark-green (check-changes) + +## [2026-11-05 05:37:10] check-changes +**File:** tests/cli_surface.rs +**Reason:** content changed since last mark-green (check-changes) + +## [2026-11-05 05:37:10] check-changes +**File:** tests/speed_paths.rs +**Reason:** content changed since last mark-green (check-changes) + +## [2026-11-05 05:37:10] record-change +**File:** src/cache.rs +**Reason:** derived cache writes unsynced with size/content checks and self-heal; user state written durably (claude/windows-verify release-5.1.0) + +## [2026-11-05 05:37:11] record-change +**File:** src/cache_commit.rs +**Reason:** derived cache writes unsynced with size/content checks and self-heal; user state written durably (claude/windows-verify release-5.1.0) + +## [2026-11-05 05:37:11] record-change +**File:** src/api.rs +**Reason:** derived cache writes unsynced with size/content checks and self-heal; user state written durably (claude/windows-verify release-5.1.0) + +## [2026-11-05 05:37:11] record-change +**File:** src/artifacts.rs +**Reason:** derived cache writes unsynced with size/content checks and self-heal; user state written durably (claude/windows-verify release-5.1.0) + +## [2026-11-05 05:37:11] record-change +**File:** tests/speed_paths.rs +**Reason:** self-heal and durable user-state tests (claude/windows-verify release-5.1.0) + +## [2026-11-05 05:37:11] record-change +**File:** tests/cli_surface.rs +**Reason:** docs name the Cargo version (claude/windows-verify release-5.1.0) + +## [2026-11-05 05:37:11] record-change +**File:** README.md +**Reason:** release 5.1.0: version, MCP bootstrap cap callout, cache durability notes (claude/windows-verify release-5.1.0) + +## [2026-11-05 05:37:11] record-change +**File:** CHANGELOG.md +**Reason:** release 5.1.0: version, MCP bootstrap cap callout, cache durability notes (claude/windows-verify release-5.1.0) + +## [2026-11-05 05:37:12] record-change +**File:** skills/run.md +**Reason:** release 5.1.0: version, MCP bootstrap cap callout, cache durability notes (claude/windows-verify release-5.1.0) + +## [2026-11-05 05:37:12] record-change +**File:** AGENTS.md +**Reason:** release 5.1.0: version, MCP bootstrap cap callout, cache durability notes (claude/windows-verify release-5.1.0) + +## [2026-11-05 05:37:12] record-change +**File:** CLAUDE.md +**Reason:** release 5.1.0: version, MCP bootstrap cap callout, cache durability notes (claude/windows-verify release-5.1.0) + +## [2026-11-05 05:37:20] record-change +**File:** Cargo.toml +**Reason:** version 5.1.0 (claude/windows-verify release-5.1.0) + +## [2026-11-05 05:37:21] record-change +**File:** Cargo.lock +**Reason:** version 5.1.0 (claude/windows-verify release-5.1.0) + diff --git a/library.md b/library.md index f7af883..c02c907 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: 35. Edges: 106. +Files: 35. Edges: 108. ## File Tree @@ -65,6 +65,7 @@ flowchart LR n0["src/api.rs"] --> n15["src/symbols.rs"] n0["src/api.rs"] --> n16["src/timeutil.rs"] n0["src/api.rs"] --> n17["src/wiki_files.rs"] + n1["src/artifacts.rs"] --> n2["src/cache.rs"] n1["src/artifacts.rs"] --> n5["src/jsonutil.rs"] n1["src/artifacts.rs"] --> n16["src/timeutil.rs"] n2["src/cache.rs"] --> n18["src/cache_commit.rs"] @@ -127,15 +128,14 @@ flowchart LR n25["tests/cache_and_artifacts.rs"] --> n19["src/lib.rs"] n26["tests/cli_surface.rs"] --> n0["src/api.rs"] n26["tests/cli_surface.rs"] --> n5["src/jsonutil.rs"] - n26["tests/cli_surface.rs"] --> n19["src/lib.rs"] ``` -Mermaid shows the first 80 edges of 106. +Mermaid shows the first 80 edges of 108. | Source | Depends on | |--------|------------| | src/api.rs | src/artifacts.rs, src/cache.rs, src/cycle_report.rs, src/daemon_svc.rs, src/jsonutil.rs, src/legacy_cache.rs, src/low_conf.rs, src/maps_out.rs, src/parse.rs, src/pending_line.rs, src/proc.rs, src/project_lock.rs, src/scope.rs, src/sha256.rs, src/symbols.rs, src/timeutil.rs, src/wiki_files.rs | -| src/artifacts.rs | src/jsonutil.rs, src/timeutil.rs | +| src/artifacts.rs | src/cache.rs, src/jsonutil.rs, src/timeutil.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/cycle_report.rs | src/jsonutil.rs | @@ -160,7 +160,7 @@ Mermaid shows the first 80 edges of 106. | 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 | -| tests/speed_paths.rs | src/api.rs, src/cache.rs, src/lib.rs, src/mcp.rs, src/sha256.rs | +| tests/speed_paths.rs | src/api.rs, src/artifacts.rs, src/cache.rs, src/lib.rs, src/mcp.rs, src/sha256.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/pending_updates.md b/pending_updates.md index 3f713ac..a1570ae 100644 --- a/pending_updates.md +++ b/pending_updates.md @@ -1,6 +1,5 @@ # Pending Updates -- src/artifacts.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. - src/jsonutil.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. - src/legacy_cache.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions. - src/lib.rs: LLM/agent edit — Parity with the Python package: wiki lookup, symbols, daemon, init, flags, MCP, cycles, and low-confidence actions.