wikifier 4.5.6__tar.gz → 4.5.8__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.6/wikifier.egg-info → wikifier-4.5.8}/PKG-INFO +1 -1
- {wikifier-4.5.6 → wikifier-4.5.8}/skills/run.md +15 -8
- {wikifier-4.5.6 → wikifier-4.5.8}/tests/test_gap_closure.py +78 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier/__init__.py +1 -1
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier/cli.py +68 -11
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier/daemon.py +113 -21
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier/health.py +423 -9
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier/mcp/server.py +38 -8
- {wikifier-4.5.6 → wikifier-4.5.8/wikifier.egg-info}/PKG-INFO +1 -1
- {wikifier-4.5.6 → wikifier-4.5.8}/CONTRIBUTING.md +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/LICENSE +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/MANIFEST.in +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/README.md +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/diagnostics.html +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/docs/spec.md +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/index.html +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/pyproject.toml +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/setup.cfg +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/tests/test_barrel_invalidation.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/tests/test_health.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/tests/test_import_cache.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/tests/test_multi_lang_parsers.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/tests/test_parsers.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/tests/test_selftest_wrappers.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier/__main__.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier/contracts.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier/diagnostics.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier/import_cache.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier/index.html +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier/library.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier/locking.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier/mcp/__init__.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier/parsers/__init__.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier/parsers/_edge.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier/parsers/bree.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier/parsers/c_cpp.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier/parsers/cdia.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier/parsers/csharp.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier/parsers/go_lang.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier/parsers/java.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier/parsers/javascript.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier/parsers/python.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier/parsers/rust.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier/resolution.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier/scripts/wikifier.bat +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier/scripts/wikifier.ps1 +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier/scripts/wikifier.sh +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier/serve.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier.egg-info/SOURCES.txt +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier.egg-info/dependency_links.txt +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier.egg-info/entry_points.txt +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/wikifier.egg-info/requires.txt +0 -0
- {wikifier-4.5.6 → wikifier-4.5.8}/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.8
|
|
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
|
|
@@ -10,12 +10,15 @@
|
|
|
10
10
|
**Package 4.5.x + gap-closure notes (additive; protocol still v0.6):**
|
|
11
11
|
- File Tree + `wikifier serve`; MCP status/attention use library + emoji (not `[GREEN]` tags).
|
|
12
12
|
- **First-run:** `init` → `update-maps` → `health --summary` → `suggest_next_actions`. Map is automatic; wiki *prose* is agent-filled over time.
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
18
|
-
-
|
|
13
|
+
- **Map-first ≠ wiki-done:** `health_score` **Map Ready** + many 🟡 *Initial stubs* means structural coverage only. Do **not** bulk-wiki stubs. Wiki a file when you edit it, then `mark-green`. Prefer `actionable_yellow` / 🔴 over raw yellow counts.
|
|
14
|
+
- **Steady-state selective work:** only 🔴 and *actionable* 🟡 (never re-wiki 🟢; never re-wiki the whole stub set). ACS uses `actionable_low_conf_edges`.
|
|
15
|
+
- **Dual scope:** `monitored_paths` = check-changes/health scope (keep lean). `update-maps --directory=` / `--max-files=` = map walk. Never set `project_root` to a multi-repo parent (e.g. `cloned_sample_projects`).
|
|
16
|
+
- **CLI target:** `wikifier --target /abs/project health --summary` (flags stripped before the command). Or `WIKIFIER_PROJECT_ROOT=…`.
|
|
17
|
+
- **Long-horizon:** `wikifier autonomous-status` (or `readiness`) before unattended runs; daemon writes `.wikifier_staging/daemon_heartbeat.json`. Soak ≥72h is still evidence work, not automatic.
|
|
18
|
+
- **Deletions:** `record-deletion` + ghost detection. **Languages:** py/js/ts/rust/go/c/c#/java (regex). journal/pending = audit queue, not Jira.
|
|
19
|
+
- **Huge monorepos:** lean `monitored_paths.txt` (not bare `.`). `WIKIFIER_CHECK_CHANGES_MAX` default 2000. Hygiene: `seed-health`, `prune-pending`, `prune-health`.
|
|
20
|
+
- **Monitor/daemon:** mtime heartbeat; agents still own wiki/mark-green. `WIKIFIER_DAEMON_MAPS_INTERVAL` default 600s; `WIKIFIER_DAEMON_MAPS=0` for check-only.
|
|
21
|
+
- Health module: `importlib.import_module("wikifier.health")` or `wikifier.health_module` — `from wikifier import health` is the *function*.
|
|
19
22
|
|
|
20
23
|
**v0.6 migration notes (packages v4.2.0–v4.3.0)** — changes are additive or strictly-better defaults; v0.5 agent behavior keeps working:
|
|
21
24
|
- `update-maps` (CLI) now runs the **pure-Python full pipeline**: every dirty file is parsed in-process, the canonical per-file cache is persisted, reverse deps + cycles + ACS are computed, and `library.md` is regenerated atomically. `--python-primary` is accepted but redundant; the in-shell first-pass was retired entirely (wikifier.sh's update-maps delegates to this pipeline; `--sh`/`--legacy-sh` are deprecated no-ops). Scoping is explicit via `--directory=`/`--max-files=` and reported in the result (`files_skipped`) — there are no silent caps.
|
|
@@ -39,11 +42,15 @@ Do **not** open megamodules (`javascript.py`, `import_cache.py`, `bree.py`) to d
|
|
|
39
42
|
| Need | Use |
|
|
40
43
|
|------|-----|
|
|
41
44
|
| First-run map | `init` → `update-maps` → `health --summary` → `suggest-next` |
|
|
42
|
-
| Steady-state | `check-changes` → edit
|
|
45
|
+
| Steady-state | `check-changes` → edit 🔴/actionable 🟡 only → `record-change` → wiki → `mark-green` |
|
|
43
46
|
| Lookup | MCP Core 6: status, check_changes, needing_attention, get_file_wiki, suggest, record/mark |
|
|
44
47
|
| Deps / cycles | `get_dependencies`, `get_dependents`, `get_cycles` (json) |
|
|
45
48
|
| Deletion | `record-deletion` |
|
|
46
|
-
|
|
|
49
|
+
| Long-horizon prep | `autonomous-status` / `readiness`; then `daemon start` with lean monitor |
|
|
50
|
+
| Hygiene | `seed-health`, `prune-pending`, `prune-health`, `validate` |
|
|
51
|
+
| Code entry | `cli.py` dispatch + `health.py` + parsers — self-tests in `tests/` + `tests/selftest/` |
|
|
52
|
+
|
|
53
|
+
**Do not open megamodules** (`javascript.py` ~2.6k, `import_cache.py` ~2.3k, `bree.py` ~2k) for workflow decisions — use tools + this table.
|
|
47
54
|
|
|
48
55
|
**CLI pure-Python vs shell:** mutators and maps prefer `python -m wikifier …`. Shell still owns `init`, `monitor`, `daemon`, `serve`, `journal`.
|
|
49
56
|
**Scope:** `monitored_paths` = change detection; `exclude_patterns` + optional `--directory` = map walk.
|
|
@@ -190,6 +190,84 @@ class TestPendingAndPollutionHygiene(TempProjectTestCase):
|
|
|
190
190
|
self.assertIn("gone_real.py", entries2)
|
|
191
191
|
|
|
192
192
|
|
|
193
|
+
class TestMapFirstTaxonomyAndReadiness(TempProjectTestCase):
|
|
194
|
+
def test_stub_yellow_is_map_ready_not_needs_attention(self):
|
|
195
|
+
health_mod = importlib.import_module("wikifier.health")
|
|
196
|
+
health_mod.upsert_entry(
|
|
197
|
+
self.root, "a.py", "🟡 Yellow",
|
|
198
|
+
"Initial stub — present in dependency map; agent should wiki + mark-green when editing",
|
|
199
|
+
)
|
|
200
|
+
health_mod.upsert_entry(
|
|
201
|
+
self.root, "b.py", "🟡 Yellow",
|
|
202
|
+
"Initial stub — parseable source under monitored_paths; agent should wiki + mark-green when editing",
|
|
203
|
+
)
|
|
204
|
+
s = health_mod.get_summary(self.root)
|
|
205
|
+
self.assertEqual(s.get("stub_yellow"), 2)
|
|
206
|
+
self.assertEqual(s.get("actionable_yellow"), 0)
|
|
207
|
+
self.assertEqual(s.get("health_score"), "Map Ready")
|
|
208
|
+
|
|
209
|
+
def test_actionable_yellow_needs_attention(self):
|
|
210
|
+
health_mod = importlib.import_module("wikifier.health")
|
|
211
|
+
health_mod.upsert_entry(
|
|
212
|
+
self.root, "a.py", "🟡 Yellow",
|
|
213
|
+
"mtime changed since last check-changes (Python primary auto-detected)",
|
|
214
|
+
)
|
|
215
|
+
s = health_mod.get_summary(self.root)
|
|
216
|
+
self.assertEqual(s.get("actionable_yellow"), 1)
|
|
217
|
+
self.assertEqual(s.get("health_score"), "Needs Attention")
|
|
218
|
+
|
|
219
|
+
def test_suggest_does_not_bulk_wiki_stubs(self):
|
|
220
|
+
health_mod = importlib.import_module("wikifier.health")
|
|
221
|
+
health_mod.upsert_entry(self.root, "a.py", "🟡 Yellow", "Initial stub — present in dependency map")
|
|
222
|
+
text = cli.suggest_next_actions(project_root=self.root, format="text")
|
|
223
|
+
self.assertIn("Map-first", text)
|
|
224
|
+
self.assertNotIn("Review the 1 🟡 Yellow file(s) only", text)
|
|
225
|
+
|
|
226
|
+
def test_detect_multi_project_container(self):
|
|
227
|
+
health_mod = importlib.import_module("wikifier.health")
|
|
228
|
+
for name in ("proj_a", "proj_b", "proj_c"):
|
|
229
|
+
d = self.root / name
|
|
230
|
+
d.mkdir()
|
|
231
|
+
(d / ".git").mkdir()
|
|
232
|
+
scope = health_mod.detect_scope_risks(self.root)
|
|
233
|
+
self.assertGreaterEqual(scope.get("child_project_count"), 3)
|
|
234
|
+
self.assertFalse(scope.get("ok"))
|
|
235
|
+
self.assertTrue(any("multi-project" in w.lower() for w in scope.get("warnings") or []))
|
|
236
|
+
|
|
237
|
+
def test_autonomous_readiness_shape(self):
|
|
238
|
+
health_mod = importlib.import_module("wikifier.health")
|
|
239
|
+
(self.root / "monitored_paths.txt").write_text("src\n", encoding="utf-8")
|
|
240
|
+
self.write("src/app.py", "x=1\n")
|
|
241
|
+
health_mod.upsert_entry(self.root, "src/app.py", "🟡 Yellow", "Initial stub — present in dependency map")
|
|
242
|
+
r = health_mod.assess_autonomous_readiness(self.root)
|
|
243
|
+
self.assertTrue(r.get("success"))
|
|
244
|
+
self.assertIn(r.get("readiness"), (
|
|
245
|
+
"blocked", "map_ok_scope_risk", "ready_for_daemon",
|
|
246
|
+
"ready_with_agent_wiki_work", "not_ready",
|
|
247
|
+
))
|
|
248
|
+
self.assertIn("long_horizon_note", r)
|
|
249
|
+
self.assertIn("metrics", r)
|
|
250
|
+
|
|
251
|
+
def test_write_metrics_snapshot_history(self):
|
|
252
|
+
health_mod = importlib.import_module("wikifier.health")
|
|
253
|
+
(self.root / "monitored_paths.txt").write_text("src\n", encoding="utf-8")
|
|
254
|
+
self.write("src/app.py", "x=1\n")
|
|
255
|
+
health_mod.upsert_entry(self.root, "src/app.py", "🟢 Green", "ok")
|
|
256
|
+
s1 = health_mod.write_metrics_snapshot(self.root, source="test1")
|
|
257
|
+
self.assertTrue(s1.get("success"))
|
|
258
|
+
latest = self.root / ".wikifier_staging" / "metrics_latest.json"
|
|
259
|
+
hist = self.root / ".wikifier_staging" / "metrics_history.jsonl"
|
|
260
|
+
self.assertTrue(latest.exists())
|
|
261
|
+
self.assertTrue(hist.exists())
|
|
262
|
+
s2 = health_mod.write_metrics_snapshot(self.root, source="test2")
|
|
263
|
+
self.assertTrue(s2.get("success"))
|
|
264
|
+
rows = health_mod.read_metrics_history(self.root, limit=10)
|
|
265
|
+
self.assertGreaterEqual(len(rows), 2)
|
|
266
|
+
self.assertEqual(rows[-1].get("source"), "test2")
|
|
267
|
+
self.assertIn("staging_bytes", rows[-1])
|
|
268
|
+
self.assertIn("health_score", rows[-1])
|
|
269
|
+
|
|
270
|
+
|
|
193
271
|
class TestMapFirstHealthAndValidate(TempProjectTestCase):
|
|
194
272
|
def test_deep_relative_keys_are_under_root(self):
|
|
195
273
|
"""Regression: deep monorepo rel paths must not be treated as abs pollution."""
|
|
@@ -728,18 +728,20 @@ def main():
|
|
|
728
728
|
i = 0
|
|
729
729
|
while i < len(argv):
|
|
730
730
|
arg = argv[i]
|
|
731
|
+
# Consume --target / --project-root so the *command* remains filtered_argv[0]
|
|
732
|
+
# (previously left --target as argv[0] → "Unknown command: --target").
|
|
731
733
|
if arg in ("--target", "--project-root") and i + 1 < len(argv):
|
|
732
734
|
project_root = argv[i + 1]
|
|
733
|
-
filtered_argv.append(arg)
|
|
734
|
-
filtered_argv.append(argv[i + 1])
|
|
735
735
|
i += 2
|
|
736
736
|
continue
|
|
737
737
|
elif arg.startswith("--target="):
|
|
738
738
|
project_root = arg.split("=", 1)[1]
|
|
739
|
-
|
|
739
|
+
i += 1
|
|
740
|
+
continue
|
|
740
741
|
elif arg.startswith("--project-root="):
|
|
741
742
|
project_root = arg.split("=", 1)[1]
|
|
742
|
-
|
|
743
|
+
i += 1
|
|
744
|
+
continue
|
|
743
745
|
elif arg in ("--use-canonical", "--use_canonical"):
|
|
744
746
|
use_canonical = True
|
|
745
747
|
filtered_argv.append(arg)
|
|
@@ -895,6 +897,30 @@ def main():
|
|
|
895
897
|
res = _health_mod.prune_health_outside_monitored(root)
|
|
896
898
|
print(_json.dumps(res, indent=2, default=str))
|
|
897
899
|
return 0 if res.get("success") else 1
|
|
900
|
+
if _cmd0 in (
|
|
901
|
+
"autonomous-status",
|
|
902
|
+
"autonomous_status",
|
|
903
|
+
"readiness",
|
|
904
|
+
"long-horizon",
|
|
905
|
+
):
|
|
906
|
+
import json as _json
|
|
907
|
+
if _health_mod is None or not hasattr(_health_mod, "assess_autonomous_readiness"):
|
|
908
|
+
print("[wikifier] health.assess_autonomous_readiness unavailable", file=sys.stderr)
|
|
909
|
+
return 1
|
|
910
|
+
root = _get_effective_root(project_root)
|
|
911
|
+
res = _health_mod.assess_autonomous_readiness(root)
|
|
912
|
+
print(_json.dumps(res, indent=2, default=str))
|
|
913
|
+
# exit 0 only when not blocked
|
|
914
|
+
return 0 if res.get("readiness") != "blocked" else 2
|
|
915
|
+
if _cmd0 in ("metrics-snapshot", "metrics_snapshot", "metrics"):
|
|
916
|
+
import json as _json
|
|
917
|
+
if _health_mod is None or not hasattr(_health_mod, "write_metrics_snapshot"):
|
|
918
|
+
print("[wikifier] write_metrics_snapshot unavailable", file=sys.stderr)
|
|
919
|
+
return 1
|
|
920
|
+
root = _get_effective_root(project_root)
|
|
921
|
+
res = _health_mod.write_metrics_snapshot(root, source="cli")
|
|
922
|
+
print(_json.dumps(res, indent=2, default=str))
|
|
923
|
+
return 0 if res.get("success") else 1
|
|
898
924
|
except Exception as e:
|
|
899
925
|
print(f"[wikifier] Python-primary {_cmd0} failed: {e}", file=sys.stderr)
|
|
900
926
|
return 1
|
|
@@ -1419,12 +1445,14 @@ def suggest_next_actions(
|
|
|
1419
1445
|
"""
|
|
1420
1446
|
root = _get_effective_root(project_root)
|
|
1421
1447
|
try:
|
|
1422
|
-
red = yellow = 0
|
|
1448
|
+
red = yellow = stub_y = action_y = 0
|
|
1423
1449
|
health_sum: Dict[str, Any] = {}
|
|
1424
1450
|
if _health_mod is not None:
|
|
1425
1451
|
health_sum = _health_mod.get_summary(root, directory) or {}
|
|
1426
|
-
red = health_sum.get("red", 0)
|
|
1427
|
-
yellow = health_sum.get("yellow", 0)
|
|
1452
|
+
red = int(health_sum.get("red", 0) or 0)
|
|
1453
|
+
yellow = int(health_sum.get("yellow", 0) or 0)
|
|
1454
|
+
stub_y = int(health_sum.get("stub_yellow", 0) or 0)
|
|
1455
|
+
action_y = int(health_sum.get("actionable_yellow", yellow - stub_y) or 0)
|
|
1428
1456
|
|
|
1429
1457
|
suggestions: List[str] = []
|
|
1430
1458
|
n = 1
|
|
@@ -1434,10 +1462,23 @@ def suggest_next_actions(
|
|
|
1434
1462
|
"Do not re-wiki 🟢 Green files."
|
|
1435
1463
|
)
|
|
1436
1464
|
n += 1
|
|
1437
|
-
if
|
|
1465
|
+
if action_y > 0:
|
|
1466
|
+
suggestions.append(
|
|
1467
|
+
f"{n}. Review {action_y} *actionable* 🟡 Yellow file(s) "
|
|
1468
|
+
"(mtime/record-change/barrel — not Initial stubs). "
|
|
1469
|
+
"record-change → wiki that file → mark-green. Skip green."
|
|
1470
|
+
)
|
|
1471
|
+
n += 1
|
|
1472
|
+
if stub_y > 0 and action_y == 0 and red == 0:
|
|
1473
|
+
suggestions.append(
|
|
1474
|
+
f"{n}. Map-first OK: {stub_y} 🟡 Initial stubs mean \"on the map\", "
|
|
1475
|
+
"NOT \"wiki this tree now\". Lookup via get_file_wiki/deps; "
|
|
1476
|
+
"write prose only when you edit a file, then mark-green."
|
|
1477
|
+
)
|
|
1478
|
+
n += 1
|
|
1479
|
+
elif stub_y > 0 and action_y > 0:
|
|
1438
1480
|
suggestions.append(
|
|
1439
|
-
f"{n}.
|
|
1440
|
-
"Skip green unless a dependent of a yellow/red requires it."
|
|
1481
|
+
f"{n}. Ignore {stub_y} map-first stubs for bulk work; only actionable yellows need wiki."
|
|
1441
1482
|
)
|
|
1442
1483
|
n += 1
|
|
1443
1484
|
if red == 0 and yellow == 0:
|
|
@@ -1445,6 +1486,15 @@ def suggest_next_actions(
|
|
|
1445
1486
|
f"{n}. Health is clean (no red/yellow). Do not re-summarize the tree; use the map for lookup only."
|
|
1446
1487
|
)
|
|
1447
1488
|
n += 1
|
|
1489
|
+
# Scope hygiene
|
|
1490
|
+
if _health_mod is not None and hasattr(_health_mod, "detect_scope_risks"):
|
|
1491
|
+
try:
|
|
1492
|
+
scope = _health_mod.detect_scope_risks(root) or {}
|
|
1493
|
+
for w in (scope.get("warnings") or [])[:2]:
|
|
1494
|
+
suggestions.append(f"{n}. SCOPE: {w}")
|
|
1495
|
+
n += 1
|
|
1496
|
+
except Exception:
|
|
1497
|
+
pass
|
|
1448
1498
|
suggestions.append(
|
|
1449
1499
|
f"{n}. Run `update_maps(directory=...)` only if imports/structure changed (not for wiki-only edits)."
|
|
1450
1500
|
)
|
|
@@ -1453,7 +1503,10 @@ def suggest_next_actions(
|
|
|
1453
1503
|
f"{n}. On yellow/red hotspots, query dependents (get_dependents) before editing callers."
|
|
1454
1504
|
)
|
|
1455
1505
|
n += 1
|
|
1456
|
-
suggestions.append(
|
|
1506
|
+
suggestions.append(
|
|
1507
|
+
f"{n}. Long-horizon: `wikifier autonomous-status` before unattended daemon; "
|
|
1508
|
+
"lean monitored_paths; never parent multi-repo folders as project_root."
|
|
1509
|
+
)
|
|
1457
1510
|
|
|
1458
1511
|
acs_note = ""
|
|
1459
1512
|
if _ic_mod is not None:
|
|
@@ -1488,10 +1541,14 @@ def suggest_next_actions(
|
|
|
1488
1541
|
"project_root": str(root),
|
|
1489
1542
|
"red": red,
|
|
1490
1543
|
"yellow": yellow,
|
|
1544
|
+
"stub_yellow": stub_y,
|
|
1545
|
+
"actionable_yellow": action_y,
|
|
1546
|
+
"health_score": health_sum.get("health_score"),
|
|
1491
1547
|
"suggestions": suggestions,
|
|
1492
1548
|
"health_summary": health_sum,
|
|
1493
1549
|
"acs_note": acs_note,
|
|
1494
1550
|
"selective_work": True,
|
|
1551
|
+
"map_first": True,
|
|
1495
1552
|
}
|
|
1496
1553
|
return "\n".join(suggestions) + (acs_note or "")
|
|
1497
1554
|
except Exception as e:
|
|
@@ -166,6 +166,42 @@ def log(msg: str, also_to_file: bool = True) -> None:
|
|
|
166
166
|
# Core check runner (used by both initial, periodic, and post-sleep paths)
|
|
167
167
|
# =============================================================================
|
|
168
168
|
|
|
169
|
+
def _write_heartbeat(event: str, ok: bool, detail: str = "") -> None:
|
|
170
|
+
"""Persist last daemon success/failure for autonomous-status / long-horizon ops."""
|
|
171
|
+
try:
|
|
172
|
+
import json as _json
|
|
173
|
+
path = ensure_state_dir() / "daemon_heartbeat.json"
|
|
174
|
+
prev = {}
|
|
175
|
+
if path.exists():
|
|
176
|
+
try:
|
|
177
|
+
prev = _json.loads(path.read_text(encoding="utf-8")) or {}
|
|
178
|
+
except Exception:
|
|
179
|
+
prev = {}
|
|
180
|
+
fail_streak = int(prev.get("consecutive_failures", 0) or 0)
|
|
181
|
+
if ok:
|
|
182
|
+
fail_streak = 0
|
|
183
|
+
else:
|
|
184
|
+
fail_streak += 1
|
|
185
|
+
payload = {
|
|
186
|
+
"ts": datetime.now().isoformat(timespec="seconds"),
|
|
187
|
+
"event": event,
|
|
188
|
+
"ok": ok,
|
|
189
|
+
"detail": (detail or "")[:400],
|
|
190
|
+
"consecutive_failures": fail_streak,
|
|
191
|
+
"project_root": str(
|
|
192
|
+
(discover_project_root() if discover_project_root else Path.cwd()).resolve()
|
|
193
|
+
),
|
|
194
|
+
}
|
|
195
|
+
path.write_text(_json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
|
196
|
+
if fail_streak >= 5:
|
|
197
|
+
log(
|
|
198
|
+
f"LONG-HORIZON WARN: {fail_streak} consecutive daemon step failures — "
|
|
199
|
+
"inspect daemon.log; consider lean monitored_paths / WIKIFIER_DAEMON_MAPS=0"
|
|
200
|
+
)
|
|
201
|
+
except Exception:
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
|
|
169
205
|
def _run_check_changes(reason: str) -> None:
|
|
170
206
|
"""
|
|
171
207
|
Run the check-changes command in a robust way.
|
|
@@ -185,16 +221,18 @@ def _run_check_changes(reason: str) -> None:
|
|
|
185
221
|
env=os.environ.copy(),
|
|
186
222
|
)
|
|
187
223
|
if result.returncode != 0:
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
)
|
|
224
|
+
msg = (result.stderr or "")[:500]
|
|
225
|
+
log(f"check-changes failed (reason={reason}): {msg}")
|
|
226
|
+
_write_heartbeat(f"check-changes:{reason}", False, msg)
|
|
192
227
|
return
|
|
193
228
|
log(f"check-changes completed (reason={reason})")
|
|
229
|
+
_write_heartbeat(f"check-changes:{reason}", True)
|
|
194
230
|
except subprocess.TimeoutExpired:
|
|
195
231
|
log(f"check-changes timed out (reason={reason})")
|
|
232
|
+
_write_heartbeat(f"check-changes:{reason}", False, "timeout")
|
|
196
233
|
except Exception as e:
|
|
197
234
|
log(f"check-changes error (reason={reason}): {e}")
|
|
235
|
+
_write_heartbeat(f"check-changes:{reason}", False, str(e))
|
|
198
236
|
|
|
199
237
|
|
|
200
238
|
def _run_python_primary_update(reason: str, force_full: bool = False) -> None:
|
|
@@ -215,13 +253,20 @@ def _run_python_primary_update(reason: str, force_full: bool = False) -> None:
|
|
|
215
253
|
files = res.get("files_to_reparse", 0)
|
|
216
254
|
persisted = res.get("sample_persisted_pairs", 0)
|
|
217
255
|
tied = res.get("barrel_creative_tied_in_pure_path", False)
|
|
256
|
+
ok = bool(res.get("success", True))
|
|
218
257
|
log(
|
|
219
258
|
f"python-primary update completed (reason={reason}): "
|
|
220
259
|
f"files_to_reparse={files} persisted_pairs={persisted} barrel_creative_tied={tied} "
|
|
221
|
-
f"root={res.get('root')}"
|
|
260
|
+
f"root={res.get('root')} success={ok}"
|
|
261
|
+
)
|
|
262
|
+
_write_heartbeat(
|
|
263
|
+
f"update-maps:{reason}",
|
|
264
|
+
ok,
|
|
265
|
+
f"parsed={res.get('files_parsed')} edges={res.get('edges_persisted')}",
|
|
222
266
|
)
|
|
223
267
|
except Exception as e:
|
|
224
268
|
log(f"python-primary update error (reason={reason}): {e}")
|
|
269
|
+
_write_heartbeat(f"update-maps:{reason}", False, str(e))
|
|
225
270
|
|
|
226
271
|
|
|
227
272
|
# =============================================================================
|
|
@@ -246,29 +291,76 @@ def daemon_loop() -> None:
|
|
|
246
291
|
|
|
247
292
|
last_check = time.time()
|
|
248
293
|
last_maps = 0.0
|
|
294
|
+
last_metrics = 0.0
|
|
295
|
+
# Metrics cadence (seconds); default 1h for soak growth without spam
|
|
296
|
+
try:
|
|
297
|
+
metrics_iv = max(0, int(os.environ.get("WIKIFIER_DAEMON_METRICS_INTERVAL", "3600") or "3600"))
|
|
298
|
+
except ValueError:
|
|
299
|
+
metrics_iv = 3600
|
|
300
|
+
|
|
301
|
+
def _maybe_metrics(reason: str, force: bool = False) -> None:
|
|
302
|
+
nonlocal last_metrics
|
|
303
|
+
if metrics_iv <= 0 and not force:
|
|
304
|
+
return
|
|
305
|
+
now_m = time.time()
|
|
306
|
+
if not force and metrics_iv > 0 and (now_m - last_metrics) < metrics_iv:
|
|
307
|
+
return
|
|
308
|
+
try:
|
|
309
|
+
from . import health as health_mod
|
|
310
|
+
if hasattr(health_mod, "write_metrics_snapshot"):
|
|
311
|
+
root = None
|
|
312
|
+
if discover_project_root is not None:
|
|
313
|
+
try:
|
|
314
|
+
root = discover_project_root()
|
|
315
|
+
except Exception:
|
|
316
|
+
root = None
|
|
317
|
+
snap = health_mod.write_metrics_snapshot(
|
|
318
|
+
root or Path.cwd(),
|
|
319
|
+
source=f"daemon:{reason}",
|
|
320
|
+
)
|
|
321
|
+
log(
|
|
322
|
+
f"metrics-snapshot (reason={reason}): "
|
|
323
|
+
f"staging_bytes={snap.get('staging_bytes')} "
|
|
324
|
+
f"score={snap.get('health_score')} ok={snap.get('success')}"
|
|
325
|
+
)
|
|
326
|
+
last_metrics = now_m
|
|
327
|
+
except Exception as me:
|
|
328
|
+
log(f"metrics-snapshot error (reason={reason}): {me}")
|
|
329
|
+
|
|
249
330
|
_run_check_changes("initial start / resume")
|
|
250
331
|
if maps_on:
|
|
251
332
|
_run_python_primary_update("initial start / resume", force_full=False)
|
|
252
333
|
last_maps = time.time()
|
|
334
|
+
_maybe_metrics("initial", force=True)
|
|
253
335
|
|
|
254
336
|
while True:
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
337
|
+
try:
|
|
338
|
+
time.sleep(POLL_INTERVAL_DEFAULT)
|
|
339
|
+
|
|
340
|
+
now = time.time()
|
|
341
|
+
gap = now - last_check
|
|
342
|
+
|
|
343
|
+
if gap > SLEEP_THRESHOLD_SECONDS:
|
|
344
|
+
log(f"Wake detected (gap of {int(gap)}s). Forced check-changes + optional maps.")
|
|
345
|
+
_run_check_changes("post-sleep resume")
|
|
346
|
+
if maps_on:
|
|
347
|
+
_run_python_primary_update("post-sleep resume", force_full=False)
|
|
348
|
+
last_maps = time.time()
|
|
349
|
+
_maybe_metrics("post-sleep", force=True)
|
|
350
|
+
|
|
351
|
+
_run_check_changes("periodic")
|
|
352
|
+
if maps_on and maps_iv > 0 and (time.time() - last_maps) >= maps_iv:
|
|
353
|
+
_run_python_primary_update("periodic maps interval", force_full=False)
|
|
265
354
|
last_maps = time.time()
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
355
|
+
_maybe_metrics("periodic")
|
|
356
|
+
last_check = time.time()
|
|
357
|
+
except KeyboardInterrupt:
|
|
358
|
+
raise
|
|
359
|
+
except Exception as loop_err:
|
|
360
|
+
# Never die on a single cycle — long-horizon autonomous ops requirement
|
|
361
|
+
log(f"daemon loop error (continuing): {loop_err}")
|
|
362
|
+
_write_heartbeat("loop", False, str(loop_err))
|
|
363
|
+
time.sleep(POLL_INTERVAL_DEFAULT)
|
|
272
364
|
|
|
273
365
|
|
|
274
366
|
# =============================================================================
|
|
@@ -3,13 +3,15 @@ Documentation health matrix (agent-first).
|
|
|
3
3
|
|
|
4
4
|
AGENT MAP:
|
|
5
5
|
load_health / save_health / upsert_entry — file_health.json SSOT (+ regenerate .md)
|
|
6
|
-
get_summary
|
|
6
|
+
get_summary — 🟢🟡🔴 + stub_yellow/actionable_yellow + health_score (Map Ready|…)
|
|
7
|
+
assess_autonomous_readiness / detect_scope_risks — long-horizon + dual-scope
|
|
7
8
|
find_ghost_entries / validate_health — map-first gaps + ghosts (parseable sources only)
|
|
8
9
|
seed_health_from_map — backfill 🟡 stubs from import_cache (warm-cache safe)
|
|
9
10
|
prune_pending_to_monitored / prune_health_outside_monitored — lean-monitor hygiene
|
|
10
11
|
mark_green / heal_stubs / apply_barrel_invalidation_reports — status mutations
|
|
11
|
-
CLI: seed-health | prune-pending | prune-health | validate
|
|
12
|
+
CLI: autonomous-status | seed-health | prune-pending | prune-health | validate
|
|
12
13
|
Agents: prefer MCP health / check_changes / mark_green; only open this for matrix policy.
|
|
14
|
+
Map Ready ≠ wiki-done — never bulk-wiki Initial stubs.
|
|
13
15
|
|
|
14
16
|
JSON Schema (v2 - additive from v1):
|
|
15
17
|
{
|
|
@@ -1149,6 +1151,359 @@ def validate_health(root: Path) -> Dict[str, Any]:
|
|
|
1149
1151
|
}
|
|
1150
1152
|
|
|
1151
1153
|
|
|
1154
|
+
def detect_scope_risks(root: Path) -> Dict[str, Any]:
|
|
1155
|
+
"""Dual-scope + multi-project root misuse detector (agent hygiene)."""
|
|
1156
|
+
root = _coerce_root(root)
|
|
1157
|
+
warnings: List[str] = []
|
|
1158
|
+
monitored = _read_monitored_rel_roots(root)
|
|
1159
|
+
bare_dot = monitored == ["."] or monitored == []
|
|
1160
|
+
if bare_dot:
|
|
1161
|
+
warnings.append(
|
|
1162
|
+
"monitored_paths is bare '.' — check-changes will thrash on large trees. "
|
|
1163
|
+
"Prefer lean package roots (e.g. src/, library/std/src)."
|
|
1164
|
+
)
|
|
1165
|
+
|
|
1166
|
+
# Parent multi-project container heuristic
|
|
1167
|
+
child_projects = 0
|
|
1168
|
+
sample_children: List[str] = []
|
|
1169
|
+
try:
|
|
1170
|
+
for p in root.iterdir():
|
|
1171
|
+
if not p.is_dir() or p.name.startswith("."):
|
|
1172
|
+
continue
|
|
1173
|
+
if (p / ".git").exists() or (p / "file_health.json").exists() or (
|
|
1174
|
+
p / ".wikifier_staging" / "import_cache.json"
|
|
1175
|
+
).exists():
|
|
1176
|
+
child_projects += 1
|
|
1177
|
+
if len(sample_children) < 8:
|
|
1178
|
+
sample_children.append(p.name)
|
|
1179
|
+
except Exception:
|
|
1180
|
+
pass
|
|
1181
|
+
if child_projects >= 3:
|
|
1182
|
+
warnings.append(
|
|
1183
|
+
f"project_root looks like a multi-project container ({child_projects} child "
|
|
1184
|
+
f"projects e.g. {sample_children}). Always target a *child* project path, "
|
|
1185
|
+
"never the parent folder (e.g. cloned_sample_projects)."
|
|
1186
|
+
)
|
|
1187
|
+
name = root.name.lower()
|
|
1188
|
+
if name in ("cloned_sample_projects", "coding_projects", "repos", "samples"):
|
|
1189
|
+
warnings.append(
|
|
1190
|
+
f"Directory name '{root.name}' often holds many repos — confirm project_root "
|
|
1191
|
+
"is a single project, not the container."
|
|
1192
|
+
)
|
|
1193
|
+
|
|
1194
|
+
return {
|
|
1195
|
+
"monitored_roots": monitored,
|
|
1196
|
+
"bare_dot_monitor": bare_dot,
|
|
1197
|
+
"child_project_count": child_projects,
|
|
1198
|
+
"child_project_sample": sample_children,
|
|
1199
|
+
"warnings": warnings,
|
|
1200
|
+
"ok": len(warnings) == 0,
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
|
|
1204
|
+
def _staging_byte_total(staging: Path, cache_path: Path) -> int:
|
|
1205
|
+
if not staging.exists():
|
|
1206
|
+
return 0
|
|
1207
|
+
total = 0
|
|
1208
|
+
try:
|
|
1209
|
+
for p in staging.rglob("*"):
|
|
1210
|
+
if p.is_file():
|
|
1211
|
+
try:
|
|
1212
|
+
total += p.stat().st_size
|
|
1213
|
+
except Exception:
|
|
1214
|
+
pass
|
|
1215
|
+
except Exception:
|
|
1216
|
+
try:
|
|
1217
|
+
return cache_path.stat().st_size if cache_path.exists() else 0
|
|
1218
|
+
except Exception:
|
|
1219
|
+
return 0
|
|
1220
|
+
return total
|
|
1221
|
+
|
|
1222
|
+
|
|
1223
|
+
def write_metrics_snapshot(
|
|
1224
|
+
root: Path,
|
|
1225
|
+
source: str = "manual",
|
|
1226
|
+
extra: Optional[Dict[str, Any]] = None,
|
|
1227
|
+
) -> Dict[str, Any]:
|
|
1228
|
+
"""Append a long-horizon metrics sample under .wikifier_staging/.
|
|
1229
|
+
|
|
1230
|
+
Files:
|
|
1231
|
+
- metrics_latest.json (last sample, overwrite)
|
|
1232
|
+
- metrics_history.jsonl (append-only; pruned to last 500 lines)
|
|
1233
|
+
|
|
1234
|
+
Call from CLI, readiness assessment, or daemon periodic ticks.
|
|
1235
|
+
Does not claim multi-day soak; enables measurable growth over calendar time.
|
|
1236
|
+
"""
|
|
1237
|
+
root = _coerce_root(root)
|
|
1238
|
+
staging = root / ".wikifier_staging"
|
|
1239
|
+
staging.mkdir(parents=True, exist_ok=True)
|
|
1240
|
+
cache_path = staging / "import_cache.json"
|
|
1241
|
+
summary = get_summary(root)
|
|
1242
|
+
scope = detect_scope_risks(root)
|
|
1243
|
+
ghosts = find_ghost_entries(root)
|
|
1244
|
+
cache_bytes = cache_path.stat().st_size if cache_path.exists() else 0
|
|
1245
|
+
staging_bytes = _staging_byte_total(staging, cache_path)
|
|
1246
|
+
journal_files = 0
|
|
1247
|
+
journal_root = root / "journal"
|
|
1248
|
+
if journal_root.exists():
|
|
1249
|
+
journal_files = sum(1 for _ in journal_root.rglob("*.md"))
|
|
1250
|
+
|
|
1251
|
+
heartbeat = None
|
|
1252
|
+
hb_path = staging / "daemon_heartbeat.json"
|
|
1253
|
+
if hb_path.exists():
|
|
1254
|
+
try:
|
|
1255
|
+
heartbeat = json.loads(hb_path.read_text(encoding="utf-8"))
|
|
1256
|
+
except Exception:
|
|
1257
|
+
heartbeat = {"error": "unreadable"}
|
|
1258
|
+
|
|
1259
|
+
snap: Dict[str, Any] = {
|
|
1260
|
+
"ts": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
1261
|
+
"source": source,
|
|
1262
|
+
"root": str(root),
|
|
1263
|
+
"health_score": summary.get("health_score"),
|
|
1264
|
+
"total": summary.get("total"),
|
|
1265
|
+
"green": summary.get("green"),
|
|
1266
|
+
"yellow": summary.get("yellow"),
|
|
1267
|
+
"red": summary.get("red"),
|
|
1268
|
+
"stub_yellow": summary.get("stub_yellow"),
|
|
1269
|
+
"actionable_yellow": summary.get("actionable_yellow"),
|
|
1270
|
+
"pending_updates": summary.get("pending_updates"),
|
|
1271
|
+
"ghost_count": len(ghosts),
|
|
1272
|
+
"cache_bytes": cache_bytes,
|
|
1273
|
+
"staging_bytes": staging_bytes,
|
|
1274
|
+
"journal_files": journal_files,
|
|
1275
|
+
"bare_dot_monitor": scope.get("bare_dot_monitor"),
|
|
1276
|
+
"scope_ok": scope.get("ok"),
|
|
1277
|
+
"daemon_heartbeat_ok": (heartbeat or {}).get("ok") if isinstance(heartbeat, dict) else None,
|
|
1278
|
+
"daemon_fail_streak": (heartbeat or {}).get("consecutive_failures")
|
|
1279
|
+
if isinstance(heartbeat, dict)
|
|
1280
|
+
else None,
|
|
1281
|
+
}
|
|
1282
|
+
if extra:
|
|
1283
|
+
snap["extra"] = extra
|
|
1284
|
+
|
|
1285
|
+
latest_path = staging / "metrics_latest.json"
|
|
1286
|
+
hist_path = staging / "metrics_history.jsonl"
|
|
1287
|
+
try:
|
|
1288
|
+
latest_path.write_text(json.dumps(snap, indent=2) + "\n", encoding="utf-8")
|
|
1289
|
+
with open(hist_path, "a", encoding="utf-8") as f:
|
|
1290
|
+
f.write(json.dumps(snap, ensure_ascii=False) + "\n")
|
|
1291
|
+
# Bound history (last 500 samples)
|
|
1292
|
+
try:
|
|
1293
|
+
lines = hist_path.read_text(encoding="utf-8").splitlines()
|
|
1294
|
+
if len(lines) > 500:
|
|
1295
|
+
hist_path.write_text("\n".join(lines[-500:]) + "\n", encoding="utf-8")
|
|
1296
|
+
except Exception:
|
|
1297
|
+
pass
|
|
1298
|
+
snap["success"] = True
|
|
1299
|
+
snap["metrics_latest_path"] = str(latest_path)
|
|
1300
|
+
snap["metrics_history_path"] = str(hist_path)
|
|
1301
|
+
except Exception as e:
|
|
1302
|
+
snap["success"] = False
|
|
1303
|
+
snap["error"] = str(e)
|
|
1304
|
+
return snap
|
|
1305
|
+
|
|
1306
|
+
|
|
1307
|
+
def read_metrics_history(root: Path, limit: int = 20) -> List[Dict[str, Any]]:
|
|
1308
|
+
"""Return up to `limit` most recent metrics samples (newest last)."""
|
|
1309
|
+
root = _coerce_root(root)
|
|
1310
|
+
hist_path = root / ".wikifier_staging" / "metrics_history.jsonl"
|
|
1311
|
+
if not hist_path.exists():
|
|
1312
|
+
return []
|
|
1313
|
+
out: List[Dict[str, Any]] = []
|
|
1314
|
+
try:
|
|
1315
|
+
lines = hist_path.read_text(encoding="utf-8").splitlines()
|
|
1316
|
+
for ln in lines[-max(1, min(limit, 500)) :]:
|
|
1317
|
+
ln = ln.strip()
|
|
1318
|
+
if not ln:
|
|
1319
|
+
continue
|
|
1320
|
+
try:
|
|
1321
|
+
out.append(json.loads(ln))
|
|
1322
|
+
except Exception:
|
|
1323
|
+
continue
|
|
1324
|
+
except Exception:
|
|
1325
|
+
return []
|
|
1326
|
+
return out
|
|
1327
|
+
|
|
1328
|
+
|
|
1329
|
+
def assess_autonomous_readiness(
|
|
1330
|
+
root: Path,
|
|
1331
|
+
write_metrics: bool = True,
|
|
1332
|
+
) -> Dict[str, Any]:
|
|
1333
|
+
"""Long-horizon autonomous ops checklist (map + daemon + scope + state).
|
|
1334
|
+
|
|
1335
|
+
Does not replace multi-day soak evidence; surfaces *whether* a target is
|
|
1336
|
+
configured safely enough for unattended check-changes/daemon loops.
|
|
1337
|
+
Optionally appends a metrics snapshot for growth tracking over calendar time.
|
|
1338
|
+
"""
|
|
1339
|
+
root = _coerce_root(root)
|
|
1340
|
+
summary = get_summary(root)
|
|
1341
|
+
scope = detect_scope_risks(root)
|
|
1342
|
+
ghosts = find_ghost_entries(root)
|
|
1343
|
+
pending_n = count_pending(root)
|
|
1344
|
+
|
|
1345
|
+
staging = root / ".wikifier_staging"
|
|
1346
|
+
cache_path = staging / "import_cache.json"
|
|
1347
|
+
lock_path = staging / ".lock"
|
|
1348
|
+
pid_path = staging / "wikifier.pid"
|
|
1349
|
+
heartbeat_path = staging / "daemon_heartbeat.json"
|
|
1350
|
+
log_path = staging / "daemon.log"
|
|
1351
|
+
metrics_latest_path = staging / "metrics_latest.json"
|
|
1352
|
+
|
|
1353
|
+
cache_bytes = cache_path.stat().st_size if cache_path.exists() else 0
|
|
1354
|
+
staging_bytes = _staging_byte_total(staging, cache_path)
|
|
1355
|
+
|
|
1356
|
+
journal_root = root / "journal"
|
|
1357
|
+
journal_days = 0
|
|
1358
|
+
journal_files = 0
|
|
1359
|
+
if journal_root.exists():
|
|
1360
|
+
for p in journal_root.rglob("*.md"):
|
|
1361
|
+
journal_files += 1
|
|
1362
|
+
try:
|
|
1363
|
+
journal_days = len({p.parent for p in journal_root.rglob("*.md")})
|
|
1364
|
+
except Exception:
|
|
1365
|
+
pass
|
|
1366
|
+
|
|
1367
|
+
daemon_running = False
|
|
1368
|
+
daemon_pid = None
|
|
1369
|
+
if pid_path.exists():
|
|
1370
|
+
try:
|
|
1371
|
+
daemon_pid = int(pid_path.read_text(encoding="utf-8").strip())
|
|
1372
|
+
os.kill(daemon_pid, 0)
|
|
1373
|
+
daemon_running = True
|
|
1374
|
+
except Exception:
|
|
1375
|
+
daemon_running = False
|
|
1376
|
+
|
|
1377
|
+
heartbeat = None
|
|
1378
|
+
if heartbeat_path.exists():
|
|
1379
|
+
try:
|
|
1380
|
+
with open(heartbeat_path, "r", encoding="utf-8") as f:
|
|
1381
|
+
heartbeat = json.load(f)
|
|
1382
|
+
except Exception:
|
|
1383
|
+
heartbeat = {"error": "unreadable"}
|
|
1384
|
+
|
|
1385
|
+
has_health = (root / "file_health.json").exists() or (root / "file_health.md").exists()
|
|
1386
|
+
has_library = (root / "library.md").exists()
|
|
1387
|
+
has_cache = cache_path.exists()
|
|
1388
|
+
|
|
1389
|
+
metrics_snap = None
|
|
1390
|
+
if write_metrics:
|
|
1391
|
+
try:
|
|
1392
|
+
metrics_snap = write_metrics_snapshot(root, source="autonomous-status")
|
|
1393
|
+
except Exception as e:
|
|
1394
|
+
metrics_snap = {"success": False, "error": str(e)}
|
|
1395
|
+
|
|
1396
|
+
history = read_metrics_history(root, limit=5)
|
|
1397
|
+
growth_note = None
|
|
1398
|
+
if len(history) >= 2:
|
|
1399
|
+
try:
|
|
1400
|
+
a, b = history[0], history[-1]
|
|
1401
|
+
da = int(b.get("staging_bytes") or 0) - int(a.get("staging_bytes") or 0)
|
|
1402
|
+
growth_note = {
|
|
1403
|
+
"samples": len(history),
|
|
1404
|
+
"staging_bytes_delta": da,
|
|
1405
|
+
"first_ts": a.get("ts"),
|
|
1406
|
+
"last_ts": b.get("ts"),
|
|
1407
|
+
}
|
|
1408
|
+
except Exception:
|
|
1409
|
+
growth_note = None
|
|
1410
|
+
|
|
1411
|
+
blockers: List[str] = []
|
|
1412
|
+
recs: List[str] = []
|
|
1413
|
+
if not has_cache and not has_library:
|
|
1414
|
+
blockers.append("No import map (run update-maps first).")
|
|
1415
|
+
if not has_health:
|
|
1416
|
+
blockers.append("No file_health — run seed-health or update-maps.")
|
|
1417
|
+
if scope["warnings"]:
|
|
1418
|
+
recs.extend(scope["warnings"])
|
|
1419
|
+
if len(ghosts) > 50:
|
|
1420
|
+
blockers.append(f"Many ghost health keys ({len(ghosts)}); run record-deletion / prune.")
|
|
1421
|
+
elif ghosts:
|
|
1422
|
+
recs.append(f"{len(ghosts)} ghost health entries — clean with record-deletion.")
|
|
1423
|
+
if pending_n > 500:
|
|
1424
|
+
recs.append(
|
|
1425
|
+
f"pending_updates={pending_n} is large; run prune-pending (auto-detected noise)."
|
|
1426
|
+
)
|
|
1427
|
+
if cache_bytes > 200 * 1024 * 1024:
|
|
1428
|
+
recs.append(
|
|
1429
|
+
f"import_cache is large ({cache_bytes // (1024*1024)}MB); "
|
|
1430
|
+
"prefer scoped update-maps and lean monitors for long runs."
|
|
1431
|
+
)
|
|
1432
|
+
if staging_bytes > 500 * 1024 * 1024:
|
|
1433
|
+
recs.append(f".wikifier_staging ~{staging_bytes // (1024*1024)}MB — watch disk growth.")
|
|
1434
|
+
if not daemon_running:
|
|
1435
|
+
recs.append(
|
|
1436
|
+
"Daemon not running. For long-horizon: "
|
|
1437
|
+
"`WIKIFIER_PROJECT_ROOT=… wikifier daemon start` "
|
|
1438
|
+
"(maps interval default 600s; WIKIFIER_DAEMON_MAPS=0 for check-only)."
|
|
1439
|
+
)
|
|
1440
|
+
else:
|
|
1441
|
+
recs.append(f"Daemon running (PID {daemon_pid}).")
|
|
1442
|
+
recs.append(
|
|
1443
|
+
"Metrics: write_metrics_snapshot / `wikifier metrics-snapshot`; "
|
|
1444
|
+
"history at .wikifier_staging/metrics_history.jsonl for soak growth."
|
|
1445
|
+
)
|
|
1446
|
+
|
|
1447
|
+
score = summary.get("health_score") or "Unknown"
|
|
1448
|
+
# Readiness tier for unattended ops (not M5 95% soak claim)
|
|
1449
|
+
if blockers:
|
|
1450
|
+
readiness = "blocked"
|
|
1451
|
+
elif scope["warnings"] and summary.get("actionable_yellow", 0) == 0 and summary.get("red", 0) == 0:
|
|
1452
|
+
readiness = "map_ok_scope_risk"
|
|
1453
|
+
elif score in ("Map Ready", "Good") and not scope["bare_dot_monitor"]:
|
|
1454
|
+
readiness = "ready_for_daemon"
|
|
1455
|
+
elif score == "Needs Attention" and summary.get("red", 0) == 0:
|
|
1456
|
+
readiness = "ready_with_agent_wiki_work"
|
|
1457
|
+
else:
|
|
1458
|
+
readiness = "not_ready"
|
|
1459
|
+
|
|
1460
|
+
return {
|
|
1461
|
+
"success": True,
|
|
1462
|
+
"root": str(root),
|
|
1463
|
+
"readiness": readiness,
|
|
1464
|
+
"health_score": score,
|
|
1465
|
+
"health_summary": summary,
|
|
1466
|
+
"scope": scope,
|
|
1467
|
+
"ghost_count": len(ghosts),
|
|
1468
|
+
"pending_updates": pending_n,
|
|
1469
|
+
"artifacts": {
|
|
1470
|
+
"has_health": has_health,
|
|
1471
|
+
"has_library": has_library,
|
|
1472
|
+
"has_cache": has_cache,
|
|
1473
|
+
"cache_bytes": cache_bytes,
|
|
1474
|
+
"staging_bytes": staging_bytes,
|
|
1475
|
+
"journal_files": journal_files,
|
|
1476
|
+
"journal_day_dirs": journal_days,
|
|
1477
|
+
"metrics_latest": str(metrics_latest_path) if metrics_latest_path.exists() else None,
|
|
1478
|
+
},
|
|
1479
|
+
"daemon": {
|
|
1480
|
+
"running": daemon_running,
|
|
1481
|
+
"pid": daemon_pid,
|
|
1482
|
+
"heartbeat": heartbeat,
|
|
1483
|
+
"log": str(log_path) if log_path.exists() else None,
|
|
1484
|
+
"maps_env": {
|
|
1485
|
+
"WIKIFIER_DAEMON_MAPS": os.environ.get("WIKIFIER_DAEMON_MAPS", "1"),
|
|
1486
|
+
"WIKIFIER_DAEMON_MAPS_INTERVAL": os.environ.get(
|
|
1487
|
+
"WIKIFIER_DAEMON_MAPS_INTERVAL", "600"
|
|
1488
|
+
),
|
|
1489
|
+
},
|
|
1490
|
+
},
|
|
1491
|
+
"metrics": metrics_snap,
|
|
1492
|
+
"metrics_growth": growth_note,
|
|
1493
|
+
"lock_path": str(lock_path),
|
|
1494
|
+
"blockers": blockers,
|
|
1495
|
+
"recommendations": recs,
|
|
1496
|
+
"long_horizon_note": (
|
|
1497
|
+
"ready_for_daemon = safe config for unattended check-changes/maps heartbeat. "
|
|
1498
|
+
"It is NOT a multi-day 0-corruption soak proof (M5.3). "
|
|
1499
|
+
"For soak: run daemon on 1–3 lean-monitored targets, append metrics-snapshot "
|
|
1500
|
+
"periodically, watch staging_bytes delta in metrics_history.jsonl, "
|
|
1501
|
+
"daemon_heartbeat consecutive_failures, and journal continuity over >=72h."
|
|
1502
|
+
),
|
|
1503
|
+
"map_first_note": summary.get("map_first_note"),
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
|
|
1152
1507
|
# ----------------------------- apply_barrel... (continues) -----------------------------
|
|
1153
1508
|
|
|
1154
1509
|
def apply_barrel_invalidation_reports(
|
|
@@ -1203,33 +1558,84 @@ def apply_barrel_invalidation_reports(
|
|
|
1203
1558
|
return updated
|
|
1204
1559
|
|
|
1205
1560
|
|
|
1561
|
+
def _is_map_first_stub_entry(entry: Dict[str, Any]) -> bool:
|
|
1562
|
+
"""True for map-first Initial stubs (not agent-action work items)."""
|
|
1563
|
+
if not isinstance(entry, dict):
|
|
1564
|
+
return False
|
|
1565
|
+
reason = str(entry.get("reason") or "")
|
|
1566
|
+
prov = str(entry.get("freshness_provenance") or "")
|
|
1567
|
+
blob = (reason + " " + prov).lower()
|
|
1568
|
+
if "initial stub" in blob:
|
|
1569
|
+
return True
|
|
1570
|
+
if "seed_health_from_map" in blob or "seed_health_for_monitored" in blob:
|
|
1571
|
+
return True
|
|
1572
|
+
if "present in dependency map" in blob:
|
|
1573
|
+
return True
|
|
1574
|
+
if "parseable source under monitored" in blob:
|
|
1575
|
+
return True
|
|
1576
|
+
return False
|
|
1577
|
+
|
|
1578
|
+
|
|
1579
|
+
def _compute_health_score(
|
|
1580
|
+
red: int,
|
|
1581
|
+
yellow: int,
|
|
1582
|
+
stub_yellow: int,
|
|
1583
|
+
actionable_yellow: int,
|
|
1584
|
+
) -> str:
|
|
1585
|
+
"""Agent-facing readiness label (map-first aware).
|
|
1586
|
+
|
|
1587
|
+
Map Ready — no reds, only map-first stubs (or empty); do NOT treat as unfinished wiki.
|
|
1588
|
+
Good — clean green-heavy tree, few actionable yellows
|
|
1589
|
+
Needs Attention — actionable yellows / pending work (not mere stubs)
|
|
1590
|
+
Critical — reds
|
|
1591
|
+
"""
|
|
1592
|
+
if red >= 3:
|
|
1593
|
+
return "Critical"
|
|
1594
|
+
if red > 0:
|
|
1595
|
+
return "Needs Attention"
|
|
1596
|
+
if actionable_yellow > 0:
|
|
1597
|
+
return "Needs Attention"
|
|
1598
|
+
if yellow > 0 and stub_yellow >= yellow and actionable_yellow == 0:
|
|
1599
|
+
return "Map Ready"
|
|
1600
|
+
if yellow == 0:
|
|
1601
|
+
return "Good"
|
|
1602
|
+
if yellow < 5:
|
|
1603
|
+
return "Good"
|
|
1604
|
+
return "Needs Attention"
|
|
1605
|
+
|
|
1606
|
+
|
|
1206
1607
|
def get_summary(root: Path, directory: Optional[str] = None, include_stale: bool = False) -> Dict[str, Any]:
|
|
1207
1608
|
"""Return a summary of the health matrix (fast even for large repos; sharded by dir).
|
|
1208
1609
|
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1610
|
+
Map-first taxonomy (additive fields):
|
|
1611
|
+
stub_yellow — Initial stub / map-seed rows (lookup OK; wiki on edit)
|
|
1612
|
+
actionable_yellow — real work (mtime, record-change, barrel stale, …)
|
|
1613
|
+
health_score — \"Map Ready\" | \"Good\" | \"Needs Attention\" | \"Critical\"
|
|
1614
|
+
|
|
1615
|
+
Agents must not treat stub_yellow as \"I understand this file.\"
|
|
1213
1616
|
"""
|
|
1214
1617
|
health = load_health(root)
|
|
1215
|
-
# Phase 5e (66): health.get_summary (format=summary path) promoted as first-class default/recommended for 20k+ creative monorepos (O(k) sharded, complements compute_acs_summary + ACS/CIABRE/BRC bounded via deque; per crit2/5 + 48/58).
|
|
1216
1618
|
entries = health.get("entries", {})
|
|
1217
1619
|
|
|
1218
1620
|
green = yellow = red = 0
|
|
1621
|
+
stub_yellow = actionable_yellow = 0
|
|
1219
1622
|
total = 0
|
|
1220
1623
|
|
|
1221
1624
|
for file_path, entry in entries.items():
|
|
1222
1625
|
if directory:
|
|
1223
|
-
# Normalize paths for comparison
|
|
1224
1626
|
if not file_path.startswith(directory.rstrip('/') + '/'):
|
|
1225
1627
|
continue
|
|
1226
1628
|
|
|
1227
1629
|
total += 1
|
|
1228
|
-
status = entry.get("status", "")
|
|
1630
|
+
status = entry.get("status", "") if isinstance(entry, dict) else ""
|
|
1229
1631
|
if "🟢" in status:
|
|
1230
1632
|
green += 1
|
|
1231
1633
|
elif "🟡" in status:
|
|
1232
1634
|
yellow += 1
|
|
1635
|
+
if _is_map_first_stub_entry(entry if isinstance(entry, dict) else {}):
|
|
1636
|
+
stub_yellow += 1
|
|
1637
|
+
else:
|
|
1638
|
+
actionable_yellow += 1
|
|
1233
1639
|
elif "🔴" in status:
|
|
1234
1640
|
red += 1
|
|
1235
1641
|
|
|
@@ -1237,12 +1643,20 @@ def get_summary(root: Path, directory: Optional[str] = None, include_stale: bool
|
|
|
1237
1643
|
pending_n = count_pending(root)
|
|
1238
1644
|
except Exception:
|
|
1239
1645
|
pending_n = 0
|
|
1646
|
+
score = _compute_health_score(red, yellow, stub_yellow, actionable_yellow)
|
|
1240
1647
|
out = {
|
|
1241
1648
|
"total": total,
|
|
1242
1649
|
"green": green,
|
|
1243
1650
|
"yellow": yellow,
|
|
1244
1651
|
"red": red,
|
|
1652
|
+
"stub_yellow": stub_yellow,
|
|
1653
|
+
"actionable_yellow": actionable_yellow,
|
|
1245
1654
|
"pending_updates": pending_n,
|
|
1655
|
+
"health_score": score,
|
|
1656
|
+
"map_first_note": (
|
|
1657
|
+
"Yellow Initial stubs = map coverage only; wiki prose is optional until you edit. "
|
|
1658
|
+
"Prefer actionable_yellow / red for work. health_score Map Ready ≠ wiki-done."
|
|
1659
|
+
),
|
|
1246
1660
|
"directory": directory or ".",
|
|
1247
1661
|
"version": health.get("version", 2),
|
|
1248
1662
|
"last_updated": health.get("last_updated")
|
|
@@ -1709,14 +1709,34 @@ def get_project_status(
|
|
|
1709
1709
|
yellow=summary["yellow"],
|
|
1710
1710
|
red=summary["red"],
|
|
1711
1711
|
pending_updates=pending_count,
|
|
1712
|
-
health_score=
|
|
1712
|
+
health_score=str(
|
|
1713
|
+
summary.get("health_score")
|
|
1714
|
+
or (
|
|
1715
|
+
"Good"
|
|
1716
|
+
if summary["red"] == 0 and summary["yellow"] < 5
|
|
1717
|
+
else "Needs Attention"
|
|
1718
|
+
if summary["red"] < 3
|
|
1719
|
+
else "Critical"
|
|
1720
|
+
)
|
|
1721
|
+
),
|
|
1713
1722
|
)
|
|
1714
|
-
# attach dep intel
|
|
1723
|
+
# attach dep intel + map-first taxonomy (additive)
|
|
1715
1724
|
if isinstance(base, dict):
|
|
1716
1725
|
base["dependency_intel"] = dep_intel
|
|
1726
|
+
for k in (
|
|
1727
|
+
"stub_yellow",
|
|
1728
|
+
"actionable_yellow",
|
|
1729
|
+
"map_first_note",
|
|
1730
|
+
"health_score",
|
|
1731
|
+
):
|
|
1732
|
+
if k in summary:
|
|
1733
|
+
base[k] = summary[k]
|
|
1717
1734
|
else:
|
|
1718
1735
|
try:
|
|
1719
1736
|
base.dependency_intel = dep_intel # type: ignore[attr-defined]
|
|
1737
|
+
for k in ("stub_yellow", "actionable_yellow", "map_first_note"):
|
|
1738
|
+
if k in summary:
|
|
1739
|
+
setattr(base, k, summary[k])
|
|
1720
1740
|
except Exception:
|
|
1721
1741
|
pass
|
|
1722
1742
|
return base
|
|
@@ -1784,14 +1804,15 @@ Use get_files_needing_attention() for the actual list. Use get_cycles(analysis=T
|
|
|
1784
1804
|
if l.strip().startswith("- ") and not l.strip().startswith("- (")
|
|
1785
1805
|
])
|
|
1786
1806
|
green = yellow = red = total = 0
|
|
1807
|
+
summary_fb: dict = {}
|
|
1787
1808
|
try:
|
|
1788
1809
|
import importlib
|
|
1789
1810
|
health_module = importlib.import_module("wikifier.health")
|
|
1790
|
-
|
|
1791
|
-
total = int(
|
|
1792
|
-
green = int(
|
|
1793
|
-
yellow = int(
|
|
1794
|
-
red = int(
|
|
1811
|
+
summary_fb = health_module.get_summary(root, directory) or {}
|
|
1812
|
+
total = int(summary_fb.get("total", 0) or 0)
|
|
1813
|
+
green = int(summary_fb.get("green", 0) or 0)
|
|
1814
|
+
yellow = int(summary_fb.get("yellow", 0) or 0)
|
|
1815
|
+
red = int(summary_fb.get("red", 0) or 0)
|
|
1795
1816
|
except Exception:
|
|
1796
1817
|
# Last resort: emoji-aware (+ legacy tag) counts from health text.
|
|
1797
1818
|
try:
|
|
@@ -1804,13 +1825,22 @@ Use get_files_needing_attention() for the actual list. Use get_cycles(analysis=T
|
|
|
1804
1825
|
pass
|
|
1805
1826
|
|
|
1806
1827
|
if format == "json":
|
|
1828
|
+
_hs = summary_fb.get("health_score")
|
|
1829
|
+
if not _hs:
|
|
1830
|
+
_hs = (
|
|
1831
|
+
"Good"
|
|
1832
|
+
if red == 0 and yellow < 5
|
|
1833
|
+
else "Needs Attention"
|
|
1834
|
+
if red < 3
|
|
1835
|
+
else "Critical"
|
|
1836
|
+
)
|
|
1807
1837
|
return ProjectHealthSummary(
|
|
1808
1838
|
total_files=total or (green + yellow + red),
|
|
1809
1839
|
green=green,
|
|
1810
1840
|
yellow=yellow,
|
|
1811
1841
|
red=red,
|
|
1812
1842
|
pending_updates=pending_count,
|
|
1813
|
-
health_score=
|
|
1843
|
+
health_score=str(_hs)
|
|
1814
1844
|
)
|
|
1815
1845
|
|
|
1816
1846
|
dir_str = f" (in {directory})" if directory else ""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wikifier
|
|
3
|
-
Version: 4.5.
|
|
3
|
+
Version: 4.5.8
|
|
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
|