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.
- {wikifier-4.6.0/wikifier.egg-info → wikifier-4.6.2}/PKG-INFO +1 -1
- {wikifier-4.6.0 → wikifier-4.6.2}/skills/run.md +3 -1
- {wikifier-4.6.0 → wikifier-4.6.2}/tests/test_agent_loop.py +148 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/__init__.py +3 -1
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/agent_loop.py +115 -25
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/cli.py +42 -19
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/health.py +89 -2
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/mcp/server.py +26 -0
- {wikifier-4.6.0 → wikifier-4.6.2/wikifier.egg-info}/PKG-INFO +1 -1
- {wikifier-4.6.0 → wikifier-4.6.2}/CONTRIBUTING.md +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/LICENSE +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/MANIFEST.in +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/README.md +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/diagnostics.html +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/docs/spec.md +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/index.html +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/pyproject.toml +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/setup.cfg +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/tests/test_barrel_invalidation.py +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/tests/test_gap_closure.py +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/tests/test_health.py +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/tests/test_import_cache.py +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/tests/test_multi_lang_parsers.py +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/tests/test_parsers.py +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/tests/test_selftest_wrappers.py +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/__main__.py +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/contracts.py +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/daemon.py +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/diagnostics.py +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/import_cache.py +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/index.html +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/library.py +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/locking.py +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/mcp/__init__.py +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/parsers/__init__.py +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/parsers/_edge.py +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/parsers/bree.py +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/parsers/c_cpp.py +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/parsers/cdia.py +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/parsers/csharp.py +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/parsers/go_lang.py +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/parsers/java.py +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/parsers/javascript.py +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/parsers/python.py +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/parsers/rust.py +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/project_root.py +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/resolution.py +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/scripts/wikifier.bat +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/scripts/wikifier.ps1 +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/scripts/wikifier.sh +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier/serve.py +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier.egg-info/SOURCES.txt +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier.egg-info/dependency_links.txt +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier.egg-info/entry_points.txt +0 -0
- {wikifier-4.6.0 → wikifier-4.6.2}/wikifier.egg-info/requires.txt +0 -0
- {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.
|
|
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
|
-
| **
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
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
|
|
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":
|
|
280
|
+
"content_dirty": True,
|
|
277
281
|
"missing": False,
|
|
278
282
|
"hash": live,
|
|
279
283
|
"reason": "no_baseline",
|
|
280
|
-
"seed_baseline":
|
|
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.
|
|
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
|
|
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
|