wikifier 4.5.4__tar.gz → 4.5.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {wikifier-4.5.4/wikifier.egg-info → wikifier-4.5.6}/PKG-INFO +1 -1
- wikifier-4.5.6/tests/test_gap_closure.py +274 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier/__init__.py +1 -1
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier/cli.py +113 -29
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier/health.py +477 -115
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier/mcp/server.py +14 -2
- {wikifier-4.5.4 → wikifier-4.5.6/wikifier.egg-info}/PKG-INFO +1 -1
- wikifier-4.5.4/tests/test_gap_closure.py +0 -128
- {wikifier-4.5.4 → wikifier-4.5.6}/CONTRIBUTING.md +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/LICENSE +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/MANIFEST.in +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/README.md +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/diagnostics.html +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/docs/spec.md +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/index.html +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/pyproject.toml +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/setup.cfg +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/skills/run.md +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/tests/test_barrel_invalidation.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/tests/test_health.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/tests/test_import_cache.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/tests/test_multi_lang_parsers.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/tests/test_parsers.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/tests/test_selftest_wrappers.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier/__main__.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier/contracts.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier/daemon.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier/diagnostics.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier/import_cache.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier/index.html +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier/library.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier/locking.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier/mcp/__init__.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier/parsers/__init__.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier/parsers/_edge.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier/parsers/bree.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier/parsers/c_cpp.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier/parsers/cdia.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier/parsers/csharp.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier/parsers/go_lang.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier/parsers/java.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier/parsers/javascript.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier/parsers/python.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier/parsers/rust.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier/resolution.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier/scripts/wikifier.bat +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier/scripts/wikifier.ps1 +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier/scripts/wikifier.sh +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier/serve.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier.egg-info/SOURCES.txt +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier.egg-info/dependency_links.txt +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier.egg-info/entry_points.txt +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier.egg-info/requires.txt +0 -0
- {wikifier-4.5.4 → wikifier-4.5.6}/wikifier.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wikifier
|
|
3
|
-
Version: 4.5.
|
|
3
|
+
Version: 4.5.6
|
|
4
4
|
Summary: Zero-dependency agent-to-agent codebase wiki for LLMs and AI agents. Autonomously maintained via record-change and mark-green for token-efficient lookup of files, dependencies, health, and summaries — across tiny scripts to 50k+ monorepos. Optional MCP server with rich tools for agents.
|
|
5
5
|
Author-email: Aron Amos <aron@example.com>
|
|
6
6
|
Maintainer: Aron Amos
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""Gap-closure regressions (G4 ACS actionable, G5 health module, G7 ghosts/deletion)."""
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import unittest
|
|
5
|
+
|
|
6
|
+
from tests._base import TempProjectTestCase
|
|
7
|
+
|
|
8
|
+
from wikifier import import_cache as ic
|
|
9
|
+
from wikifier import cli
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestHealthModuleImportG5(unittest.TestCase):
|
|
13
|
+
def test_importlib_health_is_module_with_get_summary(self):
|
|
14
|
+
mod = importlib.import_module("wikifier.health")
|
|
15
|
+
self.assertTrue(hasattr(mod, "get_summary"))
|
|
16
|
+
self.assertTrue(hasattr(mod, "find_ghost_entries"))
|
|
17
|
+
self.assertTrue(callable(mod.get_summary))
|
|
18
|
+
|
|
19
|
+
def test_health_module_alias_on_package(self):
|
|
20
|
+
import wikifier
|
|
21
|
+
self.assertTrue(hasattr(wikifier, "health_module"))
|
|
22
|
+
self.assertTrue(hasattr(wikifier.health_module, "get_summary"))
|
|
23
|
+
# Convenience function still exported as wikifier.health
|
|
24
|
+
self.assertTrue(callable(wikifier.health))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestAcsActionableG4(TempProjectTestCase):
|
|
28
|
+
def test_external_noise_excluded_from_actionable_low_conf(self):
|
|
29
|
+
cache = {}
|
|
30
|
+
# Project-internal low-conf edge
|
|
31
|
+
ic.update_file_data(
|
|
32
|
+
cache,
|
|
33
|
+
"app.py",
|
|
34
|
+
mtime=1,
|
|
35
|
+
imports=["./util"],
|
|
36
|
+
resolved_pairs=[{
|
|
37
|
+
"raw": "./util",
|
|
38
|
+
"resolved": "util.py",
|
|
39
|
+
"resolved_path": "util.py",
|
|
40
|
+
"confidence_score": 0.4,
|
|
41
|
+
"confidence_reasons": ["base:low", "via_barrel"],
|
|
42
|
+
"confidence_explanation": "Base low. Recommendation: Review project edge.",
|
|
43
|
+
"diagnostic": {"category": "project"},
|
|
44
|
+
"resolution_metadata": {"strategy": "relative-fs"},
|
|
45
|
+
}],
|
|
46
|
+
)
|
|
47
|
+
# External/stdlib-style edge (noise)
|
|
48
|
+
ic.update_file_data(
|
|
49
|
+
cache,
|
|
50
|
+
"app.py",
|
|
51
|
+
mtime=1,
|
|
52
|
+
imports=["json", "./util"],
|
|
53
|
+
resolved_pairs=[
|
|
54
|
+
{
|
|
55
|
+
"raw": "./util",
|
|
56
|
+
"resolved": "util.py",
|
|
57
|
+
"resolved_path": "util.py",
|
|
58
|
+
"confidence_score": 0.4,
|
|
59
|
+
"confidence_reasons": ["base:low"],
|
|
60
|
+
"confidence_explanation": "Project low. Recommendation: Review.",
|
|
61
|
+
"resolution_metadata": {"strategy": "relative-fs"},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"raw": "json",
|
|
65
|
+
"resolved": "json",
|
|
66
|
+
"resolved_path": "",
|
|
67
|
+
"confidence_score": 0.48,
|
|
68
|
+
"confidence_reasons": ["base:medium", "no_resolved_path"],
|
|
69
|
+
"confidence_explanation": "stdlib. Recommendation: harden import.",
|
|
70
|
+
"diagnostic": {"category": "external_or_bare"},
|
|
71
|
+
"resolution_metadata": {"strategy": "python-bare-or-external"},
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
)
|
|
75
|
+
# Note: update_file_data may replace pairs — set explicitly
|
|
76
|
+
cache["app.py"]["resolved_pairs"] = [
|
|
77
|
+
{
|
|
78
|
+
"raw": "./util",
|
|
79
|
+
"resolved": "util.py",
|
|
80
|
+
"resolved_path": "util.py",
|
|
81
|
+
"confidence_score": 0.4,
|
|
82
|
+
"confidence_reasons": ["base:low"],
|
|
83
|
+
"confidence_explanation": "Project low. Recommendation: Review.",
|
|
84
|
+
"resolution_metadata": {"strategy": "relative-fs"},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"raw": "json",
|
|
88
|
+
"resolved": "json",
|
|
89
|
+
"confidence_score": 0.48,
|
|
90
|
+
"confidence_reasons": ["base:medium"],
|
|
91
|
+
"confidence_explanation": "stdlib noise. Recommendation: ignore.",
|
|
92
|
+
"diagnostic": {"category": "external_or_bare"},
|
|
93
|
+
"resolution_metadata": {"strategy": "python-bare-or-external"},
|
|
94
|
+
},
|
|
95
|
+
]
|
|
96
|
+
summary = ic.compute_acs_summary(cache)
|
|
97
|
+
self.assertGreaterEqual(summary["low_conf_edges"], 1)
|
|
98
|
+
self.assertEqual(summary["actionable_low_conf_edges"], 1)
|
|
99
|
+
self.assertGreaterEqual(summary["external_noise_edges"], 1)
|
|
100
|
+
action = ic.get_low_confidence_edges(cache, actionable_only=True)
|
|
101
|
+
self.assertTrue(all(
|
|
102
|
+
(p.get("diagnostic") or {}).get("category") != "external_or_bare"
|
|
103
|
+
for p in action
|
|
104
|
+
))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class TestGhostAndDeletionG7(TempProjectTestCase):
|
|
108
|
+
def test_find_ghost_and_record_deletion(self):
|
|
109
|
+
health_mod = importlib.import_module("wikifier.health")
|
|
110
|
+
self.write("keep.py", "x=1\n")
|
|
111
|
+
self.write("gone.py", "y=1\n")
|
|
112
|
+
health_mod.upsert_entry(self.root, "keep.py", "🟢 Green", "ok")
|
|
113
|
+
health_mod.upsert_entry(self.root, "gone.py", "🟢 Green", "ok")
|
|
114
|
+
(self.root / "gone.py").unlink()
|
|
115
|
+
|
|
116
|
+
ghosts = health_mod.find_ghost_entries(self.root)
|
|
117
|
+
self.assertIn("gone.py", ghosts)
|
|
118
|
+
self.assertNotIn("keep.py", ghosts)
|
|
119
|
+
|
|
120
|
+
res = cli.record_deletion("gone.py", "removed in cleanup", project_root=self.root)
|
|
121
|
+
self.assertTrue(res.get("success"))
|
|
122
|
+
entries = health_mod.load_health(self.root)["entries"]
|
|
123
|
+
self.assertIn("🔴", entries["gone.py"]["status"])
|
|
124
|
+
self.assertIn("DELETED", entries["gone.py"]["reason"])
|
|
125
|
+
|
|
126
|
+
def test_record_deletion_rejects_flag_paths(self):
|
|
127
|
+
res = cli.record_deletion("--help", "oops", project_root=self.root)
|
|
128
|
+
self.assertFalse(res.get("success"))
|
|
129
|
+
self.assertIn("flag", (res.get("error") or "").lower())
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class TestPendingAndPollutionHygiene(TempProjectTestCase):
|
|
133
|
+
def test_pending_empty_marker_not_counted_and_no_dual_state(self):
|
|
134
|
+
health_mod = importlib.import_module("wikifier.health")
|
|
135
|
+
# Dual-state pollution (historical bug): empty marker + real items
|
|
136
|
+
p = self.root / "pending_updates.md"
|
|
137
|
+
p.write_text(
|
|
138
|
+
"# Pending Updates\n\n(no active items)\n- src/a.py: review\n",
|
|
139
|
+
encoding="utf-8",
|
|
140
|
+
)
|
|
141
|
+
self.assertEqual(health_mod.count_pending(self.root), 1)
|
|
142
|
+
# Normalize via remove of the only item → empty marker only
|
|
143
|
+
health_mod.remove_from_pending(self.root, "src/a.py")
|
|
144
|
+
text = p.read_text(encoding="utf-8")
|
|
145
|
+
self.assertIn("no pending", text.lower())
|
|
146
|
+
self.assertNotIn("- src/a.py", text)
|
|
147
|
+
self.assertEqual(health_mod.count_pending(self.root), 0)
|
|
148
|
+
# Add clears empty marker
|
|
149
|
+
health_mod.add_to_pending(self.root, "src/b.py", "need wiki")
|
|
150
|
+
text2 = p.read_text(encoding="utf-8")
|
|
151
|
+
self.assertIn("- src/b.py:", text2)
|
|
152
|
+
self.assertNotIn("no pending", text2.lower())
|
|
153
|
+
self.assertNotIn("no active", text2.lower())
|
|
154
|
+
self.assertEqual(health_mod.count_pending(self.root), 1)
|
|
155
|
+
summary = health_mod.get_summary(self.root)
|
|
156
|
+
self.assertEqual(summary["pending_updates"], 1)
|
|
157
|
+
|
|
158
|
+
def test_superseded_and_flag_deleted_keys_pruned(self):
|
|
159
|
+
health_mod = importlib.import_module("wikifier.health")
|
|
160
|
+
# Inject pollution keys directly then save
|
|
161
|
+
data = {
|
|
162
|
+
"version": 2,
|
|
163
|
+
"last_updated": "2026-01-01",
|
|
164
|
+
"entries": {
|
|
165
|
+
"M5.3 Cycle1 evidence append: 3 subs spawned": {
|
|
166
|
+
"status": "🔴 Red",
|
|
167
|
+
"reason": "DELETED — Historical early M5.3 launch note",
|
|
168
|
+
"last_updated": "2026-01-01",
|
|
169
|
+
},
|
|
170
|
+
"--help": {
|
|
171
|
+
"status": "🔴 Red",
|
|
172
|
+
"reason": "DELETED — removed",
|
|
173
|
+
"last_updated": "2026-01-01",
|
|
174
|
+
},
|
|
175
|
+
"real.py": {
|
|
176
|
+
"status": "🟢 Green",
|
|
177
|
+
"reason": "ok",
|
|
178
|
+
"last_updated": "2026-01-01",
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
health_mod.save_health(self.root, data)
|
|
183
|
+
entries = health_mod.load_health(self.root)["entries"]
|
|
184
|
+
self.assertNotIn("M5.3 Cycle1 evidence append: 3 subs spawned", entries)
|
|
185
|
+
self.assertNotIn("--help", entries)
|
|
186
|
+
self.assertIn("real.py", entries)
|
|
187
|
+
# Real path DELETED audit is kept
|
|
188
|
+
health_mod.upsert_entry(self.root, "gone_real.py", "🔴 Red", "DELETED — intentional")
|
|
189
|
+
entries2 = health_mod.load_health(self.root)["entries"]
|
|
190
|
+
self.assertIn("gone_real.py", entries2)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class TestMapFirstHealthAndValidate(TempProjectTestCase):
|
|
194
|
+
def test_deep_relative_keys_are_under_root(self):
|
|
195
|
+
"""Regression: deep monorepo rel paths must not be treated as abs pollution."""
|
|
196
|
+
health_mod = importlib.import_module("wikifier.health")
|
|
197
|
+
deep = "airflow-core/src/airflow/api/client/local_client.py"
|
|
198
|
+
self.assertTrue(health_mod._entry_is_under_root(self.root, deep))
|
|
199
|
+
self.assertTrue(health_mod._entry_is_under_root(self.root, "pkg/a.py"))
|
|
200
|
+
|
|
201
|
+
def test_seed_health_from_map_creates_stubs(self):
|
|
202
|
+
health_mod = importlib.import_module("wikifier.health")
|
|
203
|
+
from wikifier import import_cache as ic
|
|
204
|
+
|
|
205
|
+
self.write("pkg/a.py", "import b\n")
|
|
206
|
+
self.write("pkg/b.py", "x=1\n")
|
|
207
|
+
self.write("README.md", "# hi\n")
|
|
208
|
+
cache = {}
|
|
209
|
+
ic.update_file_data(cache, "pkg/a.py", mtime=1, imports=["b"], resolved_pairs=[])
|
|
210
|
+
ic.update_file_data(cache, "pkg/b.py", mtime=1, imports=[], resolved_pairs=[])
|
|
211
|
+
ic.save_cache(self.root, cache)
|
|
212
|
+
|
|
213
|
+
# No health file yet
|
|
214
|
+
self.assertFalse((self.root / "file_health.json").exists())
|
|
215
|
+
res = health_mod.seed_health_from_map(self.root)
|
|
216
|
+
self.assertTrue(res.get("success"))
|
|
217
|
+
self.assertGreaterEqual(res.get("seeded"), 2)
|
|
218
|
+
entries = health_mod.load_health(self.root)["entries"]
|
|
219
|
+
self.assertIn("pkg/a.py", entries)
|
|
220
|
+
self.assertIn("pkg/b.py", entries)
|
|
221
|
+
self.assertTrue(entries["pkg/a.py"]["status"].startswith("🟡"))
|
|
222
|
+
|
|
223
|
+
def test_validate_ignores_non_source_and_reports_mapped_gaps(self):
|
|
224
|
+
health_mod = importlib.import_module("wikifier.health")
|
|
225
|
+
from wikifier import import_cache as ic
|
|
226
|
+
|
|
227
|
+
self.write("src/app.py", "print(1)\n")
|
|
228
|
+
self.write("src/notes.md", "docs\n")
|
|
229
|
+
self.write("src/logo.png", "x")
|
|
230
|
+
(self.root / "monitored_paths.txt").write_text("src\n", encoding="utf-8")
|
|
231
|
+
cache = {}
|
|
232
|
+
ic.update_file_data(cache, "src/app.py", mtime=1, imports=[], resolved_pairs=[])
|
|
233
|
+
ic.update_file_data(cache, "src/other.py", mtime=1, imports=[], resolved_pairs=[])
|
|
234
|
+
ic.save_cache(self.root, cache)
|
|
235
|
+
|
|
236
|
+
# No health yet — in-scope mapped + monitored parseable gaps
|
|
237
|
+
v = health_mod.validate_health(self.root)
|
|
238
|
+
self.assertTrue(v.get("map_first"))
|
|
239
|
+
self.assertGreaterEqual(v.get("mapped_in_scope_without_health_count", 0), 1)
|
|
240
|
+
self.assertGreaterEqual(v.get("missing_count", 0), 1)
|
|
241
|
+
# notes.md / logo.png must not appear as monitored-source gaps
|
|
242
|
+
mon_missing = v.get("missing_monitored_source") or []
|
|
243
|
+
self.assertTrue(all(not m.endswith(".md") and not m.endswith(".png") for m in mon_missing))
|
|
244
|
+
|
|
245
|
+
health_mod.seed_health_from_map(self.root) # only_monitored default
|
|
246
|
+
# Seed in-scope map keys; app.py on disk + mapped in-scope covered
|
|
247
|
+
v2 = health_mod.validate_health(self.root)
|
|
248
|
+
self.assertEqual(v2.get("mapped_in_scope_without_health_count"), 0)
|
|
249
|
+
# missing_count may still include on-disk sources not in map (ok if 0 after seed of map keys only)
|
|
250
|
+
self.assertEqual(v2.get("mapped_in_scope_without_health_count"), 0)
|
|
251
|
+
|
|
252
|
+
def test_prune_pending_and_health_to_monitored(self):
|
|
253
|
+
health_mod = importlib.import_module("wikifier.health")
|
|
254
|
+
(self.root / "monitored_paths.txt").write_text("src\n", encoding="utf-8")
|
|
255
|
+
self.write("src/keep.py", "k=1\n")
|
|
256
|
+
self.write("other/out.py", "o=1\n")
|
|
257
|
+
health_mod.upsert_entry(self.root, "src/keep.py", "🟡 Yellow", "in scope")
|
|
258
|
+
health_mod.upsert_entry(self.root, "other/out.py", "🟡 Yellow", "out of scope")
|
|
259
|
+
health_mod.add_to_pending(self.root, "src/keep.py", "need wiki")
|
|
260
|
+
health_mod.add_to_pending(self.root, "other/out.py", "noise")
|
|
261
|
+
|
|
262
|
+
pr = health_mod.prune_pending_to_monitored(self.root)
|
|
263
|
+
self.assertEqual(pr.get("removed"), 1)
|
|
264
|
+
self.assertEqual(health_mod.count_pending(self.root), 1)
|
|
265
|
+
|
|
266
|
+
hr = health_mod.prune_health_outside_monitored(self.root)
|
|
267
|
+
self.assertEqual(hr.get("removed"), 1)
|
|
268
|
+
entries = health_mod.load_health(self.root)["entries"]
|
|
269
|
+
self.assertIn("src/keep.py", entries)
|
|
270
|
+
self.assertNotIn("other/out.py", entries)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
if __name__ == "__main__":
|
|
274
|
+
unittest.main()
|
|
@@ -616,23 +616,6 @@ def run_full_update(
|
|
|
616
616
|
result["parse_errors"] = parse_errors[:10]
|
|
617
617
|
result["parse_error_count"] = len(parse_errors)
|
|
618
618
|
|
|
619
|
-
# Seed health stubs for newly parsed files (map-first wiki: agents fill prose later)
|
|
620
|
-
health_seeded = 0
|
|
621
|
-
if _health_mod is not None and new_entries:
|
|
622
|
-
try:
|
|
623
|
-
existing = (_health_mod.load_health(root) or {}).get("entries") or {}
|
|
624
|
-
for rel in new_entries:
|
|
625
|
-
if rel in existing:
|
|
626
|
-
continue
|
|
627
|
-
_health_mod.upsert_entry(
|
|
628
|
-
root, rel, "🟡 Yellow",
|
|
629
|
-
"Initial stub — parsed into map; agent should wiki + mark-green when editing"
|
|
630
|
-
)
|
|
631
|
-
health_seeded += 1
|
|
632
|
-
except Exception:
|
|
633
|
-
health_seeded = 0
|
|
634
|
-
result["health_stubs_seeded"] = health_seeded
|
|
635
|
-
|
|
636
619
|
# === 4. Persist (single save; reload first to pick up the barrel flush) ===
|
|
637
620
|
cache = ic.load_cache(root) or {}
|
|
638
621
|
cache.update(new_entries)
|
|
@@ -669,6 +652,27 @@ def run_full_update(
|
|
|
669
652
|
except Exception:
|
|
670
653
|
pass
|
|
671
654
|
|
|
655
|
+
# === 5b. Map-first health stubs (always backfill; warm cache safe) ===
|
|
656
|
+
# 0-dirty incremental runs never used to create file_health.json — fixed here.
|
|
657
|
+
health_seeded = 0
|
|
658
|
+
if _health_mod is not None and hasattr(_health_mod, "seed_health_from_map"):
|
|
659
|
+
try:
|
|
660
|
+
max_seed = int(os.environ.get("WIKIFIER_HEALTH_SEED_MAX", "20000") or "20000")
|
|
661
|
+
map_keys = [k for k in cache if isinstance(k, str) and k and not k.startswith("_")]
|
|
662
|
+
seed_res = _health_mod.seed_health_from_map(
|
|
663
|
+
root, map_keys=map_keys, max_new=max_seed,
|
|
664
|
+
)
|
|
665
|
+
health_seeded = int(seed_res.get("seeded") or 0)
|
|
666
|
+
if hasattr(_health_mod, "seed_health_for_monitored_sources"):
|
|
667
|
+
disk_res = _health_mod.seed_health_for_monitored_sources(
|
|
668
|
+
root, max_new=max_seed,
|
|
669
|
+
)
|
|
670
|
+
health_seeded += int(disk_res.get("seeded") or 0)
|
|
671
|
+
except Exception as se:
|
|
672
|
+
result["health_seed_error"] = str(se)
|
|
673
|
+
health_seeded = 0
|
|
674
|
+
result["health_stubs_seeded"] = health_seeded
|
|
675
|
+
|
|
672
676
|
# === 6. library.md (atomic; pure Python) ===
|
|
673
677
|
try:
|
|
674
678
|
from .library import write_library_md
|
|
@@ -805,16 +809,40 @@ def main():
|
|
|
805
809
|
if res.get("message"):
|
|
806
810
|
print(res["message"])
|
|
807
811
|
return 0 if res.get("success", True) else 1
|
|
808
|
-
if _cmd0 == "record-change"
|
|
812
|
+
if _cmd0 == "record-change":
|
|
813
|
+
if not _args or _args[0] in ("--help", "-h", "help"):
|
|
814
|
+
print('Usage: wikifier record-change <file> "<reason>"')
|
|
815
|
+
return 0
|
|
816
|
+
if _args[0].startswith("-") or len(_args) < 2:
|
|
817
|
+
print('Usage: wikifier record-change <file> "<reason>"', file=sys.stderr)
|
|
818
|
+
return 1
|
|
809
819
|
res = record_change(_args[0], " ".join(_args[1:]), project_root=project_root)
|
|
810
820
|
print(res.get("message") or res)
|
|
811
821
|
return 0 if res.get("success") else 1
|
|
812
|
-
if _cmd0 == "mark-green"
|
|
822
|
+
if _cmd0 == "mark-green":
|
|
823
|
+
if not _args or _args[0] in ("--help", "-h", "help"):
|
|
824
|
+
print("Usage: wikifier mark-green <file> [reason]")
|
|
825
|
+
return 0
|
|
826
|
+
if _args[0].startswith("-"):
|
|
827
|
+
print("Usage: wikifier mark-green <file> [reason]", file=sys.stderr)
|
|
828
|
+
return 1
|
|
813
829
|
reason = " ".join(_args[1:]) if len(_args) > 1 else ""
|
|
814
830
|
res = mark_green(_args[0], reason, project_root=project_root)
|
|
815
831
|
print(res.get("message") or res)
|
|
816
832
|
return 0 if res.get("success") else 1
|
|
817
|
-
if _cmd0 == "record-deletion"
|
|
833
|
+
if _cmd0 == "record-deletion":
|
|
834
|
+
# Guard: `record-deletion --help` must not treat `--help` as a file path
|
|
835
|
+
# (that polluted health with a 🔴 DELETED "--help" key).
|
|
836
|
+
if not _args or _args[0] in ("--help", "-h", "help"):
|
|
837
|
+
print('Usage: wikifier record-deletion <file> "<reason>"')
|
|
838
|
+
return 0
|
|
839
|
+
if _args[0].startswith("-"):
|
|
840
|
+
print(
|
|
841
|
+
'Usage: wikifier record-deletion <file> "<reason>"\n'
|
|
842
|
+
" <file> must be a project path, not a flag.",
|
|
843
|
+
file=sys.stderr,
|
|
844
|
+
)
|
|
845
|
+
return 1
|
|
818
846
|
reason = " ".join(_args[1:]) if len(_args) > 1 else "removed"
|
|
819
847
|
res = record_deletion(_args[0], reason, project_root=project_root)
|
|
820
848
|
print(res.get("message") or res)
|
|
@@ -833,13 +861,40 @@ def main():
|
|
|
833
861
|
if _health_mod is not None:
|
|
834
862
|
root = _get_effective_root(project_root)
|
|
835
863
|
res = _health_mod.validate_health(root)
|
|
836
|
-
ghosts = []
|
|
837
|
-
if hasattr(_health_mod, "find_ghost_entries"):
|
|
838
|
-
ghosts = _health_mod.find_ghost_entries(root)
|
|
839
|
-
res["ghost_count"] = len(ghosts)
|
|
840
|
-
res["ghosts"] = ghosts[:50]
|
|
841
864
|
print(_json.dumps(res, indent=2, default=str))
|
|
865
|
+
# Map-first: exit 0 when map is covered (or no map + no monitored source gaps)
|
|
842
866
|
return 0 if res.get("missing_count", 0) == 0 else 1
|
|
867
|
+
if _cmd0 in ("seed-health", "seed-health-from-map"):
|
|
868
|
+
import json as _json
|
|
869
|
+
if _health_mod is None:
|
|
870
|
+
print("[wikifier] health module unavailable", file=sys.stderr)
|
|
871
|
+
return 1
|
|
872
|
+
root = _get_effective_root(project_root)
|
|
873
|
+
res = _health_mod.seed_health_from_map(root)
|
|
874
|
+
if hasattr(_health_mod, "seed_health_for_monitored_sources"):
|
|
875
|
+
disk = _health_mod.seed_health_for_monitored_sources(root)
|
|
876
|
+
res["disk_seeded"] = disk.get("seeded")
|
|
877
|
+
res["seeded_total"] = int(res.get("seeded") or 0) + int(disk.get("seeded") or 0)
|
|
878
|
+
print(_json.dumps(res, indent=2, default=str))
|
|
879
|
+
return 0 if res.get("success") else 1
|
|
880
|
+
if _cmd0 in ("prune-pending", "prune-pending-monitored"):
|
|
881
|
+
import json as _json
|
|
882
|
+
if _health_mod is None:
|
|
883
|
+
print("[wikifier] health module unavailable", file=sys.stderr)
|
|
884
|
+
return 1
|
|
885
|
+
root = _get_effective_root(project_root)
|
|
886
|
+
res = _health_mod.prune_pending_to_monitored(root)
|
|
887
|
+
print(_json.dumps(res, indent=2, default=str))
|
|
888
|
+
return 0 if res.get("success") else 1
|
|
889
|
+
if _cmd0 in ("prune-health-monitored", "prune-health"):
|
|
890
|
+
import json as _json
|
|
891
|
+
if _health_mod is None:
|
|
892
|
+
print("[wikifier] health module unavailable", file=sys.stderr)
|
|
893
|
+
return 1
|
|
894
|
+
root = _get_effective_root(project_root)
|
|
895
|
+
res = _health_mod.prune_health_outside_monitored(root)
|
|
896
|
+
print(_json.dumps(res, indent=2, default=str))
|
|
897
|
+
return 0 if res.get("success") else 1
|
|
843
898
|
except Exception as e:
|
|
844
899
|
print(f"[wikifier] Python-primary {_cmd0} failed: {e}", file=sys.stderr)
|
|
845
900
|
return 1
|
|
@@ -1039,8 +1094,14 @@ def _ensure_journal_entry(root: Path, action: str, file: str, reason: str) -> No
|
|
|
1039
1094
|
|
|
1040
1095
|
|
|
1041
1096
|
def _add_to_pending(root: Path, file: str, msg: str) -> None:
|
|
1042
|
-
"""
|
|
1097
|
+
"""Append to pending_updates.md via health helpers (normalized empty/items)."""
|
|
1043
1098
|
try:
|
|
1099
|
+
if _health_mod is not None and hasattr(_health_mod, "add_to_pending"):
|
|
1100
|
+
# Caller may already hold project lock (re-entrant).
|
|
1101
|
+
_health_mod._do_add_to_pending(root, file, msg) if hasattr(
|
|
1102
|
+
_health_mod, "_do_add_to_pending"
|
|
1103
|
+
) else _health_mod.add_to_pending(root, file, msg)
|
|
1104
|
+
return
|
|
1044
1105
|
p = root / "pending_updates.md"
|
|
1045
1106
|
line = f"- {file}: {msg}\n"
|
|
1046
1107
|
with open(p, "a", encoding="utf-8") as f:
|
|
@@ -1050,8 +1111,14 @@ def _add_to_pending(root: Path, file: str, msg: str) -> None:
|
|
|
1050
1111
|
|
|
1051
1112
|
|
|
1052
1113
|
def _remove_from_pending(root: Path, file: str) -> None:
|
|
1053
|
-
"""Best-effort removal (used by mark_green
|
|
1114
|
+
"""Best-effort removal (used by mark_green). Prefers health normalizer."""
|
|
1054
1115
|
try:
|
|
1116
|
+
if _health_mod is not None and hasattr(_health_mod, "_do_remove_from_pending"):
|
|
1117
|
+
_health_mod._do_remove_from_pending(root, file)
|
|
1118
|
+
return
|
|
1119
|
+
if _health_mod is not None and hasattr(_health_mod, "remove_from_pending"):
|
|
1120
|
+
_health_mod.remove_from_pending(root, file)
|
|
1121
|
+
return
|
|
1055
1122
|
p = root / "pending_updates.md"
|
|
1056
1123
|
if p.exists():
|
|
1057
1124
|
lines = [ln for ln in p.read_text(encoding="utf-8").splitlines() if file not in ln]
|
|
@@ -1161,6 +1228,8 @@ def check_changes(project_root: Optional[Union[str, Path]] = None) -> Dict[str,
|
|
|
1161
1228
|
ghosts_marked = 0
|
|
1162
1229
|
if _health_mod is not None:
|
|
1163
1230
|
root_res = root.resolve()
|
|
1231
|
+
# Prefer unlocked helpers while we already hold project lock
|
|
1232
|
+
_upsert = getattr(_health_mod, "_do_upsert_entry", None) or _health_mod.upsert_entry
|
|
1164
1233
|
for p in dirty_batch:
|
|
1165
1234
|
try:
|
|
1166
1235
|
pr = Path(p).resolve()
|
|
@@ -1169,13 +1238,20 @@ def check_changes(project_root: Optional[Union[str, Path]] = None) -> Dict[str,
|
|
|
1169
1238
|
rel = str(pr.relative_to(root_res))
|
|
1170
1239
|
except Exception:
|
|
1171
1240
|
continue
|
|
1172
|
-
|
|
1241
|
+
_upsert(
|
|
1173
1242
|
root, rel, "🟡 Yellow",
|
|
1174
1243
|
"mtime changed since last check-changes (Python primary auto-detected)"
|
|
1175
1244
|
)
|
|
1176
1245
|
_add_to_pending(root, rel, "Auto-detected modification — review and run mark-green after wiki update")
|
|
1177
1246
|
_ensure_journal_entry(root, "auto-detected", rel, "File mtime changed (check_changes Python primary)")
|
|
1178
1247
|
changed_count += 1
|
|
1248
|
+
# Keep pending queue aligned with lean monitored_paths (no flood outside scope)
|
|
1249
|
+
if hasattr(_health_mod, "prune_pending_to_monitored"):
|
|
1250
|
+
try:
|
|
1251
|
+
pr = _health_mod.prune_pending_to_monitored(root)
|
|
1252
|
+
result["pending_pruned"] = pr.get("removed", 0)
|
|
1253
|
+
except Exception:
|
|
1254
|
+
pass
|
|
1179
1255
|
|
|
1180
1256
|
# G7: surface ghost health entries (tracked path missing on disk, not already DELETED)
|
|
1181
1257
|
try:
|
|
@@ -1265,9 +1341,13 @@ def record_deletion(file: str, reason: str, project_root: Optional[Union[str, Pa
|
|
|
1265
1341
|
|
|
1266
1342
|
G7: marks 🔴 DELETED, pending + journal, and best-effort prunes barrel cache
|
|
1267
1343
|
references so deleted paths do not keep invalidating importers forever.
|
|
1344
|
+
Rejects flag-like paths (`--help`) so CLI misuse cannot pollute health.
|
|
1268
1345
|
"""
|
|
1269
1346
|
root = _get_effective_root(project_root)
|
|
1270
1347
|
result: Dict[str, Any] = {"success": False, "file": file, "project_root": str(root), "action": "deletion"}
|
|
1348
|
+
if not file or str(file).startswith("-") or str(file) in ("--help", "-h", "help"):
|
|
1349
|
+
result["error"] = "file must be a project path, not a flag/empty string"
|
|
1350
|
+
return result
|
|
1271
1351
|
try:
|
|
1272
1352
|
lock_ctx = (locking.file_lock(root) if locking is not None else _nullcontext())
|
|
1273
1353
|
with lock_ctx:
|
|
@@ -1279,7 +1359,11 @@ def record_deletion(file: str, reason: str, project_root: Optional[Union[str, Pa
|
|
|
1279
1359
|
except Exception:
|
|
1280
1360
|
rel = file
|
|
1281
1361
|
if _health_mod is not None:
|
|
1282
|
-
|
|
1362
|
+
# Prefer unlocked upsert when we already hold the project lock.
|
|
1363
|
+
if hasattr(_health_mod, "_do_upsert_entry"):
|
|
1364
|
+
_health_mod._do_upsert_entry(root, rel, "🔴 Red", f"DELETED — {reason}")
|
|
1365
|
+
else:
|
|
1366
|
+
_health_mod.upsert_entry(root, rel, "🔴 Red", f"DELETED — {reason}")
|
|
1283
1367
|
_add_to_pending(root, rel, f"File was deleted. Consider wiki archival. {reason}")
|
|
1284
1368
|
_ensure_journal_entry(root, "record-deletion", rel, reason or "No reason provided.")
|
|
1285
1369
|
prune_stats: Dict[str, Any] = {}
|