wikifier 4.6.1__tar.gz → 4.6.2__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 (56) hide show
  1. {wikifier-4.6.1/wikifier.egg-info → wikifier-4.6.2}/PKG-INFO +1 -1
  2. {wikifier-4.6.1 → wikifier-4.6.2}/skills/run.md +3 -1
  3. {wikifier-4.6.1 → wikifier-4.6.2}/tests/test_agent_loop.py +102 -0
  4. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier/__init__.py +3 -1
  5. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier/agent_loop.py +115 -25
  6. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier/cli.py +37 -0
  7. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier/health.py +83 -0
  8. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier/mcp/server.py +26 -0
  9. {wikifier-4.6.1 → wikifier-4.6.2/wikifier.egg-info}/PKG-INFO +1 -1
  10. {wikifier-4.6.1 → wikifier-4.6.2}/CONTRIBUTING.md +0 -0
  11. {wikifier-4.6.1 → wikifier-4.6.2}/LICENSE +0 -0
  12. {wikifier-4.6.1 → wikifier-4.6.2}/MANIFEST.in +0 -0
  13. {wikifier-4.6.1 → wikifier-4.6.2}/README.md +0 -0
  14. {wikifier-4.6.1 → wikifier-4.6.2}/diagnostics.html +0 -0
  15. {wikifier-4.6.1 → wikifier-4.6.2}/docs/spec.md +0 -0
  16. {wikifier-4.6.1 → wikifier-4.6.2}/index.html +0 -0
  17. {wikifier-4.6.1 → wikifier-4.6.2}/pyproject.toml +0 -0
  18. {wikifier-4.6.1 → wikifier-4.6.2}/setup.cfg +0 -0
  19. {wikifier-4.6.1 → wikifier-4.6.2}/tests/test_barrel_invalidation.py +0 -0
  20. {wikifier-4.6.1 → wikifier-4.6.2}/tests/test_gap_closure.py +0 -0
  21. {wikifier-4.6.1 → wikifier-4.6.2}/tests/test_health.py +0 -0
  22. {wikifier-4.6.1 → wikifier-4.6.2}/tests/test_import_cache.py +0 -0
  23. {wikifier-4.6.1 → wikifier-4.6.2}/tests/test_multi_lang_parsers.py +0 -0
  24. {wikifier-4.6.1 → wikifier-4.6.2}/tests/test_parsers.py +0 -0
  25. {wikifier-4.6.1 → wikifier-4.6.2}/tests/test_selftest_wrappers.py +0 -0
  26. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier/__main__.py +0 -0
  27. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier/contracts.py +0 -0
  28. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier/daemon.py +0 -0
  29. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier/diagnostics.py +0 -0
  30. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier/import_cache.py +0 -0
  31. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier/index.html +0 -0
  32. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier/library.py +0 -0
  33. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier/locking.py +0 -0
  34. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier/mcp/__init__.py +0 -0
  35. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier/parsers/__init__.py +0 -0
  36. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier/parsers/_edge.py +0 -0
  37. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier/parsers/bree.py +0 -0
  38. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier/parsers/c_cpp.py +0 -0
  39. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier/parsers/cdia.py +0 -0
  40. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier/parsers/csharp.py +0 -0
  41. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier/parsers/go_lang.py +0 -0
  42. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier/parsers/java.py +0 -0
  43. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier/parsers/javascript.py +0 -0
  44. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier/parsers/python.py +0 -0
  45. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier/parsers/rust.py +0 -0
  46. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier/project_root.py +0 -0
  47. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier/resolution.py +0 -0
  48. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier/scripts/wikifier.bat +0 -0
  49. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier/scripts/wikifier.ps1 +0 -0
  50. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier/scripts/wikifier.sh +0 -0
  51. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier/serve.py +0 -0
  52. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier.egg-info/SOURCES.txt +0 -0
  53. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier.egg-info/dependency_links.txt +0 -0
  54. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier.egg-info/entry_points.txt +0 -0
  55. {wikifier-4.6.1 → wikifier-4.6.2}/wikifier.egg-info/requires.txt +0 -0
  56. {wikifier-4.6.1 → wikifier-4.6.2}/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.1
3
+ Version: 4.6.2
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
@@ -43,8 +43,10 @@ Do **not** open megamodules (`javascript.py`, `import_cache.py`, `bree.py`) to d
43
43
  |------|-----|
44
44
  | First-run map | `init` → `update-maps` → `health --summary` → `suggest-next` |
45
45
  | **Session start (4.6+)** | `session_bootstrap` (or CLI `session-bootstrap`) — one shot: root, health, attention, `actions[]` |
46
+ | **Core tool list** | `list_core_tools` / bootstrap `core_surface` — prefer Core 6; advanced is non-core |
46
47
  | Steady-state | `check-changes` (content-honest) → edit 🔴/actionable 🟡 only → `record-change` → wiki → `mark-green` |
47
- | **Preflight edit** | `prepare_edit` / CLI `prepare-edit <file>` wiki + status + deps + dependents |
48
+ | **Hash migration** | `seed_source_content_hashes` / CLI `seed-source-hashes`baseline Greens without mass Yellow |
49
+ | **Preflight edit** | `prepare_edit` / CLI `prepare-edit <file>` — wiki + status + deps + dependents (multi-shape reverse index) |
48
50
  | **Why / journal** | `why_file` / `search_journal` — semantic trail, not mtime theater |
49
51
  | Lookup Core | `session_bootstrap`, `check_changes`, `prepare_edit`, `suggest_next_actions` (json `actions[]`), `record_change`, `mark_green` |
50
52
  | Advanced intel | `get_dependencies`, `get_dependents`, `get_cycles`, barrel/diagnostics — not daily Core |
@@ -222,5 +222,107 @@ class TestCliBootstrapEntry(TempProjectTestCase):
222
222
  self.assertIn("actions", res)
223
223
 
224
224
 
225
+ class TestSeedSourceHashes(TempProjectTestCase):
226
+ def test_seed_then_touch_ok_rewrite_yellows(self):
227
+ p = self.write("seeded.py", "print(0)\n")
228
+ health_mod.upsert_entry(self.root, "seeded.py", "🟢 Green", "legacy no hash")
229
+ data = health_mod.load_health(self.root)
230
+ self.assertNotIn("source_content_hash", data["entries"]["seeded.py"] or {})
231
+ res = health_mod.seed_source_content_hashes(self.root, only_green=True)
232
+ self.assertTrue(res.get("success"), res)
233
+ self.assertGreaterEqual(int(res.get("seeded") or 0), 1)
234
+ data2 = health_mod.load_health(self.root)
235
+ ent = data2["entries"]["seeded.py"]
236
+ self.assertIn("Green", ent["status"])
237
+ self.assertTrue(ent.get("source_content_hash"))
238
+ baseline = ent["source_content_hash"]
239
+ from wikifier import import_cache as ic
240
+ cache = {}
241
+ ic.update_file_data(cache, "seeded.py", mtime=1, imports=[], resolved_pairs=[])
242
+ ic.save_cache(self.root, cache)
243
+ os.utime(p, None)
244
+ cache = ic.load_cache(self.root)
245
+ cache["seeded.py"]["mtime"] = 1
246
+ ic.save_cache(self.root, cache)
247
+ r = cli.check_changes(project_root=self.root)
248
+ self.assertTrue(r.get("success"), r)
249
+ data3 = health_mod.load_health(self.root)
250
+ self.assertIn("Green", data3["entries"]["seeded.py"]["status"])
251
+ self.assertEqual(data3["entries"]["seeded.py"]["source_content_hash"], baseline)
252
+ p.write_text("print(99)\n", encoding="utf-8")
253
+ cache = ic.load_cache(self.root)
254
+ cache["seeded.py"]["mtime"] = 1
255
+ ic.save_cache(self.root, cache)
256
+ r2 = cli.check_changes(project_root=self.root)
257
+ self.assertTrue(r2.get("success"), r2)
258
+ self.assertGreaterEqual(int(r2.get("changes_detected") or 0), 1)
259
+ data4 = health_mod.load_health(self.root)
260
+ self.assertIn("Yellow", data4["entries"]["seeded.py"]["status"])
261
+
262
+
263
+ class TestCoreSurfaceListing(unittest.TestCase):
264
+ def test_list_core_tools_has_six(self):
265
+ from wikifier.agent_loop import list_core_tools, CORE_DAILY_NAMES
266
+ res = list_core_tools()
267
+ self.assertTrue(res.get("success"))
268
+ self.assertEqual(res.get("core_count"), 6)
269
+ names = res.get("core_names") or []
270
+ for n in (
271
+ "session_bootstrap", "check_changes", "prepare_edit",
272
+ "suggest_next_actions", "record_change", "mark_green",
273
+ ):
274
+ self.assertIn(n, names)
275
+ self.assertEqual(names, CORE_DAILY_NAMES)
276
+ adv = res.get("advanced_intel") or []
277
+ self.assertTrue(adv)
278
+ self.assertNotIn("session_bootstrap", adv) # core not listed as advanced-only
279
+
280
+
281
+ class TestPrepareEditReverseShapes(TempProjectTestCase):
282
+ def test_flat_and_nested_reverse_index(self):
283
+ self.write("lib/t.py", "x=1\n")
284
+ health_mod.upsert_entry(self.root, "lib/t.py", "🟢 Green", "ok")
285
+ from wikifier import import_cache as ic
286
+ from wikifier.agent_loop import prepare_edit, resolve_dependents_from_cache
287
+
288
+ # Flat shape
289
+ cache_flat = {
290
+ "lib/t.py": {"mtime": 1, "resolved_pairs": []},
291
+ "lib/a.py": {"mtime": 1, "resolved_pairs": [{"resolved": "lib/t.py", "resolved_path": "lib/t.py"}]},
292
+ "_reverse_dependencies": {"lib/t.py": ["lib/a.py", "lib/b.py"]},
293
+ }
294
+ deps = resolve_dependents_from_cache(cache_flat, "lib/t.py")
295
+ self.assertIn("lib/a.py", deps)
296
+ self.assertIn("lib/b.py", deps)
297
+ ic.save_cache(self.root, cache_flat)
298
+ pe = prepare_edit("lib/t.py", project_root=self.root)
299
+ self.assertTrue(pe.get("success"))
300
+ self.assertIn("lib/a.py", pe.get("dependents") or [])
301
+
302
+ # Nested index shape
303
+ cache_nest = {
304
+ "lib/t.py": {"mtime": 1, "resolved_pairs": []},
305
+ "_reverse_dependencies": {
306
+ "index": {"lib/t.py": ["lib/nested1.py", "lib/nested2.py"]},
307
+ "version": 1,
308
+ },
309
+ }
310
+ deps2 = resolve_dependents_from_cache(cache_nest, "lib/t.py")
311
+ self.assertEqual(set(deps2), {"lib/nested1.py", "lib/nested2.py"})
312
+ ic.save_cache(self.root, cache_nest)
313
+ pe2 = prepare_edit("lib/t.py", project_root=self.root)
314
+ self.assertTrue(pe2.get("success"))
315
+ self.assertIn("lib/nested1.py", pe2.get("dependents") or [])
316
+
317
+ # Dict value shape
318
+ cache_dict = {
319
+ "_reverse_dependencies": {
320
+ "lib/t.py": {"importers": ["lib/from_dict.py"]},
321
+ }
322
+ }
323
+ deps3 = resolve_dependents_from_cache(cache_dict, "lib/t.py")
324
+ self.assertIn("lib/from_dict.py", deps3)
325
+
326
+
225
327
  if __name__ == "__main__":
226
328
  unittest.main()
@@ -46,6 +46,8 @@ from .cli import (
46
46
  prepare_edit,
47
47
  search_journal,
48
48
  why_file,
49
+ seed_source_content_hashes,
50
+ list_core_tools,
49
51
  )
50
52
  from . import agent_loop
51
53
 
@@ -76,4 +78,4 @@ from .contracts import (
76
78
  compute_acs_confidence,
77
79
  )
78
80
 
79
- __version__ = "4.6.1"
81
+ __version__ = "4.6.2"
@@ -21,12 +21,122 @@ import importlib
21
21
 
22
22
  from .project_root import discover_project_root
23
23
 
24
+ # Core daily agent surface (4.6+) — prefer these every session.
25
+ CORE_DAILY_TOOLS: List[Dict[str, str]] = [
26
+ {"name": "session_bootstrap", "role": "one-shot root + health + attention + actions[]"},
27
+ {"name": "check_changes", "role": "content-honest dirty / ghosts"},
28
+ {"name": "prepare_edit", "role": "single-file preflight wiki/status/deps/dependents"},
29
+ {"name": "suggest_next_actions", "role": "selective work + dispatchable actions[]"},
30
+ {"name": "record_change", "role": "semantic why after edits"},
31
+ {"name": "mark_green", "role": "trust + source content-hash baseline"},
32
+ ]
33
+ CORE_DAILY_NAMES: List[str] = [t["name"] for t in CORE_DAILY_TOOLS]
34
+ ADVANCED_INTEL_TOOLS: List[str] = [
35
+ "get_dependencies",
36
+ "get_dependents",
37
+ "get_cycles",
38
+ "get_barrel_reports",
39
+ "get_resolution_diagnostics",
40
+ "health",
41
+ "get_project_status",
42
+ "get_files_needing_attention",
43
+ "search_journal",
44
+ "why_file",
45
+ "update_maps",
46
+ "validate",
47
+ ]
48
+
49
+
50
+ def list_core_tools() -> Dict[str, Any]:
51
+ """Return Core daily vs advanced intel tool metadata (agents prefer Core)."""
52
+ return {
53
+ "success": True,
54
+ "core_daily": list(CORE_DAILY_TOOLS),
55
+ "core_names": list(CORE_DAILY_NAMES),
56
+ "advanced_intel": list(ADVANCED_INTEL_TOOLS),
57
+ "core_count": len(CORE_DAILY_NAMES),
58
+ "note": "Use Core every session; advanced remains available but non-core.",
59
+ }
60
+
24
61
 
25
62
  def _health_module():
26
63
  """Real health submodule (not the shadowed package attribute function)."""
27
64
  return importlib.import_module("wikifier.health")
28
65
 
29
66
 
67
+ def resolve_dependents_from_cache(cache: Dict[str, Any], rel: str) -> List[str]:
68
+ """Extract reverse dependents for *rel* from multiple reverse-index shapes.
69
+
70
+ Supported:
71
+ - flat: ``_reverse_dependencies[rel] -> [importers]``
72
+ - nested index: ``_reverse_dependencies["index"][rel]``
73
+ - dependents key: ``_reverse_dependencies["dependents"][rel]``
74
+ - map value dict: ``{rel: {"importers": [...]}}`` or ``{"sources": [...]}``
75
+ - import_cache.get_reverse_dependencies when available
76
+ Fallback: scan resolved_pairs for edges pointing at rel.
77
+ """
78
+ if not rel or not isinstance(cache, dict):
79
+ return []
80
+
81
+ def _as_list(val: Any) -> List[str]:
82
+ if val is None:
83
+ return []
84
+ if isinstance(val, list):
85
+ return [str(x) for x in val if x]
86
+ if isinstance(val, dict):
87
+ for k in ("importers", "sources", "dependents", "files", "from"):
88
+ if isinstance(val.get(k), list):
89
+ return [str(x) for x in val[k] if x]
90
+ # keys-as-importers
91
+ return [str(k) for k in val.keys() if k and not str(k).startswith("_")]
92
+ if isinstance(val, str) and val:
93
+ return [val]
94
+ return []
95
+
96
+ # Prefer library helper when present (canonical flat map)
97
+ try:
98
+ from . import import_cache as ic
99
+ rev_map = ic.get_reverse_dependencies(cache) or {}
100
+ if isinstance(rev_map, dict) and rel in rev_map:
101
+ got = _as_list(rev_map.get(rel))
102
+ if got:
103
+ return got[:80]
104
+ except Exception:
105
+ pass
106
+
107
+ rev = cache.get("_reverse_dependencies") or {}
108
+ if not isinstance(rev, dict):
109
+ return []
110
+
111
+ # Direct flat
112
+ if rel in rev:
113
+ got = _as_list(rev.get(rel))
114
+ if got:
115
+ return got[:80]
116
+
117
+ # Nested common containers
118
+ for nest_key in ("index", "dependents", "map", "by_target", "targets"):
119
+ nested = rev.get(nest_key)
120
+ if isinstance(nested, dict) and rel in nested:
121
+ got = _as_list(nested.get(rel))
122
+ if got:
123
+ return got[:80]
124
+
125
+ # Fallback: O(E) scan of resolved_pairs
126
+ found: List[str] = []
127
+ for src, data in cache.items():
128
+ if not isinstance(src, str) or src.startswith("_") or not isinstance(data, dict):
129
+ continue
130
+ for p in data.get("resolved_pairs") or []:
131
+ if not isinstance(p, dict):
132
+ continue
133
+ tgt = p.get("resolved_path") or p.get("resolved") or ""
134
+ if tgt == rel or str(tgt).endswith("/" + rel) or str(tgt).endswith(rel):
135
+ if src not in found:
136
+ found.append(src)
137
+ return found[:80]
138
+
139
+
30
140
  def _root(project_root: Optional[Union[str, Path]] = None) -> Path:
31
141
  if project_root:
32
142
  try:
@@ -327,17 +437,7 @@ def prepare_edit(
327
437
  })
328
438
  out["dependencies"] = deps[:80]
329
439
  out["low_conf_edges"] = low
330
- # reverse deps
331
- rev = cache.get("_reverse_dependencies") or {}
332
- if isinstance(rev, dict):
333
- # may be {file: [importers]} or nested
334
- importers = rev.get(rel)
335
- if importers is None and isinstance(rev.get("index"), dict):
336
- importers = rev["index"].get(rel)
337
- if isinstance(importers, list):
338
- out["dependents"] = importers[:80]
339
- elif isinstance(importers, dict):
340
- out["dependents"] = list(importers.keys())[:80]
440
+ out["dependents"] = resolve_dependents_from_cache(cache, rel)
341
441
  cycles = cache.get("_cycles") or {}
342
442
  all_cyc = set(cycles.get("all_cycle_files") or [])
343
443
  out["in_cycle"] = rel in all_cyc
@@ -363,20 +463,10 @@ def session_bootstrap(
363
463
  out: Dict[str, Any] = {
364
464
  "success": True,
365
465
  "project_root": str(root),
366
- "core_surface": [
367
- "session_bootstrap",
368
- "check_changes",
369
- "prepare_edit",
370
- "suggest_next_actions",
371
- "record_change",
372
- "mark_green",
373
- ],
374
- "advanced_intel": [
375
- "get_cycles",
376
- "get_barrel_reports",
377
- "get_resolution_diagnostics",
378
- "health(format=json)",
379
- ],
466
+ "core_surface": list(CORE_DAILY_NAMES),
467
+ "core_daily": list(CORE_DAILY_TOOLS),
468
+ "core_count": len(CORE_DAILY_NAMES),
469
+ "advanced_intel": list(ADVANCED_INTEL_TOOLS),
380
470
  }
381
471
  health_sum: Dict[str, Any] = {}
382
472
  red_files: List[str] = []
@@ -772,6 +772,22 @@ def main():
772
772
  res = why_file(_args[0], project_root=project_root)
773
773
  print(_json.dumps(res, indent=2, default=str))
774
774
  return 0 if res.get("success") else 1
775
+ if _cmd0 in (
776
+ "seed-source-hashes",
777
+ "seed-source-content-hashes",
778
+ "seed_source_content_hashes",
779
+ "seed-hashes",
780
+ ):
781
+ import json as _json
782
+ force = any(a in ("--force", "-f") for a in _args)
783
+ res = seed_source_content_hashes(project_root=project_root, force=force)
784
+ print(_json.dumps(res, indent=2, default=str))
785
+ return 0 if res.get("success") else 1
786
+ if _cmd0 in ("list-core-tools", "list_core_tools", "core-tools"):
787
+ import json as _json
788
+ res = list_core_tools()
789
+ print(_json.dumps(res, indent=2, default=str))
790
+ return 0 if res.get("success") else 1
775
791
  if _cmd0 == "validate":
776
792
  import json as _json
777
793
  if _health_mod is not None:
@@ -1633,6 +1649,27 @@ def why_file(
1633
1649
  return _wf(file, project_root=project_root, max_results=max_results)
1634
1650
 
1635
1651
 
1652
+ def seed_source_content_hashes(
1653
+ project_root: Optional[Union[str, Path]] = None,
1654
+ only_green: bool = True,
1655
+ force: bool = False,
1656
+ directory: Optional[str] = None,
1657
+ ) -> Dict[str, Any]:
1658
+ """Seed source_content_hash baselines without mass Yellow (migration helper)."""
1659
+ root = _get_effective_root(project_root)
1660
+ if _health_mod is None or not hasattr(_health_mod, "seed_source_content_hashes"):
1661
+ return {"success": False, "project_root": str(root), "error": "health.seed_source_content_hashes unavailable"}
1662
+ return _health_mod.seed_source_content_hashes(
1663
+ root, only_green=only_green, force=force, directory=directory
1664
+ )
1665
+
1666
+
1667
+ def list_core_tools() -> Dict[str, Any]:
1668
+ """Core daily agent tool listing (prefer over full MCP catalog)."""
1669
+ from .agent_loop import list_core_tools as _lct
1670
+ return _lct()
1671
+
1672
+
1636
1673
  def update_maps(
1637
1674
  project_root: Optional[Union[str, Path]] = None,
1638
1675
  full: bool = False,
@@ -300,6 +300,89 @@ def classify_content_dirty(
300
300
  }
301
301
 
302
302
 
303
+ def seed_source_content_hashes(
304
+ root: "str | Path",
305
+ *,
306
+ only_green: bool = True,
307
+ force: bool = False,
308
+ directory: Optional[str] = None,
309
+ max_files: int = 50000,
310
+ ) -> Dict[str, Any]:
311
+ """Seed trusted ``source_content_hash`` for existing health entries without Yellow thrash.
312
+
313
+ Migration helper for post-4.6.x content-honest dirty: legacy 🟢 entries without a
314
+ baseline get the *current on-disk* hash recorded so subsequent mtime-only touches
315
+ do not Yellow. Status/reason are left unchanged (no mass auto-Yellow).
316
+
317
+ Args:
318
+ only_green: if True, only seed entries whose status is Green.
319
+ force: if True, re-hash even when a baseline already exists.
320
+ directory: optional path prefix filter.
321
+ max_files: safety cap.
322
+
323
+ Returns structured stats (seeded, skipped_*, missing_on_disk, errors sample).
324
+ """
325
+ root = Path(root).resolve()
326
+ result: Dict[str, Any] = {
327
+ "success": True,
328
+ "project_root": str(root),
329
+ "seeded": 0,
330
+ "skipped_has_hash": 0,
331
+ "skipped_not_green": 0,
332
+ "skipped_missing": 0,
333
+ "skipped_filter": 0,
334
+ "errors": [],
335
+ "sample_seeded": [],
336
+ }
337
+ try:
338
+ data = load_health(root)
339
+ entries = data.setdefault("entries", {})
340
+ dirty = False
341
+ dir_pref = (directory or "").rstrip("/")
342
+ for rel, ent in list(entries.items()):
343
+ if not isinstance(ent, dict):
344
+ continue
345
+ if dir_pref and not (str(rel) == dir_pref or str(rel).startswith(dir_pref + "/")):
346
+ result["skipped_filter"] += 1
347
+ continue
348
+ st = str(ent.get("status") or "")
349
+ if only_green and "Green" not in st and "🟢" not in st:
350
+ result["skipped_not_green"] += 1
351
+ continue
352
+ if ent.get("source_content_hash") and not force:
353
+ result["skipped_has_hash"] += 1
354
+ continue
355
+ src = root / rel
356
+ if not src.is_file():
357
+ result["skipped_missing"] += 1
358
+ continue
359
+ h = compute_source_content_hash(src)
360
+ if not h:
361
+ result["skipped_missing"] += 1
362
+ continue
363
+ ent["source_content_hash"] = h
364
+ # Do not change status/reason — migration only
365
+ entries[rel] = ent
366
+ dirty = True
367
+ result["seeded"] += 1
368
+ if len(result["sample_seeded"]) < 8:
369
+ result["sample_seeded"].append(rel)
370
+ if result["seeded"] >= max_files:
371
+ break
372
+ if dirty:
373
+ save_health(root, data)
374
+ result["message"] = (
375
+ f"Seeded {result['seeded']} source_content_hash baseline(s) "
376
+ f"(skipped has_hash={result['skipped_has_hash']}, "
377
+ f"not_green={result['skipped_not_green']}, missing={result['skipped_missing']})."
378
+ )
379
+ except Exception as e:
380
+ result["success"] = False
381
+ result["error"] = str(e)
382
+ result["errors"].append(str(e))
383
+ return result
384
+
385
+
303
386
  def _is_stale_wiki(root: Path, file: str, entry: Dict[str, Any]) -> Optional[Dict[str, Any]]:
304
387
  """
305
388
  Core durable stale wiki detector (B3).
@@ -526,6 +526,32 @@ def why_file(
526
526
  return {"success": False, "file": file, "project_root": str(root), "error": str(e)}
527
527
 
528
528
 
529
+ @mcp.tool()
530
+ def seed_source_content_hashes(
531
+ project_root: Optional[str] = None,
532
+ only_green: bool = True,
533
+ force: bool = False,
534
+ directory: Optional[str] = None,
535
+ ) -> dict:
536
+ """Seed source_content_hash for Green entries without mass Yellow thrash (migration)."""
537
+ root = _get_effective_root(project_root)
538
+ try:
539
+ from wikifier.cli import seed_source_content_hashes as _lib_seed
540
+ return _lib_seed(project_root=root, only_green=only_green, force=force, directory=directory)
541
+ except Exception as e:
542
+ return {"success": False, "project_root": str(root), "error": str(e)}
543
+
544
+
545
+ @mcp.tool()
546
+ def list_core_tools() -> dict:
547
+ """List Core daily agent tools vs advanced intel (prefer Core every session)."""
548
+ try:
549
+ from wikifier.cli import list_core_tools as _lib_lct
550
+ return _lib_lct()
551
+ except Exception as e:
552
+ return {"success": False, "error": str(e)}
553
+
554
+
529
555
  @mcp.tool()
530
556
  def update_maps(
531
557
  project_root: Optional[str] = None,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wikifier
3
- Version: 4.6.1
3
+ Version: 4.6.2
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