wikifier 4.5.8__tar.gz → 4.6.0__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.5.8/wikifier.egg-info → wikifier-4.6.0}/PKG-INFO +1 -1
  2. {wikifier-4.5.8 → wikifier-4.6.0}/skills/run.md +19 -15
  3. wikifier-4.6.0/tests/test_agent_loop.py +180 -0
  4. {wikifier-4.5.8 → wikifier-4.6.0}/tests/test_gap_closure.py +79 -0
  5. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier/__init__.py +6 -1
  6. wikifier-4.6.0/wikifier/agent_loop.py +486 -0
  7. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier/cli.py +232 -142
  8. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier/health.py +89 -1
  9. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier/import_cache.py +68 -10
  10. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier/mcp/server.py +67 -19
  11. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier/parsers/bree.py +2 -2
  12. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier/parsers/javascript.py +1 -1
  13. wikifier-4.6.0/wikifier/project_root.py +144 -0
  14. {wikifier-4.5.8 → wikifier-4.6.0/wikifier.egg-info}/PKG-INFO +1 -1
  15. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier.egg-info/SOURCES.txt +5 -0
  16. {wikifier-4.5.8 → wikifier-4.6.0}/CONTRIBUTING.md +0 -0
  17. {wikifier-4.5.8 → wikifier-4.6.0}/LICENSE +0 -0
  18. {wikifier-4.5.8 → wikifier-4.6.0}/MANIFEST.in +0 -0
  19. {wikifier-4.5.8 → wikifier-4.6.0}/README.md +0 -0
  20. {wikifier-4.5.8 → wikifier-4.6.0}/diagnostics.html +0 -0
  21. {wikifier-4.5.8 → wikifier-4.6.0}/docs/spec.md +0 -0
  22. {wikifier-4.5.8 → wikifier-4.6.0}/index.html +0 -0
  23. {wikifier-4.5.8 → wikifier-4.6.0}/pyproject.toml +0 -0
  24. {wikifier-4.5.8 → wikifier-4.6.0}/setup.cfg +0 -0
  25. {wikifier-4.5.8 → wikifier-4.6.0}/tests/test_barrel_invalidation.py +0 -0
  26. {wikifier-4.5.8 → wikifier-4.6.0}/tests/test_health.py +0 -0
  27. {wikifier-4.5.8 → wikifier-4.6.0}/tests/test_import_cache.py +0 -0
  28. {wikifier-4.5.8 → wikifier-4.6.0}/tests/test_multi_lang_parsers.py +0 -0
  29. {wikifier-4.5.8 → wikifier-4.6.0}/tests/test_parsers.py +0 -0
  30. {wikifier-4.5.8 → wikifier-4.6.0}/tests/test_selftest_wrappers.py +0 -0
  31. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier/__main__.py +0 -0
  32. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier/contracts.py +0 -0
  33. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier/daemon.py +0 -0
  34. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier/diagnostics.py +0 -0
  35. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier/index.html +0 -0
  36. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier/library.py +0 -0
  37. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier/locking.py +0 -0
  38. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier/mcp/__init__.py +0 -0
  39. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier/parsers/__init__.py +0 -0
  40. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier/parsers/_edge.py +0 -0
  41. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier/parsers/c_cpp.py +0 -0
  42. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier/parsers/cdia.py +0 -0
  43. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier/parsers/csharp.py +0 -0
  44. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier/parsers/go_lang.py +0 -0
  45. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier/parsers/java.py +0 -0
  46. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier/parsers/python.py +0 -0
  47. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier/parsers/rust.py +0 -0
  48. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier/resolution.py +0 -0
  49. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier/scripts/wikifier.bat +0 -0
  50. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier/scripts/wikifier.ps1 +0 -0
  51. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier/scripts/wikifier.sh +0 -0
  52. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier/serve.py +0 -0
  53. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier.egg-info/dependency_links.txt +0 -0
  54. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier.egg-info/entry_points.txt +0 -0
  55. {wikifier-4.5.8 → wikifier-4.6.0}/wikifier.egg-info/requires.txt +0 -0
  56. {wikifier-4.5.8 → wikifier-4.6.0}/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.8
3
+ Version: 4.6.0
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
@@ -42,13 +42,16 @@ Do **not** open megamodules (`javascript.py`, `import_cache.py`, `bree.py`) to d
42
42
  | Need | Use |
43
43
  |------|-----|
44
44
  | First-run map | `init` → `update-maps` → `health --summary` → `suggest-next` |
45
- | Steady-state | `check-changes` edit 🔴/actionable 🟡 only `record-change` wiki `mark-green` |
46
- | Lookup | MCP Core 6: status, check_changes, needing_attention, get_file_wiki, suggest, record/mark |
47
- | Deps / cycles | `get_dependencies`, `get_dependents`, `get_cycles` (json) |
45
+ | **Session start (4.6+)** | `session_bootstrap` (or CLI `session-bootstrap`) one shot: root, health, attention, `actions[]` |
46
+ | 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
+ | **Why / journal** | `why_file` / `search_journal` — semantic trail, not mtime theater |
49
+ | Lookup Core | `session_bootstrap`, `check_changes`, `prepare_edit`, `suggest_next_actions` (json `actions[]`), `record_change`, `mark_green` |
50
+ | Advanced intel | `get_dependencies`, `get_dependents`, `get_cycles`, barrel/diagnostics — not daily Core |
48
51
  | Deletion | `record-deletion` |
49
52
  | Long-horizon prep | `autonomous-status` / `readiness`; then `daemon start` with lean monitor |
50
53
  | Hygiene | `seed-health`, `prune-pending`, `prune-health`, `validate` |
51
- | Code entry | `cli.py` dispatch + `health.py` + parsers — self-tests in `tests/` + `tests/selftest/` |
54
+ | Code entry | `cli.py` + `agent_loop.py` + `health.py` + parsers — tests in `tests/` |
52
55
 
53
56
  **Do not open megamodules** (`javascript.py` ~2.6k, `import_cache.py` ~2.3k, `bree.py` ~2k) for workflow decisions — use tools + this table.
54
57
 
@@ -61,30 +64,31 @@ Do **not** open megamodules (`javascript.py`, `import_cache.py`, `bree.py`) to d
61
64
  **Copy this exact block into the system prompt or the very first instruction of every new LLM session that uses Wikifier:**
62
65
 
63
66
  ```
64
- You are now operating inside a Wikifier-managed codebase (Agent Protocol v0.6 — package 4.5.x).
67
+ You are now operating inside a Wikifier-managed codebase (Agent Protocol v0.6 — package 4.6.x).
65
68
 
66
69
  This is strictly an agent-to-agent wiki for token saving: map lookup (health + file wikis + deps) instead of full sources; selective wiki updates; not a human docs product or Jira.
67
70
 
68
- SELECTIVE WORK (mandatory): Only update/remove/re-wiki 🔴 Red and 🟡 Yellow files. Do not re-summarize 🟢 Green files or the whole tree. First-run builds the structural map; wiki depth is filled as you touch files.
71
+ SELECTIVE WORK (mandatory): Only update/remove/re-wiki 🔴 Red and 🟡 Yellow files. Do not re-summarize 🟢 Green files or the whole tree. First-run builds the structural map; wiki depth is filled as you touch files. check-changes is content-honest (mtime-only thrash does not re-Yellow when source hash matches mark-green baseline).
69
72
 
70
73
  FIRST ACTIONS:
71
- 1. Prefer MCP tools when connected (get_project_status, get_file_wiki, check_changes, suggest_next_actions, get_dependencies, …). Always pass project_root= for external projects.
74
+ 1. Prefer MCP Core: session_bootstrap, check_changes, prepare_edit, suggest_next_actions (use actions[]), record_change, mark_green. Always pass project_root= for external projects.
72
75
  2. CLI/library fallback:
73
- WIKIFIER_PROJECT_ROOT=/path/to/target python -m wikifier check-changes
74
- python -m wikifier health --summary
75
- python -m wikifier suggest-next # or suggest_next_actions(format="json")
76
- ... edit only red/yellow sources ...
76
+ WIKIFIER_PROJECT_ROOT=/path/to/target python -m wikifier session-bootstrap
77
+ python -m wikifier check-changes
78
+ python -m wikifier suggest-next --json # actions[] dispatchable
79
+ python -m wikifier prepare-edit path/to/file
80
+ ... edit only red/actionable yellow sources ...
77
81
  python -m wikifier record-change "path/to/file" "why (semantic). Include subid if agent work."
78
82
  ... update that file's wiki only ...
79
83
  python -m wikifier mark-green "path/to/file" "reason"
80
84
  if imports changed: python -m wikifier update-maps [--directory=src/]
81
85
  if file removed: python -m wikifier record-deletion "path" "why"
82
86
  python -m wikifier health --summary
83
- 3. Optional background: `python -m wikifier monitor` or daemon for mtime heartbeat — you still own wiki + mark-green.
84
- 4. Prioritize 🔴 then 🟡 from health/pending. Lookup greens via get_file_wiki; do not rewrite them.
85
- 5. Agent docs edits: record-change + mark-green.
87
+ 3. Optional background: `python -m wikifier monitor` or daemon — you still own wiki + mark-green.
88
+ 4. Prioritize 🔴 then actionable 🟡. Lookup greens via prepare_edit / get_file_wiki; do not rewrite them.
89
+ 5. why_file / search_journal for semantic trail.
86
90
  6. Always explicit project_root / WIKIFIER_PROJECT_ROOT for external/multi-project work.
87
- 7. End turn: health + suggest_next_actions.
91
+ 7. End turn: health + suggest_next_actions (json).
88
92
 
89
93
  Never skip record-change — semantic audit trail (journal + health + pending).
90
94
 
@@ -0,0 +1,180 @@
1
+ """Agent-first ideal-loop tests: content-dirty, bootstrap, suggest actions, preflight, journal."""
2
+
3
+ import importlib
4
+ import os
5
+ import time
6
+ import unittest
7
+ from pathlib import Path
8
+
9
+ from tests._base import TempProjectTestCase
10
+
11
+ from wikifier import cli
12
+ from wikifier.health import classify_content_dirty, compute_source_content_hash
13
+ from wikifier.agent_loop import (
14
+ session_bootstrap,
15
+ prepare_edit,
16
+ search_journal,
17
+ why_file,
18
+ build_structured_actions,
19
+ )
20
+
21
+ health_mod = importlib.import_module("wikifier.health")
22
+
23
+
24
+ class TestContentHonestDirty(TempProjectTestCase):
25
+ def test_classify_touch_only_vs_rewrite(self):
26
+ p = self.write("src/a.py", "x = 1\n")
27
+ h1 = compute_source_content_hash(p)
28
+ self.assertIsNotNone(h1)
29
+ v = classify_content_dirty(p, h1)
30
+ self.assertFalse(v["content_dirty"])
31
+ self.assertEqual(v["reason"], "content_unchanged")
32
+ # mtime thrash without content change
33
+ os.utime(p, None)
34
+ v2 = classify_content_dirty(p, h1)
35
+ self.assertFalse(v2["content_dirty"])
36
+ self.assertEqual(v2["reason"], "content_unchanged")
37
+ # rewrite bytes
38
+ p.write_text("x = 2\n", encoding="utf-8")
39
+ v3 = classify_content_dirty(p, h1)
40
+ self.assertTrue(v3["content_dirty"])
41
+ self.assertEqual(v3["reason"], "content_changed")
42
+
43
+ def test_check_changes_skips_mtime_only_after_mark_green(self):
44
+ p = self.write("mod.py", "print(1)\n")
45
+ # Seed health green with source baseline via mark_green
46
+ res_mg = cli.mark_green("mod.py", "baseline", project_root=self.root)
47
+ self.assertTrue(res_mg.get("success"), res_mg)
48
+ data = health_mod.load_health(self.root)
49
+ ent = data["entries"]["mod.py"]
50
+ self.assertIn("source_content_hash", ent)
51
+ baseline = ent["source_content_hash"]
52
+ # Ensure import_cache may list file as dirty via mtime: touch only
53
+ from wikifier import import_cache as ic
54
+ cache = {}
55
+ ic.update_file_data(cache, "mod.py", mtime=1, imports=[], resolved_pairs=[])
56
+ ic.save_cache(self.root, cache)
57
+ time.sleep(0.05)
58
+ os.utime(p, None)
59
+ # Force compute dirty by setting cache mtime older than file
60
+ cache = ic.load_cache(self.root)
61
+ cache["mod.py"]["mtime"] = 1
62
+ ic.save_cache(self.root, cache)
63
+ r = cli.check_changes(project_root=self.root)
64
+ self.assertTrue(r.get("success"), r)
65
+ # Content unchanged → should not Yellow for content
66
+ data2 = health_mod.load_health(self.root)
67
+ st = data2["entries"]["mod.py"]["status"]
68
+ self.assertIn("Green", st)
69
+ self.assertEqual(data2["entries"]["mod.py"].get("source_content_hash"), baseline)
70
+ # Real content edit → Yellow
71
+ p.write_text("print(2)\n", encoding="utf-8")
72
+ cache = ic.load_cache(self.root)
73
+ cache["mod.py"]["mtime"] = 1
74
+ ic.save_cache(self.root, cache)
75
+ r2 = cli.check_changes(project_root=self.root)
76
+ self.assertTrue(r2.get("success"), r2)
77
+ data3 = health_mod.load_health(self.root)
78
+ self.assertIn("Yellow", data3["entries"]["mod.py"]["status"])
79
+ self.assertIn("content", data3["entries"]["mod.py"]["reason"].lower())
80
+
81
+
82
+ class TestSessionBootstrapAndSuggest(TempProjectTestCase):
83
+ def test_session_bootstrap_shape(self):
84
+ self.write("a.py", "import os\n")
85
+ health_mod.upsert_entry(self.root, "a.py", "🟡 Yellow", "record-change: edit")
86
+ boot = session_bootstrap(project_root=self.root)
87
+ self.assertTrue(boot.get("success"))
88
+ self.assertEqual(Path(boot["project_root"]).resolve(), self.root)
89
+ self.assertIn("health_summary", boot)
90
+ self.assertIn("actions", boot)
91
+ self.assertIsInstance(boot["actions"], list)
92
+ self.assertTrue(any("action" in a for a in boot["actions"]))
93
+ self.assertIn("session_bootstrap", boot.get("core_surface") or [])
94
+
95
+ def test_suggest_json_has_structured_actions(self):
96
+ health_mod.upsert_entry(self.root, "b.py", "🔴 Red", "DELETED — gone")
97
+ health_mod.upsert_entry(self.root, "c.py", "🟡 Yellow", "content changed")
98
+ res = cli.suggest_next_actions(project_root=self.root, format="json")
99
+ self.assertTrue(res.get("success"))
100
+ actions = res.get("actions") or []
101
+ self.assertTrue(actions, "expected dispatchable actions")
102
+ for a in actions:
103
+ self.assertIn("action", a)
104
+ self.assertIn("priority", a)
105
+ self.assertIn("reason", a)
106
+ kinds = {a["action"] for a in actions}
107
+ self.assertTrue(kinds & {"investigate_red", "wiki_refresh", "review_acs", "update_maps_if_structure", "lookup_only", "map_first_ok", "fix_scope"})
108
+
109
+
110
+ class TestPrepareEditAndJournal(TempProjectTestCase):
111
+ def test_prepare_edit_returns_status_and_lists(self):
112
+ self.write("pkg/x.py", "from . import y\n")
113
+ health_mod.upsert_entry(self.root, "pkg/x.py", "🟢 Green", "ok")
114
+ # minimal cache edge
115
+ from wikifier import import_cache as ic
116
+ cache = {}
117
+ ic.update_file_data(
118
+ cache,
119
+ "pkg/x.py",
120
+ mtime=1,
121
+ imports=["./y"],
122
+ resolved_pairs=[{
123
+ "raw": "./y",
124
+ "resolved": "pkg/y.py",
125
+ "resolved_path": "pkg/y.py",
126
+ "confidence_score": 0.9,
127
+ }],
128
+ )
129
+ cache["_reverse_dependencies"] = {"pkg/x.py": ["pkg/z.py"]}
130
+ ic.save_cache(self.root, cache)
131
+ pe = prepare_edit("pkg/x.py", project_root=self.root)
132
+ self.assertTrue(pe.get("success"))
133
+ self.assertEqual(pe.get("file"), "pkg/x.py")
134
+ self.assertIn("Green", str(pe.get("status") or ""))
135
+ self.assertTrue(isinstance(pe.get("dependencies"), list))
136
+ self.assertIn("pkg/z.py", pe.get("dependents") or [])
137
+
138
+ def test_journal_search_and_why_file(self):
139
+ self.write("tracked.py", "v=1\n")
140
+ r = cli.record_change("tracked.py", "semantic reason alpha-subid", project_root=self.root)
141
+ self.assertTrue(r.get("success"), r)
142
+ sj = search_journal(project_root=self.root, query="alpha-subid")
143
+ self.assertTrue(sj.get("success"))
144
+ self.assertGreaterEqual(len(sj.get("matches") or []), 1)
145
+ self.assertTrue(any("alpha-subid" in (m.get("reason") or "") for m in sj["matches"]))
146
+ sj2 = search_journal(project_root=self.root, file="tracked.py")
147
+ self.assertGreaterEqual(len(sj2.get("matches") or []), 1)
148
+ why = why_file("tracked.py", project_root=self.root)
149
+ self.assertTrue(why.get("success"))
150
+ self.assertTrue(why.get("health_reason") or why.get("journal_matches"))
151
+
152
+
153
+ class TestBuildActionsUnit(unittest.TestCase):
154
+ def test_build_structured_actions_priorities(self):
155
+ acts = build_structured_actions(
156
+ red_files=["a.py"],
157
+ actionable_yellow_files=["b.py"],
158
+ red=1,
159
+ actionable_yellow=1,
160
+ acs_actionable=3,
161
+ clean=False,
162
+ )
163
+ self.assertTrue(any(a["action"] == "investigate_red" and a["file"] == "a.py" for a in acts))
164
+ self.assertTrue(any(a["action"] == "wiki_refresh" and a["file"] == "b.py" for a in acts))
165
+ self.assertTrue(any(a["action"] == "review_acs" for a in acts))
166
+ # sorted by priority
167
+ pris = [a["priority"] for a in acts]
168
+ self.assertEqual(pris, sorted(pris))
169
+
170
+
171
+ class TestCliBootstrapEntry(TempProjectTestCase):
172
+ def test_cli_session_bootstrap_function(self):
173
+ self.write("z.py", "pass\n")
174
+ res = cli.session_bootstrap(project_root=self.root)
175
+ self.assertTrue(res.get("success"))
176
+ self.assertIn("actions", res)
177
+
178
+
179
+ if __name__ == "__main__":
180
+ unittest.main()
@@ -103,6 +103,85 @@ class TestAcsActionableG4(TempProjectTestCase):
103
103
  for p in action
104
104
  ))
105
105
 
106
+ def test_dynamic_literal_noise_demoted_from_actionable(self):
107
+ """ACS v1.2: importlib.import_module(\"pkg\") static dynamics are not actionable."""
108
+ dyn_lit = {
109
+ "raw": "\"wikifier.health\"",
110
+ "resolved": "wikifier.health",
111
+ "resolved_path": None,
112
+ "is_dynamic": True,
113
+ "dynamic_type": "static",
114
+ "confidence_score": 0.05,
115
+ "confidence_reasons": [
116
+ "base:low", "conditional_context", "dynamic",
117
+ "detector:TernaryDetector",
118
+ ],
119
+ "confidence_explanation": (
120
+ 'Base low (0.05). TernaryDetector=importlib.import_module("wikifier.health"). '
121
+ "Recommendation: Runtime optional load is expected noise."
122
+ ),
123
+ "resolution_metadata": {"strategy": "python-dynamic"},
124
+ }
125
+ project_fragile = {
126
+ "raw": "./missing_local",
127
+ "resolved": "missing_local",
128
+ "resolved_path": None,
129
+ "is_dynamic": False,
130
+ "confidence_score": 0.35,
131
+ "confidence_reasons": ["base:low", "no_resolved_path"],
132
+ "confidence_explanation": "Unresolved project-local. Recommendation: fix path.",
133
+ "diagnostic": {"category": "project"},
134
+ "resolution_metadata": {"strategy": "relative-fs"},
135
+ }
136
+ cache = {
137
+ "mod.py": {
138
+ "mtime": 1,
139
+ "resolved_pairs": [dyn_lit, project_fragile],
140
+ }
141
+ }
142
+ self.assertTrue(ic._edge_is_dynamic_literal_noise(dyn_lit))
143
+ self.assertFalse(ic._edge_is_dynamic_literal_noise(project_fragile))
144
+ summary = ic.compute_acs_summary(cache)
145
+ self.assertEqual(summary.get("acs_version"), "1.2")
146
+ self.assertGreaterEqual(summary["low_conf_edges"], 2)
147
+ self.assertEqual(summary["actionable_low_conf_edges"], 1)
148
+ self.assertGreaterEqual(summary.get("dynamic_literal_noise_edges", 0), 1)
149
+ action = ic.get_low_confidence_edges(cache, actionable_only=True)
150
+ self.assertEqual(len(action), 1)
151
+ self.assertIn("missing_local", str(action[0].get("raw") or action[0].get("resolved")))
152
+
153
+
154
+ class TestLoadSafetyNoImportCycle(unittest.TestCase):
155
+ """Load-time safety: core modules import without circular-import failure."""
156
+
157
+ def test_import_wikifier_and_cycle_trio(self):
158
+ import importlib
159
+ import wikifier
160
+ self.assertTrue(hasattr(wikifier, "discover_project_root"))
161
+ # Fresh submodule loads (project_root breaks former cli↔cache↔bree SCC)
162
+ for name in (
163
+ "wikifier.project_root",
164
+ "wikifier.cli",
165
+ "wikifier.import_cache",
166
+ "wikifier.parsers.bree",
167
+ ):
168
+ mod = importlib.import_module(name)
169
+ self.assertIsNotNone(mod)
170
+ from wikifier.project_root import discover_project_root as dpr
171
+ from wikifier.cli import discover_project_root as dpr_cli
172
+ self.assertIs(dpr, dpr_cli)
173
+ root = dpr()
174
+ self.assertTrue(root.exists())
175
+
176
+ def test_bree_does_not_import_cli_at_module_level(self):
177
+ """Static check: bree source must not load-time import wikifier.cli."""
178
+ from pathlib import Path
179
+ bree_src = Path(__file__).resolve().parents[1] / "wikifier" / "parsers" / "bree.py"
180
+ text = bree_src.read_text(encoding="utf-8")
181
+ self.assertNotIn("from ..cli import", text)
182
+ self.assertNotIn("from wikifier.cli import", text)
183
+ self.assertIn("from ..project_root import", text)
184
+
106
185
 
107
186
  class TestGhostAndDeletionG7(TempProjectTestCase):
108
187
  def test_find_ghost_and_record_deletion(self):
@@ -42,7 +42,12 @@ from .cli import (
42
42
  suggest_next_actions,
43
43
  update_maps,
44
44
  health, # flat convenience func (delegates to the health module + adds scoping)
45
+ session_bootstrap,
46
+ prepare_edit,
47
+ search_journal,
48
+ why_file,
45
49
  )
50
+ from . import agent_loop
46
51
 
47
52
  # Re-export module under a non-shadowed name (G5). sys.modules['wikifier.health']
48
53
  # remains the real module; package attribute `health` is intentionally the function.
@@ -71,4 +76,4 @@ from .contracts import (
71
76
  compute_acs_confidence,
72
77
  )
73
78
 
74
- __version__ = "4.5.8"
79
+ __version__ = "4.6.0"