wikifier 4.6.4__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.4/wikifier.egg-info → wikifier-4.6.5}/PKG-INFO +4 -3
- {wikifier-4.6.4 → wikifier-4.6.5}/README.md +3 -2
- {wikifier-4.6.4 → wikifier-4.6.5}/skills/run.md +2 -1
- wikifier-4.6.5/tests/test_walk_coverage_resolvers.py +203 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier/__init__.py +2 -1
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier/agent_loop.py +40 -3
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier/cache_store.py +37 -0
- wikifier-4.6.5/wikifier/candidates.py +409 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier/cli.py +102 -174
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier/parsers/c_cpp.py +33 -10
- wikifier-4.6.5/wikifier/parsers/csharp.py +168 -0
- {wikifier-4.6.4 → wikifier-4.6.5/wikifier.egg-info}/PKG-INFO +4 -3
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier.egg-info/SOURCES.txt +3 -0
- wikifier-4.6.4/wikifier/parsers/csharp.py +0 -64
- {wikifier-4.6.4 → wikifier-4.6.5}/CONTRIBUTING.md +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/LICENSE +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/MANIFEST.in +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/diagnostics.html +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/docs/spec.md +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/index.html +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/pyproject.toml +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/setup.cfg +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/tests/test_agent_loop.py +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/tests/test_agent_scale.py +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/tests/test_barrel_invalidation.py +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/tests/test_cache_store.py +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/tests/test_gap_closure.py +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/tests/test_health.py +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/tests/test_import_cache.py +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/tests/test_multi_lang_parsers.py +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/tests/test_parsers.py +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/tests/test_selftest_wrappers.py +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier/__main__.py +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier/contracts.py +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier/daemon.py +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier/diagnostics.py +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier/health.py +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier/import_cache.py +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier/index.html +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier/library.py +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier/locking.py +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier/mcp/__init__.py +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier/mcp/server.py +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier/parsers/__init__.py +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier/parsers/_edge.py +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier/parsers/bree.py +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier/parsers/cdia.py +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier/parsers/go_lang.py +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier/parsers/java.py +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier/parsers/javascript.py +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier/parsers/python.py +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier/parsers/rust.py +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier/project_root.py +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier/resolution.py +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier/scripts/wikifier.bat +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier/scripts/wikifier.ps1 +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier/scripts/wikifier.sh +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier/serve.py +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier.egg-info/dependency_links.txt +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier.egg-info/entry_points.txt +0 -0
- {wikifier-4.6.4 → wikifier-4.6.5}/wikifier.egg-info/requires.txt +0 -0
- {wikifier-4.6.4 → 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,9 @@ 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
|
-
- **Partial-map honesty** — `map_coverage` on `update_maps` / bootstrap
|
|
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)
|
|
114
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
|
|
115
116
|
- **Scale** — reverse index + barrel invalidation so one edit doesn’t re-scan the monorepo
|
|
116
117
|
- **MCP tools** — optional server for Claude, Cursor, Cline, and other MCP clients
|
|
@@ -77,8 +77,9 @@ 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
|
-
- **Partial-map honesty** — `map_coverage` on `update_maps` / bootstrap
|
|
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)
|
|
82
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
|
|
83
84
|
- **Scale** — reverse index + barrel invalidation so one edit doesn’t re-scan the monorepo
|
|
84
85
|
- **MCP tools** — optional server for Claude, Cursor, Cline, and other MCP clients
|
|
@@ -12,7 +12,8 @@
|
|
|
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
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
|
+
- **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).
|
|
16
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`).
|
|
17
18
|
- **CLI target:** `wikifier --target /abs/project health --summary` (flags stripped before the command). Or `WIKIFIER_PROJECT_ROOT=…`.
|
|
18
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,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,
|
|
@@ -609,6 +638,7 @@ def session_bootstrap(
|
|
|
609
638
|
if isinstance(out.get("scope"), dict):
|
|
610
639
|
scope_warnings = list(out["scope"].get("warnings") or [])
|
|
611
640
|
|
|
641
|
+
map_cov = out.get("map_coverage") if isinstance(out.get("map_coverage"), dict) else {}
|
|
612
642
|
out["actions"] = build_structured_actions(
|
|
613
643
|
red_files=red_files,
|
|
614
644
|
actionable_yellow_files=actionable_yellow_files,
|
|
@@ -618,12 +648,19 @@ def session_bootstrap(
|
|
|
618
648
|
acs_actionable=acs_actionable,
|
|
619
649
|
scope_warnings=scope_warnings,
|
|
620
650
|
clean=clean,
|
|
651
|
+
map_coverage=map_cov,
|
|
621
652
|
)
|
|
622
653
|
out["work_items"] = out["actions"] # alias
|
|
623
654
|
out["selective_work"] = True
|
|
624
655
|
out["map_first"] = True
|
|
656
|
+
rem = int(map_cov.get("files_remaining_dirty") or 0) if map_cov else 0
|
|
625
657
|
out["message"] = (
|
|
626
658
|
"session_bootstrap ready — use actions[] / attention; "
|
|
627
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
|
+
)
|
|
628
665
|
)
|
|
629
666
|
return out
|
|
@@ -346,3 +346,40 @@ def get_map_coverage_from_meta(root: Path) -> Dict[str, Any]:
|
|
|
346
346
|
"actionable_low_conf_edges": acs.get("actionable_low_conf_edges"),
|
|
347
347
|
"reason_code_counts": acs.get("reason_code_counts"),
|
|
348
348
|
}
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def cache_status(root: Path) -> Dict[str, Any]:
|
|
352
|
+
"""Machine-readable dual-cache status for agents/ops (zero full pair load)."""
|
|
353
|
+
root = Path(root)
|
|
354
|
+
sp = sqlite_path(root)
|
|
355
|
+
jp = json_path(root)
|
|
356
|
+
backend = backend_name(root)
|
|
357
|
+
sqlite_bytes = int(sp.stat().st_size) if sp.is_file() else 0
|
|
358
|
+
json_bytes = int(jp.stat().st_size) if jp.is_file() else 0
|
|
359
|
+
snap = get_map_coverage_from_meta(root)
|
|
360
|
+
meta = load_meta(root, keys=("_acs_summary", "_map_coverage", "_candidate_list"))
|
|
361
|
+
acs = meta.get("_acs_summary") if isinstance(meta.get("_acs_summary"), dict) else {}
|
|
362
|
+
cand = meta.get("_candidate_list") if isinstance(meta.get("_candidate_list"), dict) else {}
|
|
363
|
+
return {
|
|
364
|
+
"success": True,
|
|
365
|
+
"project_root": str(root.resolve()) if root.exists() else str(root),
|
|
366
|
+
"cache_backend": backend,
|
|
367
|
+
"sqlite_path": str(sp) if sp.is_file() else None,
|
|
368
|
+
"json_path": str(jp) if jp.is_file() else None,
|
|
369
|
+
"sqlite_bytes": sqlite_bytes,
|
|
370
|
+
"json_bytes": json_bytes,
|
|
371
|
+
"acs_version": acs.get("acs_version") or snap.get("acs_version"),
|
|
372
|
+
"actionable_low_conf_edges": acs.get("actionable_low_conf_edges"),
|
|
373
|
+
"map_coverage": snap.get("map_coverage") or meta.get("_map_coverage"),
|
|
374
|
+
"candidate_list_count": cand.get("count"),
|
|
375
|
+
"candidate_list_directory": cand.get("directory"),
|
|
376
|
+
"dual_write_policy": (
|
|
377
|
+
"SQLite is primary. Legacy JSON dual-read remains. "
|
|
378
|
+
"Dual-write JSON only when file count ≤400 or WIKIFIER_CACHE_JSON=1; "
|
|
379
|
+
"set WIKIFIER_CACHE_JSON=0 to never dual-write. Prefer sqlite for warm agents."
|
|
380
|
+
),
|
|
381
|
+
"migrate_note": (
|
|
382
|
+
"update_maps migrates legacy import_cache.json → import_cache.sqlite once "
|
|
383
|
+
"when sqlite is missing."
|
|
384
|
+
),
|
|
385
|
+
}
|