wikifier 4.6.0__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.0/wikifier.egg-info → wikifier-4.6.2}/PKG-INFO +1 -1
  2. {wikifier-4.6.0 → wikifier-4.6.2}/skills/run.md +3 -1
  3. {wikifier-4.6.0 → wikifier-4.6.2}/tests/test_agent_loop.py +148 -0
  4. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/__init__.py +3 -1
  5. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/agent_loop.py +115 -25
  6. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/cli.py +42 -19
  7. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/health.py +89 -2
  8. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/mcp/server.py +26 -0
  9. {wikifier-4.6.0 → wikifier-4.6.2/wikifier.egg-info}/PKG-INFO +1 -1
  10. {wikifier-4.6.0 → wikifier-4.6.2}/CONTRIBUTING.md +0 -0
  11. {wikifier-4.6.0 → wikifier-4.6.2}/LICENSE +0 -0
  12. {wikifier-4.6.0 → wikifier-4.6.2}/MANIFEST.in +0 -0
  13. {wikifier-4.6.0 → wikifier-4.6.2}/README.md +0 -0
  14. {wikifier-4.6.0 → wikifier-4.6.2}/diagnostics.html +0 -0
  15. {wikifier-4.6.0 → wikifier-4.6.2}/docs/spec.md +0 -0
  16. {wikifier-4.6.0 → wikifier-4.6.2}/index.html +0 -0
  17. {wikifier-4.6.0 → wikifier-4.6.2}/pyproject.toml +0 -0
  18. {wikifier-4.6.0 → wikifier-4.6.2}/setup.cfg +0 -0
  19. {wikifier-4.6.0 → wikifier-4.6.2}/tests/test_barrel_invalidation.py +0 -0
  20. {wikifier-4.6.0 → wikifier-4.6.2}/tests/test_gap_closure.py +0 -0
  21. {wikifier-4.6.0 → wikifier-4.6.2}/tests/test_health.py +0 -0
  22. {wikifier-4.6.0 → wikifier-4.6.2}/tests/test_import_cache.py +0 -0
  23. {wikifier-4.6.0 → wikifier-4.6.2}/tests/test_multi_lang_parsers.py +0 -0
  24. {wikifier-4.6.0 → wikifier-4.6.2}/tests/test_parsers.py +0 -0
  25. {wikifier-4.6.0 → wikifier-4.6.2}/tests/test_selftest_wrappers.py +0 -0
  26. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/__main__.py +0 -0
  27. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/contracts.py +0 -0
  28. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/daemon.py +0 -0
  29. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/diagnostics.py +0 -0
  30. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/import_cache.py +0 -0
  31. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/index.html +0 -0
  32. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/library.py +0 -0
  33. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/locking.py +0 -0
  34. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/mcp/__init__.py +0 -0
  35. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/parsers/__init__.py +0 -0
  36. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/parsers/_edge.py +0 -0
  37. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/parsers/bree.py +0 -0
  38. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/parsers/c_cpp.py +0 -0
  39. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/parsers/cdia.py +0 -0
  40. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/parsers/csharp.py +0 -0
  41. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/parsers/go_lang.py +0 -0
  42. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/parsers/java.py +0 -0
  43. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/parsers/javascript.py +0 -0
  44. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/parsers/python.py +0 -0
  45. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/parsers/rust.py +0 -0
  46. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/project_root.py +0 -0
  47. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/resolution.py +0 -0
  48. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/scripts/wikifier.bat +0 -0
  49. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/scripts/wikifier.ps1 +0 -0
  50. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/scripts/wikifier.sh +0 -0
  51. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/serve.py +0 -0
  52. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier.egg-info/SOURCES.txt +0 -0
  53. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier.egg-info/dependency_links.txt +0 -0
  54. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier.egg-info/entry_points.txt +0 -0
  55. {wikifier-4.6.0 → wikifier-4.6.2}/wikifier.egg-info/requires.txt +0 -0
  56. {wikifier-4.6.0 → 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.0
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 |
@@ -78,6 +78,52 @@ class TestContentHonestDirty(TempProjectTestCase):
78
78
  self.assertIn("Yellow", data3["entries"]["mod.py"]["status"])
79
79
  self.assertIn("content", data3["entries"]["mod.py"]["reason"].lower())
80
80
 
81
+ def test_legacy_green_without_hash_rewrite_yellows(self):
82
+ """Criterion 2: Green with no source_content_hash + real rewrite must Yellow.
83
+
84
+ Must not seed post-edit hash and stay Green (post-upgrade / legacy path).
85
+ """
86
+ p = self.write("legacy.py", "print('v1')\n")
87
+ # Legacy Green: upsert only — no source_content_hash baseline
88
+ health_mod.upsert_entry(self.root, "legacy.py", "🟢 Green", "legacy green no hash")
89
+ data = health_mod.load_health(self.root)
90
+ self.assertNotIn("source_content_hash", data["entries"]["legacy.py"] or {})
91
+ from wikifier import import_cache as ic
92
+ cache = {}
93
+ ic.update_file_data(cache, "legacy.py", mtime=1, imports=[], resolved_pairs=[])
94
+ ic.save_cache(self.root, cache)
95
+ # Real content rewrite
96
+ p.write_text("print('v2-rewritten')\n", encoding="utf-8")
97
+ cache = ic.load_cache(self.root)
98
+ cache["legacy.py"]["mtime"] = 1 # force dirty vs disk
99
+ ic.save_cache(self.root, cache)
100
+ r = cli.check_changes(project_root=self.root)
101
+ self.assertTrue(r.get("success"), r)
102
+ self.assertGreaterEqual(int(r.get("changes_detected") or 0), 1)
103
+ data2 = health_mod.load_health(self.root)
104
+ ent = data2["entries"]["legacy.py"]
105
+ self.assertIn("Yellow", ent["status"])
106
+ # Must not have silently stored the post-edit hash as trusted baseline
107
+ self.assertNotEqual(
108
+ ent.get("source_content_hash"),
109
+ compute_source_content_hash(p),
110
+ "must not seed post-edit content hash without mark_green",
111
+ )
112
+ # After mark_green, baseline is set and further touch-only stays green
113
+ cli.mark_green("legacy.py", "re-baselined", project_root=self.root)
114
+ data3 = health_mod.load_health(self.root)
115
+ self.assertIn("source_content_hash", data3["entries"]["legacy.py"])
116
+ baseline = data3["entries"]["legacy.py"]["source_content_hash"]
117
+ cache = ic.load_cache(self.root)
118
+ cache["legacy.py"]["mtime"] = 1
119
+ ic.save_cache(self.root, cache)
120
+ os.utime(p, None)
121
+ r2 = cli.check_changes(project_root=self.root)
122
+ self.assertTrue(r2.get("success"), r2)
123
+ data4 = health_mod.load_health(self.root)
124
+ self.assertIn("Green", data4["entries"]["legacy.py"]["status"])
125
+ self.assertEqual(data4["entries"]["legacy.py"]["source_content_hash"], baseline)
126
+
81
127
 
82
128
  class TestSessionBootstrapAndSuggest(TempProjectTestCase):
83
129
  def test_session_bootstrap_shape(self):
@@ -176,5 +222,107 @@ class TestCliBootstrapEntry(TempProjectTestCase):
176
222
  self.assertIn("actions", res)
177
223
 
178
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
+
179
327
  if __name__ == "__main__":
180
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.0"
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:
@@ -1208,48 +1224,34 @@ def check_changes(project_root: Optional[Union[str, Path]] = None) -> Dict[str,
1208
1224
  if stored_hash and live and stored_hash == live:
1209
1225
  verdict = {"content_dirty": False, "reason": "content_unchanged", "seed_baseline": False, "hash": live}
1210
1226
  elif not stored_hash and live:
1211
- verdict = {"content_dirty": False, "reason": "no_baseline", "seed_baseline": True, "hash": live}
1227
+ # no baseline dirty (do not seed post-edit hash)
1228
+ verdict = {"content_dirty": True, "reason": "no_baseline", "seed_baseline": False, "hash": live}
1212
1229
  elif live and stored_hash and stored_hash != live:
1213
1230
  verdict = {"content_dirty": True, "reason": "content_changed", "seed_baseline": False, "hash": live}
1214
1231
  except Exception:
1215
1232
  pass
1216
1233
 
1217
- if verdict.get("seed_baseline") and verdict.get("hash") and isinstance(ent, dict):
1218
- ent["source_content_hash"] = verdict["hash"]
1219
- entries[rel] = ent
1220
- health_dirty = True
1221
- seeded_baselines += 1
1222
- # Do not Yellow solely for first baseline seed on mtime thrash
1223
- continue
1224
1234
  if not verdict.get("content_dirty") and verdict.get("reason") == "content_unchanged":
1225
1235
  skipped_mtime_only += 1
1226
1236
  continue
1227
- # Content changed (or unclassifiable): Yellow with content reason
1237
+ # Content changed, no baseline, or unclassifiable: Yellow.
1238
+ # Never write source_content_hash here — only mark_green sets the
1239
+ # trusted baseline (avoids seeding post-edit bytes and staying Green).
1228
1240
  reason = (
1229
1241
  "content changed since last trusted baseline (check_changes content-honest)"
1230
1242
  if verdict.get("reason") == "content_changed"
1231
1243
  else "content change or no baseline (check_changes content-honest auto-detect)"
1232
1244
  )
1233
1245
  _upsert(root, rel, "🟡 Yellow", reason)
1234
- # refresh local entries view after upsert
1235
1246
  try:
1236
1247
  health_data = _health_mod.load_health(root)
1237
1248
  entries = health_data.setdefault("entries", {})
1238
- if verdict.get("hash") and rel in entries and isinstance(entries[rel], dict):
1239
- # keep prior baseline until mark-green; do not update hash on yellow
1240
- pass
1241
1249
  health_dirty = False # upsert already saved
1242
1250
  except Exception:
1243
1251
  pass
1244
1252
  _add_to_pending(root, rel, "Content change auto-detected — review and run mark-green after wiki update")
1245
1253
  _ensure_journal_entry(root, "auto-detected", rel, reason)
1246
1254
  changed_count += 1
1247
- if health_dirty and health_data is not None:
1248
- try:
1249
- if hasattr(_health_mod, "save_health"):
1250
- _health_mod.save_health(root, health_data)
1251
- except Exception:
1252
- pass
1253
1255
  # Keep pending queue aligned with lean monitored_paths (no flood outside scope)
1254
1256
  if hasattr(_health_mod, "prune_pending_to_monitored"):
1255
1257
  try:
@@ -1647,6 +1649,27 @@ def why_file(
1647
1649
  return _wf(file, project_root=project_root, max_results=max_results)
1648
1650
 
1649
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
+
1650
1673
  def update_maps(
1651
1674
  project_root: Optional[Union[str, Path]] = None,
1652
1675
  full: bool = False,
@@ -272,12 +272,16 @@ def classify_content_dirty(
272
272
  "seed_baseline": False,
273
273
  }
274
274
  if not stored_hash:
275
+ # No trusted baseline (legacy Green / never mark-green): when this path is
276
+ # under check_changes dirty observation, treat as needing attention so a
277
+ # post-edit rewrite cannot silently re-seed the *new* bytes and stay Green.
278
+ # Callers that only want to migrate baselines should use an explicit seed helper.
275
279
  return {
276
- "content_dirty": False,
280
+ "content_dirty": True,
277
281
  "missing": False,
278
282
  "hash": live,
279
283
  "reason": "no_baseline",
280
- "seed_baseline": True,
284
+ "seed_baseline": False,
281
285
  }
282
286
  if stored_hash == live:
283
287
  return {
@@ -296,6 +300,89 @@ def classify_content_dirty(
296
300
  }
297
301
 
298
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
+
299
386
  def _is_stale_wiki(root: Path, file: str, entry: Dict[str, Any]) -> Optional[Dict[str, Any]]:
300
387
  """
301
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.0
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