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.
Files changed (53) hide show
  1. {wikifier-4.5.5/wikifier.egg-info → wikifier-4.5.6}/PKG-INFO +1 -1
  2. {wikifier-4.5.5 → wikifier-4.5.6}/tests/test_gap_closure.py +80 -0
  3. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/__init__.py +1 -1
  4. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/cli.py +63 -23
  5. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/health.py +349 -38
  6. {wikifier-4.5.5 → wikifier-4.5.6/wikifier.egg-info}/PKG-INFO +1 -1
  7. {wikifier-4.5.5 → wikifier-4.5.6}/CONTRIBUTING.md +0 -0
  8. {wikifier-4.5.5 → wikifier-4.5.6}/LICENSE +0 -0
  9. {wikifier-4.5.5 → wikifier-4.5.6}/MANIFEST.in +0 -0
  10. {wikifier-4.5.5 → wikifier-4.5.6}/README.md +0 -0
  11. {wikifier-4.5.5 → wikifier-4.5.6}/diagnostics.html +0 -0
  12. {wikifier-4.5.5 → wikifier-4.5.6}/docs/spec.md +0 -0
  13. {wikifier-4.5.5 → wikifier-4.5.6}/index.html +0 -0
  14. {wikifier-4.5.5 → wikifier-4.5.6}/pyproject.toml +0 -0
  15. {wikifier-4.5.5 → wikifier-4.5.6}/setup.cfg +0 -0
  16. {wikifier-4.5.5 → wikifier-4.5.6}/skills/run.md +0 -0
  17. {wikifier-4.5.5 → wikifier-4.5.6}/tests/test_barrel_invalidation.py +0 -0
  18. {wikifier-4.5.5 → wikifier-4.5.6}/tests/test_health.py +0 -0
  19. {wikifier-4.5.5 → wikifier-4.5.6}/tests/test_import_cache.py +0 -0
  20. {wikifier-4.5.5 → wikifier-4.5.6}/tests/test_multi_lang_parsers.py +0 -0
  21. {wikifier-4.5.5 → wikifier-4.5.6}/tests/test_parsers.py +0 -0
  22. {wikifier-4.5.5 → wikifier-4.5.6}/tests/test_selftest_wrappers.py +0 -0
  23. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/__main__.py +0 -0
  24. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/contracts.py +0 -0
  25. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/daemon.py +0 -0
  26. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/diagnostics.py +0 -0
  27. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/import_cache.py +0 -0
  28. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/index.html +0 -0
  29. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/library.py +0 -0
  30. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/locking.py +0 -0
  31. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/mcp/__init__.py +0 -0
  32. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/mcp/server.py +0 -0
  33. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/parsers/__init__.py +0 -0
  34. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/parsers/_edge.py +0 -0
  35. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/parsers/bree.py +0 -0
  36. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/parsers/c_cpp.py +0 -0
  37. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/parsers/cdia.py +0 -0
  38. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/parsers/csharp.py +0 -0
  39. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/parsers/go_lang.py +0 -0
  40. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/parsers/java.py +0 -0
  41. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/parsers/javascript.py +0 -0
  42. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/parsers/python.py +0 -0
  43. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/parsers/rust.py +0 -0
  44. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/resolution.py +0 -0
  45. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/scripts/wikifier.bat +0 -0
  46. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/scripts/wikifier.ps1 +0 -0
  47. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/scripts/wikifier.sh +0 -0
  48. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier/serve.py +0 -0
  49. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier.egg-info/SOURCES.txt +0 -0
  50. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier.egg-info/dependency_links.txt +0 -0
  51. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier.egg-info/entry_points.txt +0 -0
  52. {wikifier-4.5.5 → wikifier-4.5.6}/wikifier.egg-info/requires.txt +0 -0
  53. {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.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()
@@ -71,4 +71,4 @@ from .contracts import (
71
71
  compute_acs_confidence,
72
72
  )
73
73
 
74
- __version__ = "4.5.5"
74
+ __version__ = "4.5.6"
@@ -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
- _health_mod.upsert_entry(
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 — missing disk paths vs missing wiki rows
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: python -m wikifier.health <summary|validate|heal-stubs|...>
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
- Filters pollution from other trees (e.g. worktrees, main wikifier source during dogfood).
160
- Uses relative_to (raises on not-under) for correctness even on prefix-overlap cases like
161
- "root/home/..." treating bad relative key.
162
- Heuristic: keys that look like fs-absolute (start home/ or many parts) are resolved as absolute.
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
- # M5.1 hardening (gaps 2/3/5 health/cli for external abs paths; ONLY from M5-Dogfood-Assessment-Report.md full + Progress tail last100 + key: "without pollution or "path does not exist"" on alt/Recipe/Consistency/llama/llvm after abs monitored_paths.txt + health auto-prune out-of-root + ./wikifier.sh + CLI guards + python-primary w/ dir/max; ~20+ BRC y on alt from AdversarialScaffoldGenerator/CrossMCPRecipeValidator/MCPOrchestrationDashboard etc w/ chains; stele 6 mismatches; chisel 84 edges/0; llvm 168k u/4min/1363 chains; agent 100t/7e doom; current 60%; 8 gaps 10 DoD; 1-2y lean "Pruned 0" everywhere. Added /coding_projects/ + / for sibling cross (Chisel/Trammel/stele/Coord) + general abs; if not already fixed history. #2 zero-dep, #8 boundary (wikifier/health only), #9 measurable exact #s, #1 spectrum, #7 MA context.
169
- if p.is_absolute() or key_str.startswith(("home/", "/home/", "Users/", "/Users/", "/coding_projects/", "coding_projects/", "/")) or len(p.parts) > 5:
170
- # treat as absolute fs path (common when str(p) was stored for outside files)
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
- p = p.resolve()
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
- ".py", ".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs", ".md", ".json",
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
- Reliable, subshell-free implementation of 'validate'.
779
- Scans monitored paths (respecting excludes + internal skips), reports files
780
- missing from the health matrix (JSON or migrated MD).
781
- Returns structured result for shell/Python/MCP callers.
782
- Always succeeds (exit code driven by caller); never mutates state.
783
- G7: also reports ghost_entries (health keys with missing disk paths).
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
- monitored_file = root / "monitored_paths.txt"
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
- # prune internal dirs
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)) # dedup + stable
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(missing),
832
- "missing": 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
- "root": str(root)
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.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