wikifier 4.6.2__tar.gz → 4.6.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. {wikifier-4.6.2/wikifier.egg-info → wikifier-4.6.4}/PKG-INFO +39 -19
  2. {wikifier-4.6.2 → wikifier-4.6.4}/README.md +38 -18
  3. {wikifier-4.6.2 → wikifier-4.6.4}/skills/run.md +2 -1
  4. wikifier-4.6.4/tests/test_agent_scale.py +244 -0
  5. wikifier-4.6.4/tests/test_cache_store.py +193 -0
  6. {wikifier-4.6.2 → wikifier-4.6.4}/tests/test_gap_closure.py +5 -2
  7. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/__init__.py +1 -1
  8. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/agent_loop.py +56 -3
  9. wikifier-4.6.4/wikifier/cache_store.py +348 -0
  10. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/cli.py +262 -29
  11. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/import_cache.py +306 -78
  12. wikifier-4.6.4/wikifier/parsers/go_lang.py +164 -0
  13. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/parsers/rust.py +70 -0
  14. {wikifier-4.6.2 → wikifier-4.6.4/wikifier.egg-info}/PKG-INFO +39 -19
  15. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier.egg-info/SOURCES.txt +4 -0
  16. wikifier-4.6.2/wikifier/parsers/go_lang.py +0 -87
  17. {wikifier-4.6.2 → wikifier-4.6.4}/CONTRIBUTING.md +0 -0
  18. {wikifier-4.6.2 → wikifier-4.6.4}/LICENSE +0 -0
  19. {wikifier-4.6.2 → wikifier-4.6.4}/MANIFEST.in +0 -0
  20. {wikifier-4.6.2 → wikifier-4.6.4}/diagnostics.html +0 -0
  21. {wikifier-4.6.2 → wikifier-4.6.4}/docs/spec.md +0 -0
  22. {wikifier-4.6.2 → wikifier-4.6.4}/index.html +0 -0
  23. {wikifier-4.6.2 → wikifier-4.6.4}/pyproject.toml +0 -0
  24. {wikifier-4.6.2 → wikifier-4.6.4}/setup.cfg +0 -0
  25. {wikifier-4.6.2 → wikifier-4.6.4}/tests/test_agent_loop.py +0 -0
  26. {wikifier-4.6.2 → wikifier-4.6.4}/tests/test_barrel_invalidation.py +0 -0
  27. {wikifier-4.6.2 → wikifier-4.6.4}/tests/test_health.py +0 -0
  28. {wikifier-4.6.2 → wikifier-4.6.4}/tests/test_import_cache.py +0 -0
  29. {wikifier-4.6.2 → wikifier-4.6.4}/tests/test_multi_lang_parsers.py +0 -0
  30. {wikifier-4.6.2 → wikifier-4.6.4}/tests/test_parsers.py +0 -0
  31. {wikifier-4.6.2 → wikifier-4.6.4}/tests/test_selftest_wrappers.py +0 -0
  32. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/__main__.py +0 -0
  33. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/contracts.py +0 -0
  34. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/daemon.py +0 -0
  35. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/diagnostics.py +0 -0
  36. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/health.py +0 -0
  37. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/index.html +0 -0
  38. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/library.py +0 -0
  39. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/locking.py +0 -0
  40. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/mcp/__init__.py +0 -0
  41. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/mcp/server.py +0 -0
  42. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/parsers/__init__.py +0 -0
  43. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/parsers/_edge.py +0 -0
  44. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/parsers/bree.py +0 -0
  45. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/parsers/c_cpp.py +0 -0
  46. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/parsers/cdia.py +0 -0
  47. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/parsers/csharp.py +0 -0
  48. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/parsers/java.py +0 -0
  49. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/parsers/javascript.py +0 -0
  50. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/parsers/python.py +0 -0
  51. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/project_root.py +0 -0
  52. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/resolution.py +0 -0
  53. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/scripts/wikifier.bat +0 -0
  54. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/scripts/wikifier.ps1 +0 -0
  55. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/scripts/wikifier.sh +0 -0
  56. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/serve.py +0 -0
  57. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier.egg-info/dependency_links.txt +0 -0
  58. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier.egg-info/entry_points.txt +0 -0
  59. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier.egg-info/requires.txt +0 -0
  60. {wikifier-4.6.2 → wikifier-4.6.4}/wikifier.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wikifier
3
- Version: 4.6.2
3
+ Version: 4.6.4
4
4
  Summary: Zero-dependency agent-to-agent codebase wiki for LLMs and AI agents. Autonomously maintained via record-change and mark-green for token-efficient lookup of files, dependencies, health, and summaries — across tiny scripts to 50k+ monorepos. Optional MCP server with rich tools for agents.
5
5
  Author-email: Aron Amos <aron@example.com>
6
6
  Maintainer: Aron Amos
@@ -84,27 +84,34 @@ Always set an explicit root for external trees: `WIKIFIER_PROJECT_ROOT=/abs/path
84
84
 
85
85
  ## Steady state (only touch what needs it)
86
86
 
87
- Full protocol: [`skills/run.md`](skills/run.md).
87
+ Full protocol: [`skills/run.md`](skills/run.md) (Agent Protocol v0.6 — package **4.6.x**).
88
88
 
89
89
  ```bash
90
- wikifier check-changes # yellow dirty files; red ghosts (missing paths)
91
- # prioritize 🔴 then 🟡 do NOT re-wiki 🟢 Green files
90
+ wikifier session-bootstrap # one-shot: root, health, attention, actions[]
91
+ wikifier check-changes # content-honest dirty; red ghosts (missing paths)
92
+ # prioritize 🔴 then *actionable* 🟡 — do NOT re-wiki 🟢 Green files
93
+ wikifier prepare-edit path/file.py # wiki + status + deps/dependents preflight
92
94
  # ... edit only those sources ...
93
95
  wikifier record-change "path/file.py" "why this changed" # required
94
96
  # ... refresh that file’s wiki summary only ...
95
97
  wikifier mark-green "path/file.py"
96
- wikifier update-maps # only if imports/structure changed
98
+ wikifier update-maps # only if imports/structure changed (warm 0-dirty is cheap)
97
99
  # removals:
98
100
  wikifier record-deletion "path/gone.py" "why removed"
99
101
  ```
100
102
 
101
- MCP **Core 6** (start every session): `get_project_status`, `check_changes`, `get_files_needing_attention`, `get_file_wiki`, `suggest_next_actions`, `record_change` / `mark_green`. Intel as needed: `get_dependencies`, `get_dependents`, `get_cycles`. Always pass `project_root=` for external trees.
103
+ **Core 6** (prefer every session MCP or library/CLI):
104
+ `session_bootstrap` → `check_changes` → `prepare_edit` → `suggest_next_actions` (json `actions[]`) → `record_change` → `mark_green`.
105
+
106
+ Advanced intel as needed: `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).
102
107
 
103
108
  ## What you get
104
109
 
105
- - **Import analysis** — Python, JS/TS (ESM/CJS, barrels), Rust, Go, C/C++ includes, C# usings; per-edge confidence; name-routed barrel expansion for TS/JS
110
+ - **Import analysis** — Python, JS/TS (ESM/CJS, barrels), Rust (`use`/`mod` + best-effort `crate::` paths), Go, C/C++ includes, C# usings; per-edge confidence; barrel expansion for TS/JS
106
111
  - **Incremental pipeline** — pure-Python `update-maps`: dirty parse → import cache → reverse deps → cycles → `library.md`
107
- - **Selective agent work** — health + suggest bias to 🔴/🟡 only; ACS *actionable* low-conf excludes stdlib/external noise
112
+ - **Warm agent maps (4.6.3+ / 4.6.4+)** — zero-dirty fast path skips full graph/`library.md` rewrite; **stdlib SQLite** import cache (dual-read legacy JSON) so warm dirty scans use mtime/hash index not multi‑MB pair JSON; content-hash dirty; `--directory=` scopes walks
113
+ - **Partial-map honesty** — `map_coverage` on `update_maps` / bootstrap (`complete`, `files_remaining_dirty`, `acs_version`, `cache_backend`); budgeted runs are not “done”
114
+ - **Selective agent work** — health + suggest bias to 🔴/actionable 🟡 only; **ACS v1.3** `reason_code` / `agent_signal`; prefer `actionable_low_conf_edges` + reason codes — **never** raw `low_conf_edges` alone
108
115
  - **Scale** — reverse index + barrel invalidation so one edit doesn’t re-scan the monorepo
109
116
  - **MCP tools** — optional server for Claude, Cursor, Cline, and other MCP clients
110
117
  - **Zero core dependencies** — stdlib only; forks can add their own stack on top
@@ -112,32 +119,45 @@ MCP **Core 6** (start every session): `get_project_status`, `check_changes`, `ge
112
119
 
113
120
  ## Performance (measured)
114
121
 
115
- | Project | Scale | Full `update-maps` |
116
- |---------|-------|--------------------|
117
- | llama_index | ~3.8k Python files | ~8.5s |
118
- | Babylon.js | ~3.9k TS files, barrel-heavy | ~4.5 min (scoped re-runs ~80s) |
119
- | Large trees (e.g. LLVM-scale) | tens of thousands of files | candidate scan in seconds |
122
+ Full / heavy runs (historical order-of-magnitude):
123
+
124
+ | Project | Scale | Full / heavy `update-maps` |
125
+ |---------|-------|----------------------------|
126
+ | llama_index | ~3.8k Python files | ~8.5s class full |
127
+ | Babylon.js | ~3.9k TS files, barrel-heavy | minutes full; scoped re-runs tens of seconds |
128
+ | Large trees (e.g. LLVM-scale) | tens of thousands of files | use lean monitor + `--directory` / `--max-files` |
129
+
130
+ Warm **0-dirty** re-runs after 4.6.3 (same machine; scoped where noted) — agent session path:
131
+
132
+ | Project | Scope | Warm2 `update-maps` |
133
+ |---------|-------|---------------------|
134
+ | Wikifier (self) | incremental full | ~43 ms |
135
+ | redox | `src` | ~17 ms |
136
+ | llama_index | `llama-index-core` | ~0.6 s |
137
+ | rust | `library/std/src` (budgeted) | ~0.7 s (vs multi-second full-tree walk before scoped collect) |
120
138
 
121
- Tests: `python -m unittest discover tests` (stdlib only).
139
+ Tests: `python -m unittest discover tests` (stdlib only; 86+ cases including agent-scale edges).
122
140
 
123
141
  ## Commands
124
142
 
125
143
  | Command | Purpose |
126
144
  |---------|---------|
127
145
  | `wikifier init [--target DIR]` | Bootstrap project + human `index.html` |
128
- | `wikifier check-changes` | Incremental scan health / pending |
146
+ | `wikifier session-bootstrap` | Session start: health, attention, dispatchable `actions[]` |
147
+ | `wikifier check-changes` | Content-honest scan → health / pending |
148
+ | `wikifier prepare-edit <file>` | Preflight: status, wiki snippet, deps, dependents |
129
149
  | `wikifier record-change <file> "reason"` | Log *why* (required after edits) |
130
- | `wikifier mark-green <file>` | Mark wiki current |
150
+ | `wikifier mark-green <file>` | Mark wiki current + source content-hash baseline |
131
151
  | `wikifier record-deletion <file> "reason"` | Mark removed paths 🔴 + prune barrel refs |
132
- | `wikifier suggest-next` | Next actions (🔴/🟡 only) |
133
- | `wikifier update-maps [--directory=src/] [--max-files=N]` | Rebuild graph + `library.md` |
152
+ | `wikifier suggest-next` | Next actions (🔴/actionable 🟡 only; `--json` for `actions[]`) |
153
+ | `wikifier update-maps [--directory=src/] [--max-files=N]` | Rebuild graph + `library.md` (warm 0-dirty is fast) |
134
154
  | `wikifier health [--summary\|--json]` | Health matrix (machine-friendly flags) |
135
155
  | `wikifier validate` | Missing wiki rows + ghost paths |
136
156
  | `wikifier cycles` | Circular deps + break hints |
137
157
  | `wikifier monitor` / `daemon` | Background maintenance (`WIKIFIER_DAEMON_MAPS=0` for check-only) |
138
158
  | `wikifier serve` | Localhost dashboard with Run/Stop |
139
159
 
140
- Library: `from wikifier import check_changes, record_change, mark_green, suggest_next_actions, update_maps, health`.
160
+ Library: `from wikifier import session_bootstrap, prepare_edit, check_changes, record_change, mark_green, suggest_next_actions, update_maps, health, list_core_tools`.
141
161
 
142
162
  ## MCP
143
163
 
@@ -52,27 +52,34 @@ Always set an explicit root for external trees: `WIKIFIER_PROJECT_ROOT=/abs/path
52
52
 
53
53
  ## Steady state (only touch what needs it)
54
54
 
55
- Full protocol: [`skills/run.md`](skills/run.md).
55
+ Full protocol: [`skills/run.md`](skills/run.md) (Agent Protocol v0.6 — package **4.6.x**).
56
56
 
57
57
  ```bash
58
- wikifier check-changes # yellow dirty files; red ghosts (missing paths)
59
- # prioritize 🔴 then 🟡 do NOT re-wiki 🟢 Green files
58
+ wikifier session-bootstrap # one-shot: root, health, attention, actions[]
59
+ wikifier check-changes # content-honest dirty; red ghosts (missing paths)
60
+ # prioritize 🔴 then *actionable* 🟡 — do NOT re-wiki 🟢 Green files
61
+ wikifier prepare-edit path/file.py # wiki + status + deps/dependents preflight
60
62
  # ... edit only those sources ...
61
63
  wikifier record-change "path/file.py" "why this changed" # required
62
64
  # ... refresh that file’s wiki summary only ...
63
65
  wikifier mark-green "path/file.py"
64
- wikifier update-maps # only if imports/structure changed
66
+ wikifier update-maps # only if imports/structure changed (warm 0-dirty is cheap)
65
67
  # removals:
66
68
  wikifier record-deletion "path/gone.py" "why removed"
67
69
  ```
68
70
 
69
- MCP **Core 6** (start every session): `get_project_status`, `check_changes`, `get_files_needing_attention`, `get_file_wiki`, `suggest_next_actions`, `record_change` / `mark_green`. Intel as needed: `get_dependencies`, `get_dependents`, `get_cycles`. Always pass `project_root=` for external trees.
71
+ **Core 6** (prefer every session MCP or library/CLI):
72
+ `session_bootstrap` → `check_changes` → `prepare_edit` → `suggest_next_actions` (json `actions[]`) → `record_change` → `mark_green`.
73
+
74
+ Advanced intel as needed: `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).
70
75
 
71
76
  ## What you get
72
77
 
73
- - **Import analysis** — Python, JS/TS (ESM/CJS, barrels), Rust, Go, C/C++ includes, C# usings; per-edge confidence; name-routed barrel expansion for TS/JS
78
+ - **Import analysis** — Python, JS/TS (ESM/CJS, barrels), Rust (`use`/`mod` + best-effort `crate::` paths), Go, C/C++ includes, C# usings; per-edge confidence; barrel expansion for TS/JS
74
79
  - **Incremental pipeline** — pure-Python `update-maps`: dirty parse → import cache → reverse deps → cycles → `library.md`
75
- - **Selective agent work** — health + suggest bias to 🔴/🟡 only; ACS *actionable* low-conf excludes stdlib/external noise
80
+ - **Warm agent maps (4.6.3+ / 4.6.4+)** — zero-dirty fast path skips full graph/`library.md` rewrite; **stdlib SQLite** import cache (dual-read legacy JSON) so warm dirty scans use mtime/hash index not multi‑MB pair JSON; content-hash dirty; `--directory=` scopes walks
81
+ - **Partial-map honesty** — `map_coverage` on `update_maps` / bootstrap (`complete`, `files_remaining_dirty`, `acs_version`, `cache_backend`); budgeted runs are not “done”
82
+ - **Selective agent work** — health + suggest bias to 🔴/actionable 🟡 only; **ACS v1.3** `reason_code` / `agent_signal`; prefer `actionable_low_conf_edges` + reason codes — **never** raw `low_conf_edges` alone
76
83
  - **Scale** — reverse index + barrel invalidation so one edit doesn’t re-scan the monorepo
77
84
  - **MCP tools** — optional server for Claude, Cursor, Cline, and other MCP clients
78
85
  - **Zero core dependencies** — stdlib only; forks can add their own stack on top
@@ -80,32 +87,45 @@ MCP **Core 6** (start every session): `get_project_status`, `check_changes`, `ge
80
87
 
81
88
  ## Performance (measured)
82
89
 
83
- | Project | Scale | Full `update-maps` |
84
- |---------|-------|--------------------|
85
- | llama_index | ~3.8k Python files | ~8.5s |
86
- | Babylon.js | ~3.9k TS files, barrel-heavy | ~4.5 min (scoped re-runs ~80s) |
87
- | Large trees (e.g. LLVM-scale) | tens of thousands of files | candidate scan in seconds |
90
+ Full / heavy runs (historical order-of-magnitude):
91
+
92
+ | Project | Scale | Full / heavy `update-maps` |
93
+ |---------|-------|----------------------------|
94
+ | llama_index | ~3.8k Python files | ~8.5s class full |
95
+ | Babylon.js | ~3.9k TS files, barrel-heavy | minutes full; scoped re-runs tens of seconds |
96
+ | Large trees (e.g. LLVM-scale) | tens of thousands of files | use lean monitor + `--directory` / `--max-files` |
97
+
98
+ Warm **0-dirty** re-runs after 4.6.3 (same machine; scoped where noted) — agent session path:
99
+
100
+ | Project | Scope | Warm2 `update-maps` |
101
+ |---------|-------|---------------------|
102
+ | Wikifier (self) | incremental full | ~43 ms |
103
+ | redox | `src` | ~17 ms |
104
+ | llama_index | `llama-index-core` | ~0.6 s |
105
+ | rust | `library/std/src` (budgeted) | ~0.7 s (vs multi-second full-tree walk before scoped collect) |
88
106
 
89
- Tests: `python -m unittest discover tests` (stdlib only).
107
+ Tests: `python -m unittest discover tests` (stdlib only; 86+ cases including agent-scale edges).
90
108
 
91
109
  ## Commands
92
110
 
93
111
  | Command | Purpose |
94
112
  |---------|---------|
95
113
  | `wikifier init [--target DIR]` | Bootstrap project + human `index.html` |
96
- | `wikifier check-changes` | Incremental scan health / pending |
114
+ | `wikifier session-bootstrap` | Session start: health, attention, dispatchable `actions[]` |
115
+ | `wikifier check-changes` | Content-honest scan → health / pending |
116
+ | `wikifier prepare-edit <file>` | Preflight: status, wiki snippet, deps, dependents |
97
117
  | `wikifier record-change <file> "reason"` | Log *why* (required after edits) |
98
- | `wikifier mark-green <file>` | Mark wiki current |
118
+ | `wikifier mark-green <file>` | Mark wiki current + source content-hash baseline |
99
119
  | `wikifier record-deletion <file> "reason"` | Mark removed paths 🔴 + prune barrel refs |
100
- | `wikifier suggest-next` | Next actions (🔴/🟡 only) |
101
- | `wikifier update-maps [--directory=src/] [--max-files=N]` | Rebuild graph + `library.md` |
120
+ | `wikifier suggest-next` | Next actions (🔴/actionable 🟡 only; `--json` for `actions[]`) |
121
+ | `wikifier update-maps [--directory=src/] [--max-files=N]` | Rebuild graph + `library.md` (warm 0-dirty is fast) |
102
122
  | `wikifier health [--summary\|--json]` | Health matrix (machine-friendly flags) |
103
123
  | `wikifier validate` | Missing wiki rows + ghost paths |
104
124
  | `wikifier cycles` | Circular deps + break hints |
105
125
  | `wikifier monitor` / `daemon` | Background maintenance (`WIKIFIER_DAEMON_MAPS=0` for check-only) |
106
126
  | `wikifier serve` | Localhost dashboard with Run/Stop |
107
127
 
108
- Library: `from wikifier import check_changes, record_change, mark_green, suggest_next_actions, update_maps, health`.
128
+ Library: `from wikifier import session_bootstrap, prepare_edit, check_changes, record_change, mark_green, suggest_next_actions, update_maps, health, list_core_tools`.
109
129
 
110
130
  ## MCP
111
131
 
@@ -11,7 +11,8 @@
11
11
  - File Tree + `wikifier serve`; MCP status/attention use library + emoji (not `[GREEN]` tags).
12
12
  - **First-run:** `init` → `update-maps` → `health --summary` → `suggest_next_actions`. Map is automatic; wiki *prose* is agent-filled over time.
13
13
  - **Map-first ≠ wiki-done:** `health_score` **Map Ready** + many 🟡 *Initial stubs* means structural coverage only. Do **not** bulk-wiki stubs. Wiki a file when you edit it, then `mark-green`. Prefer `actionable_yellow` / 🔴 over raw yellow counts.
14
- - **Steady-state selective work:** only 🔴 and *actionable* 🟡 (never re-wiki 🟢; never re-wiki the whole stub set). ACS uses `actionable_low_conf_edges`.
14
+ - **Steady-state selective work:** only 🔴 and *actionable* 🟡 (never re-wiki 🟢; never re-wiki the whole stub set). **ACS v1.3:** use `actionable_low_conf_edges` + `reason_code_counts` / `agent_signal` (`skip`|`investigate`). Do **not** thrash on raw `low_conf_edges` (includes external/bare scores ~0.48). Prefer `map_coverage.complete` / `files_remaining_dirty` after budgeted `update-maps` — `success: true` ≠ map complete.
15
+ - **Warm maps:** import cache prefers stdlib SQLite (`.wikifier_staging/import_cache.sqlite`); zero-dirty path uses mtime index + meta, not full pair payload.
15
16
  - **Dual scope:** `monitored_paths` = check-changes/health scope (keep lean). `update-maps --directory=` / `--max-files=` = map walk. Never set `project_root` to a multi-repo parent (e.g. `cloned_sample_projects`).
16
17
  - **CLI target:** `wikifier --target /abs/project health --summary` (flags stripped before the command). Or `WIKIFIER_PROJECT_ROOT=…`.
17
18
  - **Long-horizon:** `wikifier autonomous-status` (or `readiness`) before unattended runs; daemon writes `.wikifier_staging/daemon_heartbeat.json`. Soak ≥72h is still evidence work, not automatic.
@@ -0,0 +1,244 @@
1
+ """Agent-scale perf/accuracy path tests (zero-dirty, content-hash dirty, ACS v1.3).
2
+
3
+ Drives shipped library APIs only — no reimplementation of the unit under test.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ import time
10
+ import unittest
11
+ from pathlib import Path
12
+
13
+ from tests._base import TempProjectTestCase
14
+
15
+ from wikifier import cli
16
+ from wikifier import import_cache as ic
17
+ from wikifier.import_cache import (
18
+ classify_edge_agent_signal,
19
+ compute_acs_summary,
20
+ compute_file_content_hash,
21
+ )
22
+
23
+
24
+ class TestContentHashDirty(TempProjectTestCase):
25
+ def test_mtime_thrash_does_not_reparse_when_content_hash_matches(self):
26
+ p = self.write("pkg/a.py", "import os\nx = 1\n")
27
+ # First map: populates cache + content_hash
28
+ r1 = cli.update_maps(project_root=self.root, full=True)
29
+ self.assertTrue(r1.get("success"), r1)
30
+ self.assertGreaterEqual(int(r1.get("files_parsed") or 0), 1)
31
+ cache = ic.load_cache(self.root)
32
+ ent = cache.get("pkg/a.py") or cache.get("a.py")
33
+ # find entry
34
+ if ent is None:
35
+ for k, v in cache.items():
36
+ if str(k).endswith("a.py") and isinstance(v, dict):
37
+ ent = v
38
+ break
39
+ self.assertIsInstance(ent, dict, msg=f"cache keys={list(cache)[:20]}")
40
+ self.assertIn("content_hash", ent)
41
+ stored = ent["content_hash"]
42
+ live = compute_file_content_hash(p)
43
+ self.assertEqual(stored, live)
44
+
45
+ # Force stale mtime in cache, then touch file (same bytes)
46
+ ent["mtime"] = 1
47
+ ic.save_cache(self.root, cache)
48
+ time.sleep(0.05)
49
+ os.utime(p, None)
50
+
51
+ updates = []
52
+ need = ic.compute_files_needing_reparse(
53
+ self.root, [p], full_rebuild=False, content_stable_mtime_updates=updates
54
+ )
55
+ self.assertEqual(need, [], msg=f"should not reparse content-stable; updates={updates}")
56
+ self.assertGreaterEqual(len(updates), 1)
57
+
58
+ def test_content_change_does_reparse(self):
59
+ p = self.write("b.py", "y = 1\n")
60
+ r1 = cli.update_maps(project_root=self.root, full=True)
61
+ self.assertTrue(r1.get("success"), r1)
62
+ cache = ic.load_cache(self.root)
63
+ ent = cache.get("b.py")
64
+ self.assertIsInstance(ent, dict)
65
+ ent["mtime"] = 1
66
+ # Keep old hash so mtime+hash mismatch forces reparse after rewrite
67
+ ic.save_cache(self.root, cache)
68
+ p.write_text("y = 2\n", encoding="utf-8")
69
+ time.sleep(0.02)
70
+ need = ic.compute_files_needing_reparse(self.root, [p], full_rebuild=False)
71
+ self.assertEqual(len(need), 1)
72
+ self.assertEqual(need[0].resolve(), p.resolve())
73
+
74
+
75
+ class TestZeroDirtyFastPath(TempProjectTestCase):
76
+ def test_second_update_maps_uses_fast_path(self):
77
+ self.write("m.py", "print(1)\n")
78
+ r1 = cli.update_maps(project_root=self.root, full=True)
79
+ self.assertTrue(r1.get("success"), r1)
80
+ # Ensure library exists so fast path can skip rewrite
81
+ self.assertTrue((self.root / "library.md").is_file() or r1.get("library", {}).get("success"))
82
+
83
+ r2 = cli.update_maps(project_root=self.root, full=False)
84
+ self.assertTrue(r2.get("success"), r2)
85
+ self.assertEqual(int(r2.get("files_parsed") or 0), 0)
86
+ self.assertEqual(int(r2.get("files_to_reparse") or 0), 0)
87
+ self.assertTrue(r2.get("zero_dirty_fast_path"), r2)
88
+ lib = r2.get("library") or {}
89
+ self.assertTrue(lib.get("success"), lib)
90
+ # Prefer skip when library already present
91
+ if (self.root / "library.md").is_file():
92
+ self.assertTrue(lib.get("skipped") or lib.get("path"))
93
+
94
+ def test_zero_dirty_persists_acs_v13_to_disk(self):
95
+ """Warm zero-dirty path must write ACS 1.3 + reason_code_counts to import_cache."""
96
+ self.write("n.py", "import os\nx=1\n")
97
+ r1 = cli.update_maps(project_root=self.root, full=True)
98
+ self.assertTrue(r1.get("success"), r1)
99
+ # Simulate pre-1.3 cache left on disk (the bug: warm path upgraded memory only)
100
+ cache = ic.load_cache(self.root)
101
+ cache["_acs_summary"] = {
102
+ "acs_version": "1.2",
103
+ "total_scored_edges": 1,
104
+ "avg_confidence": 0.5,
105
+ "low_conf_edges": 1,
106
+ "actionable_low_conf_edges": 0,
107
+ }
108
+ ic.save_cache(self.root, cache)
109
+ on_disk = ic.load_cache(self.root)
110
+ self.assertEqual(str((on_disk.get("_acs_summary") or {}).get("acs_version")), "1.2")
111
+
112
+ r2 = cli.update_maps(project_root=self.root, full=False)
113
+ self.assertTrue(r2.get("success"), r2)
114
+ self.assertTrue(r2.get("zero_dirty_fast_path"), r2)
115
+ self.assertEqual((r2.get("acs") or {}).get("acs_version"), "1.3")
116
+
117
+ reloaded = ic.load_cache(self.root)
118
+ acs = reloaded.get("_acs_summary") or {}
119
+ self.assertEqual(acs.get("acs_version"), "1.3", msg=acs)
120
+ self.assertIn("reason_code_counts", acs)
121
+ self.assertIsInstance(acs.get("reason_code_counts"), dict)
122
+
123
+ def test_max_files_skip_honesty(self):
124
+ for i in range(5):
125
+ self.write(f"f{i}.py", f"v = {i}\n")
126
+ r = cli.update_maps(project_root=self.root, full=True, max_files=2)
127
+ self.assertTrue(r.get("success"), r)
128
+ self.assertEqual(int(r.get("files_parsed") or 0), 2)
129
+ self.assertEqual(int(r.get("files_skipped") or 0), 3)
130
+
131
+
132
+ class TestACSv13(TempProjectTestCase):
133
+ def test_classify_edge_reason_codes(self):
134
+ ext = {
135
+ "raw": "os",
136
+ "resolved": "",
137
+ "confidence_score": 0.48,
138
+ "diagnostic": {"category": "external_or_bare"},
139
+ "resolution_metadata": {"strategy": "python-bare-or-external"},
140
+ }
141
+ sig = classify_edge_agent_signal(ext)
142
+ self.assertEqual(sig["agent_signal"], "skip")
143
+ self.assertEqual(sig["reason_code"], "external_or_bare")
144
+ self.assertFalse(sig["actionable"])
145
+
146
+ unres = {"raw": ".missing", "resolved": "", "confidence_score": 0.4}
147
+ sig2 = classify_edge_agent_signal(unres)
148
+ self.assertEqual(sig2["agent_signal"], "investigate")
149
+ self.assertEqual(sig2["reason_code"], "unresolved_project")
150
+ self.assertTrue(sig2["actionable"])
151
+
152
+ ok = {"raw": "./x", "resolved": "x.py", "confidence_score": 0.95}
153
+ sig3 = classify_edge_agent_signal(ok)
154
+ self.assertEqual(sig3["reason_code"], "high_confidence_ok")
155
+ self.assertFalse(sig3["actionable"])
156
+
157
+ def test_compute_acs_summary_v13_shape(self):
158
+ cache = {
159
+ "a.py": {
160
+ "mtime": 1,
161
+ "resolved_pairs": [
162
+ {
163
+ "raw": "os",
164
+ "resolved": "",
165
+ "confidence_score": 0.48,
166
+ "diagnostic": {"category": "external_or_bare"},
167
+ "resolution_metadata": {"strategy": "python-bare-or-external"},
168
+ },
169
+ {
170
+ "raw": ".b",
171
+ "resolved": "b.py",
172
+ "confidence_score": 0.5,
173
+ "confidence_explanation": "low internal",
174
+ },
175
+ {
176
+ "raw": ".missing",
177
+ "resolved": "",
178
+ "confidence_score": 0.4,
179
+ "confidence_explanation": "unresolved project",
180
+ },
181
+ ],
182
+ }
183
+ }
184
+ acs = compute_acs_summary(cache)
185
+ self.assertEqual(acs["acs_version"], "1.3")
186
+ self.assertIn("reason_code_counts", acs)
187
+ self.assertIn("agent_signal_counts", acs)
188
+ self.assertGreaterEqual(acs["actionable_low_conf_edges"], 2)
189
+ self.assertGreaterEqual(acs["external_noise_edges"], 1)
190
+ self.assertIn("investigate", acs["agent_signal_counts"])
191
+
192
+
193
+ class TestCoreLoopEdges(TempProjectTestCase):
194
+ def test_session_bootstrap_and_prepare_edit(self):
195
+ self.write("hub.py", "import os\n")
196
+ cli.update_maps(project_root=self.root, full=True)
197
+ boot = cli.session_bootstrap(project_root=self.root)
198
+ self.assertTrue(boot.get("success"), boot)
199
+ self.assertIn("actions", boot)
200
+ pe = cli.prepare_edit("hub.py", project_root=self.root)
201
+ self.assertTrue(pe.get("success"), pe)
202
+ self.assertIn("status", pe)
203
+
204
+ def test_invalid_project_root_structured(self):
205
+ # missing path should not crash core helpers
206
+ missing = self.root / "no_such_project_dir"
207
+ r = cli.check_changes(project_root=missing)
208
+ self.assertIsInstance(r, dict)
209
+ # either success with empty or structured failure — never exception
210
+ self.assertIn("success", r)
211
+
212
+ def test_prepare_edit_missing_file_fails(self):
213
+ r = cli.prepare_edit("does_not_exist_xyz.py", project_root=self.root)
214
+ self.assertIsInstance(r, dict)
215
+ self.assertFalse(r.get("success"), r)
216
+ self.assertIn("error", r)
217
+
218
+ def test_multi_repo_parent_scope_warning_or_ok_flag(self):
219
+ # Bootstrap on a real root must set scope.ok; parent-of-many is agent anti-pattern
220
+ boot = cli.session_bootstrap(project_root=self.root)
221
+ self.assertTrue(boot.get("success"), boot)
222
+ scope = boot.get("scope") or {}
223
+ self.assertIn("ok", scope)
224
+
225
+
226
+ class TestRustCrateResolve(TempProjectTestCase):
227
+ def test_crate_use_resolves_sibling_module(self):
228
+ from wikifier.parsers import rust as rust_parser
229
+
230
+ lib = self.write("src/lib.rs", "mod foo;\npub use crate::foo::bar;\n")
231
+ self.write("src/foo.rs", "pub fn bar() {}\n")
232
+ edges = rust_parser.parse_rust_imports(str(lib))
233
+ uses = [e for e in edges if e.get("statement_type") == "use"]
234
+ self.assertTrue(uses)
235
+ # at least one use should resolve to foo.rs when crate path works
236
+ resolved_any = [e for e in uses if e.get("resolved_path")]
237
+ self.assertTrue(
238
+ resolved_any,
239
+ msg=f"expected crate:: resolve; edges={uses}",
240
+ )
241
+
242
+
243
+ if __name__ == "__main__":
244
+ unittest.main()
@@ -0,0 +1,193 @@
1
+ """SQLite cache store + map_coverage + warm path (shipped APIs, zero-dep)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import time
7
+ import unittest
8
+ from pathlib import Path
9
+
10
+ from tests._base import TempProjectTestCase
11
+
12
+ from wikifier import cli
13
+ from wikifier import import_cache as ic
14
+ from wikifier import cache_store as cs
15
+
16
+
17
+ class TestSqliteCacheRoundTrip(TempProjectTestCase):
18
+ def test_save_load_sqlite_primary(self):
19
+ self.write("a.py", "import os\n")
20
+ r = cli.update_maps(project_root=self.root, full=True)
21
+ self.assertTrue(r.get("success"), r)
22
+ self.assertTrue(cs.has_sqlite(self.root), "sqlite should exist after save")
23
+ cache = ic.load_cache(self.root)
24
+ self.assertIn("a.py", cache)
25
+ self.assertIsInstance(cache["a.py"].get("mtime"), int)
26
+ # light index works without requiring pairs field presence
27
+ idx = ic.load_mtime_index(self.root)
28
+ self.assertIn("a.py", idx)
29
+ self.assertEqual(idx["a.py"]["mtime"], cache["a.py"]["mtime"])
30
+
31
+ def test_legacy_json_dual_read_then_migrate(self):
32
+ # Plant legacy JSON only (no sqlite)
33
+ staging = self.root / ".wikifier_staging"
34
+ staging.mkdir(parents=True, exist_ok=True)
35
+ import json
36
+ legacy = {
37
+ "x.py": {
38
+ "mtime": 1,
39
+ "content_hash": "sha256:abc",
40
+ "imports": ["os"],
41
+ "resolved": [],
42
+ "resolved_pairs": [{"raw": "os", "resolved": "", "confidence_score": 0.48}],
43
+ },
44
+ "_acs_summary": {
45
+ "acs_version": "1.2",
46
+ "total_scored_edges": 1,
47
+ "actionable_low_conf_edges": 0,
48
+ "low_conf_edges": 1,
49
+ },
50
+ }
51
+ (staging / "import_cache.json").write_text(
52
+ json.dumps(legacy), encoding="utf-8"
53
+ )
54
+ loaded = ic.load_cache(self.root)
55
+ self.assertIn("x.py", loaded)
56
+ # update_maps migrates
57
+ self.write("x.py", "import os\n")
58
+ r = cli.update_maps(project_root=self.root, full=False)
59
+ self.assertTrue(r.get("success"), r)
60
+ self.assertTrue(cs.has_sqlite(self.root))
61
+
62
+
63
+ class TestWarmPathNoFullJsonTax(TempProjectTestCase):
64
+ def test_zero_dirty_reports_sqlite_backend_and_coverage(self):
65
+ self.write("m.py", "print(1)\n")
66
+ r1 = cli.update_maps(project_root=self.root, full=True)
67
+ self.assertTrue(r1.get("success"), r1)
68
+ r2 = cli.update_maps(project_root=self.root, full=False)
69
+ self.assertTrue(r2.get("success"), r2)
70
+ self.assertTrue(r2.get("zero_dirty_fast_path"), r2)
71
+ self.assertIn(r2.get("cache_backend"), ("sqlite", "sqlite+json", "json"))
72
+ cov = r2.get("map_coverage") or {}
73
+ self.assertTrue(cov.get("complete"), cov)
74
+ self.assertIn("files_remaining_dirty", cov)
75
+ self.assertEqual(int(cov["files_remaining_dirty"]), 0)
76
+
77
+ def test_zero_dirty_skips_full_payload_load_cache_dict(self):
78
+ """Shipped warm path must not deserialize full file payloads on 0-dirty.
79
+
80
+ Criterion: after SQLite is warm, zero-dirty update_maps must not call
81
+ cache_store.load_cache_dict (full pairs) or ic.load_cache (legacy full).
82
+ Light index + meta only.
83
+ """
84
+ self.write("warm.py", "print(0)\n")
85
+ r1 = cli.update_maps(project_root=self.root, full=True)
86
+ self.assertTrue(r1.get("success"), r1)
87
+ self.assertTrue(cs.has_sqlite(self.root))
88
+ # Second run seeds zero-dirty + sqlite
89
+ r_seed = cli.update_maps(project_root=self.root, full=False)
90
+ self.assertTrue(r_seed.get("zero_dirty_fast_path"), r_seed)
91
+
92
+ full_loads = {"load_cache_dict": 0, "load_cache": 0}
93
+ real_lcd = cs.load_cache_dict
94
+ real_lc = ic.load_cache
95
+
96
+ def counting_lcd(root):
97
+ full_loads["load_cache_dict"] += 1
98
+ return real_lcd(root)
99
+
100
+ def counting_lc(root):
101
+ full_loads["load_cache"] += 1
102
+ return real_lc(root)
103
+
104
+ # Patch at the modules the shipped path imports from
105
+ import wikifier.cache_store as cs_mod
106
+ import wikifier.import_cache as ic_mod
107
+ orig_cs = cs_mod.load_cache_dict
108
+ orig_ic = ic_mod.load_cache
109
+ cs_mod.load_cache_dict = counting_lcd # type: ignore
110
+ ic_mod.load_cache = counting_lc # type: ignore
111
+ try:
112
+ r2 = cli.update_maps(project_root=self.root, full=False)
113
+ finally:
114
+ cs_mod.load_cache_dict = orig_cs # type: ignore
115
+ ic_mod.load_cache = orig_ic # type: ignore
116
+
117
+ self.assertTrue(r2.get("success"), r2)
118
+ self.assertTrue(r2.get("zero_dirty_fast_path"), r2)
119
+ self.assertEqual(
120
+ full_loads["load_cache_dict"],
121
+ 0,
122
+ f"warm 0-dirty must not call load_cache_dict; counts={full_loads}",
123
+ )
124
+ self.assertEqual(
125
+ full_loads["load_cache"],
126
+ 0,
127
+ f"warm 0-dirty must not call load_cache; counts={full_loads}",
128
+ )
129
+
130
+ def test_max_files_coverage_incomplete(self):
131
+ for i in range(6):
132
+ self.write(f"f{i}.py", f"v={i}\n")
133
+ r = cli.update_maps(project_root=self.root, full=True, max_files=2)
134
+ self.assertTrue(r.get("success"), r)
135
+ cov = r.get("map_coverage") or {}
136
+ self.assertFalse(cov.get("complete"), cov)
137
+ self.assertEqual(int(cov.get("files_skipped") or 0), 4)
138
+ self.assertGreaterEqual(int(cov.get("files_remaining_dirty") or 0), 4)
139
+ self.assertIn("agent_note", cov)
140
+
141
+ def test_mtime_index_skips_content_stable(self):
142
+ p = self.write("s.py", "x=1\n")
143
+ cli.update_maps(project_root=self.root, full=True)
144
+ cache = ic.load_cache(self.root)
145
+ ent = cache["s.py"]
146
+ ent["mtime"] = 1
147
+ ic.save_cache(self.root, cache)
148
+ time.sleep(0.05)
149
+ os.utime(p, None)
150
+ updates = []
151
+ need = ic.compute_files_needing_reparse(
152
+ self.root, [p], content_stable_mtime_updates=updates
153
+ )
154
+ # content_hash should make this stable once hash stored
155
+ if ent.get("content_hash"):
156
+ self.assertEqual(need, [])
157
+ self.assertGreaterEqual(len(updates), 1)
158
+
159
+
160
+ class TestBootstrapCoverageAndAcsGuidance(TempProjectTestCase):
161
+ def test_bootstrap_surfaces_acs_guidance(self):
162
+ self.write("h.py", "import os\n")
163
+ cli.update_maps(project_root=self.root, full=True)
164
+ boot = cli.session_bootstrap(project_root=self.root)
165
+ self.assertTrue(boot.get("success"), boot)
166
+ self.assertIn("acs_guidance", boot)
167
+ self.assertIn("actionable_low_conf_edges", boot["acs_guidance"])
168
+ acs = boot.get("acs") or {}
169
+ self.assertIn("acs_version", acs)
170
+
171
+
172
+ class TestGoModResolve(TempProjectTestCase):
173
+ def test_same_module_import_resolves(self):
174
+ from wikifier.parsers import go_lang as go
175
+
176
+ self.write(
177
+ "go.mod",
178
+ "module example.com/demo\n\ngo 1.21\n",
179
+ )
180
+ self.write("pkg/util/util.go", "package util\nfunc X() {}\n")
181
+ main = self.write(
182
+ "cmd/main.go",
183
+ 'package main\nimport "example.com/demo/pkg/util"\n',
184
+ )
185
+ edges = go.parse_go_imports(str(main))
186
+ self.assertTrue(edges)
187
+ resolved = [e for e in edges if e.get("resolved_path")]
188
+ self.assertTrue(resolved, msg=f"edges={edges}")
189
+ self.assertIn("util", resolved[0]["resolved_path"])
190
+
191
+
192
+ if __name__ == "__main__":
193
+ unittest.main()