wikifier 4.6.3__tar.gz → 4.6.5__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.3/wikifier.egg-info → wikifier-4.6.5}/PKG-INFO +5 -3
- {wikifier-4.6.3 → wikifier-4.6.5}/README.md +4 -2
- {wikifier-4.6.3 → wikifier-4.6.5}/skills/run.md +3 -1
- wikifier-4.6.5/tests/test_cache_store.py +193 -0
- wikifier-4.6.5/tests/test_walk_coverage_resolvers.py +203 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier/__init__.py +2 -1
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier/agent_loop.py +78 -6
- wikifier-4.6.5/wikifier/cache_store.py +385 -0
- wikifier-4.6.5/wikifier/candidates.py +409 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier/cli.py +242 -228
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier/import_cache.py +110 -38
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier/parsers/c_cpp.py +33 -10
- wikifier-4.6.5/wikifier/parsers/csharp.py +168 -0
- wikifier-4.6.5/wikifier/parsers/go_lang.py +164 -0
- {wikifier-4.6.3 → wikifier-4.6.5/wikifier.egg-info}/PKG-INFO +5 -3
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier.egg-info/SOURCES.txt +6 -0
- wikifier-4.6.3/wikifier/parsers/csharp.py +0 -64
- wikifier-4.6.3/wikifier/parsers/go_lang.py +0 -87
- {wikifier-4.6.3 → wikifier-4.6.5}/CONTRIBUTING.md +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/LICENSE +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/MANIFEST.in +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/diagnostics.html +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/docs/spec.md +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/index.html +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/pyproject.toml +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/setup.cfg +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/tests/test_agent_loop.py +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/tests/test_agent_scale.py +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/tests/test_barrel_invalidation.py +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/tests/test_gap_closure.py +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/tests/test_health.py +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/tests/test_import_cache.py +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/tests/test_multi_lang_parsers.py +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/tests/test_parsers.py +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/tests/test_selftest_wrappers.py +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier/__main__.py +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier/contracts.py +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier/daemon.py +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier/diagnostics.py +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier/health.py +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier/index.html +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier/library.py +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier/locking.py +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier/mcp/__init__.py +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier/mcp/server.py +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier/parsers/__init__.py +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier/parsers/_edge.py +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier/parsers/bree.py +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier/parsers/cdia.py +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier/parsers/java.py +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier/parsers/javascript.py +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier/parsers/python.py +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier/parsers/rust.py +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier/project_root.py +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier/resolution.py +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier/scripts/wikifier.bat +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier/scripts/wikifier.ps1 +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier/scripts/wikifier.sh +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier/serve.py +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier.egg-info/dependency_links.txt +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier.egg-info/entry_points.txt +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/wikifier.egg-info/requires.txt +0 -0
- {wikifier-4.6.3 → wikifier-4.6.5}/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.5
|
|
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
|
|
@@ -109,8 +109,10 @@ Advanced intel as needed: `get_dependencies`, `get_dependents`, `get_cycles`, ba
|
|
|
109
109
|
|
|
110
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
|
|
111
111
|
- **Incremental pipeline** — pure-Python `update-maps`: dirty parse → import cache → reverse deps → cycles → `library.md`
|
|
112
|
-
- **Warm agent maps (4.6.3
|
|
113
|
-
- **
|
|
112
|
+
- **Warm agent maps (4.6.3–4.6.5)** — zero-dirty fast path; **stdlib SQLite** cache; **candidate list reuse** + scoped/monitored-first walks (no whole-tree thrash); content-hash dirty
|
|
113
|
+
- **Partial-map honesty** — `map_coverage` on `update_maps` / bootstrap / **`suggest_next`**; incomplete maps yield `update_maps_until_complete` actions
|
|
114
|
+
- **Cache ops** — `wikifier cache-status` (backend, bytes, ACS, coverage, dual-write policy)
|
|
115
|
+
- **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
|
|
114
116
|
- **Scale** — reverse index + barrel invalidation so one edit doesn’t re-scan the monorepo
|
|
115
117
|
- **MCP tools** — optional server for Claude, Cursor, Cline, and other MCP clients
|
|
116
118
|
- **Zero core dependencies** — stdlib only; forks can add their own stack on top
|
|
@@ -77,8 +77,10 @@ Advanced intel as needed: `get_dependencies`, `get_dependents`, `get_cycles`, ba
|
|
|
77
77
|
|
|
78
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
|
|
79
79
|
- **Incremental pipeline** — pure-Python `update-maps`: dirty parse → import cache → reverse deps → cycles → `library.md`
|
|
80
|
-
- **Warm agent maps (4.6.3
|
|
81
|
-
- **
|
|
80
|
+
- **Warm agent maps (4.6.3–4.6.5)** — zero-dirty fast path; **stdlib SQLite** cache; **candidate list reuse** + scoped/monitored-first walks (no whole-tree thrash); content-hash dirty
|
|
81
|
+
- **Partial-map honesty** — `map_coverage` on `update_maps` / bootstrap / **`suggest_next`**; incomplete maps yield `update_maps_until_complete` actions
|
|
82
|
+
- **Cache ops** — `wikifier cache-status` (backend, bytes, ACS, coverage, dual-write policy)
|
|
83
|
+
- **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
|
|
82
84
|
- **Scale** — reverse index + barrel invalidation so one edit doesn’t re-scan the monorepo
|
|
83
85
|
- **MCP tools** — optional server for Claude, Cursor, Cline, and other MCP clients
|
|
84
86
|
- **Zero core dependencies** — stdlib only; forks can add their own stack on top
|
|
@@ -11,7 +11,9 @@
|
|
|
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. Candidate lists reuse when scope fingerprint is unchanged; prefer `--directory=` / lean `monitored_paths` (not bare `.`).
|
|
16
|
+
- **Cache ops:** `wikifier cache-status` / `from wikifier import cache_status` — backend, sizes, `acs_version`, `map_coverage`. Dual-write JSON only ≤400 files or `WIKIFIER_CACHE_JSON=1` (set `0` to never dual-write).
|
|
15
17
|
- **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
18
|
- **CLI target:** `wikifier --target /abs/project health --summary` (flags stripped before the command). Or `WIKIFIER_PROJECT_ROOT=…`.
|
|
17
19
|
- **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,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()
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Walk cost, Core map_coverage, thin C#/C++ resolve, cache-status (shipped APIs)."""
|
|
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 cache_store as cs
|
|
14
|
+
from wikifier.candidates import (
|
|
15
|
+
collect_candidate_source_files,
|
|
16
|
+
try_cached_candidate_rels,
|
|
17
|
+
candidate_list_meta,
|
|
18
|
+
)
|
|
19
|
+
from wikifier.agent_loop import build_structured_actions
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TestScopedCollect(TempProjectTestCase):
|
|
23
|
+
def test_directory_scope_limits_candidates(self):
|
|
24
|
+
self.write("pkg/a.py", "x=1\n")
|
|
25
|
+
self.write("other/b.py", "y=1\n")
|
|
26
|
+
cands = collect_candidate_source_files(self.root, directory="pkg")
|
|
27
|
+
rels = []
|
|
28
|
+
for p in cands:
|
|
29
|
+
try:
|
|
30
|
+
rels.append(str(Path(p).resolve().relative_to(self.root.resolve())))
|
|
31
|
+
except Exception:
|
|
32
|
+
rels.append(os.path.realpath(str(p)))
|
|
33
|
+
self.assertTrue(any("pkg" in r and r.endswith("a.py") for r in rels), rels)
|
|
34
|
+
self.assertFalse(any("other" in r for r in rels), rels)
|
|
35
|
+
|
|
36
|
+
def test_wiki_monitored_paths_do_not_collapse_map(self):
|
|
37
|
+
"""monitored_paths with only wiki files must not yield a 1-file map."""
|
|
38
|
+
self.write("pkg/a.py", "x=1\n")
|
|
39
|
+
self.write("pkg/b.py", "y=1\n")
|
|
40
|
+
self.write("skills/run.md", "# wiki\n")
|
|
41
|
+
self.write(
|
|
42
|
+
"monitored_paths.txt",
|
|
43
|
+
"# wiki only\nskills/run.md\nREADME.md\npkg/a.py\n",
|
|
44
|
+
)
|
|
45
|
+
self.write("README.md", "# r\n")
|
|
46
|
+
cands = collect_candidate_source_files(self.root, directory=None, use_monitored=True)
|
|
47
|
+
# Must walk full tree, not just a.py from monitored
|
|
48
|
+
py = [p for p in cands if p.suffix == ".py"]
|
|
49
|
+
self.assertGreaterEqual(len(py), 2, [str(p) for p in py])
|
|
50
|
+
|
|
51
|
+
def test_candidate_reuse_true_when_git_clean(self):
|
|
52
|
+
"""Second warm reuses candidate list when scope clean — honest assert."""
|
|
53
|
+
self.write("m.py", "print(1)\n")
|
|
54
|
+
r1 = cli.update_maps(project_root=self.root, full=True)
|
|
55
|
+
self.assertTrue(r1.get("success"), r1)
|
|
56
|
+
n1 = int(r1.get("parseable_files") or 0)
|
|
57
|
+
self.assertGreaterEqual(n1, 1)
|
|
58
|
+
r2 = cli.update_maps(project_root=self.root, full=False)
|
|
59
|
+
self.assertTrue(r2.get("success"), r2)
|
|
60
|
+
r3 = cli.update_maps(project_root=self.root, full=False)
|
|
61
|
+
self.assertTrue(r3.get("success"), r3)
|
|
62
|
+
self.assertTrue(
|
|
63
|
+
r3.get("candidates_reused") is True,
|
|
64
|
+
f"expected reuse on third warm; r2={r2.get('candidates_reused')} r3={r3}",
|
|
65
|
+
)
|
|
66
|
+
self.assertEqual(int(r3.get("parseable_files") or 0), n1)
|
|
67
|
+
|
|
68
|
+
def test_nested_new_file_invalidates_reuse(self):
|
|
69
|
+
"""Adding a nested source file must expand candidates (not stale reuse)."""
|
|
70
|
+
self.write("pkg/a.py", "x=1\n")
|
|
71
|
+
r1 = cli.update_maps(project_root=self.root, full=True)
|
|
72
|
+
self.assertTrue(r1.get("success"), r1)
|
|
73
|
+
n1 = int(r1.get("parseable_files") or 0)
|
|
74
|
+
# Nested new file under existing package
|
|
75
|
+
self.write("pkg/nested/b.py", "y=2\n")
|
|
76
|
+
time.sleep(0.05)
|
|
77
|
+
r2 = cli.update_maps(project_root=self.root, full=False)
|
|
78
|
+
self.assertTrue(r2.get("success"), r2)
|
|
79
|
+
n2 = int(r2.get("parseable_files") or 0)
|
|
80
|
+
self.assertGreater(
|
|
81
|
+
n2, n1,
|
|
82
|
+
f"nested add must expand parseable_files: before={n1} after={n2} reused={r2.get('candidates_reused')}",
|
|
83
|
+
)
|
|
84
|
+
# Must not have reused stale list of size n1
|
|
85
|
+
if r2.get("candidates_reused"):
|
|
86
|
+
self.fail("must not reuse candidate list after nested source add")
|
|
87
|
+
|
|
88
|
+
def test_poisoned_one_file_blob_not_reused(self):
|
|
89
|
+
"""Plant 1-file candidate blob with matching fp; live tree has 2 sources → no reuse."""
|
|
90
|
+
self.write("pkg/a.py", "x=1\n")
|
|
91
|
+
self.write("pkg/b.py", "y=2\n")
|
|
92
|
+
r1 = cli.update_maps(project_root=self.root, full=True)
|
|
93
|
+
self.assertTrue(r1.get("success"), r1)
|
|
94
|
+
live_n = int(r1.get("parseable_files") or 0)
|
|
95
|
+
self.assertGreaterEqual(live_n, 2)
|
|
96
|
+
# Poison meta: same directory scope, current fp, but only one rel
|
|
97
|
+
from wikifier.candidates import scope_fingerprint, candidate_list_meta
|
|
98
|
+
from wikifier import cache_store as cs
|
|
99
|
+
fp = scope_fingerprint(self.root, None)
|
|
100
|
+
poison = {
|
|
101
|
+
"fp": fp,
|
|
102
|
+
"directory": None,
|
|
103
|
+
"count": 1,
|
|
104
|
+
"rels": ["pkg/a.py"],
|
|
105
|
+
}
|
|
106
|
+
cs.save_meta_key(self.root, "_candidate_list", poison)
|
|
107
|
+
# Direct API: try_cached must return None
|
|
108
|
+
meta = cs.load_meta(self.root, keys=("_candidate_list",))
|
|
109
|
+
from wikifier.candidates import try_cached_candidate_rels
|
|
110
|
+
cached = try_cached_candidate_rels(meta, self.root, None)
|
|
111
|
+
self.assertIsNone(
|
|
112
|
+
cached,
|
|
113
|
+
f"poisoned 1-file blob must not reuse when live has {live_n} sources",
|
|
114
|
+
)
|
|
115
|
+
# Shipped update_maps must not report parseable_files=1 with reused=true
|
|
116
|
+
r2 = cli.update_maps(project_root=self.root, full=False)
|
|
117
|
+
self.assertTrue(r2.get("success"), r2)
|
|
118
|
+
self.assertGreaterEqual(int(r2.get("parseable_files") or 0), 2, r2)
|
|
119
|
+
self.assertFalse(
|
|
120
|
+
r2.get("candidates_reused") is True and int(r2.get("parseable_files") or 0) == 1,
|
|
121
|
+
r2,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class TestCoverageInCore(TempProjectTestCase):
|
|
126
|
+
def test_suggest_surfaces_incomplete_map_action(self):
|
|
127
|
+
for i in range(5):
|
|
128
|
+
self.write(f"f{i}.py", f"v={i}\n")
|
|
129
|
+
r = cli.update_maps(project_root=self.root, full=True, max_files=2)
|
|
130
|
+
self.assertTrue(r.get("success"), r)
|
|
131
|
+
cov = r.get("map_coverage") or {}
|
|
132
|
+
self.assertFalse(cov.get("complete"), cov)
|
|
133
|
+
sug = cli.suggest_next_actions(project_root=self.root, format="json")
|
|
134
|
+
self.assertTrue(sug.get("success"), sug)
|
|
135
|
+
self.assertIn("map_coverage", sug)
|
|
136
|
+
self.assertFalse((sug.get("map_coverage") or {}).get("complete"))
|
|
137
|
+
actions = sug.get("actions") or []
|
|
138
|
+
kinds = [a.get("action") for a in actions]
|
|
139
|
+
self.assertIn("update_maps_until_complete", kinds, actions)
|
|
140
|
+
text = " ".join(sug.get("suggestions") or [])
|
|
141
|
+
self.assertIn("INCOMPLETE", text.upper())
|
|
142
|
+
|
|
143
|
+
def test_build_structured_actions_incomplete(self):
|
|
144
|
+
acts = build_structured_actions(
|
|
145
|
+
clean=True,
|
|
146
|
+
map_coverage={"complete": False, "files_remaining_dirty": 9},
|
|
147
|
+
)
|
|
148
|
+
self.assertTrue(any(a["action"] == "update_maps_until_complete" for a in acts))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class TestCacheStatus(TempProjectTestCase):
|
|
152
|
+
def test_cache_status_shape(self):
|
|
153
|
+
self.write("z.py", "z=1\n")
|
|
154
|
+
cli.update_maps(project_root=self.root, full=True)
|
|
155
|
+
st = cli.cache_status(project_root=self.root)
|
|
156
|
+
self.assertTrue(st.get("success"), st)
|
|
157
|
+
self.assertIn(st.get("cache_backend"), ("sqlite", "sqlite+json", "json"))
|
|
158
|
+
self.assertIn("sqlite_bytes", st)
|
|
159
|
+
self.assertIn("dual_write_policy", st)
|
|
160
|
+
self.assertIn("migrate_note", st)
|
|
161
|
+
st2 = cs.cache_status(self.root)
|
|
162
|
+
self.assertEqual(st2.get("cache_backend"), st.get("cache_backend"))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class TestCsharpProjectResolve(TempProjectTestCase):
|
|
166
|
+
def test_project_namespace_resolves(self):
|
|
167
|
+
from wikifier.parsers import csharp as cs_parser
|
|
168
|
+
|
|
169
|
+
self.write(
|
|
170
|
+
"Demo.csproj",
|
|
171
|
+
"<Project><PropertyGroup><RootNamespace>Demo</RootNamespace></PropertyGroup></Project>\n",
|
|
172
|
+
)
|
|
173
|
+
self.write("Util/Helper.cs", "namespace Demo.Util { class Helper {} }\n")
|
|
174
|
+
main = self.write(
|
|
175
|
+
"Program.cs",
|
|
176
|
+
"using Demo.Util;\nnamespace Demo { class Program {} }\n",
|
|
177
|
+
)
|
|
178
|
+
edges = cs_parser.parse_csharp_imports(str(main))
|
|
179
|
+
self.assertTrue(edges)
|
|
180
|
+
resolved = [e for e in edges if e.get("resolved_path")]
|
|
181
|
+
self.assertTrue(resolved, msg=f"edges={edges}")
|
|
182
|
+
self.assertTrue(
|
|
183
|
+
any("Util" in (e.get("resolved_path") or "") for e in resolved),
|
|
184
|
+
resolved,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class TestCCppLocalInclude(TempProjectTestCase):
|
|
189
|
+
def test_include_dir_resolve(self):
|
|
190
|
+
from wikifier.parsers import c_cpp as cc
|
|
191
|
+
|
|
192
|
+
self.write("include/foo.h", "#pragma once\n")
|
|
193
|
+
src = self.write("src/main.c", '#include "foo.h"\nint main(){return 0;}\n')
|
|
194
|
+
edges = cc.parse_c_cpp_imports(str(src))
|
|
195
|
+
self.assertTrue(edges)
|
|
196
|
+
self.assertTrue(
|
|
197
|
+
any(e.get("resolved_path") and "foo.h" in e["resolved_path"] for e in edges),
|
|
198
|
+
edges,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
if __name__ == "__main__":
|
|
203
|
+
unittest.main()
|
|
@@ -48,6 +48,7 @@ from .cli import (
|
|
|
48
48
|
why_file,
|
|
49
49
|
seed_source_content_hashes,
|
|
50
50
|
list_core_tools,
|
|
51
|
+
cache_status,
|
|
51
52
|
)
|
|
52
53
|
from . import agent_loop
|
|
53
54
|
|
|
@@ -78,4 +79,4 @@ from .contracts import (
|
|
|
78
79
|
compute_acs_confidence,
|
|
79
80
|
)
|
|
80
81
|
|
|
81
|
-
__version__ = "4.6.
|
|
82
|
+
__version__ = "4.6.5"
|
|
@@ -176,6 +176,7 @@ def build_structured_actions(
|
|
|
176
176
|
scope_warnings: Optional[List[str]] = None,
|
|
177
177
|
clean: bool = False,
|
|
178
178
|
max_file_actions: int = 12,
|
|
179
|
+
map_coverage: Optional[Dict[str, Any]] = None,
|
|
179
180
|
) -> List[Dict[str, Any]]:
|
|
180
181
|
"""Build dispatchable action objects for agents (not prose-only).
|
|
181
182
|
|
|
@@ -183,6 +184,28 @@ def build_structured_actions(
|
|
|
183
184
|
"""
|
|
184
185
|
actions: List[Dict[str, Any]] = []
|
|
185
186
|
prio = 1
|
|
187
|
+
cov = map_coverage if isinstance(map_coverage, dict) else {}
|
|
188
|
+
remaining = int(cov.get("files_remaining_dirty") or 0)
|
|
189
|
+
complete = cov.get("complete")
|
|
190
|
+
# Incomplete map under budget — higher priority than ACS thrash
|
|
191
|
+
if complete is False or remaining > 0:
|
|
192
|
+
actions.append({
|
|
193
|
+
"action": "update_maps_until_complete",
|
|
194
|
+
"file": None,
|
|
195
|
+
"priority": 3,
|
|
196
|
+
"reason": (
|
|
197
|
+
f"Map incomplete: files_remaining_dirty={remaining}, complete={complete}. "
|
|
198
|
+
"Re-run update_maps (same directory/max_files) until map_coverage.complete=true; "
|
|
199
|
+
"success alone does not mean the map is done."
|
|
200
|
+
),
|
|
201
|
+
"preflight": ["update_maps"],
|
|
202
|
+
"map_coverage": {
|
|
203
|
+
"complete": complete,
|
|
204
|
+
"files_remaining_dirty": remaining,
|
|
205
|
+
"files_skipped": cov.get("files_skipped"),
|
|
206
|
+
"budget_max_files": cov.get("budget_max_files"),
|
|
207
|
+
},
|
|
208
|
+
})
|
|
186
209
|
for f in (red_files or [])[:max_file_actions]:
|
|
187
210
|
actions.append({
|
|
188
211
|
"action": "investigate_red",
|
|
@@ -201,7 +224,7 @@ def build_structured_actions(
|
|
|
201
224
|
"preflight": ["prepare_edit", "why_file"],
|
|
202
225
|
})
|
|
203
226
|
prio = min(prio + 1, 9)
|
|
204
|
-
if red == 0 and actionable_yellow == 0 and clean:
|
|
227
|
+
if red == 0 and actionable_yellow == 0 and clean and remaining == 0:
|
|
205
228
|
actions.append({
|
|
206
229
|
"action": "lookup_only",
|
|
207
230
|
"file": None,
|
|
@@ -222,7 +245,10 @@ def build_structured_actions(
|
|
|
222
245
|
"action": "review_acs",
|
|
223
246
|
"file": None,
|
|
224
247
|
"priority": 4,
|
|
225
|
-
"reason":
|
|
248
|
+
"reason": (
|
|
249
|
+
f"{acs_actionable} actionable low-confidence project edges "
|
|
250
|
+
"(prefer actionable_low_conf_edges + reason_code_counts; not raw low_conf_edges)"
|
|
251
|
+
),
|
|
226
252
|
"preflight": ["get_dependencies"],
|
|
227
253
|
})
|
|
228
254
|
for w in (scope_warnings or [])[:2]:
|
|
@@ -233,7 +259,10 @@ def build_structured_actions(
|
|
|
233
259
|
"reason": str(w),
|
|
234
260
|
"preflight": [],
|
|
235
261
|
})
|
|
236
|
-
if not any(
|
|
262
|
+
if not any(
|
|
263
|
+
a["action"] in ("update_maps", "update_maps_until_complete", "update_maps_if_structure")
|
|
264
|
+
for a in actions
|
|
265
|
+
):
|
|
237
266
|
actions.append({
|
|
238
267
|
"action": "update_maps_if_structure",
|
|
239
268
|
"file": None,
|
|
@@ -548,8 +577,33 @@ def session_bootstrap(
|
|
|
548
577
|
acs_actionable = 0
|
|
549
578
|
try:
|
|
550
579
|
from . import import_cache as ic
|
|
551
|
-
|
|
552
|
-
|
|
580
|
+
try:
|
|
581
|
+
from . import cache_store as cs
|
|
582
|
+
except Exception:
|
|
583
|
+
cs = None # type: ignore
|
|
584
|
+
# Prefer light meta read (SQLite) over full multi-MB pair deserialize
|
|
585
|
+
acs: Dict[str, Any] = {}
|
|
586
|
+
cyc: Dict[str, Any] = {}
|
|
587
|
+
if cs is not None:
|
|
588
|
+
meta = cs.load_meta(root, keys=("_acs_summary", "_cycles", "_map_coverage"))
|
|
589
|
+
acs = meta.get("_acs_summary") if isinstance(meta.get("_acs_summary"), dict) else {}
|
|
590
|
+
cyc = meta.get("_cycles") if isinstance(meta.get("_cycles"), dict) else {}
|
|
591
|
+
cov = meta.get("_map_coverage") if isinstance(meta.get("_map_coverage"), dict) else {}
|
|
592
|
+
if cov:
|
|
593
|
+
out["map_coverage"] = cov
|
|
594
|
+
out["cache_backend"] = cs.backend_name(root)
|
|
595
|
+
needs_full = (
|
|
596
|
+
not acs
|
|
597
|
+
or str(acs.get("acs_version") or "") < "1.3"
|
|
598
|
+
or "reason_code_counts" not in acs
|
|
599
|
+
or "actionable_low_conf_edges" not in acs
|
|
600
|
+
)
|
|
601
|
+
if needs_full:
|
|
602
|
+
cache = ic.load_cache(root) or {}
|
|
603
|
+
acs = ic.ensure_acs_summary_persisted(cache, root) or {}
|
|
604
|
+
cyc = cache.get("_cycles") or {}
|
|
605
|
+
if cs is not None and not out.get("cache_backend"):
|
|
606
|
+
out["cache_backend"] = cs.backend_name(root)
|
|
553
607
|
acs_actionable = int(acs.get("actionable_low_conf_edges") or 0)
|
|
554
608
|
out["acs"] = {
|
|
555
609
|
"acs_version": acs.get("acs_version"),
|
|
@@ -557,12 +611,22 @@ def session_bootstrap(
|
|
|
557
611
|
"low_conf_edges": acs.get("low_conf_edges"),
|
|
558
612
|
"dynamic_literal_noise_edges": acs.get("dynamic_literal_noise_edges"),
|
|
559
613
|
"avg_confidence": acs.get("avg_confidence"),
|
|
614
|
+
"reason_code_counts": acs.get("reason_code_counts"),
|
|
615
|
+
"agent_signal_counts": acs.get("agent_signal_counts"),
|
|
560
616
|
}
|
|
561
|
-
|
|
617
|
+
# Agents must not thrash on raw low_conf_edges (ACS v1.3 scores more edges)
|
|
618
|
+
out["acs_guidance"] = (
|
|
619
|
+
"Prefer actionable_low_conf_edges + reason_code_counts "
|
|
620
|
+
"(skip external_or_bare/dynamic_literal); do not use low_conf_edges alone as a work queue."
|
|
621
|
+
)
|
|
562
622
|
out["cycles"] = {
|
|
563
623
|
"cyclic_scc_count": (cyc.get("stats") or {}).get("cyclic_scc_count"),
|
|
564
624
|
"all_cycle_files_sample": (cyc.get("all_cycle_files") or [])[:10],
|
|
565
625
|
}
|
|
626
|
+
if "map_coverage" not in out and cs is not None:
|
|
627
|
+
snap = cs.get_map_coverage_from_meta(root)
|
|
628
|
+
if snap.get("map_coverage"):
|
|
629
|
+
out["map_coverage"] = snap["map_coverage"]
|
|
566
630
|
except Exception as e:
|
|
567
631
|
out["acs_error"] = str(e)
|
|
568
632
|
|
|
@@ -574,6 +638,7 @@ def session_bootstrap(
|
|
|
574
638
|
if isinstance(out.get("scope"), dict):
|
|
575
639
|
scope_warnings = list(out["scope"].get("warnings") or [])
|
|
576
640
|
|
|
641
|
+
map_cov = out.get("map_coverage") if isinstance(out.get("map_coverage"), dict) else {}
|
|
577
642
|
out["actions"] = build_structured_actions(
|
|
578
643
|
red_files=red_files,
|
|
579
644
|
actionable_yellow_files=actionable_yellow_files,
|
|
@@ -583,12 +648,19 @@ def session_bootstrap(
|
|
|
583
648
|
acs_actionable=acs_actionable,
|
|
584
649
|
scope_warnings=scope_warnings,
|
|
585
650
|
clean=clean,
|
|
651
|
+
map_coverage=map_cov,
|
|
586
652
|
)
|
|
587
653
|
out["work_items"] = out["actions"] # alias
|
|
588
654
|
out["selective_work"] = True
|
|
589
655
|
out["map_first"] = True
|
|
656
|
+
rem = int(map_cov.get("files_remaining_dirty") or 0) if map_cov else 0
|
|
590
657
|
out["message"] = (
|
|
591
658
|
"session_bootstrap ready — use actions[] / attention; "
|
|
592
659
|
"prepare_edit(file) before large edits; record_change after edits."
|
|
660
|
+
+ (
|
|
661
|
+
f" MAP INCOMPLETE: files_remaining_dirty={rem} — re-run update_maps until complete."
|
|
662
|
+
if rem > 0 or map_cov.get("complete") is False
|
|
663
|
+
else ""
|
|
664
|
+
)
|
|
593
665
|
)
|
|
594
666
|
return out
|