wikifier 4.6.2__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.
- {wikifier-4.6.2/wikifier.egg-info → wikifier-4.6.3}/PKG-INFO +38 -19
- {wikifier-4.6.2 → wikifier-4.6.3}/README.md +37 -18
- wikifier-4.6.3/tests/test_agent_scale.py +244 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/tests/test_gap_closure.py +5 -2
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier/__init__.py +1 -1
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier/agent_loop.py +18 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier/cli.py +161 -14
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier/import_cache.py +198 -42
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier/parsers/rust.py +70 -0
- {wikifier-4.6.2 → wikifier-4.6.3/wikifier.egg-info}/PKG-INFO +38 -19
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier.egg-info/SOURCES.txt +1 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/CONTRIBUTING.md +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/LICENSE +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/MANIFEST.in +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/diagnostics.html +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/docs/spec.md +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/index.html +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/pyproject.toml +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/setup.cfg +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/skills/run.md +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/tests/test_agent_loop.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/tests/test_barrel_invalidation.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/tests/test_health.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/tests/test_import_cache.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/tests/test_multi_lang_parsers.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/tests/test_parsers.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/tests/test_selftest_wrappers.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier/__main__.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier/contracts.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier/daemon.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier/diagnostics.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier/health.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier/index.html +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier/library.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier/locking.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier/mcp/__init__.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier/mcp/server.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier/parsers/__init__.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier/parsers/_edge.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier/parsers/bree.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier/parsers/c_cpp.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier/parsers/cdia.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier/parsers/csharp.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier/parsers/go_lang.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier/parsers/java.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier/parsers/javascript.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier/parsers/python.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier/project_root.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier/resolution.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier/scripts/wikifier.bat +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier/scripts/wikifier.ps1 +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier/scripts/wikifier.sh +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier/serve.py +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier.egg-info/dependency_links.txt +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier.egg-info/entry_points.txt +0 -0
- {wikifier-4.6.2 → wikifier-4.6.3}/wikifier.egg-info/requires.txt +0 -0
- {wikifier-4.6.2 → 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.
|
|
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
|
|
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+)** — 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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
|
118
|
-
|
|
119
|
-
|
|
|
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
|
|
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 (
|
|
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
|
|
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+)** — 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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
|
86
|
-
|
|
87
|
-
|
|
|
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
|
|
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 (
|
|
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
|
|
|
@@ -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
|
|
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
|
-
|
|
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")))
|
|
@@ -377,6 +377,24 @@ def prepare_edit(
|
|
|
377
377
|
"low_conf_edges": 0,
|
|
378
378
|
"next_step": "Edit if needed → record_change → refresh wiki → mark_green",
|
|
379
379
|
}
|
|
380
|
+
# Missing path: structured failure (agents must not treat empty preflight as OK)
|
|
381
|
+
src_path = root / rel
|
|
382
|
+
if not src_path.is_file() and not Path(file).is_file():
|
|
383
|
+
out["success"] = False
|
|
384
|
+
out["error"] = f"file not found: {rel}"
|
|
385
|
+
out["next_step"] = "Pass an existing project-relative source path"
|
|
386
|
+
# Still attach health/cache if present (ghost / deleted) for impact analysis
|
|
387
|
+
try:
|
|
388
|
+
health_mod = _health_module()
|
|
389
|
+
data = health_mod.load_health(root)
|
|
390
|
+
ent = (data.get("entries") or {}).get(rel) or {}
|
|
391
|
+
if ent:
|
|
392
|
+
out["status"] = ent.get("status")
|
|
393
|
+
out["reason"] = ent.get("reason")
|
|
394
|
+
out["ghost"] = True
|
|
395
|
+
except Exception:
|
|
396
|
+
pass
|
|
397
|
+
return out
|
|
380
398
|
try:
|
|
381
399
|
health_mod = _health_module()
|
|
382
400
|
data = health_mod.load_health(root)
|