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.
- {wikifier-4.6.2/wikifier.egg-info → wikifier-4.6.4}/PKG-INFO +39 -19
- {wikifier-4.6.2 → wikifier-4.6.4}/README.md +38 -18
- {wikifier-4.6.2 → wikifier-4.6.4}/skills/run.md +2 -1
- wikifier-4.6.4/tests/test_agent_scale.py +244 -0
- wikifier-4.6.4/tests/test_cache_store.py +193 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/tests/test_gap_closure.py +5 -2
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/__init__.py +1 -1
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/agent_loop.py +56 -3
- wikifier-4.6.4/wikifier/cache_store.py +348 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/cli.py +262 -29
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/import_cache.py +306 -78
- wikifier-4.6.4/wikifier/parsers/go_lang.py +164 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/parsers/rust.py +70 -0
- {wikifier-4.6.2 → wikifier-4.6.4/wikifier.egg-info}/PKG-INFO +39 -19
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier.egg-info/SOURCES.txt +4 -0
- wikifier-4.6.2/wikifier/parsers/go_lang.py +0 -87
- {wikifier-4.6.2 → wikifier-4.6.4}/CONTRIBUTING.md +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/LICENSE +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/MANIFEST.in +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/diagnostics.html +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/docs/spec.md +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/index.html +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/pyproject.toml +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/setup.cfg +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/tests/test_agent_loop.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/tests/test_barrel_invalidation.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/tests/test_health.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/tests/test_import_cache.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/tests/test_multi_lang_parsers.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/tests/test_parsers.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/tests/test_selftest_wrappers.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/__main__.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/contracts.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/daemon.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/diagnostics.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/health.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/index.html +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/library.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/locking.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/mcp/__init__.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/mcp/server.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/parsers/__init__.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/parsers/_edge.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/parsers/bree.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/parsers/c_cpp.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/parsers/cdia.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/parsers/csharp.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/parsers/java.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/parsers/javascript.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/parsers/python.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/project_root.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/resolution.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/scripts/wikifier.bat +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/scripts/wikifier.ps1 +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/scripts/wikifier.sh +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier/serve.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier.egg-info/dependency_links.txt +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier.egg-info/entry_points.txt +0 -0
- {wikifier-4.6.2 → wikifier-4.6.4}/wikifier.egg-info/requires.txt +0 -0
- {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.
|
|
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
|
|
91
|
-
#
|
|
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
|
-
|
|
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;
|
|
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
|
-
- **
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
|
118
|
-
|
|
119
|
-
|
|
|
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
|
|
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 (
|
|
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
|
|
59
|
-
#
|
|
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
|
-
|
|
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;
|
|
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
|
-
- **
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
|
86
|
-
|
|
87
|
-
|
|
|
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
|
|
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 (
|
|
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
|
|
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()
|