wikifier 4.5.4__tar.gz → 4.5.5__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.5}/PKG-INFO +1 -1
- {wikifier-4.5.4 → wikifier-4.5.5}/tests/test_gap_closure.py +66 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier/__init__.py +1 -1
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier/cli.py +50 -6
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier/health.py +128 -77
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier/mcp/server.py +14 -2
- {wikifier-4.5.4 → wikifier-4.5.5/wikifier.egg-info}/PKG-INFO +1 -1
- {wikifier-4.5.4 → wikifier-4.5.5}/CONTRIBUTING.md +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/LICENSE +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/MANIFEST.in +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/README.md +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/diagnostics.html +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/docs/spec.md +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/index.html +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/pyproject.toml +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/setup.cfg +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/skills/run.md +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/tests/test_barrel_invalidation.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/tests/test_health.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/tests/test_import_cache.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/tests/test_multi_lang_parsers.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/tests/test_parsers.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/tests/test_selftest_wrappers.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier/__main__.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier/contracts.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier/daemon.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier/diagnostics.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier/import_cache.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier/index.html +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier/library.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier/locking.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier/mcp/__init__.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier/parsers/__init__.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier/parsers/_edge.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier/parsers/bree.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier/parsers/c_cpp.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier/parsers/cdia.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier/parsers/csharp.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier/parsers/go_lang.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier/parsers/java.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier/parsers/javascript.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier/parsers/python.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier/parsers/rust.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier/resolution.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier/scripts/wikifier.bat +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier/scripts/wikifier.ps1 +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier/scripts/wikifier.sh +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier/serve.py +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier.egg-info/SOURCES.txt +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier.egg-info/dependency_links.txt +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier.egg-info/entry_points.txt +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/wikifier.egg-info/requires.txt +0 -0
- {wikifier-4.5.4 → wikifier-4.5.5}/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.5
|
|
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
|
|
@@ -123,6 +123,72 @@ class TestGhostAndDeletionG7(TempProjectTestCase):
|
|
|
123
123
|
self.assertIn("🔴", entries["gone.py"]["status"])
|
|
124
124
|
self.assertIn("DELETED", entries["gone.py"]["reason"])
|
|
125
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
|
+
|
|
126
192
|
|
|
127
193
|
if __name__ == "__main__":
|
|
128
194
|
unittest.main()
|
|
@@ -805,16 +805,40 @@ def main():
|
|
|
805
805
|
if res.get("message"):
|
|
806
806
|
print(res["message"])
|
|
807
807
|
return 0 if res.get("success", True) else 1
|
|
808
|
-
if _cmd0 == "record-change"
|
|
808
|
+
if _cmd0 == "record-change":
|
|
809
|
+
if not _args or _args[0] in ("--help", "-h", "help"):
|
|
810
|
+
print('Usage: wikifier record-change <file> "<reason>"')
|
|
811
|
+
return 0
|
|
812
|
+
if _args[0].startswith("-") or len(_args) < 2:
|
|
813
|
+
print('Usage: wikifier record-change <file> "<reason>"', file=sys.stderr)
|
|
814
|
+
return 1
|
|
809
815
|
res = record_change(_args[0], " ".join(_args[1:]), project_root=project_root)
|
|
810
816
|
print(res.get("message") or res)
|
|
811
817
|
return 0 if res.get("success") else 1
|
|
812
|
-
if _cmd0 == "mark-green"
|
|
818
|
+
if _cmd0 == "mark-green":
|
|
819
|
+
if not _args or _args[0] in ("--help", "-h", "help"):
|
|
820
|
+
print("Usage: wikifier mark-green <file> [reason]")
|
|
821
|
+
return 0
|
|
822
|
+
if _args[0].startswith("-"):
|
|
823
|
+
print("Usage: wikifier mark-green <file> [reason]", file=sys.stderr)
|
|
824
|
+
return 1
|
|
813
825
|
reason = " ".join(_args[1:]) if len(_args) > 1 else ""
|
|
814
826
|
res = mark_green(_args[0], reason, project_root=project_root)
|
|
815
827
|
print(res.get("message") or res)
|
|
816
828
|
return 0 if res.get("success") else 1
|
|
817
|
-
if _cmd0 == "record-deletion"
|
|
829
|
+
if _cmd0 == "record-deletion":
|
|
830
|
+
# Guard: `record-deletion --help` must not treat `--help` as a file path
|
|
831
|
+
# (that polluted health with a 🔴 DELETED "--help" key).
|
|
832
|
+
if not _args or _args[0] in ("--help", "-h", "help"):
|
|
833
|
+
print('Usage: wikifier record-deletion <file> "<reason>"')
|
|
834
|
+
return 0
|
|
835
|
+
if _args[0].startswith("-"):
|
|
836
|
+
print(
|
|
837
|
+
'Usage: wikifier record-deletion <file> "<reason>"\n'
|
|
838
|
+
" <file> must be a project path, not a flag.",
|
|
839
|
+
file=sys.stderr,
|
|
840
|
+
)
|
|
841
|
+
return 1
|
|
818
842
|
reason = " ".join(_args[1:]) if len(_args) > 1 else "removed"
|
|
819
843
|
res = record_deletion(_args[0], reason, project_root=project_root)
|
|
820
844
|
print(res.get("message") or res)
|
|
@@ -1039,8 +1063,14 @@ def _ensure_journal_entry(root: Path, action: str, file: str, reason: str) -> No
|
|
|
1039
1063
|
|
|
1040
1064
|
|
|
1041
1065
|
def _add_to_pending(root: Path, file: str, msg: str) -> None:
|
|
1042
|
-
"""
|
|
1066
|
+
"""Append to pending_updates.md via health helpers (normalized empty/items)."""
|
|
1043
1067
|
try:
|
|
1068
|
+
if _health_mod is not None and hasattr(_health_mod, "add_to_pending"):
|
|
1069
|
+
# Caller may already hold project lock (re-entrant).
|
|
1070
|
+
_health_mod._do_add_to_pending(root, file, msg) if hasattr(
|
|
1071
|
+
_health_mod, "_do_add_to_pending"
|
|
1072
|
+
) else _health_mod.add_to_pending(root, file, msg)
|
|
1073
|
+
return
|
|
1044
1074
|
p = root / "pending_updates.md"
|
|
1045
1075
|
line = f"- {file}: {msg}\n"
|
|
1046
1076
|
with open(p, "a", encoding="utf-8") as f:
|
|
@@ -1050,8 +1080,14 @@ def _add_to_pending(root: Path, file: str, msg: str) -> None:
|
|
|
1050
1080
|
|
|
1051
1081
|
|
|
1052
1082
|
def _remove_from_pending(root: Path, file: str) -> None:
|
|
1053
|
-
"""Best-effort removal (used by mark_green
|
|
1083
|
+
"""Best-effort removal (used by mark_green). Prefers health normalizer."""
|
|
1054
1084
|
try:
|
|
1085
|
+
if _health_mod is not None and hasattr(_health_mod, "_do_remove_from_pending"):
|
|
1086
|
+
_health_mod._do_remove_from_pending(root, file)
|
|
1087
|
+
return
|
|
1088
|
+
if _health_mod is not None and hasattr(_health_mod, "remove_from_pending"):
|
|
1089
|
+
_health_mod.remove_from_pending(root, file)
|
|
1090
|
+
return
|
|
1055
1091
|
p = root / "pending_updates.md"
|
|
1056
1092
|
if p.exists():
|
|
1057
1093
|
lines = [ln for ln in p.read_text(encoding="utf-8").splitlines() if file not in ln]
|
|
@@ -1265,9 +1301,13 @@ def record_deletion(file: str, reason: str, project_root: Optional[Union[str, Pa
|
|
|
1265
1301
|
|
|
1266
1302
|
G7: marks 🔴 DELETED, pending + journal, and best-effort prunes barrel cache
|
|
1267
1303
|
references so deleted paths do not keep invalidating importers forever.
|
|
1304
|
+
Rejects flag-like paths (`--help`) so CLI misuse cannot pollute health.
|
|
1268
1305
|
"""
|
|
1269
1306
|
root = _get_effective_root(project_root)
|
|
1270
1307
|
result: Dict[str, Any] = {"success": False, "file": file, "project_root": str(root), "action": "deletion"}
|
|
1308
|
+
if not file or str(file).startswith("-") or str(file) in ("--help", "-h", "help"):
|
|
1309
|
+
result["error"] = "file must be a project path, not a flag/empty string"
|
|
1310
|
+
return result
|
|
1271
1311
|
try:
|
|
1272
1312
|
lock_ctx = (locking.file_lock(root) if locking is not None else _nullcontext())
|
|
1273
1313
|
with lock_ctx:
|
|
@@ -1279,7 +1319,11 @@ def record_deletion(file: str, reason: str, project_root: Optional[Union[str, Pa
|
|
|
1279
1319
|
except Exception:
|
|
1280
1320
|
rel = file
|
|
1281
1321
|
if _health_mod is not None:
|
|
1282
|
-
|
|
1322
|
+
# Prefer unlocked upsert when we already hold the project lock.
|
|
1323
|
+
if hasattr(_health_mod, "_do_upsert_entry"):
|
|
1324
|
+
_health_mod._do_upsert_entry(root, rel, "🔴 Red", f"DELETED — {reason}")
|
|
1325
|
+
else:
|
|
1326
|
+
_health_mod.upsert_entry(root, rel, "🔴 Red", f"DELETED — {reason}")
|
|
1283
1327
|
_add_to_pending(root, rel, f"File was deleted. Consider wiki archival. {reason}")
|
|
1284
1328
|
_ensure_journal_entry(root, "record-deletion", rel, reason or "No reason provided.")
|
|
1285
1329
|
prune_stats: Dict[str, Any] = {}
|
|
@@ -64,13 +64,60 @@ def _coerce_root(root) -> Path:
|
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
# Post-v4.0 + M5.3 gate complete + cleanup hygiene:
|
|
67
|
-
# Aggressively prune superseded historical wiki-note entries (e.g. early "M5.3 Cycle1 evidence append
|
|
68
|
-
# that
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
#
|
|
67
|
+
# Aggressively prune superseded historical wiki-note entries (e.g. early "M5.3 Cycle1 evidence append …")
|
|
68
|
+
# that are free-form note keys, not real project paths. These pollute the health matrix that agents use.
|
|
69
|
+
# Keep real M5 docs (Progress, Milestones, Assessment, p6, M5.1-cross).
|
|
70
|
+
# Real path deletions via record-deletion (e.g. "src/foo.py" + 🔴 DELETED) stay as audit rows.
|
|
71
|
+
# Free-form DELETED non-path keys (e.g. accidental `record-deletion --help`) are always pruned.
|
|
72
72
|
SUPERSEDED_PATTERNS = ["m5.3 cycle1", "cycle1 evidence append", "early m5.3 launch note", "m5.3 cycle1 evidence"]
|
|
73
73
|
|
|
74
|
+
# Path-like health keys (real files we may keep as DELETED audits). Everything else that is
|
|
75
|
+
# already DELETED and not path-shaped is pollution.
|
|
76
|
+
_SOURCE_SUFFIXES = (
|
|
77
|
+
".py", ".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs", ".md", ".json",
|
|
78
|
+
".sh", ".toml", ".yaml", ".yml", ".html", ".css", ".rs", ".go", ".java",
|
|
79
|
+
".cs", ".c", ".h", ".cpp", ".hpp", ".cc",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _looks_like_path_key(key: str) -> bool:
|
|
84
|
+
"""True if health key looks like a project-relative or absolute file path."""
|
|
85
|
+
if not key or not isinstance(key, str):
|
|
86
|
+
return False
|
|
87
|
+
if key.startswith(("-", "--")):
|
|
88
|
+
return False
|
|
89
|
+
if "/" in key or "\\" in key:
|
|
90
|
+
return True
|
|
91
|
+
return key.endswith(_SOURCE_SUFFIXES)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _is_pollution_health_key(key: str, entry: Optional[Dict[str, Any]] = None) -> bool:
|
|
95
|
+
"""Keys that must never remain in the agent-facing health matrix."""
|
|
96
|
+
if not isinstance(key, str) or not key:
|
|
97
|
+
return True
|
|
98
|
+
kl = key.lower()
|
|
99
|
+
if any(p in kl for p in SUPERSEDED_PATTERNS):
|
|
100
|
+
return True
|
|
101
|
+
ent = entry or {}
|
|
102
|
+
reason = str(ent.get("reason") or "")
|
|
103
|
+
status = str(ent.get("status") or "")
|
|
104
|
+
# Accidental flag keys / free-form notes already marked DELETED (not real paths).
|
|
105
|
+
if not _looks_like_path_key(key) and ("DELETED" in reason or "DELETED" in status):
|
|
106
|
+
return True
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _prune_entries(root: Path, entries: Dict[str, Any]) -> Dict[str, Any]:
|
|
111
|
+
"""Drop out-of-tree pollution + superseded free-form note keys."""
|
|
112
|
+
pruned: Dict[str, Any] = {}
|
|
113
|
+
for k, v in (entries or {}).items():
|
|
114
|
+
if not _entry_is_under_root(root, k):
|
|
115
|
+
continue
|
|
116
|
+
if _is_pollution_health_key(k, v if isinstance(v, dict) else None):
|
|
117
|
+
continue
|
|
118
|
+
pruned[k] = v
|
|
119
|
+
return pruned
|
|
120
|
+
|
|
74
121
|
|
|
75
122
|
def _get_health_path(root: "str | Path") -> Path:
|
|
76
123
|
root = _coerce_root(root)
|
|
@@ -273,23 +320,9 @@ def load_health(root: "str | Path") -> Dict[str, Any]:
|
|
|
273
320
|
data["entries"] = normalized_entries
|
|
274
321
|
if version < 2:
|
|
275
322
|
data["version"] = 2 # mark for future save; additive, no breakage
|
|
276
|
-
# M5 dogfood fix:
|
|
277
|
-
#
|
|
278
|
-
|
|
279
|
-
entries = data.get("entries", {}) or {}
|
|
280
|
-
pruned = {k: v for k, v in entries.items() if _entry_is_under_root(root, k)}
|
|
281
|
-
# Post-v4.0 superseded historical prune (see SUPERSEDED_PATTERNS at top).
|
|
282
|
-
# Leave explicit Red DELETED audit records (they are the agent-visible archival marker).
|
|
283
|
-
for k in list(pruned.keys()):
|
|
284
|
-
kl = str(k).lower()
|
|
285
|
-
if any(p in kl for p in SUPERSEDED_PATTERNS):
|
|
286
|
-
ent = pruned.get(k, {})
|
|
287
|
-
status = str(ent.get("status", ""))
|
|
288
|
-
reason = str(ent.get("reason", "")).lower()
|
|
289
|
-
if status.startswith("🔴") and ("deleted" in reason or "superseded" in reason):
|
|
290
|
-
continue # keep the audit Red
|
|
291
|
-
del pruned[k]
|
|
292
|
-
data["entries"] = pruned
|
|
323
|
+
# M5 dogfood fix: drop out-of-tree + free-form superseded/DELETED pollution.
|
|
324
|
+
# Views (get_*) are clean; next save persists pruned json.
|
|
325
|
+
data["entries"] = _prune_entries(root, data.get("entries", {}) or {})
|
|
293
326
|
return data
|
|
294
327
|
|
|
295
328
|
# Migration path: if JSON doesn't exist but MD does
|
|
@@ -306,20 +339,7 @@ def load_health(root: "str | Path") -> Dict[str, Any]:
|
|
|
306
339
|
norm[k] = _normalize_health_entry_local(v)
|
|
307
340
|
migrated["entries"] = norm
|
|
308
341
|
migrated["version"] = 2
|
|
309
|
-
|
|
310
|
-
entries = migrated.get("entries", {}) or {}
|
|
311
|
-
pruned = {k: v for k, v in entries.items() if _entry_is_under_root(root, k)}
|
|
312
|
-
# Post-v4.0 superseded historical prune (see SUPERSEDED_PATTERNS at top).
|
|
313
|
-
for k in list(pruned.keys()):
|
|
314
|
-
kl = str(k).lower()
|
|
315
|
-
if any(p in kl for p in SUPERSEDED_PATTERNS):
|
|
316
|
-
ent = pruned.get(k, {})
|
|
317
|
-
status = str(ent.get("status", ""))
|
|
318
|
-
reason = str(ent.get("reason", "")).lower()
|
|
319
|
-
if status.startswith("🔴") and ("deleted" in reason or "superseded" in reason):
|
|
320
|
-
continue
|
|
321
|
-
del pruned[k]
|
|
322
|
-
migrated["entries"] = pruned
|
|
342
|
+
migrated["entries"] = _prune_entries(root, migrated.get("entries", {}) or {})
|
|
323
343
|
return migrated
|
|
324
344
|
|
|
325
345
|
# Fresh project (start at v2 with Health B fields ready)
|
|
@@ -350,21 +370,9 @@ def _do_save_health(root: "str | Path", health_data: Dict[str, Any]) -> None:
|
|
|
350
370
|
health_data["last_updated"] = _timestamp()
|
|
351
371
|
health_data["version"] = 2 # B durable: ensure v2 on every save (additive fields)
|
|
352
372
|
|
|
353
|
-
# M5: always prune out-of-tree
|
|
373
|
+
# M5: always prune out-of-tree + free-form pollution so file_health.json stays clean
|
|
354
374
|
if "entries" in health_data:
|
|
355
|
-
entries = health_data.get("entries") or {}
|
|
356
|
-
pruned = {k: v for k, v in entries.items() if _entry_is_under_root(root, k)}
|
|
357
|
-
# Post-v4.0 superseded historical prune (see SUPERSEDED_PATTERNS at top).
|
|
358
|
-
for k in list(pruned.keys()):
|
|
359
|
-
kl = str(k).lower()
|
|
360
|
-
if any(p in kl for p in SUPERSEDED_PATTERNS):
|
|
361
|
-
ent = pruned.get(k, {})
|
|
362
|
-
status = str(ent.get("status", ""))
|
|
363
|
-
reason = str(ent.get("reason", "")).lower()
|
|
364
|
-
if status.startswith("🔴") and ("deleted" in reason or "superseded" in reason):
|
|
365
|
-
continue
|
|
366
|
-
del pruned[k]
|
|
367
|
-
health_data["entries"] = pruned
|
|
375
|
+
health_data["entries"] = _prune_entries(root, health_data.get("entries") or {})
|
|
368
376
|
|
|
369
377
|
with open(json_path, "w", encoding="utf-8") as f:
|
|
370
378
|
json.dump(health_data, f, indent=2, ensure_ascii=False)
|
|
@@ -450,11 +458,9 @@ def _do_upsert_entry(root: "str | Path", file: str, status: str, reason: str = "
|
|
|
450
458
|
file = _normalize_to_relative(root, file)
|
|
451
459
|
if not _entry_is_under_root(root, file):
|
|
452
460
|
return
|
|
453
|
-
#
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
if not (status.startswith("🔴") and ("deleted" in reason.lower() or "superseded" in reason.lower())):
|
|
457
|
-
return
|
|
461
|
+
# Never re-introduce free-form superseded notes or flag-key pollution.
|
|
462
|
+
if _is_pollution_health_key(file, {"status": status, "reason": reason}):
|
|
463
|
+
return
|
|
458
464
|
health = load_health(root)
|
|
459
465
|
existing = health.get("entries", {}).get(file, {})
|
|
460
466
|
# Start from normalized existing to keep B fields
|
|
@@ -479,40 +485,74 @@ def _do_upsert_entry(root: "str | Path", file: str, status: str, reason: str = "
|
|
|
479
485
|
# These ensure pending_updates.md mutations are atomic with health under the project lock
|
|
480
486
|
# (per locking.py contract). Eliminates races/duplicates with add_pending from shell/monitor.
|
|
481
487
|
|
|
488
|
+
_EMPTY_PENDING_MARKER = "(no pending items — run check-changes after making edits)"
|
|
489
|
+
_EMPTY_PENDING_LINES = [
|
|
490
|
+
"# Pending Updates",
|
|
491
|
+
"",
|
|
492
|
+
_EMPTY_PENDING_MARKER,
|
|
493
|
+
]
|
|
494
|
+
|
|
495
|
+
|
|
482
496
|
def _get_pending_path(root: Path) -> Path:
|
|
483
497
|
return root / PENDING_MD
|
|
484
498
|
|
|
485
499
|
|
|
500
|
+
def _is_pending_item_line(ln: str) -> bool:
|
|
501
|
+
"""True for real work-queue bullets (`- path: msg`), not headers/empty markers."""
|
|
502
|
+
s = (ln or "").strip()
|
|
503
|
+
return s.startswith("- ") and not s.startswith("- (")
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def _is_empty_pending_marker(ln: str) -> bool:
|
|
507
|
+
s = (ln or "").strip().lower()
|
|
508
|
+
if not s or s.startswith("#"):
|
|
509
|
+
return False
|
|
510
|
+
return "no pending" in s or "no active items" in s or s == _EMPTY_PENDING_MARKER.lower()
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _pending_item_lines(lines: List[str]) -> List[str]:
|
|
514
|
+
return [ln for ln in lines if _is_pending_item_line(ln)]
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _normalize_pending_lines(lines: List[str]) -> List[str]:
|
|
518
|
+
"""Canonical pending_updates.md: header + empty marker XOR bullet list (never both)."""
|
|
519
|
+
items = _pending_item_lines(lines or [])
|
|
520
|
+
if not items:
|
|
521
|
+
return list(_EMPTY_PENDING_LINES)
|
|
522
|
+
seen = set()
|
|
523
|
+
uniq: List[str] = []
|
|
524
|
+
for it in items:
|
|
525
|
+
if it not in seen:
|
|
526
|
+
seen.add(it)
|
|
527
|
+
uniq.append(it)
|
|
528
|
+
return ["# Pending Updates", ""] + uniq
|
|
529
|
+
|
|
530
|
+
|
|
486
531
|
def _read_pending_lines(root: Path) -> List[str]:
|
|
487
532
|
"""Read pending file as list of lines; return sensible default header if missing."""
|
|
488
533
|
p = _get_pending_path(root)
|
|
489
534
|
if not p.exists():
|
|
490
|
-
return
|
|
491
|
-
"# Pending Updates",
|
|
492
|
-
"",
|
|
493
|
-
"(no pending items — run check-changes after making edits)"
|
|
494
|
-
]
|
|
535
|
+
return list(_EMPTY_PENDING_LINES)
|
|
495
536
|
try:
|
|
496
537
|
with open(p, "r", encoding="utf-8") as f:
|
|
497
538
|
return f.read().splitlines(keepends=False)
|
|
498
539
|
except Exception:
|
|
499
|
-
|
|
500
|
-
return ["# Pending Updates", "", "(no pending items — run check-changes after making edits)"]
|
|
540
|
+
return list(_EMPTY_PENDING_LINES)
|
|
501
541
|
|
|
502
542
|
|
|
503
543
|
def _write_pending_lines(root: Path, lines: List[str]) -> None:
|
|
504
|
-
"""Atomic-ish write of pending (tmp + mv
|
|
544
|
+
"""Atomic-ish write of pending (tmp + mv). Always normalized (no dual empty+items)."""
|
|
505
545
|
p = _get_pending_path(root)
|
|
506
546
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
547
|
+
normalized = _normalize_pending_lines(lines)
|
|
507
548
|
tmp = p.with_suffix(".tmp")
|
|
508
549
|
try:
|
|
509
550
|
with open(tmp, "w", encoding="utf-8") as f:
|
|
510
|
-
f.write("\n".join(
|
|
511
|
-
os.replace(tmp, p)
|
|
551
|
+
f.write("\n".join(normalized) + "\n")
|
|
552
|
+
os.replace(tmp, p)
|
|
512
553
|
except Exception:
|
|
513
|
-
# Fallback direct write
|
|
514
554
|
with open(p, "w", encoding="utf-8") as f:
|
|
515
|
-
f.write("\n".join(
|
|
555
|
+
f.write("\n".join(normalized) + "\n")
|
|
516
556
|
try:
|
|
517
557
|
if tmp.exists():
|
|
518
558
|
tmp.unlink()
|
|
@@ -520,13 +560,19 @@ def _write_pending_lines(root: Path, lines: List[str]) -> None:
|
|
|
520
560
|
pass
|
|
521
561
|
|
|
522
562
|
|
|
563
|
+
def count_pending(root: Path) -> int:
|
|
564
|
+
"""Count real pending work items (bullet lines only; empty markers ignored)."""
|
|
565
|
+
return len(_pending_item_lines(_read_pending_lines(root)))
|
|
566
|
+
|
|
567
|
+
|
|
523
568
|
def _do_remove_from_pending(root: Path, file: str) -> int:
|
|
524
|
-
"""Internal: idempotent remove of any lines containing file (fixed-string
|
|
569
|
+
"""Internal: idempotent remove of any item lines containing file (fixed-string)."""
|
|
525
570
|
lines = _read_pending_lines(root)
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
571
|
+
items_before = _pending_item_lines(lines)
|
|
572
|
+
new_items = [ln for ln in items_before if file not in ln]
|
|
573
|
+
removed = len(items_before) - len(new_items)
|
|
574
|
+
# Always rewrite so dual "(no active)" + items and stale markers get fixed.
|
|
575
|
+
_write_pending_lines(root, new_items)
|
|
530
576
|
return removed
|
|
531
577
|
|
|
532
578
|
|
|
@@ -540,12 +586,13 @@ def remove_from_pending(root: Path, file: str) -> int:
|
|
|
540
586
|
|
|
541
587
|
|
|
542
588
|
def _do_add_to_pending(root: Path, file: str, msg: str) -> None:
|
|
543
|
-
"""Internal add (idempotent
|
|
589
|
+
"""Internal add (idempotent). Strips empty markers when real items exist."""
|
|
544
590
|
lines = _read_pending_lines(root)
|
|
545
591
|
entry = f"- {file}: {msg}"
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
592
|
+
items = _pending_item_lines(lines)
|
|
593
|
+
if entry not in items:
|
|
594
|
+
items.append(entry)
|
|
595
|
+
_write_pending_lines(root, items)
|
|
549
596
|
|
|
550
597
|
|
|
551
598
|
def add_to_pending(root: Path, file: str, msg: str) -> None:
|
|
@@ -875,12 +922,16 @@ def get_summary(root: Path, directory: Optional[str] = None, include_stale: bool
|
|
|
875
922
|
elif "🔴" in status:
|
|
876
923
|
red += 1
|
|
877
924
|
|
|
925
|
+
try:
|
|
926
|
+
pending_n = count_pending(root)
|
|
927
|
+
except Exception:
|
|
928
|
+
pending_n = 0
|
|
878
929
|
out = {
|
|
879
930
|
"total": total,
|
|
880
931
|
"green": green,
|
|
881
932
|
"yellow": yellow,
|
|
882
933
|
"red": red,
|
|
883
|
-
"pending_updates":
|
|
934
|
+
"pending_updates": pending_n,
|
|
884
935
|
"directory": directory or ".",
|
|
885
936
|
"version": health.get("version", 2),
|
|
886
937
|
"last_updated": health.get("last_updated")
|
|
@@ -1644,7 +1644,13 @@ def get_project_status(
|
|
|
1644
1644
|
summary = health_module.get_summary(root, directory)
|
|
1645
1645
|
pending = _read_file_safe("pending_updates.md", root=root)
|
|
1646
1646
|
|
|
1647
|
-
|
|
1647
|
+
if hasattr(health_module, "count_pending"):
|
|
1648
|
+
pending_count = int(health_module.count_pending(root))
|
|
1649
|
+
else:
|
|
1650
|
+
pending_count = len([
|
|
1651
|
+
l for l in pending.splitlines()
|
|
1652
|
+
if l.strip().startswith("- ") and not l.strip().startswith("- (")
|
|
1653
|
+
])
|
|
1648
1654
|
|
|
1649
1655
|
# ACS + CIABRE + Wave 2 Barrel/BRC surfacing uniformity: lightweight stats + invalidation reports foundation in project status (MCP primary for agents)
|
|
1650
1656
|
dep_intel = {}
|
|
@@ -1770,7 +1776,13 @@ Use get_files_needing_attention() for the actual list. Use get_cycles(analysis=T
|
|
|
1770
1776
|
# yielded zeros and lied to agents.
|
|
1771
1777
|
root = _get_effective_root(project_root)
|
|
1772
1778
|
pending = _read_file_safe("pending_updates.md", root=root)
|
|
1773
|
-
|
|
1779
|
+
if hasattr(health_module, "count_pending"):
|
|
1780
|
+
pending_count = int(health_module.count_pending(root))
|
|
1781
|
+
else:
|
|
1782
|
+
pending_count = len([
|
|
1783
|
+
l for l in pending.splitlines()
|
|
1784
|
+
if l.strip().startswith("- ") and not l.strip().startswith("- (")
|
|
1785
|
+
])
|
|
1774
1786
|
green = yellow = red = total = 0
|
|
1775
1787
|
try:
|
|
1776
1788
|
import importlib
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wikifier
|
|
3
|
-
Version: 4.5.
|
|
3
|
+
Version: 4.5.5
|
|
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
|