wikifier 4.5.8__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.8/wikifier.egg-info → wikifier-4.5.9}/PKG-INFO +1 -1
- {wikifier-4.5.8 → wikifier-4.5.9}/tests/test_gap_closure.py +79 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier/__init__.py +1 -1
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier/cli.py +3 -130
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier/import_cache.py +68 -10
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier/parsers/bree.py +2 -2
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier/parsers/javascript.py +1 -1
- wikifier-4.5.9/wikifier/project_root.py +144 -0
- {wikifier-4.5.8 → wikifier-4.5.9/wikifier.egg-info}/PKG-INFO +1 -1
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier.egg-info/SOURCES.txt +2 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/CONTRIBUTING.md +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/LICENSE +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/MANIFEST.in +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/README.md +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/diagnostics.html +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/docs/spec.md +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/index.html +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/pyproject.toml +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/setup.cfg +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/skills/run.md +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/tests/test_barrel_invalidation.py +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/tests/test_health.py +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/tests/test_import_cache.py +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/tests/test_multi_lang_parsers.py +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/tests/test_parsers.py +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/tests/test_selftest_wrappers.py +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier/__main__.py +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier/contracts.py +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier/daemon.py +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier/diagnostics.py +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier/health.py +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier/index.html +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier/library.py +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier/locking.py +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier/mcp/__init__.py +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier/mcp/server.py +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier/parsers/__init__.py +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier/parsers/_edge.py +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier/parsers/c_cpp.py +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier/parsers/cdia.py +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier/parsers/csharp.py +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier/parsers/go_lang.py +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier/parsers/java.py +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier/parsers/python.py +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier/parsers/rust.py +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier/resolution.py +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier/scripts/wikifier.bat +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier/scripts/wikifier.ps1 +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier/scripts/wikifier.sh +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier/serve.py +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier.egg-info/dependency_links.txt +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier.egg-info/entry_points.txt +0 -0
- {wikifier-4.5.8 → wikifier-4.5.9}/wikifier.egg-info/requires.txt +0 -0
- {wikifier-4.5.8 → 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
|
|
@@ -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):
|
|
@@ -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
|
# =============================================================================
|
|
@@ -993,6 +993,54 @@ def _edge_is_external_noise(pair: Dict[str, Any]) -> bool:
|
|
|
993
993
|
return False
|
|
994
994
|
|
|
995
995
|
|
|
996
|
+
def _edge_is_dynamic_literal_noise(pair: Dict[str, Any]) -> bool:
|
|
997
|
+
"""True for dynamic imports of static string literals (not agent actionable risk).
|
|
998
|
+
|
|
999
|
+
ACS v1.2: demote importlib.import_module(\"pkg\"), __import__(\"pkg\"), and other
|
|
1000
|
+
is_dynamic + dynamic_type=static string-literal edges. These are intentional runtime
|
|
1001
|
+
loads (often optional/try fallbacks), not unresolved project graph holes.
|
|
1002
|
+
|
|
1003
|
+
Keep as telemetry (still in low_conf_edges) but exclude from actionable_low_conf_edges.
|
|
1004
|
+
"""
|
|
1005
|
+
if not isinstance(pair, dict):
|
|
1006
|
+
return False
|
|
1007
|
+
reasons = pair.get("confidence_reasons") or []
|
|
1008
|
+
reason_l = " ".join(str(r).lower() for r in reasons if isinstance(r, str))
|
|
1009
|
+
is_dyn = bool(pair.get("is_dynamic")) or ("dynamic" in reason_l)
|
|
1010
|
+
if not is_dyn:
|
|
1011
|
+
return False
|
|
1012
|
+
|
|
1013
|
+
dtype = str(pair.get("dynamic_type") or "").lower()
|
|
1014
|
+
raw = str(pair.get("raw") or pair.get("raw_module") or pair.get("module") or "").strip()
|
|
1015
|
+
expl = str(pair.get("confidence_explanation") or "")
|
|
1016
|
+
resolved = str(pair.get("resolved") or "").strip()
|
|
1017
|
+
|
|
1018
|
+
# Explicit importlib / __import__ traces → always non-actionable for ACS
|
|
1019
|
+
# (includes static string loads and parser traces that mention the call form).
|
|
1020
|
+
if "import_module(" in expl or "__import__(" in expl or "importlib.import_module" in expl:
|
|
1021
|
+
return True
|
|
1022
|
+
|
|
1023
|
+
# dynamic_type=static means LDSI recovered a concrete string literal
|
|
1024
|
+
if dtype in ("static", "string", "literal"):
|
|
1025
|
+
# Quoted raw like "\"wikifier.health\"" or '"./x"'
|
|
1026
|
+
if (raw.startswith("\"") and raw.endswith("\"")) or (raw.startswith("'") and raw.endswith("'")):
|
|
1027
|
+
return True
|
|
1028
|
+
# Unquoted but resolved/raw is a dotted package name (no path variables)
|
|
1029
|
+
cand = raw or resolved
|
|
1030
|
+
if cand and "${" not in cand and "+" not in cand:
|
|
1031
|
+
inner = cand.strip().strip("\"'")
|
|
1032
|
+
if inner and not any(ch in inner for ch in ("/", "\\", " ", "(", ")")):
|
|
1033
|
+
# pure module id literal (e.g. wikifier.health) — not a computed path
|
|
1034
|
+
if "." in inner or inner.isidentifier():
|
|
1035
|
+
return True
|
|
1036
|
+
return False
|
|
1037
|
+
|
|
1038
|
+
|
|
1039
|
+
def _edge_is_non_actionable_noise(pair: Dict[str, Any]) -> bool:
|
|
1040
|
+
"""Union of external/stdlib noise + dynamic-literal noise (ACS actionable filter)."""
|
|
1041
|
+
return _edge_is_external_noise(pair) or _edge_is_dynamic_literal_noise(pair)
|
|
1042
|
+
|
|
1043
|
+
|
|
996
1044
|
def compute_acs_summary(
|
|
997
1045
|
cache: Dict[str, Any],
|
|
998
1046
|
max_samples: int = 5,
|
|
@@ -1013,6 +1061,9 @@ def compute_acs_summary(
|
|
|
1013
1061
|
- external_noise_edges: count of scored edges classified as external noise
|
|
1014
1062
|
- sample_actionable_low_conf_explanations: samples for agent action only
|
|
1015
1063
|
Agents should prefer actionable_* for next-steps; low_conf_edges remains full telemetry.
|
|
1064
|
+
|
|
1065
|
+
ACS v1.2: also demotes dynamic string-literal noise (importlib.import_module(\"…\"),
|
|
1066
|
+
is_dynamic+dynamic_type=static) from actionable counts. Full low_conf telemetry unchanged.
|
|
1016
1067
|
"""
|
|
1017
1068
|
# Phase 5e (66): compute_acs_summary + get_acs_summary promoted first-class default (O(k) bounded samples via ACS/CIABRE, deque-style in practice) for 20k+ creative; format=summary paths in MCP/CLI/health default to this + barrel summary (per 48/58/50/57, crit2/5 long-term WS A).
|
|
1018
1069
|
t0 = time.time()
|
|
@@ -1021,6 +1072,7 @@ def compute_acs_summary(
|
|
|
1021
1072
|
low_count = 0
|
|
1022
1073
|
actionable_low = 0
|
|
1023
1074
|
external_noise = 0
|
|
1075
|
+
dynamic_literal_noise = 0
|
|
1024
1076
|
reason_counts: Dict[str, int] = {}
|
|
1025
1077
|
samples: List[str] = [] # full expls for lowest-risk (prioritized)
|
|
1026
1078
|
|
|
@@ -1037,9 +1089,13 @@ def compute_acs_summary(
|
|
|
1037
1089
|
sc = p.get("confidence_score")
|
|
1038
1090
|
expl = p.get("confidence_explanation") or ""
|
|
1039
1091
|
reasons = p.get("confidence_reasons") or []
|
|
1040
|
-
|
|
1041
|
-
|
|
1092
|
+
is_ext = _edge_is_external_noise(p)
|
|
1093
|
+
is_dyn_lit = _edge_is_dynamic_literal_noise(p)
|
|
1094
|
+
is_noise = is_ext or is_dyn_lit
|
|
1095
|
+
if is_ext:
|
|
1042
1096
|
external_noise += 1
|
|
1097
|
+
if is_dyn_lit:
|
|
1098
|
+
dynamic_literal_noise += 1
|
|
1043
1099
|
if isinstance(sc, (int, float)):
|
|
1044
1100
|
scf = float(sc)
|
|
1045
1101
|
sum_score += scf
|
|
@@ -1073,13 +1129,14 @@ def compute_acs_summary(
|
|
|
1073
1129
|
top_reasons = sorted(reason_counts.items(), key=lambda x: -x[1])[:6]
|
|
1074
1130
|
|
|
1075
1131
|
return {
|
|
1076
|
-
"acs_version": "1.
|
|
1132
|
+
"acs_version": "1.2",
|
|
1077
1133
|
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
1078
1134
|
"total_scored_edges": total,
|
|
1079
1135
|
"avg_confidence": avg,
|
|
1080
1136
|
"low_conf_edges": low_count,
|
|
1081
1137
|
"actionable_low_conf_edges": actionable_low,
|
|
1082
1138
|
"external_noise_edges": external_noise,
|
|
1139
|
+
"dynamic_literal_noise_edges": dynamic_literal_noise,
|
|
1083
1140
|
"low_conf_threshold": low_threshold,
|
|
1084
1141
|
"top_risk_reasons": dict(top_reasons),
|
|
1085
1142
|
"sample_low_conf_explanations": samples, # full Recommendation text for agents
|
|
@@ -1116,12 +1173,12 @@ def ensure_acs_summary_persisted(
|
|
|
1116
1173
|
always-available oracle in primary surfaces without requiring explicit update first.
|
|
1117
1174
|
"""
|
|
1118
1175
|
acs = get_acs_summary(cache)
|
|
1119
|
-
# G4: recompute when missing OR pre-1.
|
|
1176
|
+
# G4/v1.2: recompute when missing OR pre-1.2 summaries (dynamic-literal demotion)
|
|
1120
1177
|
needs = (
|
|
1121
1178
|
not acs
|
|
1122
1179
|
or acs.get("total_scored_edges", 0) == 0
|
|
1123
1180
|
or "actionable_low_conf_edges" not in acs
|
|
1124
|
-
or str(acs.get("acs_version") or "") < "1.
|
|
1181
|
+
or str(acs.get("acs_version") or "") < "1.2"
|
|
1125
1182
|
)
|
|
1126
1183
|
if needs:
|
|
1127
1184
|
acs = compute_acs_summary(cache)
|
|
@@ -1705,8 +1762,8 @@ def get_low_confidence_edges(
|
|
|
1705
1762
|
Complements get_unresolved_imports; used for ACS-style hotspots.
|
|
1706
1763
|
Includes full provenance/diagnostic when present (from python/JS parity).
|
|
1707
1764
|
|
|
1708
|
-
actionable_only=True (G4): skip external/bare/stdlib noise so
|
|
1709
|
-
`import json`
|
|
1765
|
+
actionable_only=True (G4/v1.2): skip external/bare/stdlib + dynamic-literal noise so
|
|
1766
|
+
agents do not treat `import json` or importlib.import_module(\"pkg\") as action items.
|
|
1710
1767
|
"""
|
|
1711
1768
|
results: List[Dict[str, Any]] = []
|
|
1712
1769
|
for rel, data in cache.items():
|
|
@@ -1714,7 +1771,7 @@ def get_low_confidence_edges(
|
|
|
1714
1771
|
for p in (data.get("resolved_pairs") or []):
|
|
1715
1772
|
if not isinstance(p, dict):
|
|
1716
1773
|
continue
|
|
1717
|
-
if actionable_only and
|
|
1774
|
+
if actionable_only and _edge_is_non_actionable_noise(p):
|
|
1718
1775
|
continue
|
|
1719
1776
|
score = p.get("confidence_score")
|
|
1720
1777
|
conf_str = (p.get("confidence") or p.get("resolution_confidence") or "").lower()
|
|
@@ -1727,7 +1784,7 @@ def get_low_confidence_edges(
|
|
|
1727
1784
|
except Exception:
|
|
1728
1785
|
is_low = conf_str in ("low", "unresolved")
|
|
1729
1786
|
if is_low or not p.get("resolved_path"):
|
|
1730
|
-
if actionable_only and not p.get("resolved_path") and
|
|
1787
|
+
if actionable_only and not p.get("resolved_path") and _edge_is_non_actionable_noise(p):
|
|
1731
1788
|
continue
|
|
1732
1789
|
entry = dict(p)
|
|
1733
1790
|
entry.setdefault("src", rel)
|
|
@@ -1855,7 +1912,8 @@ def generate_update_events(
|
|
|
1855
1912
|
# Defensive root
|
|
1856
1913
|
if root is None:
|
|
1857
1914
|
try:
|
|
1858
|
-
|
|
1915
|
+
# Load-safe: project_root (not cli) — avoids import_cache→cli→import_cache cycle
|
|
1916
|
+
from .project_root import discover_project_root
|
|
1859
1917
|
root = discover_project_root()
|
|
1860
1918
|
except Exception:
|
|
1861
1919
|
root = Path(".").resolve()
|
|
@@ -121,8 +121,8 @@ def _get_project_root_fallback(default: Optional[Union[str, Path]] = None) -> Pa
|
|
|
121
121
|
from subdirs of user monorepos. Safe, zero-dep, never raises.
|
|
122
122
|
"""
|
|
123
123
|
try:
|
|
124
|
-
#
|
|
125
|
-
from ..
|
|
124
|
+
# Load-safe: project_root (not cli) — avoids bree→cli→import_cache→bree cycle
|
|
125
|
+
from ..project_root import discover_project_root
|
|
126
126
|
root = discover_project_root()
|
|
127
127
|
if root:
|
|
128
128
|
return Path(root).resolve()
|
|
@@ -216,7 +216,7 @@ def _get_project_root_fallback(default: Optional[Union[str, Path]] = None) -> Pa
|
|
|
216
216
|
|
|
217
217
|
try:
|
|
218
218
|
# inside parsers/ -> ..cli sibling
|
|
219
|
-
from ..
|
|
219
|
+
from ..project_root import discover_project_root
|
|
220
220
|
root = discover_project_root()
|
|
221
221
|
if root:
|
|
222
222
|
r = Path(root).resolve()
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Canonical project-root discovery (load-time safe).
|
|
3
|
+
|
|
4
|
+
AGENT MAP:
|
|
5
|
+
discover_project_root() — env → markers → cwd (no imports of cli/import_cache/bree)
|
|
6
|
+
Used by cli (re-export), import_cache, bree, daemon, serve, mcp
|
|
7
|
+
Extracted so bree/import_cache never import cli (breaks cyclic SCC).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import List
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def discover_project_root() -> Path:
|
|
18
|
+
"""
|
|
19
|
+
Unified discovery helper for the *target project root* (user codebase / monorepo).
|
|
20
|
+
|
|
21
|
+
This is the canonical implementation for CLI, MCP, direct Python use, and
|
|
22
|
+
is mirrored (same priority + markers) by the shell script's discover_project_root().
|
|
23
|
+
|
|
24
|
+
Highest-leverage fix for External / Packaged Full-Update Robustness (Gap #1):
|
|
25
|
+
Makes `pip install wikifier` + `wikifier ...` (or direct packaged sh) reliable
|
|
26
|
+
from any external monorepo cwd without requiring manual WIKIFIER_PROJECT_ROOT
|
|
27
|
+
or --target on every invocation. Zero-dependency.
|
|
28
|
+
|
|
29
|
+
Wave 3 hardening (symlinks + pnpm/yarn stores):
|
|
30
|
+
- Consults $PWD (and $OLDPWD) logical cwd in addition to Path.cwd() / .resolve().
|
|
31
|
+
- Walks parent chains from *logical* start points first; this ensures that when
|
|
32
|
+
cwd is deep inside a pnpm symlinked store layout (e.g. monorepo/node_modules/.pnpm/pkg@1/.../pkg
|
|
33
|
+
whose physical target is /global-store/...), the logical ancestor chain still reaches
|
|
34
|
+
the real project root markers (.git, package.json etc.) outside the store.
|
|
35
|
+
- Falls back to resolved physical; returns the outermost marker root found.
|
|
36
|
+
- Prevents packaged/parser/daemon from accidentally using a store dir as PROJECT_ROOT.
|
|
37
|
+
|
|
38
|
+
Wave 4 further hardening for complex monorepo layouts:
|
|
39
|
+
- Collects *all* candidate marker roots across logical/physical/realpath chains (no early return).
|
|
40
|
+
- Selects the outermost (shallowest fs path) preferring .git roots, then lockfiles (pnpm/yarn), then other.
|
|
41
|
+
This correctly picks true monorepo root even when deep sub-packages have their own package.json / yarn workspaces.
|
|
42
|
+
- Expanded common markers: pnpm-lock.yaml, yarn.lock, lerna.json, nx.json, turbo.json, rush.json, workspace yamls.
|
|
43
|
+
- Explicit skipping of node_modules/.pnpm / .yarn / .pnp internals as candidate roots (even on logical paths).
|
|
44
|
+
- Additional os.path.realpath chains for nested/broken symlink monorepo layouts.
|
|
45
|
+
- Daemon, parsers (via _get_*_fallback), run_full_update all inherit automatically.
|
|
46
|
+
"""
|
|
47
|
+
# 1. Explicit env var (highest priority, supports all the R6 --target flows)
|
|
48
|
+
env_root = os.environ.get("WIKIFIER_PROJECT_ROOT")
|
|
49
|
+
if env_root:
|
|
50
|
+
p = Path(env_root).expanduser().resolve()
|
|
51
|
+
if p.exists():
|
|
52
|
+
return p
|
|
53
|
+
|
|
54
|
+
# Wave 3/4: logical cwd (PWD/OLDPWD) + physical + realpath fallbacks for symlink/pnpm/yarn/nested
|
|
55
|
+
cwd = Path.cwd()
|
|
56
|
+
logical_cwd_str = os.environ.get("PWD") or os.environ.get("OLDPWD") or str(cwd)
|
|
57
|
+
try:
|
|
58
|
+
logical_cwd = Path(logical_cwd_str).expanduser()
|
|
59
|
+
except Exception:
|
|
60
|
+
logical_cwd = cwd
|
|
61
|
+
|
|
62
|
+
start_points: List[Path] = []
|
|
63
|
+
for start in (logical_cwd, cwd, cwd.resolve()):
|
|
64
|
+
if start and start not in start_points:
|
|
65
|
+
start_points.append(start)
|
|
66
|
+
|
|
67
|
+
# Wave 4: realpath variants for deeper symlink nest robustness (e.g. git submodules + pnpm links)
|
|
68
|
+
for sp in list(start_points):
|
|
69
|
+
try:
|
|
70
|
+
rp = Path(os.path.realpath(str(sp)))
|
|
71
|
+
if rp not in start_points:
|
|
72
|
+
start_points.append(rp)
|
|
73
|
+
except Exception:
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
# 2/3. Markers: wikifier-specific + expanded common monorepo (Wave 4)
|
|
77
|
+
common_markers = [
|
|
78
|
+
".git", "package.json", "pyproject.toml", "setup.py", "setup.cfg",
|
|
79
|
+
"Cargo.toml", "go.mod", ".hg",
|
|
80
|
+
# Wave 4 monorepo root indicators (for workspace/subpkg layouts + yarn/pnpm detection)
|
|
81
|
+
"pnpm-lock.yaml", "yarn.lock", "lerna.json", "nx.json", "turbo.json",
|
|
82
|
+
"rush.json", "workspace.json", "pnpm-workspace.yaml", ".yarnrc.yml"
|
|
83
|
+
]
|
|
84
|
+
store_indicators = (".pnpm", ".yarn", ".pnp", ".pnp.cjs", "virtual-store", ".store")
|
|
85
|
+
|
|
86
|
+
candidates: List[Path] = []
|
|
87
|
+
for start in start_points:
|
|
88
|
+
try:
|
|
89
|
+
parent_chain = [start] + list(start.parents)
|
|
90
|
+
except Exception:
|
|
91
|
+
parent_chain = [start]
|
|
92
|
+
|
|
93
|
+
# Wikifier markers (post-init)
|
|
94
|
+
for parent in parent_chain:
|
|
95
|
+
try:
|
|
96
|
+
pstr = str(parent)
|
|
97
|
+
if any(ind in pstr for ind in store_indicators) and "node_modules" in pstr:
|
|
98
|
+
continue # never treat deep pnpm/yarn store paths as project root
|
|
99
|
+
if ((parent / ".wikifier").is_dir() or
|
|
100
|
+
(parent / "monitored_paths.txt").exists() or
|
|
101
|
+
(parent / ".wikifier/config").exists()):
|
|
102
|
+
if parent not in candidates:
|
|
103
|
+
candidates.append(parent)
|
|
104
|
+
except Exception:
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
# Common + monorepo markers (any-scale external)
|
|
108
|
+
for parent in parent_chain:
|
|
109
|
+
try:
|
|
110
|
+
pstr = str(parent)
|
|
111
|
+
if any(ind in pstr for ind in store_indicators) and "node_modules" in pstr:
|
|
112
|
+
continue
|
|
113
|
+
for marker in common_markers:
|
|
114
|
+
if (parent / marker).exists():
|
|
115
|
+
if parent not in candidates:
|
|
116
|
+
candidates.append(parent)
|
|
117
|
+
except Exception:
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
if candidates:
|
|
121
|
+
# Wave 4: pick outermost/shallowest, with strong preference for .git (true monorepo root)
|
|
122
|
+
# even if a deeper packages/*/package.json also matches. Solves workspace subdir case.
|
|
123
|
+
def _root_key(p: Path):
|
|
124
|
+
try:
|
|
125
|
+
rp = p.resolve()
|
|
126
|
+
has_git = (rp / ".git").exists() or (rp / ".git").is_file() # worktree support
|
|
127
|
+
depth = len(rp.parts)
|
|
128
|
+
return (0 if has_git else 1, depth, str(rp))
|
|
129
|
+
except Exception:
|
|
130
|
+
return (99, 99999, str(p))
|
|
131
|
+
|
|
132
|
+
best = min(candidates, key=_root_key)
|
|
133
|
+
try:
|
|
134
|
+
return best.resolve() if best.exists() else best
|
|
135
|
+
except Exception:
|
|
136
|
+
return best
|
|
137
|
+
|
|
138
|
+
# 4. Last-resort sensible default: the directory the user is running from.
|
|
139
|
+
# This is the key robustness fix vs. old "fall back to package dir".
|
|
140
|
+
# Prefer resolved for the final fallback.
|
|
141
|
+
try:
|
|
142
|
+
return cwd.resolve()
|
|
143
|
+
except Exception:
|
|
144
|
+
return cwd
|
|
@@ -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
|
|
@@ -16,6 +16,7 @@ pyproject.toml
|
|
|
16
16
|
./wikifier/index.html
|
|
17
17
|
./wikifier/library.py
|
|
18
18
|
./wikifier/locking.py
|
|
19
|
+
./wikifier/project_root.py
|
|
19
20
|
./wikifier/resolution.py
|
|
20
21
|
./wikifier/serve.py
|
|
21
22
|
./wikifier/mcp/__init__.py
|
|
@@ -54,6 +55,7 @@ wikifier/import_cache.py
|
|
|
54
55
|
wikifier/index.html
|
|
55
56
|
wikifier/library.py
|
|
56
57
|
wikifier/locking.py
|
|
58
|
+
wikifier/project_root.py
|
|
57
59
|
wikifier/resolution.py
|
|
58
60
|
wikifier/serve.py
|
|
59
61
|
wikifier.egg-info/PKG-INFO
|
|
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
|