wikifier 4.5.6__tar.gz → 4.5.9__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.9}/PKG-INFO +1 -1
- {wikifier-4.5.6 → wikifier-4.5.9}/skills/run.md +15 -8
- {wikifier-4.5.6 → wikifier-4.5.9}/tests/test_gap_closure.py +157 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier/__init__.py +1 -1
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier/cli.py +71 -141
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier/daemon.py +113 -21
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier/health.py +423 -9
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier/import_cache.py +68 -10
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier/mcp/server.py +38 -8
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier/parsers/bree.py +2 -2
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier/parsers/javascript.py +1 -1
- wikifier-4.5.9/wikifier/project_root.py +144 -0
- {wikifier-4.5.6 → wikifier-4.5.9/wikifier.egg-info}/PKG-INFO +1 -1
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier.egg-info/SOURCES.txt +2 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/CONTRIBUTING.md +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/LICENSE +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/MANIFEST.in +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/README.md +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/diagnostics.html +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/docs/spec.md +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/index.html +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/pyproject.toml +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/setup.cfg +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/tests/test_barrel_invalidation.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/tests/test_health.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/tests/test_import_cache.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/tests/test_multi_lang_parsers.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/tests/test_parsers.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/tests/test_selftest_wrappers.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier/__main__.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier/contracts.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier/diagnostics.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier/index.html +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier/library.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier/locking.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier/mcp/__init__.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier/parsers/__init__.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier/parsers/_edge.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier/parsers/c_cpp.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier/parsers/cdia.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier/parsers/csharp.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier/parsers/go_lang.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier/parsers/java.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier/parsers/python.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier/parsers/rust.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier/resolution.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier/scripts/wikifier.bat +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier/scripts/wikifier.ps1 +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier/scripts/wikifier.sh +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier/serve.py +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier.egg-info/dependency_links.txt +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier.egg-info/entry_points.txt +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/wikifier.egg-info/requires.txt +0 -0
- {wikifier-4.5.6 → wikifier-4.5.9}/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.9
|
|
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.
|
|
@@ -103,6 +103,85 @@ class TestAcsActionableG4(TempProjectTestCase):
|
|
|
103
103
|
for p in action
|
|
104
104
|
))
|
|
105
105
|
|
|
106
|
+
def test_dynamic_literal_noise_demoted_from_actionable(self):
|
|
107
|
+
"""ACS v1.2: importlib.import_module(\"pkg\") static dynamics are not actionable."""
|
|
108
|
+
dyn_lit = {
|
|
109
|
+
"raw": "\"wikifier.health\"",
|
|
110
|
+
"resolved": "wikifier.health",
|
|
111
|
+
"resolved_path": None,
|
|
112
|
+
"is_dynamic": True,
|
|
113
|
+
"dynamic_type": "static",
|
|
114
|
+
"confidence_score": 0.05,
|
|
115
|
+
"confidence_reasons": [
|
|
116
|
+
"base:low", "conditional_context", "dynamic",
|
|
117
|
+
"detector:TernaryDetector",
|
|
118
|
+
],
|
|
119
|
+
"confidence_explanation": (
|
|
120
|
+
'Base low (0.05). TernaryDetector=importlib.import_module("wikifier.health"). '
|
|
121
|
+
"Recommendation: Runtime optional load is expected noise."
|
|
122
|
+
),
|
|
123
|
+
"resolution_metadata": {"strategy": "python-dynamic"},
|
|
124
|
+
}
|
|
125
|
+
project_fragile = {
|
|
126
|
+
"raw": "./missing_local",
|
|
127
|
+
"resolved": "missing_local",
|
|
128
|
+
"resolved_path": None,
|
|
129
|
+
"is_dynamic": False,
|
|
130
|
+
"confidence_score": 0.35,
|
|
131
|
+
"confidence_reasons": ["base:low", "no_resolved_path"],
|
|
132
|
+
"confidence_explanation": "Unresolved project-local. Recommendation: fix path.",
|
|
133
|
+
"diagnostic": {"category": "project"},
|
|
134
|
+
"resolution_metadata": {"strategy": "relative-fs"},
|
|
135
|
+
}
|
|
136
|
+
cache = {
|
|
137
|
+
"mod.py": {
|
|
138
|
+
"mtime": 1,
|
|
139
|
+
"resolved_pairs": [dyn_lit, project_fragile],
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
self.assertTrue(ic._edge_is_dynamic_literal_noise(dyn_lit))
|
|
143
|
+
self.assertFalse(ic._edge_is_dynamic_literal_noise(project_fragile))
|
|
144
|
+
summary = ic.compute_acs_summary(cache)
|
|
145
|
+
self.assertEqual(summary.get("acs_version"), "1.2")
|
|
146
|
+
self.assertGreaterEqual(summary["low_conf_edges"], 2)
|
|
147
|
+
self.assertEqual(summary["actionable_low_conf_edges"], 1)
|
|
148
|
+
self.assertGreaterEqual(summary.get("dynamic_literal_noise_edges", 0), 1)
|
|
149
|
+
action = ic.get_low_confidence_edges(cache, actionable_only=True)
|
|
150
|
+
self.assertEqual(len(action), 1)
|
|
151
|
+
self.assertIn("missing_local", str(action[0].get("raw") or action[0].get("resolved")))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class TestLoadSafetyNoImportCycle(unittest.TestCase):
|
|
155
|
+
"""Load-time safety: core modules import without circular-import failure."""
|
|
156
|
+
|
|
157
|
+
def test_import_wikifier_and_cycle_trio(self):
|
|
158
|
+
import importlib
|
|
159
|
+
import wikifier
|
|
160
|
+
self.assertTrue(hasattr(wikifier, "discover_project_root"))
|
|
161
|
+
# Fresh submodule loads (project_root breaks former cli↔cache↔bree SCC)
|
|
162
|
+
for name in (
|
|
163
|
+
"wikifier.project_root",
|
|
164
|
+
"wikifier.cli",
|
|
165
|
+
"wikifier.import_cache",
|
|
166
|
+
"wikifier.parsers.bree",
|
|
167
|
+
):
|
|
168
|
+
mod = importlib.import_module(name)
|
|
169
|
+
self.assertIsNotNone(mod)
|
|
170
|
+
from wikifier.project_root import discover_project_root as dpr
|
|
171
|
+
from wikifier.cli import discover_project_root as dpr_cli
|
|
172
|
+
self.assertIs(dpr, dpr_cli)
|
|
173
|
+
root = dpr()
|
|
174
|
+
self.assertTrue(root.exists())
|
|
175
|
+
|
|
176
|
+
def test_bree_does_not_import_cli_at_module_level(self):
|
|
177
|
+
"""Static check: bree source must not load-time import wikifier.cli."""
|
|
178
|
+
from pathlib import Path
|
|
179
|
+
bree_src = Path(__file__).resolve().parents[1] / "wikifier" / "parsers" / "bree.py"
|
|
180
|
+
text = bree_src.read_text(encoding="utf-8")
|
|
181
|
+
self.assertNotIn("from ..cli import", text)
|
|
182
|
+
self.assertNotIn("from wikifier.cli import", text)
|
|
183
|
+
self.assertIn("from ..project_root import", text)
|
|
184
|
+
|
|
106
185
|
|
|
107
186
|
class TestGhostAndDeletionG7(TempProjectTestCase):
|
|
108
187
|
def test_find_ghost_and_record_deletion(self):
|
|
@@ -190,6 +269,84 @@ class TestPendingAndPollutionHygiene(TempProjectTestCase):
|
|
|
190
269
|
self.assertIn("gone_real.py", entries2)
|
|
191
270
|
|
|
192
271
|
|
|
272
|
+
class TestMapFirstTaxonomyAndReadiness(TempProjectTestCase):
|
|
273
|
+
def test_stub_yellow_is_map_ready_not_needs_attention(self):
|
|
274
|
+
health_mod = importlib.import_module("wikifier.health")
|
|
275
|
+
health_mod.upsert_entry(
|
|
276
|
+
self.root, "a.py", "🟡 Yellow",
|
|
277
|
+
"Initial stub — present in dependency map; agent should wiki + mark-green when editing",
|
|
278
|
+
)
|
|
279
|
+
health_mod.upsert_entry(
|
|
280
|
+
self.root, "b.py", "🟡 Yellow",
|
|
281
|
+
"Initial stub — parseable source under monitored_paths; agent should wiki + mark-green when editing",
|
|
282
|
+
)
|
|
283
|
+
s = health_mod.get_summary(self.root)
|
|
284
|
+
self.assertEqual(s.get("stub_yellow"), 2)
|
|
285
|
+
self.assertEqual(s.get("actionable_yellow"), 0)
|
|
286
|
+
self.assertEqual(s.get("health_score"), "Map Ready")
|
|
287
|
+
|
|
288
|
+
def test_actionable_yellow_needs_attention(self):
|
|
289
|
+
health_mod = importlib.import_module("wikifier.health")
|
|
290
|
+
health_mod.upsert_entry(
|
|
291
|
+
self.root, "a.py", "🟡 Yellow",
|
|
292
|
+
"mtime changed since last check-changes (Python primary auto-detected)",
|
|
293
|
+
)
|
|
294
|
+
s = health_mod.get_summary(self.root)
|
|
295
|
+
self.assertEqual(s.get("actionable_yellow"), 1)
|
|
296
|
+
self.assertEqual(s.get("health_score"), "Needs Attention")
|
|
297
|
+
|
|
298
|
+
def test_suggest_does_not_bulk_wiki_stubs(self):
|
|
299
|
+
health_mod = importlib.import_module("wikifier.health")
|
|
300
|
+
health_mod.upsert_entry(self.root, "a.py", "🟡 Yellow", "Initial stub — present in dependency map")
|
|
301
|
+
text = cli.suggest_next_actions(project_root=self.root, format="text")
|
|
302
|
+
self.assertIn("Map-first", text)
|
|
303
|
+
self.assertNotIn("Review the 1 🟡 Yellow file(s) only", text)
|
|
304
|
+
|
|
305
|
+
def test_detect_multi_project_container(self):
|
|
306
|
+
health_mod = importlib.import_module("wikifier.health")
|
|
307
|
+
for name in ("proj_a", "proj_b", "proj_c"):
|
|
308
|
+
d = self.root / name
|
|
309
|
+
d.mkdir()
|
|
310
|
+
(d / ".git").mkdir()
|
|
311
|
+
scope = health_mod.detect_scope_risks(self.root)
|
|
312
|
+
self.assertGreaterEqual(scope.get("child_project_count"), 3)
|
|
313
|
+
self.assertFalse(scope.get("ok"))
|
|
314
|
+
self.assertTrue(any("multi-project" in w.lower() for w in scope.get("warnings") or []))
|
|
315
|
+
|
|
316
|
+
def test_autonomous_readiness_shape(self):
|
|
317
|
+
health_mod = importlib.import_module("wikifier.health")
|
|
318
|
+
(self.root / "monitored_paths.txt").write_text("src\n", encoding="utf-8")
|
|
319
|
+
self.write("src/app.py", "x=1\n")
|
|
320
|
+
health_mod.upsert_entry(self.root, "src/app.py", "🟡 Yellow", "Initial stub — present in dependency map")
|
|
321
|
+
r = health_mod.assess_autonomous_readiness(self.root)
|
|
322
|
+
self.assertTrue(r.get("success"))
|
|
323
|
+
self.assertIn(r.get("readiness"), (
|
|
324
|
+
"blocked", "map_ok_scope_risk", "ready_for_daemon",
|
|
325
|
+
"ready_with_agent_wiki_work", "not_ready",
|
|
326
|
+
))
|
|
327
|
+
self.assertIn("long_horizon_note", r)
|
|
328
|
+
self.assertIn("metrics", r)
|
|
329
|
+
|
|
330
|
+
def test_write_metrics_snapshot_history(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/app.py", "x=1\n")
|
|
334
|
+
health_mod.upsert_entry(self.root, "src/app.py", "🟢 Green", "ok")
|
|
335
|
+
s1 = health_mod.write_metrics_snapshot(self.root, source="test1")
|
|
336
|
+
self.assertTrue(s1.get("success"))
|
|
337
|
+
latest = self.root / ".wikifier_staging" / "metrics_latest.json"
|
|
338
|
+
hist = self.root / ".wikifier_staging" / "metrics_history.jsonl"
|
|
339
|
+
self.assertTrue(latest.exists())
|
|
340
|
+
self.assertTrue(hist.exists())
|
|
341
|
+
s2 = health_mod.write_metrics_snapshot(self.root, source="test2")
|
|
342
|
+
self.assertTrue(s2.get("success"))
|
|
343
|
+
rows = health_mod.read_metrics_history(self.root, limit=10)
|
|
344
|
+
self.assertGreaterEqual(len(rows), 2)
|
|
345
|
+
self.assertEqual(rows[-1].get("source"), "test2")
|
|
346
|
+
self.assertIn("staging_bytes", rows[-1])
|
|
347
|
+
self.assertIn("health_score", rows[-1])
|
|
348
|
+
|
|
349
|
+
|
|
193
350
|
class TestMapFirstHealthAndValidate(TempProjectTestCase):
|
|
194
351
|
def test_deep_relative_keys_are_under_root(self):
|
|
195
352
|
"""Regression: deep monorepo rel paths must not be treated as abs pollution."""
|
|
@@ -21,136 +21,9 @@ from pathlib import Path
|
|
|
21
21
|
from typing import Any, Dict, List, Optional
|
|
22
22
|
from contextlib import nullcontext as _nullcontext
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
Unified discovery helper for the *target project root* (user codebase / monorepo).
|
|
28
|
-
|
|
29
|
-
This is the canonical implementation for CLI, MCP, direct Python use, and
|
|
30
|
-
is mirrored (same priority + markers) by the shell script's discover_project_root().
|
|
31
|
-
|
|
32
|
-
Highest-leverage fix for External / Packaged Full-Update Robustness (Gap #1):
|
|
33
|
-
Makes `pip install wikifier` + `wikifier ...` (or direct packaged sh) reliable
|
|
34
|
-
from any external monorepo cwd without requiring manual WIKIFIER_PROJECT_ROOT
|
|
35
|
-
or --target on every invocation. Zero-dependency.
|
|
36
|
-
|
|
37
|
-
Wave 3 hardening (symlinks + pnpm/yarn stores):
|
|
38
|
-
- Consults $PWD (and $OLDPWD) logical cwd in addition to Path.cwd() / .resolve().
|
|
39
|
-
- Walks parent chains from *logical* start points first; this ensures that when
|
|
40
|
-
cwd is deep inside a pnpm symlinked store layout (e.g. monorepo/node_modules/.pnpm/pkg@1/.../pkg
|
|
41
|
-
whose physical target is /global-store/...), the logical ancestor chain still reaches
|
|
42
|
-
the real project root markers (.git, package.json etc.) outside the store.
|
|
43
|
-
- Falls back to resolved physical; returns the outermost marker root found.
|
|
44
|
-
- Prevents packaged/parser/daemon from accidentally using a store dir as PROJECT_ROOT.
|
|
45
|
-
|
|
46
|
-
Wave 4 further hardening for complex monorepo layouts:
|
|
47
|
-
- Collects *all* candidate marker roots across logical/physical/realpath chains (no early return).
|
|
48
|
-
- Selects the outermost (shallowest fs path) preferring .git roots, then lockfiles (pnpm/yarn), then other.
|
|
49
|
-
This correctly picks true monorepo root even when deep sub-packages have their own package.json / yarn workspaces.
|
|
50
|
-
- Expanded common markers: pnpm-lock.yaml, yarn.lock, lerna.json, nx.json, turbo.json, rush.json, workspace yamls.
|
|
51
|
-
- Explicit skipping of node_modules/.pnpm / .yarn / .pnp internals as candidate roots (even on logical paths).
|
|
52
|
-
- Additional os.path.realpath chains for nested/broken symlink monorepo layouts.
|
|
53
|
-
- Daemon, parsers (via _get_*_fallback), run_full_update all inherit automatically.
|
|
54
|
-
"""
|
|
55
|
-
# 1. Explicit env var (highest priority, supports all the R6 --target flows)
|
|
56
|
-
env_root = os.environ.get("WIKIFIER_PROJECT_ROOT")
|
|
57
|
-
if env_root:
|
|
58
|
-
p = Path(env_root).expanduser().resolve()
|
|
59
|
-
if p.exists():
|
|
60
|
-
return p
|
|
61
|
-
|
|
62
|
-
# Wave 3/4: logical cwd (PWD/OLDPWD) + physical + realpath fallbacks for symlink/pnpm/yarn/nested
|
|
63
|
-
cwd = Path.cwd()
|
|
64
|
-
logical_cwd_str = os.environ.get("PWD") or os.environ.get("OLDPWD") or str(cwd)
|
|
65
|
-
try:
|
|
66
|
-
logical_cwd = Path(logical_cwd_str).expanduser()
|
|
67
|
-
except Exception:
|
|
68
|
-
logical_cwd = cwd
|
|
69
|
-
|
|
70
|
-
start_points: List[Path] = []
|
|
71
|
-
for start in (logical_cwd, cwd, cwd.resolve()):
|
|
72
|
-
if start and start not in start_points:
|
|
73
|
-
start_points.append(start)
|
|
74
|
-
|
|
75
|
-
# Wave 4: realpath variants for deeper symlink nest robustness (e.g. git submodules + pnpm links)
|
|
76
|
-
for sp in list(start_points):
|
|
77
|
-
try:
|
|
78
|
-
rp = Path(os.path.realpath(str(sp)))
|
|
79
|
-
if rp not in start_points:
|
|
80
|
-
start_points.append(rp)
|
|
81
|
-
except Exception:
|
|
82
|
-
continue
|
|
83
|
-
|
|
84
|
-
# 2/3. Markers: wikifier-specific + expanded common monorepo (Wave 4)
|
|
85
|
-
wikifier_specific = [".wikifier", "monitored_paths.txt", ".wikifier/config"]
|
|
86
|
-
common_markers = [
|
|
87
|
-
".git", "package.json", "pyproject.toml", "setup.py", "setup.cfg",
|
|
88
|
-
"Cargo.toml", "go.mod", ".hg",
|
|
89
|
-
# Wave 4 monorepo root indicators (for workspace/subpkg layouts + yarn/pnpm detection)
|
|
90
|
-
"pnpm-lock.yaml", "yarn.lock", "lerna.json", "nx.json", "turbo.json",
|
|
91
|
-
"rush.json", "workspace.json", "pnpm-workspace.yaml", ".yarnrc.yml"
|
|
92
|
-
]
|
|
93
|
-
store_indicators = (".pnpm", ".yarn", ".pnp", ".pnp.cjs", "virtual-store", ".store")
|
|
94
|
-
|
|
95
|
-
candidates: List[Path] = []
|
|
96
|
-
for start in start_points:
|
|
97
|
-
try:
|
|
98
|
-
parent_chain = [start] + list(start.parents)
|
|
99
|
-
except Exception:
|
|
100
|
-
parent_chain = [start]
|
|
101
|
-
|
|
102
|
-
# Wikifier markers (post-init)
|
|
103
|
-
for parent in parent_chain:
|
|
104
|
-
try:
|
|
105
|
-
pstr = str(parent)
|
|
106
|
-
if any(ind in pstr for ind in store_indicators) and "node_modules" in pstr:
|
|
107
|
-
continue # never treat deep pnpm/yarn store paths as project root
|
|
108
|
-
if ((parent / ".wikifier").is_dir() or
|
|
109
|
-
(parent / "monitored_paths.txt").exists() or
|
|
110
|
-
(parent / ".wikifier/config").exists()):
|
|
111
|
-
if parent not in candidates:
|
|
112
|
-
candidates.append(parent)
|
|
113
|
-
except Exception:
|
|
114
|
-
continue
|
|
115
|
-
|
|
116
|
-
# Common + monorepo markers (any-scale external)
|
|
117
|
-
for parent in parent_chain:
|
|
118
|
-
try:
|
|
119
|
-
pstr = str(parent)
|
|
120
|
-
if any(ind in pstr for ind in store_indicators) and "node_modules" in pstr:
|
|
121
|
-
continue
|
|
122
|
-
for marker in common_markers:
|
|
123
|
-
if (parent / marker).exists():
|
|
124
|
-
if parent not in candidates:
|
|
125
|
-
candidates.append(parent)
|
|
126
|
-
except Exception:
|
|
127
|
-
continue
|
|
128
|
-
|
|
129
|
-
if candidates:
|
|
130
|
-
# Wave 4: pick outermost/shallowest, with strong preference for .git (true monorepo root)
|
|
131
|
-
# even if a deeper packages/*/package.json also matches. Solves workspace subdir case.
|
|
132
|
-
def _root_key(p: Path):
|
|
133
|
-
try:
|
|
134
|
-
rp = p.resolve()
|
|
135
|
-
has_git = (rp / ".git").exists() or (rp / ".git").is_file() # worktree support
|
|
136
|
-
depth = len(rp.parts)
|
|
137
|
-
return (0 if has_git else 1, depth, str(rp))
|
|
138
|
-
except Exception:
|
|
139
|
-
return (99, 99999, str(p))
|
|
140
|
-
|
|
141
|
-
best = min(candidates, key=_root_key)
|
|
142
|
-
try:
|
|
143
|
-
return best.resolve() if best.exists() else best
|
|
144
|
-
except Exception:
|
|
145
|
-
return best
|
|
146
|
-
|
|
147
|
-
# 4. Last-resort sensible default: the directory the user is running from.
|
|
148
|
-
# This is the key robustness fix vs. old "fall back to package dir".
|
|
149
|
-
# Prefer resolved for the final fallback.
|
|
150
|
-
try:
|
|
151
|
-
return cwd.resolve()
|
|
152
|
-
except Exception:
|
|
153
|
-
return cwd
|
|
24
|
+
# Canonical discovery lives in project_root (no cli↔cache↔bree load cycle).
|
|
25
|
+
# Re-export for public API: `from wikifier.cli import discover_project_root`.
|
|
26
|
+
from .project_root import discover_project_root # noqa: F401
|
|
154
27
|
|
|
155
28
|
|
|
156
29
|
# =============================================================================
|
|
@@ -728,18 +601,20 @@ def main():
|
|
|
728
601
|
i = 0
|
|
729
602
|
while i < len(argv):
|
|
730
603
|
arg = argv[i]
|
|
604
|
+
# Consume --target / --project-root so the *command* remains filtered_argv[0]
|
|
605
|
+
# (previously left --target as argv[0] → "Unknown command: --target").
|
|
731
606
|
if arg in ("--target", "--project-root") and i + 1 < len(argv):
|
|
732
607
|
project_root = argv[i + 1]
|
|
733
|
-
filtered_argv.append(arg)
|
|
734
|
-
filtered_argv.append(argv[i + 1])
|
|
735
608
|
i += 2
|
|
736
609
|
continue
|
|
737
610
|
elif arg.startswith("--target="):
|
|
738
611
|
project_root = arg.split("=", 1)[1]
|
|
739
|
-
|
|
612
|
+
i += 1
|
|
613
|
+
continue
|
|
740
614
|
elif arg.startswith("--project-root="):
|
|
741
615
|
project_root = arg.split("=", 1)[1]
|
|
742
|
-
|
|
616
|
+
i += 1
|
|
617
|
+
continue
|
|
743
618
|
elif arg in ("--use-canonical", "--use_canonical"):
|
|
744
619
|
use_canonical = True
|
|
745
620
|
filtered_argv.append(arg)
|
|
@@ -895,6 +770,30 @@ def main():
|
|
|
895
770
|
res = _health_mod.prune_health_outside_monitored(root)
|
|
896
771
|
print(_json.dumps(res, indent=2, default=str))
|
|
897
772
|
return 0 if res.get("success") else 1
|
|
773
|
+
if _cmd0 in (
|
|
774
|
+
"autonomous-status",
|
|
775
|
+
"autonomous_status",
|
|
776
|
+
"readiness",
|
|
777
|
+
"long-horizon",
|
|
778
|
+
):
|
|
779
|
+
import json as _json
|
|
780
|
+
if _health_mod is None or not hasattr(_health_mod, "assess_autonomous_readiness"):
|
|
781
|
+
print("[wikifier] health.assess_autonomous_readiness unavailable", file=sys.stderr)
|
|
782
|
+
return 1
|
|
783
|
+
root = _get_effective_root(project_root)
|
|
784
|
+
res = _health_mod.assess_autonomous_readiness(root)
|
|
785
|
+
print(_json.dumps(res, indent=2, default=str))
|
|
786
|
+
# exit 0 only when not blocked
|
|
787
|
+
return 0 if res.get("readiness") != "blocked" else 2
|
|
788
|
+
if _cmd0 in ("metrics-snapshot", "metrics_snapshot", "metrics"):
|
|
789
|
+
import json as _json
|
|
790
|
+
if _health_mod is None or not hasattr(_health_mod, "write_metrics_snapshot"):
|
|
791
|
+
print("[wikifier] write_metrics_snapshot unavailable", file=sys.stderr)
|
|
792
|
+
return 1
|
|
793
|
+
root = _get_effective_root(project_root)
|
|
794
|
+
res = _health_mod.write_metrics_snapshot(root, source="cli")
|
|
795
|
+
print(_json.dumps(res, indent=2, default=str))
|
|
796
|
+
return 0 if res.get("success") else 1
|
|
898
797
|
except Exception as e:
|
|
899
798
|
print(f"[wikifier] Python-primary {_cmd0} failed: {e}", file=sys.stderr)
|
|
900
799
|
return 1
|
|
@@ -1419,12 +1318,14 @@ def suggest_next_actions(
|
|
|
1419
1318
|
"""
|
|
1420
1319
|
root = _get_effective_root(project_root)
|
|
1421
1320
|
try:
|
|
1422
|
-
red = yellow = 0
|
|
1321
|
+
red = yellow = stub_y = action_y = 0
|
|
1423
1322
|
health_sum: Dict[str, Any] = {}
|
|
1424
1323
|
if _health_mod is not None:
|
|
1425
1324
|
health_sum = _health_mod.get_summary(root, directory) or {}
|
|
1426
|
-
red = health_sum.get("red", 0)
|
|
1427
|
-
yellow = health_sum.get("yellow", 0)
|
|
1325
|
+
red = int(health_sum.get("red", 0) or 0)
|
|
1326
|
+
yellow = int(health_sum.get("yellow", 0) or 0)
|
|
1327
|
+
stub_y = int(health_sum.get("stub_yellow", 0) or 0)
|
|
1328
|
+
action_y = int(health_sum.get("actionable_yellow", yellow - stub_y) or 0)
|
|
1428
1329
|
|
|
1429
1330
|
suggestions: List[str] = []
|
|
1430
1331
|
n = 1
|
|
@@ -1434,10 +1335,23 @@ def suggest_next_actions(
|
|
|
1434
1335
|
"Do not re-wiki 🟢 Green files."
|
|
1435
1336
|
)
|
|
1436
1337
|
n += 1
|
|
1437
|
-
if
|
|
1338
|
+
if action_y > 0:
|
|
1339
|
+
suggestions.append(
|
|
1340
|
+
f"{n}. Review {action_y} *actionable* 🟡 Yellow file(s) "
|
|
1341
|
+
"(mtime/record-change/barrel — not Initial stubs). "
|
|
1342
|
+
"record-change → wiki that file → mark-green. Skip green."
|
|
1343
|
+
)
|
|
1344
|
+
n += 1
|
|
1345
|
+
if stub_y > 0 and action_y == 0 and red == 0:
|
|
1438
1346
|
suggestions.append(
|
|
1439
|
-
f"{n}.
|
|
1440
|
-
"
|
|
1347
|
+
f"{n}. Map-first OK: {stub_y} 🟡 Initial stubs mean \"on the map\", "
|
|
1348
|
+
"NOT \"wiki this tree now\". Lookup via get_file_wiki/deps; "
|
|
1349
|
+
"write prose only when you edit a file, then mark-green."
|
|
1350
|
+
)
|
|
1351
|
+
n += 1
|
|
1352
|
+
elif stub_y > 0 and action_y > 0:
|
|
1353
|
+
suggestions.append(
|
|
1354
|
+
f"{n}. Ignore {stub_y} map-first stubs for bulk work; only actionable yellows need wiki."
|
|
1441
1355
|
)
|
|
1442
1356
|
n += 1
|
|
1443
1357
|
if red == 0 and yellow == 0:
|
|
@@ -1445,6 +1359,15 @@ def suggest_next_actions(
|
|
|
1445
1359
|
f"{n}. Health is clean (no red/yellow). Do not re-summarize the tree; use the map for lookup only."
|
|
1446
1360
|
)
|
|
1447
1361
|
n += 1
|
|
1362
|
+
# Scope hygiene
|
|
1363
|
+
if _health_mod is not None and hasattr(_health_mod, "detect_scope_risks"):
|
|
1364
|
+
try:
|
|
1365
|
+
scope = _health_mod.detect_scope_risks(root) or {}
|
|
1366
|
+
for w in (scope.get("warnings") or [])[:2]:
|
|
1367
|
+
suggestions.append(f"{n}. SCOPE: {w}")
|
|
1368
|
+
n += 1
|
|
1369
|
+
except Exception:
|
|
1370
|
+
pass
|
|
1448
1371
|
suggestions.append(
|
|
1449
1372
|
f"{n}. Run `update_maps(directory=...)` only if imports/structure changed (not for wiki-only edits)."
|
|
1450
1373
|
)
|
|
@@ -1453,7 +1376,10 @@ def suggest_next_actions(
|
|
|
1453
1376
|
f"{n}. On yellow/red hotspots, query dependents (get_dependents) before editing callers."
|
|
1454
1377
|
)
|
|
1455
1378
|
n += 1
|
|
1456
|
-
suggestions.append(
|
|
1379
|
+
suggestions.append(
|
|
1380
|
+
f"{n}. Long-horizon: `wikifier autonomous-status` before unattended daemon; "
|
|
1381
|
+
"lean monitored_paths; never parent multi-repo folders as project_root."
|
|
1382
|
+
)
|
|
1457
1383
|
|
|
1458
1384
|
acs_note = ""
|
|
1459
1385
|
if _ic_mod is not None:
|
|
@@ -1488,10 +1414,14 @@ def suggest_next_actions(
|
|
|
1488
1414
|
"project_root": str(root),
|
|
1489
1415
|
"red": red,
|
|
1490
1416
|
"yellow": yellow,
|
|
1417
|
+
"stub_yellow": stub_y,
|
|
1418
|
+
"actionable_yellow": action_y,
|
|
1419
|
+
"health_score": health_sum.get("health_score"),
|
|
1491
1420
|
"suggestions": suggestions,
|
|
1492
1421
|
"health_summary": health_sum,
|
|
1493
1422
|
"acs_note": acs_note,
|
|
1494
1423
|
"selective_work": True,
|
|
1424
|
+
"map_first": True,
|
|
1495
1425
|
}
|
|
1496
1426
|
return "\n".join(suggestions) + (acs_note or "")
|
|
1497
1427
|
except Exception as e:
|