wikifier 4.6.1__tar.gz → 4.6.3__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 (57) hide show
  1. {wikifier-4.6.1/wikifier.egg-info → wikifier-4.6.3}/PKG-INFO +38 -19
  2. {wikifier-4.6.1 → wikifier-4.6.3}/README.md +37 -18
  3. {wikifier-4.6.1 → wikifier-4.6.3}/skills/run.md +3 -1
  4. {wikifier-4.6.1 → wikifier-4.6.3}/tests/test_agent_loop.py +102 -0
  5. wikifier-4.6.3/tests/test_agent_scale.py +244 -0
  6. {wikifier-4.6.1 → wikifier-4.6.3}/tests/test_gap_closure.py +5 -2
  7. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier/__init__.py +3 -1
  8. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier/agent_loop.py +133 -25
  9. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier/cli.py +198 -14
  10. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier/health.py +83 -0
  11. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier/import_cache.py +198 -42
  12. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier/mcp/server.py +26 -0
  13. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier/parsers/rust.py +70 -0
  14. {wikifier-4.6.1 → wikifier-4.6.3/wikifier.egg-info}/PKG-INFO +38 -19
  15. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier.egg-info/SOURCES.txt +1 -0
  16. {wikifier-4.6.1 → wikifier-4.6.3}/CONTRIBUTING.md +0 -0
  17. {wikifier-4.6.1 → wikifier-4.6.3}/LICENSE +0 -0
  18. {wikifier-4.6.1 → wikifier-4.6.3}/MANIFEST.in +0 -0
  19. {wikifier-4.6.1 → wikifier-4.6.3}/diagnostics.html +0 -0
  20. {wikifier-4.6.1 → wikifier-4.6.3}/docs/spec.md +0 -0
  21. {wikifier-4.6.1 → wikifier-4.6.3}/index.html +0 -0
  22. {wikifier-4.6.1 → wikifier-4.6.3}/pyproject.toml +0 -0
  23. {wikifier-4.6.1 → wikifier-4.6.3}/setup.cfg +0 -0
  24. {wikifier-4.6.1 → wikifier-4.6.3}/tests/test_barrel_invalidation.py +0 -0
  25. {wikifier-4.6.1 → wikifier-4.6.3}/tests/test_health.py +0 -0
  26. {wikifier-4.6.1 → wikifier-4.6.3}/tests/test_import_cache.py +0 -0
  27. {wikifier-4.6.1 → wikifier-4.6.3}/tests/test_multi_lang_parsers.py +0 -0
  28. {wikifier-4.6.1 → wikifier-4.6.3}/tests/test_parsers.py +0 -0
  29. {wikifier-4.6.1 → wikifier-4.6.3}/tests/test_selftest_wrappers.py +0 -0
  30. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier/__main__.py +0 -0
  31. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier/contracts.py +0 -0
  32. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier/daemon.py +0 -0
  33. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier/diagnostics.py +0 -0
  34. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier/index.html +0 -0
  35. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier/library.py +0 -0
  36. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier/locking.py +0 -0
  37. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier/mcp/__init__.py +0 -0
  38. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier/parsers/__init__.py +0 -0
  39. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier/parsers/_edge.py +0 -0
  40. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier/parsers/bree.py +0 -0
  41. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier/parsers/c_cpp.py +0 -0
  42. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier/parsers/cdia.py +0 -0
  43. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier/parsers/csharp.py +0 -0
  44. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier/parsers/go_lang.py +0 -0
  45. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier/parsers/java.py +0 -0
  46. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier/parsers/javascript.py +0 -0
  47. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier/parsers/python.py +0 -0
  48. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier/project_root.py +0 -0
  49. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier/resolution.py +0 -0
  50. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier/scripts/wikifier.bat +0 -0
  51. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier/scripts/wikifier.ps1 +0 -0
  52. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier/scripts/wikifier.sh +0 -0
  53. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier/serve.py +0 -0
  54. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier.egg-info/dependency_links.txt +0 -0
  55. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier.egg-info/entry_points.txt +0 -0
  56. {wikifier-4.6.1 → wikifier-4.6.3}/wikifier.egg-info/requires.txt +0 -0
  57. {wikifier-4.6.1 → wikifier-4.6.3}/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.1
3
+ Version: 4.6.3
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,33 @@ 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+)** — zero-dirty fast path skips full graph/`library.md` rewrite when nothing reparsed; content-hash dirty so mtime thrash ≠ reparse; `--directory=` scopes the candidate walk (monorepo budgets)
113
+ - **Selective agent work** — health + suggest bias to 🔴/actionable 🟡 only; **ACS v1.3** `reason_code` / `agent_signal` (`skip`|`investigate`); prefer `actionable_low_conf_edges` over raw low-conf noise
108
114
  - **Scale** — reverse index + barrel invalidation so one edit doesn’t re-scan the monorepo
109
115
  - **MCP tools** — optional server for Claude, Cursor, Cline, and other MCP clients
110
116
  - **Zero core dependencies** — stdlib only; forks can add their own stack on top
@@ -112,32 +118,45 @@ MCP **Core 6** (start every session): `get_project_status`, `check_changes`, `ge
112
118
 
113
119
  ## Performance (measured)
114
120
 
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 |
121
+ Full / heavy runs (historical order-of-magnitude):
122
+
123
+ | Project | Scale | Full / heavy `update-maps` |
124
+ |---------|-------|----------------------------|
125
+ | llama_index | ~3.8k Python files | ~8.5s class full |
126
+ | Babylon.js | ~3.9k TS files, barrel-heavy | minutes full; scoped re-runs tens of seconds |
127
+ | Large trees (e.g. LLVM-scale) | tens of thousands of files | use lean monitor + `--directory` / `--max-files` |
128
+
129
+ Warm **0-dirty** re-runs after 4.6.3 (same machine; scoped where noted) — agent session path:
130
+
131
+ | Project | Scope | Warm2 `update-maps` |
132
+ |---------|-------|---------------------|
133
+ | Wikifier (self) | incremental full | ~43 ms |
134
+ | redox | `src` | ~17 ms |
135
+ | llama_index | `llama-index-core` | ~0.6 s |
136
+ | rust | `library/std/src` (budgeted) | ~0.7 s (vs multi-second full-tree walk before scoped collect) |
120
137
 
121
- Tests: `python -m unittest discover tests` (stdlib only).
138
+ Tests: `python -m unittest discover tests` (stdlib only; 86+ cases including agent-scale edges).
122
139
 
123
140
  ## Commands
124
141
 
125
142
  | Command | Purpose |
126
143
  |---------|---------|
127
144
  | `wikifier init [--target DIR]` | Bootstrap project + human `index.html` |
128
- | `wikifier check-changes` | Incremental scan health / pending |
145
+ | `wikifier session-bootstrap` | Session start: health, attention, dispatchable `actions[]` |
146
+ | `wikifier check-changes` | Content-honest scan → health / pending |
147
+ | `wikifier prepare-edit <file>` | Preflight: status, wiki snippet, deps, dependents |
129
148
  | `wikifier record-change <file> "reason"` | Log *why* (required after edits) |
130
- | `wikifier mark-green <file>` | Mark wiki current |
149
+ | `wikifier mark-green <file>` | Mark wiki current + source content-hash baseline |
131
150
  | `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` |
151
+ | `wikifier suggest-next` | Next actions (🔴/actionable 🟡 only; `--json` for `actions[]`) |
152
+ | `wikifier update-maps [--directory=src/] [--max-files=N]` | Rebuild graph + `library.md` (warm 0-dirty is fast) |
134
153
  | `wikifier health [--summary\|--json]` | Health matrix (machine-friendly flags) |
135
154
  | `wikifier validate` | Missing wiki rows + ghost paths |
136
155
  | `wikifier cycles` | Circular deps + break hints |
137
156
  | `wikifier monitor` / `daemon` | Background maintenance (`WIKIFIER_DAEMON_MAPS=0` for check-only) |
138
157
  | `wikifier serve` | Localhost dashboard with Run/Stop |
139
158
 
140
- Library: `from wikifier import check_changes, record_change, mark_green, suggest_next_actions, update_maps, health`.
159
+ Library: `from wikifier import session_bootstrap, prepare_edit, check_changes, record_change, mark_green, suggest_next_actions, update_maps, health, list_core_tools`.
141
160
 
142
161
  ## MCP
143
162
 
@@ -52,27 +52,33 @@ 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+)** — zero-dirty fast path skips full graph/`library.md` rewrite when nothing reparsed; content-hash dirty so mtime thrash ≠ reparse; `--directory=` scopes the candidate walk (monorepo budgets)
81
+ - **Selective agent work** — health + suggest bias to 🔴/actionable 🟡 only; **ACS v1.3** `reason_code` / `agent_signal` (`skip`|`investigate`); prefer `actionable_low_conf_edges` over raw low-conf noise
76
82
  - **Scale** — reverse index + barrel invalidation so one edit doesn’t re-scan the monorepo
77
83
  - **MCP tools** — optional server for Claude, Cursor, Cline, and other MCP clients
78
84
  - **Zero core dependencies** — stdlib only; forks can add their own stack on top
@@ -80,32 +86,45 @@ MCP **Core 6** (start every session): `get_project_status`, `check_changes`, `ge
80
86
 
81
87
  ## Performance (measured)
82
88
 
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 |
89
+ Full / heavy runs (historical order-of-magnitude):
90
+
91
+ | Project | Scale | Full / heavy `update-maps` |
92
+ |---------|-------|----------------------------|
93
+ | llama_index | ~3.8k Python files | ~8.5s class full |
94
+ | Babylon.js | ~3.9k TS files, barrel-heavy | minutes full; scoped re-runs tens of seconds |
95
+ | Large trees (e.g. LLVM-scale) | tens of thousands of files | use lean monitor + `--directory` / `--max-files` |
96
+
97
+ Warm **0-dirty** re-runs after 4.6.3 (same machine; scoped where noted) — agent session path:
98
+
99
+ | Project | Scope | Warm2 `update-maps` |
100
+ |---------|-------|---------------------|
101
+ | Wikifier (self) | incremental full | ~43 ms |
102
+ | redox | `src` | ~17 ms |
103
+ | llama_index | `llama-index-core` | ~0.6 s |
104
+ | rust | `library/std/src` (budgeted) | ~0.7 s (vs multi-second full-tree walk before scoped collect) |
88
105
 
89
- Tests: `python -m unittest discover tests` (stdlib only).
106
+ Tests: `python -m unittest discover tests` (stdlib only; 86+ cases including agent-scale edges).
90
107
 
91
108
  ## Commands
92
109
 
93
110
  | Command | Purpose |
94
111
  |---------|---------|
95
112
  | `wikifier init [--target DIR]` | Bootstrap project + human `index.html` |
96
- | `wikifier check-changes` | Incremental scan health / pending |
113
+ | `wikifier session-bootstrap` | Session start: health, attention, dispatchable `actions[]` |
114
+ | `wikifier check-changes` | Content-honest scan → health / pending |
115
+ | `wikifier prepare-edit <file>` | Preflight: status, wiki snippet, deps, dependents |
97
116
  | `wikifier record-change <file> "reason"` | Log *why* (required after edits) |
98
- | `wikifier mark-green <file>` | Mark wiki current |
117
+ | `wikifier mark-green <file>` | Mark wiki current + source content-hash baseline |
99
118
  | `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` |
119
+ | `wikifier suggest-next` | Next actions (🔴/actionable 🟡 only; `--json` for `actions[]`) |
120
+ | `wikifier update-maps [--directory=src/] [--max-files=N]` | Rebuild graph + `library.md` (warm 0-dirty is fast) |
102
121
  | `wikifier health [--summary\|--json]` | Health matrix (machine-friendly flags) |
103
122
  | `wikifier validate` | Missing wiki rows + ghost paths |
104
123
  | `wikifier cycles` | Circular deps + break hints |
105
124
  | `wikifier monitor` / `daemon` | Background maintenance (`WIKIFIER_DAEMON_MAPS=0` for check-only) |
106
125
  | `wikifier serve` | Localhost dashboard with Run/Stop |
107
126
 
108
- Library: `from wikifier import check_changes, record_change, mark_green, suggest_next_actions, update_maps, health`.
127
+ Library: `from wikifier import session_bootstrap, prepare_edit, check_changes, record_change, mark_green, suggest_next_actions, update_maps, health, list_core_tools`.
109
128
 
110
129
  ## MCP
111
130
 
@@ -43,8 +43,10 @@ Do **not** open megamodules (`javascript.py`, `import_cache.py`, `bree.py`) to d
43
43
  |------|-----|
44
44
  | First-run map | `init` → `update-maps` → `health --summary` → `suggest-next` |
45
45
  | **Session start (4.6+)** | `session_bootstrap` (or CLI `session-bootstrap`) — one shot: root, health, attention, `actions[]` |
46
+ | **Core tool list** | `list_core_tools` / bootstrap `core_surface` — prefer Core 6; advanced is non-core |
46
47
  | Steady-state | `check-changes` (content-honest) → edit 🔴/actionable 🟡 only → `record-change` → wiki → `mark-green` |
47
- | **Preflight edit** | `prepare_edit` / CLI `prepare-edit <file>` wiki + status + deps + dependents |
48
+ | **Hash migration** | `seed_source_content_hashes` / CLI `seed-source-hashes`baseline Greens without mass Yellow |
49
+ | **Preflight edit** | `prepare_edit` / CLI `prepare-edit <file>` — wiki + status + deps + dependents (multi-shape reverse index) |
48
50
  | **Why / journal** | `why_file` / `search_journal` — semantic trail, not mtime theater |
49
51
  | Lookup Core | `session_bootstrap`, `check_changes`, `prepare_edit`, `suggest_next_actions` (json `actions[]`), `record_change`, `mark_green` |
50
52
  | Advanced intel | `get_dependencies`, `get_dependents`, `get_cycles`, barrel/diagnostics — not daily Core |
@@ -222,5 +222,107 @@ class TestCliBootstrapEntry(TempProjectTestCase):
222
222
  self.assertIn("actions", res)
223
223
 
224
224
 
225
+ class TestSeedSourceHashes(TempProjectTestCase):
226
+ def test_seed_then_touch_ok_rewrite_yellows(self):
227
+ p = self.write("seeded.py", "print(0)\n")
228
+ health_mod.upsert_entry(self.root, "seeded.py", "🟢 Green", "legacy no hash")
229
+ data = health_mod.load_health(self.root)
230
+ self.assertNotIn("source_content_hash", data["entries"]["seeded.py"] or {})
231
+ res = health_mod.seed_source_content_hashes(self.root, only_green=True)
232
+ self.assertTrue(res.get("success"), res)
233
+ self.assertGreaterEqual(int(res.get("seeded") or 0), 1)
234
+ data2 = health_mod.load_health(self.root)
235
+ ent = data2["entries"]["seeded.py"]
236
+ self.assertIn("Green", ent["status"])
237
+ self.assertTrue(ent.get("source_content_hash"))
238
+ baseline = ent["source_content_hash"]
239
+ from wikifier import import_cache as ic
240
+ cache = {}
241
+ ic.update_file_data(cache, "seeded.py", mtime=1, imports=[], resolved_pairs=[])
242
+ ic.save_cache(self.root, cache)
243
+ os.utime(p, None)
244
+ cache = ic.load_cache(self.root)
245
+ cache["seeded.py"]["mtime"] = 1
246
+ ic.save_cache(self.root, cache)
247
+ r = cli.check_changes(project_root=self.root)
248
+ self.assertTrue(r.get("success"), r)
249
+ data3 = health_mod.load_health(self.root)
250
+ self.assertIn("Green", data3["entries"]["seeded.py"]["status"])
251
+ self.assertEqual(data3["entries"]["seeded.py"]["source_content_hash"], baseline)
252
+ p.write_text("print(99)\n", encoding="utf-8")
253
+ cache = ic.load_cache(self.root)
254
+ cache["seeded.py"]["mtime"] = 1
255
+ ic.save_cache(self.root, cache)
256
+ r2 = cli.check_changes(project_root=self.root)
257
+ self.assertTrue(r2.get("success"), r2)
258
+ self.assertGreaterEqual(int(r2.get("changes_detected") or 0), 1)
259
+ data4 = health_mod.load_health(self.root)
260
+ self.assertIn("Yellow", data4["entries"]["seeded.py"]["status"])
261
+
262
+
263
+ class TestCoreSurfaceListing(unittest.TestCase):
264
+ def test_list_core_tools_has_six(self):
265
+ from wikifier.agent_loop import list_core_tools, CORE_DAILY_NAMES
266
+ res = list_core_tools()
267
+ self.assertTrue(res.get("success"))
268
+ self.assertEqual(res.get("core_count"), 6)
269
+ names = res.get("core_names") or []
270
+ for n in (
271
+ "session_bootstrap", "check_changes", "prepare_edit",
272
+ "suggest_next_actions", "record_change", "mark_green",
273
+ ):
274
+ self.assertIn(n, names)
275
+ self.assertEqual(names, CORE_DAILY_NAMES)
276
+ adv = res.get("advanced_intel") or []
277
+ self.assertTrue(adv)
278
+ self.assertNotIn("session_bootstrap", adv) # core not listed as advanced-only
279
+
280
+
281
+ class TestPrepareEditReverseShapes(TempProjectTestCase):
282
+ def test_flat_and_nested_reverse_index(self):
283
+ self.write("lib/t.py", "x=1\n")
284
+ health_mod.upsert_entry(self.root, "lib/t.py", "🟢 Green", "ok")
285
+ from wikifier import import_cache as ic
286
+ from wikifier.agent_loop import prepare_edit, resolve_dependents_from_cache
287
+
288
+ # Flat shape
289
+ cache_flat = {
290
+ "lib/t.py": {"mtime": 1, "resolved_pairs": []},
291
+ "lib/a.py": {"mtime": 1, "resolved_pairs": [{"resolved": "lib/t.py", "resolved_path": "lib/t.py"}]},
292
+ "_reverse_dependencies": {"lib/t.py": ["lib/a.py", "lib/b.py"]},
293
+ }
294
+ deps = resolve_dependents_from_cache(cache_flat, "lib/t.py")
295
+ self.assertIn("lib/a.py", deps)
296
+ self.assertIn("lib/b.py", deps)
297
+ ic.save_cache(self.root, cache_flat)
298
+ pe = prepare_edit("lib/t.py", project_root=self.root)
299
+ self.assertTrue(pe.get("success"))
300
+ self.assertIn("lib/a.py", pe.get("dependents") or [])
301
+
302
+ # Nested index shape
303
+ cache_nest = {
304
+ "lib/t.py": {"mtime": 1, "resolved_pairs": []},
305
+ "_reverse_dependencies": {
306
+ "index": {"lib/t.py": ["lib/nested1.py", "lib/nested2.py"]},
307
+ "version": 1,
308
+ },
309
+ }
310
+ deps2 = resolve_dependents_from_cache(cache_nest, "lib/t.py")
311
+ self.assertEqual(set(deps2), {"lib/nested1.py", "lib/nested2.py"})
312
+ ic.save_cache(self.root, cache_nest)
313
+ pe2 = prepare_edit("lib/t.py", project_root=self.root)
314
+ self.assertTrue(pe2.get("success"))
315
+ self.assertIn("lib/nested1.py", pe2.get("dependents") or [])
316
+
317
+ # Dict value shape
318
+ cache_dict = {
319
+ "_reverse_dependencies": {
320
+ "lib/t.py": {"importers": ["lib/from_dict.py"]},
321
+ }
322
+ }
323
+ deps3 = resolve_dependents_from_cache(cache_dict, "lib/t.py")
324
+ self.assertIn("lib/from_dict.py", deps3)
325
+
326
+
225
327
  if __name__ == "__main__":
226
328
  unittest.main()
@@ -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()
@@ -104,7 +104,7 @@ class TestAcsActionableG4(TempProjectTestCase):
104
104
  ))
105
105
 
106
106
  def test_dynamic_literal_noise_demoted_from_actionable(self):
107
- """ACS v1.2: importlib.import_module(\"pkg\") static dynamics are not actionable."""
107
+ """ACS v1.2+: importlib.import_module(\"pkg\") static dynamics are not actionable."""
108
108
  dyn_lit = {
109
109
  "raw": "\"wikifier.health\"",
110
110
  "resolved": "wikifier.health",
@@ -142,10 +142,13 @@ class TestAcsActionableG4(TempProjectTestCase):
142
142
  self.assertTrue(ic._edge_is_dynamic_literal_noise(dyn_lit))
143
143
  self.assertFalse(ic._edge_is_dynamic_literal_noise(project_fragile))
144
144
  summary = ic.compute_acs_summary(cache)
145
- self.assertEqual(summary.get("acs_version"), "1.2")
145
+ # v1.3 additive (reason codes); still demotes dynamic literals from actionable
146
+ self.assertGreaterEqual(str(summary.get("acs_version") or ""), "1.2")
146
147
  self.assertGreaterEqual(summary["low_conf_edges"], 2)
147
148
  self.assertEqual(summary["actionable_low_conf_edges"], 1)
148
149
  self.assertGreaterEqual(summary.get("dynamic_literal_noise_edges", 0), 1)
150
+ if "reason_code_counts" in summary:
151
+ self.assertIn("dynamic_literal", summary["reason_code_counts"] or {})
149
152
  action = ic.get_low_confidence_edges(cache, actionable_only=True)
150
153
  self.assertEqual(len(action), 1)
151
154
  self.assertIn("missing_local", str(action[0].get("raw") or action[0].get("resolved")))
@@ -46,6 +46,8 @@ from .cli import (
46
46
  prepare_edit,
47
47
  search_journal,
48
48
  why_file,
49
+ seed_source_content_hashes,
50
+ list_core_tools,
49
51
  )
50
52
  from . import agent_loop
51
53
 
@@ -76,4 +78,4 @@ from .contracts import (
76
78
  compute_acs_confidence,
77
79
  )
78
80
 
79
- __version__ = "4.6.1"
81
+ __version__ = "4.6.3"