wikifier 4.5.5__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.
Files changed (53) hide show
  1. {wikifier-4.5.5/wikifier.egg-info → wikifier-4.5.8}/PKG-INFO +1 -1
  2. {wikifier-4.5.5 → wikifier-4.5.8}/skills/run.md +15 -8
  3. {wikifier-4.5.5 → wikifier-4.5.8}/tests/test_gap_closure.py +158 -0
  4. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier/__init__.py +1 -1
  5. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier/cli.py +131 -34
  6. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier/daemon.py +113 -21
  7. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier/health.py +771 -46
  8. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier/mcp/server.py +38 -8
  9. {wikifier-4.5.5 → wikifier-4.5.8/wikifier.egg-info}/PKG-INFO +1 -1
  10. {wikifier-4.5.5 → wikifier-4.5.8}/CONTRIBUTING.md +0 -0
  11. {wikifier-4.5.5 → wikifier-4.5.8}/LICENSE +0 -0
  12. {wikifier-4.5.5 → wikifier-4.5.8}/MANIFEST.in +0 -0
  13. {wikifier-4.5.5 → wikifier-4.5.8}/README.md +0 -0
  14. {wikifier-4.5.5 → wikifier-4.5.8}/diagnostics.html +0 -0
  15. {wikifier-4.5.5 → wikifier-4.5.8}/docs/spec.md +0 -0
  16. {wikifier-4.5.5 → wikifier-4.5.8}/index.html +0 -0
  17. {wikifier-4.5.5 → wikifier-4.5.8}/pyproject.toml +0 -0
  18. {wikifier-4.5.5 → wikifier-4.5.8}/setup.cfg +0 -0
  19. {wikifier-4.5.5 → wikifier-4.5.8}/tests/test_barrel_invalidation.py +0 -0
  20. {wikifier-4.5.5 → wikifier-4.5.8}/tests/test_health.py +0 -0
  21. {wikifier-4.5.5 → wikifier-4.5.8}/tests/test_import_cache.py +0 -0
  22. {wikifier-4.5.5 → wikifier-4.5.8}/tests/test_multi_lang_parsers.py +0 -0
  23. {wikifier-4.5.5 → wikifier-4.5.8}/tests/test_parsers.py +0 -0
  24. {wikifier-4.5.5 → wikifier-4.5.8}/tests/test_selftest_wrappers.py +0 -0
  25. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier/__main__.py +0 -0
  26. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier/contracts.py +0 -0
  27. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier/diagnostics.py +0 -0
  28. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier/import_cache.py +0 -0
  29. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier/index.html +0 -0
  30. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier/library.py +0 -0
  31. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier/locking.py +0 -0
  32. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier/mcp/__init__.py +0 -0
  33. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier/parsers/__init__.py +0 -0
  34. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier/parsers/_edge.py +0 -0
  35. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier/parsers/bree.py +0 -0
  36. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier/parsers/c_cpp.py +0 -0
  37. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier/parsers/cdia.py +0 -0
  38. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier/parsers/csharp.py +0 -0
  39. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier/parsers/go_lang.py +0 -0
  40. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier/parsers/java.py +0 -0
  41. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier/parsers/javascript.py +0 -0
  42. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier/parsers/python.py +0 -0
  43. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier/parsers/rust.py +0 -0
  44. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier/resolution.py +0 -0
  45. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier/scripts/wikifier.bat +0 -0
  46. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier/scripts/wikifier.ps1 +0 -0
  47. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier/scripts/wikifier.sh +0 -0
  48. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier/serve.py +0 -0
  49. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier.egg-info/SOURCES.txt +0 -0
  50. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier.egg-info/dependency_links.txt +0 -0
  51. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier.egg-info/entry_points.txt +0 -0
  52. {wikifier-4.5.5 → wikifier-4.5.8}/wikifier.egg-info/requires.txt +0 -0
  53. {wikifier-4.5.5 → 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.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
- - **Steady-state selective work:** only 🔴/🟡 (never re-wiki 🟢 unless required). ACS suggestions use `actionable_low_conf_edges` (stdlib/external bare demoted).
14
- - **Deletions:** `record-deletion` + check-changes ghost detection (missing paths Red DELETED).
15
- - **Languages:** deep import maps = Python, JS/TS, Rust, Go, C/C++, C#, Java (regex; not full toolchain resolution). journal/pending = audit queue, not Jira.
16
- - **Huge monorepos:** lean `monitored_paths.txt` (package roots, not bare `.`). Optional `WIKIFIER_CHECK_CHANGES_MAX` (default 2000). `update-maps` seeds Yellow health stubs for newly parsed files (map-first).
17
- - **Monitor:** optional background `monitor`/`daemon` for mtime heartbeat; agents still own wiki/mark-green.
18
- - Health module: use `importlib.import_module("wikifier.health")` or `wikifier.health_module` `from wikifier import health` is the *function*.
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 🔴/🟡 only → `record-change` → wiki → `mark-green` |
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
- | Code entry | `wikifier/cli.py` (dispatch) + `wikifier/health.py` + parsers self-tests live in `tests/` + `tests/selftest/` |
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,5 +190,163 @@ 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
+
271
+ class TestMapFirstHealthAndValidate(TempProjectTestCase):
272
+ def test_deep_relative_keys_are_under_root(self):
273
+ """Regression: deep monorepo rel paths must not be treated as abs pollution."""
274
+ health_mod = importlib.import_module("wikifier.health")
275
+ deep = "airflow-core/src/airflow/api/client/local_client.py"
276
+ self.assertTrue(health_mod._entry_is_under_root(self.root, deep))
277
+ self.assertTrue(health_mod._entry_is_under_root(self.root, "pkg/a.py"))
278
+
279
+ def test_seed_health_from_map_creates_stubs(self):
280
+ health_mod = importlib.import_module("wikifier.health")
281
+ from wikifier import import_cache as ic
282
+
283
+ self.write("pkg/a.py", "import b\n")
284
+ self.write("pkg/b.py", "x=1\n")
285
+ self.write("README.md", "# hi\n")
286
+ cache = {}
287
+ ic.update_file_data(cache, "pkg/a.py", mtime=1, imports=["b"], resolved_pairs=[])
288
+ ic.update_file_data(cache, "pkg/b.py", mtime=1, imports=[], resolved_pairs=[])
289
+ ic.save_cache(self.root, cache)
290
+
291
+ # No health file yet
292
+ self.assertFalse((self.root / "file_health.json").exists())
293
+ res = health_mod.seed_health_from_map(self.root)
294
+ self.assertTrue(res.get("success"))
295
+ self.assertGreaterEqual(res.get("seeded"), 2)
296
+ entries = health_mod.load_health(self.root)["entries"]
297
+ self.assertIn("pkg/a.py", entries)
298
+ self.assertIn("pkg/b.py", entries)
299
+ self.assertTrue(entries["pkg/a.py"]["status"].startswith("🟡"))
300
+
301
+ def test_validate_ignores_non_source_and_reports_mapped_gaps(self):
302
+ health_mod = importlib.import_module("wikifier.health")
303
+ from wikifier import import_cache as ic
304
+
305
+ self.write("src/app.py", "print(1)\n")
306
+ self.write("src/notes.md", "docs\n")
307
+ self.write("src/logo.png", "x")
308
+ (self.root / "monitored_paths.txt").write_text("src\n", encoding="utf-8")
309
+ cache = {}
310
+ ic.update_file_data(cache, "src/app.py", mtime=1, imports=[], resolved_pairs=[])
311
+ ic.update_file_data(cache, "src/other.py", mtime=1, imports=[], resolved_pairs=[])
312
+ ic.save_cache(self.root, cache)
313
+
314
+ # No health yet — in-scope mapped + monitored parseable gaps
315
+ v = health_mod.validate_health(self.root)
316
+ self.assertTrue(v.get("map_first"))
317
+ self.assertGreaterEqual(v.get("mapped_in_scope_without_health_count", 0), 1)
318
+ self.assertGreaterEqual(v.get("missing_count", 0), 1)
319
+ # notes.md / logo.png must not appear as monitored-source gaps
320
+ mon_missing = v.get("missing_monitored_source") or []
321
+ self.assertTrue(all(not m.endswith(".md") and not m.endswith(".png") for m in mon_missing))
322
+
323
+ health_mod.seed_health_from_map(self.root) # only_monitored default
324
+ # Seed in-scope map keys; app.py on disk + mapped in-scope covered
325
+ v2 = health_mod.validate_health(self.root)
326
+ self.assertEqual(v2.get("mapped_in_scope_without_health_count"), 0)
327
+ # missing_count may still include on-disk sources not in map (ok if 0 after seed of map keys only)
328
+ self.assertEqual(v2.get("mapped_in_scope_without_health_count"), 0)
329
+
330
+ def test_prune_pending_and_health_to_monitored(self):
331
+ health_mod = importlib.import_module("wikifier.health")
332
+ (self.root / "monitored_paths.txt").write_text("src\n", encoding="utf-8")
333
+ self.write("src/keep.py", "k=1\n")
334
+ self.write("other/out.py", "o=1\n")
335
+ health_mod.upsert_entry(self.root, "src/keep.py", "🟡 Yellow", "in scope")
336
+ health_mod.upsert_entry(self.root, "other/out.py", "🟡 Yellow", "out of scope")
337
+ health_mod.add_to_pending(self.root, "src/keep.py", "need wiki")
338
+ health_mod.add_to_pending(self.root, "other/out.py", "noise")
339
+
340
+ pr = health_mod.prune_pending_to_monitored(self.root)
341
+ self.assertEqual(pr.get("removed"), 1)
342
+ self.assertEqual(health_mod.count_pending(self.root), 1)
343
+
344
+ hr = health_mod.prune_health_outside_monitored(self.root)
345
+ self.assertEqual(hr.get("removed"), 1)
346
+ entries = health_mod.load_health(self.root)["entries"]
347
+ self.assertIn("src/keep.py", entries)
348
+ self.assertNotIn("other/out.py", entries)
349
+
350
+
193
351
  if __name__ == "__main__":
194
352
  unittest.main()
@@ -71,4 +71,4 @@ from .contracts import (
71
71
  compute_acs_confidence,
72
72
  )
73
73
 
74
- __version__ = "4.5.5"
74
+ __version__ = "4.5.8"
@@ -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
@@ -724,18 +728,20 @@ def main():
724
728
  i = 0
725
729
  while i < len(argv):
726
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").
727
733
  if arg in ("--target", "--project-root") and i + 1 < len(argv):
728
734
  project_root = argv[i + 1]
729
- filtered_argv.append(arg)
730
- filtered_argv.append(argv[i + 1])
731
735
  i += 2
732
736
  continue
733
737
  elif arg.startswith("--target="):
734
738
  project_root = arg.split("=", 1)[1]
735
- filtered_argv.append(arg)
739
+ i += 1
740
+ continue
736
741
  elif arg.startswith("--project-root="):
737
742
  project_root = arg.split("=", 1)[1]
738
- filtered_argv.append(arg)
743
+ i += 1
744
+ continue
739
745
  elif arg in ("--use-canonical", "--use_canonical"):
740
746
  use_canonical = True
741
747
  filtered_argv.append(arg)
@@ -857,13 +863,64 @@ def main():
857
863
  if _health_mod is not None:
858
864
  root = _get_effective_root(project_root)
859
865
  res = _health_mod.validate_health(root)
860
- ghosts = []
861
- if hasattr(_health_mod, "find_ghost_entries"):
862
- ghosts = _health_mod.find_ghost_entries(root)
863
- res["ghost_count"] = len(ghosts)
864
- res["ghosts"] = ghosts[:50]
865
866
  print(_json.dumps(res, indent=2, default=str))
867
+ # Map-first: exit 0 when map is covered (or no map + no monitored source gaps)
866
868
  return 0 if res.get("missing_count", 0) == 0 else 1
869
+ if _cmd0 in ("seed-health", "seed-health-from-map"):
870
+ import json as _json
871
+ if _health_mod is None:
872
+ print("[wikifier] health module unavailable", file=sys.stderr)
873
+ return 1
874
+ root = _get_effective_root(project_root)
875
+ res = _health_mod.seed_health_from_map(root)
876
+ if hasattr(_health_mod, "seed_health_for_monitored_sources"):
877
+ disk = _health_mod.seed_health_for_monitored_sources(root)
878
+ res["disk_seeded"] = disk.get("seeded")
879
+ res["seeded_total"] = int(res.get("seeded") or 0) + int(disk.get("seeded") or 0)
880
+ print(_json.dumps(res, indent=2, default=str))
881
+ return 0 if res.get("success") else 1
882
+ if _cmd0 in ("prune-pending", "prune-pending-monitored"):
883
+ import json as _json
884
+ if _health_mod is None:
885
+ print("[wikifier] health module unavailable", file=sys.stderr)
886
+ return 1
887
+ root = _get_effective_root(project_root)
888
+ res = _health_mod.prune_pending_to_monitored(root)
889
+ print(_json.dumps(res, indent=2, default=str))
890
+ return 0 if res.get("success") else 1
891
+ if _cmd0 in ("prune-health-monitored", "prune-health"):
892
+ import json as _json
893
+ if _health_mod is None:
894
+ print("[wikifier] health module unavailable", file=sys.stderr)
895
+ return 1
896
+ root = _get_effective_root(project_root)
897
+ res = _health_mod.prune_health_outside_monitored(root)
898
+ print(_json.dumps(res, indent=2, default=str))
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
867
924
  except Exception as e:
868
925
  print(f"[wikifier] Python-primary {_cmd0} failed: {e}", file=sys.stderr)
869
926
  return 1
@@ -1197,6 +1254,8 @@ def check_changes(project_root: Optional[Union[str, Path]] = None) -> Dict[str,
1197
1254
  ghosts_marked = 0
1198
1255
  if _health_mod is not None:
1199
1256
  root_res = root.resolve()
1257
+ # Prefer unlocked helpers while we already hold project lock
1258
+ _upsert = getattr(_health_mod, "_do_upsert_entry", None) or _health_mod.upsert_entry
1200
1259
  for p in dirty_batch:
1201
1260
  try:
1202
1261
  pr = Path(p).resolve()
@@ -1205,13 +1264,20 @@ def check_changes(project_root: Optional[Union[str, Path]] = None) -> Dict[str,
1205
1264
  rel = str(pr.relative_to(root_res))
1206
1265
  except Exception:
1207
1266
  continue
1208
- _health_mod.upsert_entry(
1267
+ _upsert(
1209
1268
  root, rel, "🟡 Yellow",
1210
1269
  "mtime changed since last check-changes (Python primary auto-detected)"
1211
1270
  )
1212
1271
  _add_to_pending(root, rel, "Auto-detected modification — review and run mark-green after wiki update")
1213
1272
  _ensure_journal_entry(root, "auto-detected", rel, "File mtime changed (check_changes Python primary)")
1214
1273
  changed_count += 1
1274
+ # Keep pending queue aligned with lean monitored_paths (no flood outside scope)
1275
+ if hasattr(_health_mod, "prune_pending_to_monitored"):
1276
+ try:
1277
+ pr = _health_mod.prune_pending_to_monitored(root)
1278
+ result["pending_pruned"] = pr.get("removed", 0)
1279
+ except Exception:
1280
+ pass
1215
1281
 
1216
1282
  # G7: surface ghost health entries (tracked path missing on disk, not already DELETED)
1217
1283
  try:
@@ -1379,12 +1445,14 @@ def suggest_next_actions(
1379
1445
  """
1380
1446
  root = _get_effective_root(project_root)
1381
1447
  try:
1382
- red = yellow = 0
1448
+ red = yellow = stub_y = action_y = 0
1383
1449
  health_sum: Dict[str, Any] = {}
1384
1450
  if _health_mod is not None:
1385
1451
  health_sum = _health_mod.get_summary(root, directory) or {}
1386
- red = health_sum.get("red", 0)
1387
- 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)
1388
1456
 
1389
1457
  suggestions: List[str] = []
1390
1458
  n = 1
@@ -1394,10 +1462,23 @@ def suggest_next_actions(
1394
1462
  "Do not re-wiki 🟢 Green files."
1395
1463
  )
1396
1464
  n += 1
1397
- if yellow > 0:
1465
+ if action_y > 0:
1398
1466
  suggestions.append(
1399
- f"{n}. Review the {yellow} 🟡 Yellow file(s) only — record-change intent, refresh wiki, mark-green. "
1400
- "Skip green unless a dependent of a yellow/red requires it."
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:
1480
+ suggestions.append(
1481
+ f"{n}. Ignore {stub_y} map-first stubs for bulk work; only actionable yellows need wiki."
1401
1482
  )
1402
1483
  n += 1
1403
1484
  if red == 0 and yellow == 0:
@@ -1405,6 +1486,15 @@ def suggest_next_actions(
1405
1486
  f"{n}. Health is clean (no red/yellow). Do not re-summarize the tree; use the map for lookup only."
1406
1487
  )
1407
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
1408
1498
  suggestions.append(
1409
1499
  f"{n}. Run `update_maps(directory=...)` only if imports/structure changed (not for wiki-only edits)."
1410
1500
  )
@@ -1413,7 +1503,10 @@ def suggest_next_actions(
1413
1503
  f"{n}. On yellow/red hotspots, query dependents (get_dependents) before editing callers."
1414
1504
  )
1415
1505
  n += 1
1416
- suggestions.append(f"{n}. Review journal/ for recent record-change intent (self-audit).")
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
+ )
1417
1510
 
1418
1511
  acs_note = ""
1419
1512
  if _ic_mod is not None:
@@ -1448,10 +1541,14 @@ def suggest_next_actions(
1448
1541
  "project_root": str(root),
1449
1542
  "red": red,
1450
1543
  "yellow": yellow,
1544
+ "stub_yellow": stub_y,
1545
+ "actionable_yellow": action_y,
1546
+ "health_score": health_sum.get("health_score"),
1451
1547
  "suggestions": suggestions,
1452
1548
  "health_summary": health_sum,
1453
1549
  "acs_note": acs_note,
1454
1550
  "selective_work": True,
1551
+ "map_first": True,
1455
1552
  }
1456
1553
  return "\n".join(suggestions) + (acs_note or "")
1457
1554
  except Exception as e: