wikifier 4.5.5__tar.gz → 4.5.6__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.5.5/wikifier.egg-info → wikifier-4.5.6}/PKG-INFO +1 -1
- {wikifier-4.5.5 → wikifier-4.5.6}/tests/test_gap_closure.py +80 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/__init__.py +1 -1
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/cli.py +63 -23
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/health.py +349 -38
- {wikifier-4.5.5 → wikifier-4.5.6/wikifier.egg-info}/PKG-INFO +1 -1
- {wikifier-4.5.5 → wikifier-4.5.6}/CONTRIBUTING.md +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/LICENSE +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/MANIFEST.in +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/README.md +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/diagnostics.html +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/docs/spec.md +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/index.html +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/pyproject.toml +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/setup.cfg +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/skills/run.md +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/tests/test_barrel_invalidation.py +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/tests/test_health.py +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/tests/test_import_cache.py +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/tests/test_multi_lang_parsers.py +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/tests/test_parsers.py +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/tests/test_selftest_wrappers.py +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/__main__.py +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/contracts.py +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/daemon.py +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/diagnostics.py +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/import_cache.py +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/index.html +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/library.py +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/locking.py +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/mcp/__init__.py +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/mcp/server.py +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/parsers/__init__.py +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/parsers/_edge.py +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/parsers/bree.py +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/parsers/c_cpp.py +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/parsers/cdia.py +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/parsers/csharp.py +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/parsers/go_lang.py +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/parsers/java.py +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/parsers/javascript.py +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/parsers/python.py +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/parsers/rust.py +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/resolution.py +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/scripts/wikifier.bat +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/scripts/wikifier.ps1 +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/scripts/wikifier.sh +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/serve.py +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier.egg-info/SOURCES.txt +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier.egg-info/dependency_links.txt +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier.egg-info/entry_points.txt +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/wikifier.egg-info/requires.txt +0 -0
- {wikifier-4.5.5 → wikifier-4.5.6}/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.5.
|
|
3
|
+
Version: 4.5.6
|
|
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
|
|
@@ -190,5 +190,85 @@ class TestPendingAndPollutionHygiene(TempProjectTestCase):
|
|
|
190
190
|
self.assertIn("gone_real.py", entries2)
|
|
191
191
|
|
|
192
192
|
|
|
193
|
+
class TestMapFirstHealthAndValidate(TempProjectTestCase):
|
|
194
|
+
def test_deep_relative_keys_are_under_root(self):
|
|
195
|
+
"""Regression: deep monorepo rel paths must not be treated as abs pollution."""
|
|
196
|
+
health_mod = importlib.import_module("wikifier.health")
|
|
197
|
+
deep = "airflow-core/src/airflow/api/client/local_client.py"
|
|
198
|
+
self.assertTrue(health_mod._entry_is_under_root(self.root, deep))
|
|
199
|
+
self.assertTrue(health_mod._entry_is_under_root(self.root, "pkg/a.py"))
|
|
200
|
+
|
|
201
|
+
def test_seed_health_from_map_creates_stubs(self):
|
|
202
|
+
health_mod = importlib.import_module("wikifier.health")
|
|
203
|
+
from wikifier import import_cache as ic
|
|
204
|
+
|
|
205
|
+
self.write("pkg/a.py", "import b\n")
|
|
206
|
+
self.write("pkg/b.py", "x=1\n")
|
|
207
|
+
self.write("README.md", "# hi\n")
|
|
208
|
+
cache = {}
|
|
209
|
+
ic.update_file_data(cache, "pkg/a.py", mtime=1, imports=["b"], resolved_pairs=[])
|
|
210
|
+
ic.update_file_data(cache, "pkg/b.py", mtime=1, imports=[], resolved_pairs=[])
|
|
211
|
+
ic.save_cache(self.root, cache)
|
|
212
|
+
|
|
213
|
+
# No health file yet
|
|
214
|
+
self.assertFalse((self.root / "file_health.json").exists())
|
|
215
|
+
res = health_mod.seed_health_from_map(self.root)
|
|
216
|
+
self.assertTrue(res.get("success"))
|
|
217
|
+
self.assertGreaterEqual(res.get("seeded"), 2)
|
|
218
|
+
entries = health_mod.load_health(self.root)["entries"]
|
|
219
|
+
self.assertIn("pkg/a.py", entries)
|
|
220
|
+
self.assertIn("pkg/b.py", entries)
|
|
221
|
+
self.assertTrue(entries["pkg/a.py"]["status"].startswith("🟡"))
|
|
222
|
+
|
|
223
|
+
def test_validate_ignores_non_source_and_reports_mapped_gaps(self):
|
|
224
|
+
health_mod = importlib.import_module("wikifier.health")
|
|
225
|
+
from wikifier import import_cache as ic
|
|
226
|
+
|
|
227
|
+
self.write("src/app.py", "print(1)\n")
|
|
228
|
+
self.write("src/notes.md", "docs\n")
|
|
229
|
+
self.write("src/logo.png", "x")
|
|
230
|
+
(self.root / "monitored_paths.txt").write_text("src\n", encoding="utf-8")
|
|
231
|
+
cache = {}
|
|
232
|
+
ic.update_file_data(cache, "src/app.py", mtime=1, imports=[], resolved_pairs=[])
|
|
233
|
+
ic.update_file_data(cache, "src/other.py", mtime=1, imports=[], resolved_pairs=[])
|
|
234
|
+
ic.save_cache(self.root, cache)
|
|
235
|
+
|
|
236
|
+
# No health yet — in-scope mapped + monitored parseable gaps
|
|
237
|
+
v = health_mod.validate_health(self.root)
|
|
238
|
+
self.assertTrue(v.get("map_first"))
|
|
239
|
+
self.assertGreaterEqual(v.get("mapped_in_scope_without_health_count", 0), 1)
|
|
240
|
+
self.assertGreaterEqual(v.get("missing_count", 0), 1)
|
|
241
|
+
# notes.md / logo.png must not appear as monitored-source gaps
|
|
242
|
+
mon_missing = v.get("missing_monitored_source") or []
|
|
243
|
+
self.assertTrue(all(not m.endswith(".md") and not m.endswith(".png") for m in mon_missing))
|
|
244
|
+
|
|
245
|
+
health_mod.seed_health_from_map(self.root) # only_monitored default
|
|
246
|
+
# Seed in-scope map keys; app.py on disk + mapped in-scope covered
|
|
247
|
+
v2 = health_mod.validate_health(self.root)
|
|
248
|
+
self.assertEqual(v2.get("mapped_in_scope_without_health_count"), 0)
|
|
249
|
+
# missing_count may still include on-disk sources not in map (ok if 0 after seed of map keys only)
|
|
250
|
+
self.assertEqual(v2.get("mapped_in_scope_without_health_count"), 0)
|
|
251
|
+
|
|
252
|
+
def test_prune_pending_and_health_to_monitored(self):
|
|
253
|
+
health_mod = importlib.import_module("wikifier.health")
|
|
254
|
+
(self.root / "monitored_paths.txt").write_text("src\n", encoding="utf-8")
|
|
255
|
+
self.write("src/keep.py", "k=1\n")
|
|
256
|
+
self.write("other/out.py", "o=1\n")
|
|
257
|
+
health_mod.upsert_entry(self.root, "src/keep.py", "🟡 Yellow", "in scope")
|
|
258
|
+
health_mod.upsert_entry(self.root, "other/out.py", "🟡 Yellow", "out of scope")
|
|
259
|
+
health_mod.add_to_pending(self.root, "src/keep.py", "need wiki")
|
|
260
|
+
health_mod.add_to_pending(self.root, "other/out.py", "noise")
|
|
261
|
+
|
|
262
|
+
pr = health_mod.prune_pending_to_monitored(self.root)
|
|
263
|
+
self.assertEqual(pr.get("removed"), 1)
|
|
264
|
+
self.assertEqual(health_mod.count_pending(self.root), 1)
|
|
265
|
+
|
|
266
|
+
hr = health_mod.prune_health_outside_monitored(self.root)
|
|
267
|
+
self.assertEqual(hr.get("removed"), 1)
|
|
268
|
+
entries = health_mod.load_health(self.root)["entries"]
|
|
269
|
+
self.assertIn("src/keep.py", entries)
|
|
270
|
+
self.assertNotIn("other/out.py", entries)
|
|
271
|
+
|
|
272
|
+
|
|
193
273
|
if __name__ == "__main__":
|
|
194
274
|
unittest.main()
|
|
@@ -616,23 +616,6 @@ def run_full_update(
|
|
|
616
616
|
result["parse_errors"] = parse_errors[:10]
|
|
617
617
|
result["parse_error_count"] = len(parse_errors)
|
|
618
618
|
|
|
619
|
-
# Seed health stubs for newly parsed files (map-first wiki: agents fill prose later)
|
|
620
|
-
health_seeded = 0
|
|
621
|
-
if _health_mod is not None and new_entries:
|
|
622
|
-
try:
|
|
623
|
-
existing = (_health_mod.load_health(root) or {}).get("entries") or {}
|
|
624
|
-
for rel in new_entries:
|
|
625
|
-
if rel in existing:
|
|
626
|
-
continue
|
|
627
|
-
_health_mod.upsert_entry(
|
|
628
|
-
root, rel, "🟡 Yellow",
|
|
629
|
-
"Initial stub — parsed into map; agent should wiki + mark-green when editing"
|
|
630
|
-
)
|
|
631
|
-
health_seeded += 1
|
|
632
|
-
except Exception:
|
|
633
|
-
health_seeded = 0
|
|
634
|
-
result["health_stubs_seeded"] = health_seeded
|
|
635
|
-
|
|
636
619
|
# === 4. Persist (single save; reload first to pick up the barrel flush) ===
|
|
637
620
|
cache = ic.load_cache(root) or {}
|
|
638
621
|
cache.update(new_entries)
|
|
@@ -669,6 +652,27 @@ def run_full_update(
|
|
|
669
652
|
except Exception:
|
|
670
653
|
pass
|
|
671
654
|
|
|
655
|
+
# === 5b. Map-first health stubs (always backfill; warm cache safe) ===
|
|
656
|
+
# 0-dirty incremental runs never used to create file_health.json — fixed here.
|
|
657
|
+
health_seeded = 0
|
|
658
|
+
if _health_mod is not None and hasattr(_health_mod, "seed_health_from_map"):
|
|
659
|
+
try:
|
|
660
|
+
max_seed = int(os.environ.get("WIKIFIER_HEALTH_SEED_MAX", "20000") or "20000")
|
|
661
|
+
map_keys = [k for k in cache if isinstance(k, str) and k and not k.startswith("_")]
|
|
662
|
+
seed_res = _health_mod.seed_health_from_map(
|
|
663
|
+
root, map_keys=map_keys, max_new=max_seed,
|
|
664
|
+
)
|
|
665
|
+
health_seeded = int(seed_res.get("seeded") or 0)
|
|
666
|
+
if hasattr(_health_mod, "seed_health_for_monitored_sources"):
|
|
667
|
+
disk_res = _health_mod.seed_health_for_monitored_sources(
|
|
668
|
+
root, max_new=max_seed,
|
|
669
|
+
)
|
|
670
|
+
health_seeded += int(disk_res.get("seeded") or 0)
|
|
671
|
+
except Exception as se:
|
|
672
|
+
result["health_seed_error"] = str(se)
|
|
673
|
+
health_seeded = 0
|
|
674
|
+
result["health_stubs_seeded"] = health_seeded
|
|
675
|
+
|
|
672
676
|
# === 6. library.md (atomic; pure Python) ===
|
|
673
677
|
try:
|
|
674
678
|
from .library import write_library_md
|
|
@@ -857,13 +861,40 @@ def main():
|
|
|
857
861
|
if _health_mod is not None:
|
|
858
862
|
root = _get_effective_root(project_root)
|
|
859
863
|
res = _health_mod.validate_health(root)
|
|
860
|
-
ghosts = []
|
|
861
|
-
if hasattr(_health_mod, "find_ghost_entries"):
|
|
862
|
-
ghosts = _health_mod.find_ghost_entries(root)
|
|
863
|
-
res["ghost_count"] = len(ghosts)
|
|
864
|
-
res["ghosts"] = ghosts[:50]
|
|
865
864
|
print(_json.dumps(res, indent=2, default=str))
|
|
865
|
+
# Map-first: exit 0 when map is covered (or no map + no monitored source gaps)
|
|
866
866
|
return 0 if res.get("missing_count", 0) == 0 else 1
|
|
867
|
+
if _cmd0 in ("seed-health", "seed-health-from-map"):
|
|
868
|
+
import json as _json
|
|
869
|
+
if _health_mod is None:
|
|
870
|
+
print("[wikifier] health module unavailable", file=sys.stderr)
|
|
871
|
+
return 1
|
|
872
|
+
root = _get_effective_root(project_root)
|
|
873
|
+
res = _health_mod.seed_health_from_map(root)
|
|
874
|
+
if hasattr(_health_mod, "seed_health_for_monitored_sources"):
|
|
875
|
+
disk = _health_mod.seed_health_for_monitored_sources(root)
|
|
876
|
+
res["disk_seeded"] = disk.get("seeded")
|
|
877
|
+
res["seeded_total"] = int(res.get("seeded") or 0) + int(disk.get("seeded") or 0)
|
|
878
|
+
print(_json.dumps(res, indent=2, default=str))
|
|
879
|
+
return 0 if res.get("success") else 1
|
|
880
|
+
if _cmd0 in ("prune-pending", "prune-pending-monitored"):
|
|
881
|
+
import json as _json
|
|
882
|
+
if _health_mod is None:
|
|
883
|
+
print("[wikifier] health module unavailable", file=sys.stderr)
|
|
884
|
+
return 1
|
|
885
|
+
root = _get_effective_root(project_root)
|
|
886
|
+
res = _health_mod.prune_pending_to_monitored(root)
|
|
887
|
+
print(_json.dumps(res, indent=2, default=str))
|
|
888
|
+
return 0 if res.get("success") else 1
|
|
889
|
+
if _cmd0 in ("prune-health-monitored", "prune-health"):
|
|
890
|
+
import json as _json
|
|
891
|
+
if _health_mod is None:
|
|
892
|
+
print("[wikifier] health module unavailable", file=sys.stderr)
|
|
893
|
+
return 1
|
|
894
|
+
root = _get_effective_root(project_root)
|
|
895
|
+
res = _health_mod.prune_health_outside_monitored(root)
|
|
896
|
+
print(_json.dumps(res, indent=2, default=str))
|
|
897
|
+
return 0 if res.get("success") else 1
|
|
867
898
|
except Exception as e:
|
|
868
899
|
print(f"[wikifier] Python-primary {_cmd0} failed: {e}", file=sys.stderr)
|
|
869
900
|
return 1
|
|
@@ -1197,6 +1228,8 @@ def check_changes(project_root: Optional[Union[str, Path]] = None) -> Dict[str,
|
|
|
1197
1228
|
ghosts_marked = 0
|
|
1198
1229
|
if _health_mod is not None:
|
|
1199
1230
|
root_res = root.resolve()
|
|
1231
|
+
# Prefer unlocked helpers while we already hold project lock
|
|
1232
|
+
_upsert = getattr(_health_mod, "_do_upsert_entry", None) or _health_mod.upsert_entry
|
|
1200
1233
|
for p in dirty_batch:
|
|
1201
1234
|
try:
|
|
1202
1235
|
pr = Path(p).resolve()
|
|
@@ -1205,13 +1238,20 @@ def check_changes(project_root: Optional[Union[str, Path]] = None) -> Dict[str,
|
|
|
1205
1238
|
rel = str(pr.relative_to(root_res))
|
|
1206
1239
|
except Exception:
|
|
1207
1240
|
continue
|
|
1208
|
-
|
|
1241
|
+
_upsert(
|
|
1209
1242
|
root, rel, "🟡 Yellow",
|
|
1210
1243
|
"mtime changed since last check-changes (Python primary auto-detected)"
|
|
1211
1244
|
)
|
|
1212
1245
|
_add_to_pending(root, rel, "Auto-detected modification — review and run mark-green after wiki update")
|
|
1213
1246
|
_ensure_journal_entry(root, "auto-detected", rel, "File mtime changed (check_changes Python primary)")
|
|
1214
1247
|
changed_count += 1
|
|
1248
|
+
# Keep pending queue aligned with lean monitored_paths (no flood outside scope)
|
|
1249
|
+
if hasattr(_health_mod, "prune_pending_to_monitored"):
|
|
1250
|
+
try:
|
|
1251
|
+
pr = _health_mod.prune_pending_to_monitored(root)
|
|
1252
|
+
result["pending_pruned"] = pr.get("removed", 0)
|
|
1253
|
+
except Exception:
|
|
1254
|
+
pass
|
|
1215
1255
|
|
|
1216
1256
|
# G7: surface ghost health entries (tracked path missing on disk, not already DELETED)
|
|
1217
1257
|
try:
|
|
@@ -4,9 +4,11 @@ Documentation health matrix (agent-first).
|
|
|
4
4
|
AGENT MAP:
|
|
5
5
|
load_health / save_health / upsert_entry — file_health.json SSOT (+ regenerate .md)
|
|
6
6
|
get_summary / get_files_needing_attention — 🟢🟡🔴 counts and lists
|
|
7
|
-
find_ghost_entries / validate_health —
|
|
7
|
+
find_ghost_entries / validate_health — map-first gaps + ghosts (parseable sources only)
|
|
8
|
+
seed_health_from_map — backfill 🟡 stubs from import_cache (warm-cache safe)
|
|
9
|
+
prune_pending_to_monitored / prune_health_outside_monitored — lean-monitor hygiene
|
|
8
10
|
mark_green / heal_stubs / apply_barrel_invalidation_reports — status mutations
|
|
9
|
-
CLI:
|
|
11
|
+
CLI: seed-health | prune-pending | prune-health | validate | health --summary
|
|
10
12
|
Agents: prefer MCP health / check_changes / mark_green; only open this for matrix policy.
|
|
11
13
|
|
|
12
14
|
JSON Schema (v2 - additive from v1):
|
|
@@ -156,21 +158,31 @@ def _normalize_to_relative(root: Path, file_key: str) -> str:
|
|
|
156
158
|
|
|
157
159
|
def _entry_is_under_root(root: Path, file_key: str) -> bool:
|
|
158
160
|
"""Return True only for entries whose path (abs or rel) resolves under the project root.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
161
|
+
|
|
162
|
+
Filters pollution from other trees (worktrees, cross-project abs paths).
|
|
163
|
+
Relative monorepo keys may be deep (e.g. a/b/c/d/e/f.py) — never treat depth alone
|
|
164
|
+
as absolute (that broke seed_health_from_map on airflow/llvm-style trees).
|
|
163
165
|
"""
|
|
164
166
|
try:
|
|
165
167
|
p = Path(file_key)
|
|
166
168
|
r = root.resolve()
|
|
167
169
|
key_str = str(file_key)
|
|
168
|
-
#
|
|
169
|
-
|
|
170
|
-
|
|
170
|
+
# Absolute-looking keys only (not "many path parts" — deep relative is valid).
|
|
171
|
+
looks_abs = (
|
|
172
|
+
p.is_absolute()
|
|
173
|
+
or key_str.startswith((
|
|
174
|
+
"home/", "/home/", "Users/", "/Users/",
|
|
175
|
+
"/coding_projects/", "coding_projects/",
|
|
176
|
+
"/var/", "/tmp/", "/private/",
|
|
177
|
+
))
|
|
178
|
+
)
|
|
179
|
+
if looks_abs:
|
|
171
180
|
if not p.is_absolute():
|
|
172
181
|
p = Path("/") / p
|
|
173
|
-
|
|
182
|
+
try:
|
|
183
|
+
p = p.resolve()
|
|
184
|
+
except Exception:
|
|
185
|
+
p = Path("/") / key_str.lstrip("/")
|
|
174
186
|
else:
|
|
175
187
|
p = (root / p).resolve()
|
|
176
188
|
_ = p.relative_to(r)
|
|
@@ -738,6 +750,50 @@ def _build_simple_exclude_set(root: Path) -> set:
|
|
|
738
750
|
return exc
|
|
739
751
|
|
|
740
752
|
|
|
753
|
+
# Parseable / deep-map source suffixes (align with update-maps collectors).
|
|
754
|
+
# validate map-first only requires health for these — not every README/png under monitored.
|
|
755
|
+
PARSEABLE_SOURCE_SUFFIXES = frozenset({
|
|
756
|
+
".py", ".pyi",
|
|
757
|
+
".js", ".jsx", ".mjs", ".cjs",
|
|
758
|
+
".ts", ".tsx",
|
|
759
|
+
".rs",
|
|
760
|
+
".go",
|
|
761
|
+
".c", ".h", ".cc", ".cpp", ".cxx", ".hpp", ".hh",
|
|
762
|
+
".cs",
|
|
763
|
+
".java",
|
|
764
|
+
})
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def _read_monitored_rel_roots(root: Path) -> List[str]:
|
|
768
|
+
monitored_file = root / "monitored_paths.txt"
|
|
769
|
+
if monitored_file.exists():
|
|
770
|
+
try:
|
|
771
|
+
roots = [
|
|
772
|
+
ln.strip()
|
|
773
|
+
for ln in monitored_file.read_text(encoding="utf-8").splitlines()
|
|
774
|
+
if ln.strip() and not ln.strip().startswith("#")
|
|
775
|
+
]
|
|
776
|
+
if roots:
|
|
777
|
+
return roots
|
|
778
|
+
except Exception:
|
|
779
|
+
pass
|
|
780
|
+
return ["."]
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def _under_monitored(rel: str, monitored_rels: List[str]) -> bool:
|
|
784
|
+
"""True if rel is inside any monitored relative root (or monitor is '.')."""
|
|
785
|
+
if not monitored_rels or monitored_rels == ["."]:
|
|
786
|
+
return True
|
|
787
|
+
rel_n = rel.replace("\\", "/").lstrip("./")
|
|
788
|
+
for m in monitored_rels:
|
|
789
|
+
m_n = m.replace("\\", "/").rstrip("/")
|
|
790
|
+
if m_n in (".", ""):
|
|
791
|
+
return True
|
|
792
|
+
if rel_n == m_n or rel_n.startswith(m_n + "/"):
|
|
793
|
+
return True
|
|
794
|
+
return False
|
|
795
|
+
|
|
796
|
+
|
|
741
797
|
def find_ghost_entries(root: Path) -> List[str]:
|
|
742
798
|
"""G7: Health keys that look like project files but no longer exist on disk.
|
|
743
799
|
|
|
@@ -747,9 +803,8 @@ def find_ghost_entries(root: Path) -> List[str]:
|
|
|
747
803
|
root = Path(root).resolve()
|
|
748
804
|
health = load_health(root)
|
|
749
805
|
ghosts: List[str] = []
|
|
750
|
-
source_suffixes = (
|
|
751
|
-
".
|
|
752
|
-
".sh", ".toml", ".yaml", ".yml",
|
|
806
|
+
source_suffixes = tuple(PARSEABLE_SOURCE_SUFFIXES) + (
|
|
807
|
+
".md", ".json", ".sh", ".toml", ".yaml", ".yml", ".html",
|
|
753
808
|
)
|
|
754
809
|
for key, entry in (health.get("entries") or {}).items():
|
|
755
810
|
if not isinstance(key, str) or not key or key.startswith("/"):
|
|
@@ -773,34 +828,262 @@ def find_ghost_entries(root: Path) -> List[str]:
|
|
|
773
828
|
return sorted(set(ghosts))
|
|
774
829
|
|
|
775
830
|
|
|
831
|
+
def _load_map_file_keys(root: Path) -> List[str]:
|
|
832
|
+
"""Non-reserved import_cache keys (mapped source files)."""
|
|
833
|
+
try:
|
|
834
|
+
from . import import_cache as ic
|
|
835
|
+
cache = ic.load_cache(root) or {}
|
|
836
|
+
except Exception:
|
|
837
|
+
cache = {}
|
|
838
|
+
cache_path = root / ".wikifier_staging" / "import_cache.json"
|
|
839
|
+
if cache_path.exists():
|
|
840
|
+
try:
|
|
841
|
+
with open(cache_path, "r", encoding="utf-8") as f:
|
|
842
|
+
cache = json.load(f) or {}
|
|
843
|
+
except Exception:
|
|
844
|
+
cache = {}
|
|
845
|
+
keys = []
|
|
846
|
+
for k in cache:
|
|
847
|
+
if not isinstance(k, str) or not k or k.startswith("_"):
|
|
848
|
+
continue
|
|
849
|
+
if _looks_like_path_key(k):
|
|
850
|
+
keys.append(k)
|
|
851
|
+
return keys
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
def seed_health_from_map(
|
|
855
|
+
root: Path,
|
|
856
|
+
map_keys: Optional[List[str]] = None,
|
|
857
|
+
max_new: int = 10000,
|
|
858
|
+
only_monitored: Optional[bool] = None,
|
|
859
|
+
reason: str = (
|
|
860
|
+
"Initial stub — present in dependency map; "
|
|
861
|
+
"agent should wiki + mark-green when editing"
|
|
862
|
+
),
|
|
863
|
+
) -> Dict[str, Any]:
|
|
864
|
+
"""Map-first: ensure mapped files have at least a 🟡 health stub.
|
|
865
|
+
|
|
866
|
+
Fixes warm-cache projects where update-maps never re-parses (0 dirty) and
|
|
867
|
+
therefore never creates file_health.json. Batched single save (no N locks).
|
|
868
|
+
|
|
869
|
+
only_monitored: default True when monitored_paths is lean (not bare '.'),
|
|
870
|
+
so we do not seed the entire monorepo map into health then fight prune.
|
|
871
|
+
"""
|
|
872
|
+
root = _coerce_root(root)
|
|
873
|
+
keys = list(map_keys) if map_keys is not None else _load_map_file_keys(root)
|
|
874
|
+
max_new = max(0, min(int(max_new), 100000))
|
|
875
|
+
monitored = _read_monitored_rel_roots(root)
|
|
876
|
+
if only_monitored is None:
|
|
877
|
+
only_monitored = not (monitored == ["."] or monitored == [])
|
|
878
|
+
if only_monitored:
|
|
879
|
+
keys = [k for k in keys if _under_monitored(k, monitored)]
|
|
880
|
+
|
|
881
|
+
def _work() -> Dict[str, Any]:
|
|
882
|
+
health = load_health(root)
|
|
883
|
+
entries = health.setdefault("entries", {})
|
|
884
|
+
before = len(entries)
|
|
885
|
+
seeded = 0
|
|
886
|
+
now = _timestamp()
|
|
887
|
+
for rel in keys:
|
|
888
|
+
if seeded >= max_new:
|
|
889
|
+
break
|
|
890
|
+
if not isinstance(rel, str) or not rel or rel.startswith("_"):
|
|
891
|
+
continue
|
|
892
|
+
if rel in entries:
|
|
893
|
+
continue
|
|
894
|
+
if not _entry_is_under_root(root, rel) or _is_pollution_health_key(rel):
|
|
895
|
+
continue
|
|
896
|
+
base = {
|
|
897
|
+
"status": "🟡 Yellow",
|
|
898
|
+
"last_updated": now,
|
|
899
|
+
"reason": reason,
|
|
900
|
+
"freshness_provenance": "seed_health_from_map",
|
|
901
|
+
}
|
|
902
|
+
if contracts and hasattr(contracts, "normalize_health_entry"):
|
|
903
|
+
base = contracts.normalize_health_entry(base)
|
|
904
|
+
else:
|
|
905
|
+
base = _normalize_health_entry_local(base)
|
|
906
|
+
entries[rel] = base
|
|
907
|
+
seeded += 1
|
|
908
|
+
if seeded > 0 or before == 0:
|
|
909
|
+
# Always persist when empty health so file_health.json exists for agents
|
|
910
|
+
health["entries"] = entries
|
|
911
|
+
_do_save_health(root, health)
|
|
912
|
+
return {
|
|
913
|
+
"success": True,
|
|
914
|
+
"seeded": seeded,
|
|
915
|
+
"mapped_keys_considered": len(keys),
|
|
916
|
+
"mapped_keys": len(keys),
|
|
917
|
+
"only_monitored": only_monitored,
|
|
918
|
+
"monitored": monitored,
|
|
919
|
+
"health_entries_before": before,
|
|
920
|
+
"health_entries_after": len(entries),
|
|
921
|
+
"max_new": max_new,
|
|
922
|
+
"root": str(root),
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
if locking:
|
|
926
|
+
with locking.file_lock(root):
|
|
927
|
+
return _work()
|
|
928
|
+
return _work()
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
def seed_health_for_monitored_sources(
|
|
932
|
+
root: Path,
|
|
933
|
+
max_new: int = 20000,
|
|
934
|
+
reason: str = (
|
|
935
|
+
"Initial stub — parseable source under monitored_paths; "
|
|
936
|
+
"agent should wiki + mark-green when editing"
|
|
937
|
+
),
|
|
938
|
+
) -> Dict[str, Any]:
|
|
939
|
+
"""Walk monitored parseable sources and stub any missing health rows.
|
|
940
|
+
|
|
941
|
+
Complements seed_health_from_map when the map is scoped/partial but
|
|
942
|
+
monitored_paths still contains on-disk sources (e.g. rust std tree).
|
|
943
|
+
"""
|
|
944
|
+
root = _coerce_root(root)
|
|
945
|
+
monitored_roots = _read_monitored_rel_roots(root)
|
|
946
|
+
excludes = _build_simple_exclude_set(root)
|
|
947
|
+
internal_skips = {".git", ".wikifier_staging", "journal", "Logged_issues", "node_modules"}
|
|
948
|
+
found: List[str] = []
|
|
949
|
+
for r in monitored_roots:
|
|
950
|
+
base = (root / r).resolve()
|
|
951
|
+
if not base.exists():
|
|
952
|
+
continue
|
|
953
|
+
for dirpath, dirnames, filenames in os.walk(base):
|
|
954
|
+
dirnames[:] = [d for d in dirnames if d not in internal_skips and d not in excludes]
|
|
955
|
+
for fname in filenames:
|
|
956
|
+
fpath = Path(dirpath) / fname
|
|
957
|
+
if fpath.suffix.lower() not in PARSEABLE_SOURCE_SUFFIXES:
|
|
958
|
+
continue
|
|
959
|
+
try:
|
|
960
|
+
rel = str(fpath.relative_to(root))
|
|
961
|
+
except Exception:
|
|
962
|
+
continue
|
|
963
|
+
parts = Path(rel).parts
|
|
964
|
+
if any(p in excludes or p in internal_skips for p in parts):
|
|
965
|
+
continue
|
|
966
|
+
found.append(rel)
|
|
967
|
+
return seed_health_from_map(
|
|
968
|
+
root,
|
|
969
|
+
map_keys=found,
|
|
970
|
+
max_new=max_new,
|
|
971
|
+
only_monitored=False,
|
|
972
|
+
reason=reason,
|
|
973
|
+
)
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
def prune_pending_to_monitored(root: Path, drop_auto_detected: bool = True) -> Dict[str, Any]:
|
|
977
|
+
"""Drop pending bullets outside monitored_paths (and optional auto-detected noise).
|
|
978
|
+
|
|
979
|
+
drop_auto_detected: remove check-changes thrash lines ("Auto-detected modification").
|
|
980
|
+
Map-first stubs live as 🟡 health rows; they do not need a huge pending flood.
|
|
981
|
+
"""
|
|
982
|
+
root = _coerce_root(root)
|
|
983
|
+
monitored = _read_monitored_rel_roots(root)
|
|
984
|
+
|
|
985
|
+
def _work() -> Dict[str, Any]:
|
|
986
|
+
lines = _read_pending_lines(root)
|
|
987
|
+
items = _pending_item_lines(lines)
|
|
988
|
+
kept: List[str] = []
|
|
989
|
+
removed = 0
|
|
990
|
+
removed_auto = 0
|
|
991
|
+
for ln in items:
|
|
992
|
+
body = ln[2:] if ln.startswith("- ") else ln
|
|
993
|
+
fpath = body.split(":", 1)[0].strip()
|
|
994
|
+
msg = body.split(":", 1)[1] if ":" in body else ""
|
|
995
|
+
if drop_auto_detected and "auto-detected" in msg.lower():
|
|
996
|
+
removed += 1
|
|
997
|
+
removed_auto += 1
|
|
998
|
+
continue
|
|
999
|
+
if _under_monitored(fpath, monitored):
|
|
1000
|
+
kept.append(ln)
|
|
1001
|
+
else:
|
|
1002
|
+
removed += 1
|
|
1003
|
+
_write_pending_lines(root, kept)
|
|
1004
|
+
return {
|
|
1005
|
+
"success": True,
|
|
1006
|
+
"removed": removed,
|
|
1007
|
+
"removed_auto_detected": removed_auto,
|
|
1008
|
+
"kept": len(kept),
|
|
1009
|
+
"monitored": monitored,
|
|
1010
|
+
"root": str(root),
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
if locking:
|
|
1014
|
+
with locking.file_lock(root):
|
|
1015
|
+
return _work()
|
|
1016
|
+
return _work()
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
def prune_health_outside_monitored(
|
|
1020
|
+
root: Path,
|
|
1021
|
+
keep_deleted_audits: bool = True,
|
|
1022
|
+
) -> Dict[str, Any]:
|
|
1023
|
+
"""Remove health rows outside lean monitored_paths (cuts yellow floods).
|
|
1024
|
+
|
|
1025
|
+
Keeps 🔴 DELETED audit rows when keep_deleted_audits=True.
|
|
1026
|
+
"""
|
|
1027
|
+
root = _coerce_root(root)
|
|
1028
|
+
monitored = _read_monitored_rel_roots(root)
|
|
1029
|
+
|
|
1030
|
+
def _work() -> Dict[str, Any]:
|
|
1031
|
+
health = load_health(root)
|
|
1032
|
+
entries = health.get("entries") or {}
|
|
1033
|
+
kept: Dict[str, Any] = {}
|
|
1034
|
+
removed = 0
|
|
1035
|
+
for k, v in entries.items():
|
|
1036
|
+
ent = v if isinstance(v, dict) else {}
|
|
1037
|
+
status = str(ent.get("status") or "")
|
|
1038
|
+
reason = str(ent.get("reason") or "")
|
|
1039
|
+
if keep_deleted_audits and ("DELETED" in reason or "DELETED" in status):
|
|
1040
|
+
if _looks_like_path_key(k):
|
|
1041
|
+
kept[k] = v
|
|
1042
|
+
continue
|
|
1043
|
+
if _under_monitored(str(k), monitored):
|
|
1044
|
+
kept[k] = v
|
|
1045
|
+
else:
|
|
1046
|
+
removed += 1
|
|
1047
|
+
health["entries"] = kept
|
|
1048
|
+
_do_save_health(root, health)
|
|
1049
|
+
return {
|
|
1050
|
+
"success": True,
|
|
1051
|
+
"removed": removed,
|
|
1052
|
+
"kept": len(kept),
|
|
1053
|
+
"monitored": monitored,
|
|
1054
|
+
"root": str(root),
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
if locking:
|
|
1058
|
+
with locking.file_lock(root):
|
|
1059
|
+
return _work()
|
|
1060
|
+
return _work()
|
|
1061
|
+
|
|
1062
|
+
|
|
776
1063
|
def validate_health(root: Path) -> Dict[str, Any]:
|
|
777
1064
|
"""
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
1065
|
+
Map-first health validation (agent-friendly).
|
|
1066
|
+
|
|
1067
|
+
- Scans **parseable source files** under monitored_paths only (not every
|
|
1068
|
+
README/asset). Non-source files never count as missing.
|
|
1069
|
+
- Also reports mapped files (import_cache) lacking a health row — the true
|
|
1070
|
+
map-first gap when warm cache never re-parsed.
|
|
1071
|
+
- G7: ghost_entries (health keys with missing disk paths).
|
|
1072
|
+
|
|
1073
|
+
Never mutates state. Exit code is caller-driven.
|
|
784
1074
|
"""
|
|
1075
|
+
root = _coerce_root(root)
|
|
785
1076
|
health = load_health(root)
|
|
786
1077
|
entries = health.get("entries", {})
|
|
787
1078
|
known = set(entries.keys())
|
|
788
1079
|
|
|
789
|
-
|
|
790
|
-
if monitored_file.exists():
|
|
791
|
-
try:
|
|
792
|
-
monitored_roots = [ln.strip() for ln in monitored_file.read_text(encoding="utf-8").splitlines()
|
|
793
|
-
if ln.strip() and not ln.strip().startswith("#")]
|
|
794
|
-
except Exception:
|
|
795
|
-
monitored_roots = ["."]
|
|
796
|
-
else:
|
|
797
|
-
monitored_roots = ["."]
|
|
798
|
-
|
|
1080
|
+
monitored_roots = _read_monitored_rel_roots(root)
|
|
799
1081
|
excludes = _build_simple_exclude_set(root)
|
|
800
|
-
internal_skips = {".git", ".wikifier_staging", "journal", "Logged_issues"}
|
|
1082
|
+
internal_skips = {".git", ".wikifier_staging", "journal", "Logged_issues", "node_modules"}
|
|
801
1083
|
|
|
802
1084
|
missing: List[str] = []
|
|
803
1085
|
total_scanned = 0
|
|
1086
|
+
non_source_skipped = 0
|
|
804
1087
|
|
|
805
1088
|
for r in monitored_roots:
|
|
806
1089
|
if not r:
|
|
@@ -809,32 +1092,60 @@ def validate_health(root: Path) -> Dict[str, Any]:
|
|
|
809
1092
|
if not base.exists():
|
|
810
1093
|
continue
|
|
811
1094
|
for dirpath, dirnames, filenames in os.walk(base):
|
|
812
|
-
|
|
813
|
-
dirnames[:] = [d for d in dirnames if d not in internal_skips]
|
|
1095
|
+
dirnames[:] = [d for d in dirnames if d not in internal_skips and d not in excludes]
|
|
814
1096
|
for fname in filenames:
|
|
815
1097
|
fpath = Path(dirpath) / fname
|
|
1098
|
+
suf = fpath.suffix.lower()
|
|
1099
|
+
if suf not in PARSEABLE_SOURCE_SUFFIXES:
|
|
1100
|
+
non_source_skipped += 1
|
|
1101
|
+
continue
|
|
816
1102
|
total_scanned += 1
|
|
817
1103
|
try:
|
|
818
1104
|
rel = str(fpath.relative_to(root))
|
|
819
1105
|
except Exception:
|
|
820
1106
|
rel = str(fpath)
|
|
821
|
-
# skip if any exclude component matches
|
|
822
1107
|
parts = Path(rel).parts
|
|
823
1108
|
if any(p in excludes or p in internal_skips for p in parts):
|
|
824
1109
|
continue
|
|
825
1110
|
if rel not in known:
|
|
826
1111
|
missing.append(rel)
|
|
827
1112
|
|
|
828
|
-
missing = sorted(set(missing))
|
|
1113
|
+
missing = sorted(set(missing))
|
|
829
1114
|
ghosts = find_ghost_entries(root)
|
|
1115
|
+
|
|
1116
|
+
mapped_keys = _load_map_file_keys(root)
|
|
1117
|
+
mapped_without_health = sorted(k for k in mapped_keys if k not in known)
|
|
1118
|
+
# In-scope map gaps only (respect lean monitored_paths)
|
|
1119
|
+
mapped_in_scope = [k for k in mapped_keys if _under_monitored(k, monitored_roots)]
|
|
1120
|
+
mapped_in_scope_without_health = sorted(k for k in mapped_in_scope if k not in known)
|
|
1121
|
+
# Primary agent signal: monitored parseable missing ∪ in-scope mapped missing
|
|
1122
|
+
primary_missing = sorted(set(missing) | set(mapped_in_scope_without_health))
|
|
1123
|
+
|
|
830
1124
|
return {
|
|
831
|
-
"missing_count": len(
|
|
832
|
-
"missing":
|
|
1125
|
+
"missing_count": len(primary_missing),
|
|
1126
|
+
"missing": primary_missing[:200],
|
|
1127
|
+
"missing_monitored_source_count": len(missing),
|
|
1128
|
+
"missing_monitored_source": missing[:200],
|
|
1129
|
+
"mapped_without_health_count": len(mapped_without_health),
|
|
1130
|
+
"mapped_without_health": mapped_without_health[:50],
|
|
1131
|
+
"mapped_in_scope_without_health_count": len(mapped_in_scope_without_health),
|
|
1132
|
+
"mapped_in_scope_without_health": mapped_in_scope_without_health[:200],
|
|
1133
|
+
"mapped_in_scope_count": len(mapped_in_scope),
|
|
833
1134
|
"ghost_count": len(ghosts),
|
|
834
|
-
"ghosts": ghosts,
|
|
1135
|
+
"ghosts": ghosts[:200],
|
|
835
1136
|
"total_scanned": total_scanned,
|
|
1137
|
+
"non_source_skipped": non_source_skipped,
|
|
836
1138
|
"health_entries": len(known),
|
|
837
|
-
"
|
|
1139
|
+
"mapped_files": len(mapped_keys),
|
|
1140
|
+
"monitored_roots": monitored_roots,
|
|
1141
|
+
"map_first": True,
|
|
1142
|
+
"note": (
|
|
1143
|
+
"Map-first validate: missing_count = parseable sources under monitored_paths "
|
|
1144
|
+
"plus mapped files under those paths lacking health. Full-map gaps outside "
|
|
1145
|
+
"lean monitors are informational (mapped_without_health_count). Non-source "
|
|
1146
|
+
"files are ignored."
|
|
1147
|
+
),
|
|
1148
|
+
"root": str(root),
|
|
838
1149
|
}
|
|
839
1150
|
|
|
840
1151
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wikifier
|
|
3
|
-
Version: 4.5.
|
|
3
|
+
Version: 4.5.6
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|