wikifier 4.6.7__tar.gz → 4.6.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.
Files changed (64) hide show
  1. {wikifier-4.6.7/wikifier.egg-info → wikifier-4.6.9}/PKG-INFO +31 -17
  2. {wikifier-4.6.7 → wikifier-4.6.9}/README.md +30 -16
  3. {wikifier-4.6.7 → wikifier-4.6.9}/skills/run.md +84 -13
  4. wikifier-4.6.9/tests/test_gap_amendment_2026_08.py +238 -0
  5. wikifier-4.6.9/tests/test_init_seed.py +123 -0
  6. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/__init__.py +1 -1
  7. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/agent_loop.py +101 -22
  8. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/cli.py +9 -0
  9. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/library.py +38 -6
  10. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/locking.py +63 -18
  11. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/scripts/wikifier.sh +44 -6
  12. {wikifier-4.6.7 → wikifier-4.6.9/wikifier.egg-info}/PKG-INFO +31 -17
  13. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier.egg-info/SOURCES.txt +2 -0
  14. {wikifier-4.6.7 → wikifier-4.6.9}/CONTRIBUTING.md +0 -0
  15. {wikifier-4.6.7 → wikifier-4.6.9}/LICENSE +0 -0
  16. {wikifier-4.6.7 → wikifier-4.6.9}/MANIFEST.in +0 -0
  17. {wikifier-4.6.7 → wikifier-4.6.9}/diagnostics.html +0 -0
  18. {wikifier-4.6.7 → wikifier-4.6.9}/docs/spec.md +0 -0
  19. {wikifier-4.6.7 → wikifier-4.6.9}/index.html +0 -0
  20. {wikifier-4.6.7 → wikifier-4.6.9}/pyproject.toml +0 -0
  21. {wikifier-4.6.7 → wikifier-4.6.9}/setup.cfg +0 -0
  22. {wikifier-4.6.7 → wikifier-4.6.9}/tests/test_agent_loop.py +0 -0
  23. {wikifier-4.6.7 → wikifier-4.6.9}/tests/test_agent_scale.py +0 -0
  24. {wikifier-4.6.7 → wikifier-4.6.9}/tests/test_barrel_invalidation.py +0 -0
  25. {wikifier-4.6.7 → wikifier-4.6.9}/tests/test_cache_store.py +0 -0
  26. {wikifier-4.6.7 → wikifier-4.6.9}/tests/test_gap_closure.py +0 -0
  27. {wikifier-4.6.7 → wikifier-4.6.9}/tests/test_health.py +0 -0
  28. {wikifier-4.6.7 → wikifier-4.6.9}/tests/test_import_cache.py +0 -0
  29. {wikifier-4.6.7 → wikifier-4.6.9}/tests/test_index_map_paths.py +0 -0
  30. {wikifier-4.6.7 → wikifier-4.6.9}/tests/test_multi_lang_parsers.py +0 -0
  31. {wikifier-4.6.7 → wikifier-4.6.9}/tests/test_parsers.py +0 -0
  32. {wikifier-4.6.7 → wikifier-4.6.9}/tests/test_selftest_wrappers.py +0 -0
  33. {wikifier-4.6.7 → wikifier-4.6.9}/tests/test_walk_coverage_resolvers.py +0 -0
  34. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/__main__.py +0 -0
  35. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/cache_store.py +0 -0
  36. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/candidates.py +0 -0
  37. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/contracts.py +0 -0
  38. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/daemon.py +0 -0
  39. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/diagnostics.py +0 -0
  40. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/health.py +0 -0
  41. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/import_cache.py +0 -0
  42. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/index.html +0 -0
  43. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/mcp/__init__.py +0 -0
  44. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/mcp/server.py +0 -0
  45. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/parsers/__init__.py +0 -0
  46. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/parsers/_edge.py +0 -0
  47. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/parsers/bree.py +0 -0
  48. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/parsers/c_cpp.py +0 -0
  49. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/parsers/cdia.py +0 -0
  50. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/parsers/csharp.py +0 -0
  51. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/parsers/go_lang.py +0 -0
  52. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/parsers/java.py +0 -0
  53. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/parsers/javascript.py +0 -0
  54. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/parsers/python.py +0 -0
  55. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/parsers/rust.py +0 -0
  56. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/project_root.py +0 -0
  57. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/resolution.py +0 -0
  58. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/scripts/wikifier.bat +0 -0
  59. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/scripts/wikifier.ps1 +0 -0
  60. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier/serve.py +0 -0
  61. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier.egg-info/dependency_links.txt +0 -0
  62. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier.egg-info/entry_points.txt +0 -0
  63. {wikifier-4.6.7 → wikifier-4.6.9}/wikifier.egg-info/requires.txt +0 -0
  64. {wikifier-4.6.7 → wikifier-4.6.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.6.7
3
+ Version: 4.6.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
@@ -52,7 +52,14 @@ Works from small scripts to large monorepos. **Deep import/include maps** (zero-
52
52
  | C# | `.cs` | `using` namespaces |
53
53
  | Java | `.java` | `import` / `import static` |
54
54
 
55
- Health/journal still work for any monitored path. Parsers are **pragmatic regex** (not full cargo/`go.mod`/classpath/`-I` resolution). Prefer **lean `monitored_paths.txt`** on huge monorepos; raise dirty cap with `WIKIFIER_CHECK_CHANGES_MAX` (default 2000) only when needed.
55
+ Health/journal still work for any monitored path. Parsers are **pragmatic regex** (not full cargo/`go.mod`/classpath/`-I` resolution). On huge monorepos, split scope deliberately:
56
+
57
+ | File | Surface |
58
+ |------|---------|
59
+ | **`map_paths.txt`** | Package roots for **import maps** (`update-maps` walk). Prefer package dirs (`src/`, `packages/foo/`) — not a wiki-only file list. |
60
+ | **`monitored_paths.txt`** | **Wiki / health** watch list (can be individual `.md` files). Does **not** define the map. |
61
+
62
+ Or pass `--directory=pkg/` / `--max-files=N` per run. Raise dirty cap with `WIKIFIER_CHECK_CHANGES_MAX` (default 2000) only when needed. Never set `project_root` to a multi-repo parent of clones.
56
63
 
57
64
  ## Why
58
65
 
@@ -74,7 +81,8 @@ pip install wikifier # pure Python stdlib core — no runtime deps
74
81
  pip install wikifier[mcp] # optional Model Context Protocol (MCP) server
75
82
 
76
83
  cd /path/to/your/project
77
- wikifier init # seeds + human index.html
84
+ wikifier init # seeds index.html + lean path-list templates
85
+ # Edit monitored_paths.txt + map_paths.txt to package roots (not bare ".") on real trees
78
86
  wikifier update-maps # full structural map → library.md + import cache
79
87
  wikifier health --summary # matrix counts
80
88
  wikifier suggest-next # or MCP suggest_next_actions — 🔴/🟡 only
@@ -82,6 +90,8 @@ wikifier suggest-next # or MCP suggest_next_actions — 🔴/🟡 only
82
90
 
83
91
  Always set an explicit root for external trees: `WIKIFIER_PROJECT_ROOT=/abs/path wikifier …`
84
92
 
93
+ **MCP `session_bootstrap` → `readiness: blocked`?** That means lean scope and/or the map are missing (often bare `.` monitor + never ran `update-maps`) — not a broken install. Fix: write lean `monitored_paths.txt` / `map_paths.txt`, then `update-maps`. Agent contract: **`skills/run.md`** § *Readiness blocked*; dogfood: `Findings/readiness-blocked-bare-monitor-2026-07.md`.
94
+
85
95
  ## Steady state (only touch what needs it)
86
96
 
87
97
  Full protocol: [`skills/run.md`](skills/run.md) (Agent Protocol v0.6 — package **4.6.x**).
@@ -109,11 +119,11 @@ Advanced intel as needed: `get_dependencies`, `get_dependents`, `get_cycles`, ba
109
119
 
110
120
  - **Import analysis** — Python, JS/TS (ESM/CJS, barrels), Rust (`use`/`mod` + best-effort `crate::` paths), Go, C/C++ includes, C# usings; per-edge confidence; barrel expansion for TS/JS
111
121
  - **Incremental pipeline** — pure-Python `update-maps`: dirty parse → import cache → reverse deps → cycles → `library.md`
112
- - **Warm agent maps (4.6.3–4.6.6)** — zero-dirty + **index-first** candidates (re-list only on fingerprint/index disagreement); **stdlib SQLite**; content-hash dirty
113
- - **Two path lists** — `map_paths.txt` = map package roots; `monitored_paths.txt` = wiki/health watch (independent)
122
+ - **Warm agent maps (4.6.3–4.6.7)** — zero-dirty + **index-first** candidates (re-list only when fingerprint / map-scoped index / live count disagree); **MapScope** keeps collect, live count, index filter, and prune aligned; **stdlib SQLite**; content-hash dirty
123
+ - **Two path lists** — `map_paths.txt` = map package roots; `monitored_paths.txt` = wiki/health watch (**independent** — wiki file lists never collapse the map)
114
124
  - **Partial-map honesty** — `map_coverage` on `update_maps` / bootstrap / **`suggest_next`**; `update_maps_until_complete` when incomplete
115
- - **Cache ops** — `wikifier cache-status`; JSON dual-write **opt-in only** (`WIKIFIER_CACHE_JSON=1`); dual-read for migrate
116
- - **Selective agent work** — health + suggest bias to 🔴/actionable 🟡 only; **ACS v1.3** `reason_code` / `agent_signal`; prefer `actionable_low_conf_edges` + reason codes — **never** raw `low_conf_edges` alone
125
+ - **Cache ops** — `wikifier cache-status`; JSON dual-write **deprecated default-off** (`WIKIFIER_CACHE_JSON=1` opt-in); dual-read for migrate
126
+ - **Selective agent work** — health + suggest bias to 🔴/actionable 🟡 only; **ACS v1.3** `reason_code` / `agent_signal`; prefer `actionable_low_conf_edges` + reason codes — **never** raw `low_conf_edges` averages alone
117
127
  - **Scale** — reverse index + barrel invalidation so one edit doesn’t re-scan the monorepo
118
128
  - **MCP tools** — optional server for Claude, Cursor, Cline, and other MCP clients
119
129
  - **Zero core dependencies** — stdlib only; forks can add their own stack on top
@@ -127,18 +137,21 @@ Full / heavy runs (historical order-of-magnitude):
127
137
  |---------|-------|----------------------------|
128
138
  | llama_index | ~3.8k Python files | ~8.5s class full |
129
139
  | Babylon.js | ~3.9k TS files, barrel-heavy | minutes full; scoped re-runs tens of seconds |
130
- | Large trees (e.g. LLVM-scale) | tens of thousands of files | use lean monitor + `--directory` / `--max-files` |
140
+ | Large trees (e.g. LLVM-scale) | tens of thousands of files | `map_paths` / `--directory` / `--max-files` — never unscoped one-shot |
141
+
142
+ Warm **0-dirty** re-runs after **4.6.7** (same machine class; scoped; candidates **reused** — agent session path):
131
143
 
132
- Warm **0-dirty** re-runs after 4.6.3 (same machine; scoped where noted) — agent session path:
144
+ | Project | Scope | Warm `update-maps` | n |
145
+ |---------|-------|--------------------|---|
146
+ | Wikifier (self) | `map_paths`: `wikifier/` + `tests/` | **~30 ms** | 50 |
147
+ | llama_index | `llama-index-core` | **~76 ms** | 724 |
148
+ | rust | `library/std` | **~79 ms** | 719 |
149
+ | airflow | `airflow-core` | **~180 ms** | 1920 |
150
+ | Babylon.js | `packages` | **~400 ms** | 3895 |
133
151
 
134
- | Project | Scope | Warm2 `update-maps` |
135
- |---------|-------|---------------------|
136
- | Wikifier (self) | incremental full | ~43 ms |
137
- | redox | `src` | ~17 ms |
138
- | llama_index | `llama-index-core` | ~0.6 s |
139
- | rust | `library/std/src` (budgeted) | ~0.7 s (vs multi-second full-tree walk before scoped collect) |
152
+ Residual floor on large scopes is mtime/stat + live count under MapScope (not full JSON re-walk). Sub-100ms is not a hard SLA on every 1k+ tree.
140
153
 
141
- Tests: `python -m unittest discover tests` (stdlib only; 86+ cases including agent-scale edges).
154
+ Tests: `python -m unittest discover tests` (stdlib only; **125** cases including MapScope / index-first / dual-write).
142
155
 
143
156
  ## Commands
144
157
 
@@ -152,7 +165,8 @@ Tests: `python -m unittest discover tests` (stdlib only; 86+ cases including age
152
165
  | `wikifier mark-green <file>` | Mark wiki current + source content-hash baseline |
153
166
  | `wikifier record-deletion <file> "reason"` | Mark removed paths 🔴 + prune barrel refs |
154
167
  | `wikifier suggest-next` | Next actions (🔴/actionable 🟡 only; `--json` for `actions[]`) |
155
- | `wikifier update-maps [--directory=src/] [--max-files=N]` | Rebuild graph + `library.md` (warm 0-dirty is fast) |
168
+ | `wikifier update-maps [--directory=src/] [--max-files=N]` | Rebuild graph + `library.md` (warm 0-dirty is fast; honors `map_paths.txt`) |
169
+ | `wikifier cache-status` | SQLite/JSON backend, dual-write policy, coverage snapshot (no full pair load) |
156
170
  | `wikifier health [--summary\|--json]` | Health matrix (machine-friendly flags) |
157
171
  | `wikifier validate` | Missing wiki rows + ghost paths |
158
172
  | `wikifier cycles` | Circular deps + break hints |
@@ -20,7 +20,14 @@ Works from small scripts to large monorepos. **Deep import/include maps** (zero-
20
20
  | C# | `.cs` | `using` namespaces |
21
21
  | Java | `.java` | `import` / `import static` |
22
22
 
23
- Health/journal still work for any monitored path. Parsers are **pragmatic regex** (not full cargo/`go.mod`/classpath/`-I` resolution). Prefer **lean `monitored_paths.txt`** on huge monorepos; raise dirty cap with `WIKIFIER_CHECK_CHANGES_MAX` (default 2000) only when needed.
23
+ Health/journal still work for any monitored path. Parsers are **pragmatic regex** (not full cargo/`go.mod`/classpath/`-I` resolution). On huge monorepos, split scope deliberately:
24
+
25
+ | File | Surface |
26
+ |------|---------|
27
+ | **`map_paths.txt`** | Package roots for **import maps** (`update-maps` walk). Prefer package dirs (`src/`, `packages/foo/`) — not a wiki-only file list. |
28
+ | **`monitored_paths.txt`** | **Wiki / health** watch list (can be individual `.md` files). Does **not** define the map. |
29
+
30
+ Or pass `--directory=pkg/` / `--max-files=N` per run. Raise dirty cap with `WIKIFIER_CHECK_CHANGES_MAX` (default 2000) only when needed. Never set `project_root` to a multi-repo parent of clones.
24
31
 
25
32
  ## Why
26
33
 
@@ -42,7 +49,8 @@ pip install wikifier # pure Python stdlib core — no runtime deps
42
49
  pip install wikifier[mcp] # optional Model Context Protocol (MCP) server
43
50
 
44
51
  cd /path/to/your/project
45
- wikifier init # seeds + human index.html
52
+ wikifier init # seeds index.html + lean path-list templates
53
+ # Edit monitored_paths.txt + map_paths.txt to package roots (not bare ".") on real trees
46
54
  wikifier update-maps # full structural map → library.md + import cache
47
55
  wikifier health --summary # matrix counts
48
56
  wikifier suggest-next # or MCP suggest_next_actions — 🔴/🟡 only
@@ -50,6 +58,8 @@ wikifier suggest-next # or MCP suggest_next_actions — 🔴/🟡 only
50
58
 
51
59
  Always set an explicit root for external trees: `WIKIFIER_PROJECT_ROOT=/abs/path wikifier …`
52
60
 
61
+ **MCP `session_bootstrap` → `readiness: blocked`?** That means lean scope and/or the map are missing (often bare `.` monitor + never ran `update-maps`) — not a broken install. Fix: write lean `monitored_paths.txt` / `map_paths.txt`, then `update-maps`. Agent contract: **`skills/run.md`** § *Readiness blocked*; dogfood: `Findings/readiness-blocked-bare-monitor-2026-07.md`.
62
+
53
63
  ## Steady state (only touch what needs it)
54
64
 
55
65
  Full protocol: [`skills/run.md`](skills/run.md) (Agent Protocol v0.6 — package **4.6.x**).
@@ -77,11 +87,11 @@ Advanced intel as needed: `get_dependencies`, `get_dependents`, `get_cycles`, ba
77
87
 
78
88
  - **Import analysis** — Python, JS/TS (ESM/CJS, barrels), Rust (`use`/`mod` + best-effort `crate::` paths), Go, C/C++ includes, C# usings; per-edge confidence; barrel expansion for TS/JS
79
89
  - **Incremental pipeline** — pure-Python `update-maps`: dirty parse → import cache → reverse deps → cycles → `library.md`
80
- - **Warm agent maps (4.6.3–4.6.6)** — zero-dirty + **index-first** candidates (re-list only on fingerprint/index disagreement); **stdlib SQLite**; content-hash dirty
81
- - **Two path lists** — `map_paths.txt` = map package roots; `monitored_paths.txt` = wiki/health watch (independent)
90
+ - **Warm agent maps (4.6.3–4.6.7)** — zero-dirty + **index-first** candidates (re-list only when fingerprint / map-scoped index / live count disagree); **MapScope** keeps collect, live count, index filter, and prune aligned; **stdlib SQLite**; content-hash dirty
91
+ - **Two path lists** — `map_paths.txt` = map package roots; `monitored_paths.txt` = wiki/health watch (**independent** — wiki file lists never collapse the map)
82
92
  - **Partial-map honesty** — `map_coverage` on `update_maps` / bootstrap / **`suggest_next`**; `update_maps_until_complete` when incomplete
83
- - **Cache ops** — `wikifier cache-status`; JSON dual-write **opt-in only** (`WIKIFIER_CACHE_JSON=1`); dual-read for migrate
84
- - **Selective agent work** — health + suggest bias to 🔴/actionable 🟡 only; **ACS v1.3** `reason_code` / `agent_signal`; prefer `actionable_low_conf_edges` + reason codes — **never** raw `low_conf_edges` alone
93
+ - **Cache ops** — `wikifier cache-status`; JSON dual-write **deprecated default-off** (`WIKIFIER_CACHE_JSON=1` opt-in); dual-read for migrate
94
+ - **Selective agent work** — health + suggest bias to 🔴/actionable 🟡 only; **ACS v1.3** `reason_code` / `agent_signal`; prefer `actionable_low_conf_edges` + reason codes — **never** raw `low_conf_edges` averages alone
85
95
  - **Scale** — reverse index + barrel invalidation so one edit doesn’t re-scan the monorepo
86
96
  - **MCP tools** — optional server for Claude, Cursor, Cline, and other MCP clients
87
97
  - **Zero core dependencies** — stdlib only; forks can add their own stack on top
@@ -95,18 +105,21 @@ Full / heavy runs (historical order-of-magnitude):
95
105
  |---------|-------|----------------------------|
96
106
  | llama_index | ~3.8k Python files | ~8.5s class full |
97
107
  | Babylon.js | ~3.9k TS files, barrel-heavy | minutes full; scoped re-runs tens of seconds |
98
- | Large trees (e.g. LLVM-scale) | tens of thousands of files | use lean monitor + `--directory` / `--max-files` |
108
+ | Large trees (e.g. LLVM-scale) | tens of thousands of files | `map_paths` / `--directory` / `--max-files` — never unscoped one-shot |
109
+
110
+ Warm **0-dirty** re-runs after **4.6.7** (same machine class; scoped; candidates **reused** — agent session path):
99
111
 
100
- Warm **0-dirty** re-runs after 4.6.3 (same machine; scoped where noted) — agent session path:
112
+ | Project | Scope | Warm `update-maps` | n |
113
+ |---------|-------|--------------------|---|
114
+ | Wikifier (self) | `map_paths`: `wikifier/` + `tests/` | **~30 ms** | 50 |
115
+ | llama_index | `llama-index-core` | **~76 ms** | 724 |
116
+ | rust | `library/std` | **~79 ms** | 719 |
117
+ | airflow | `airflow-core` | **~180 ms** | 1920 |
118
+ | Babylon.js | `packages` | **~400 ms** | 3895 |
101
119
 
102
- | Project | Scope | Warm2 `update-maps` |
103
- |---------|-------|---------------------|
104
- | Wikifier (self) | incremental full | ~43 ms |
105
- | redox | `src` | ~17 ms |
106
- | llama_index | `llama-index-core` | ~0.6 s |
107
- | rust | `library/std/src` (budgeted) | ~0.7 s (vs multi-second full-tree walk before scoped collect) |
120
+ Residual floor on large scopes is mtime/stat + live count under MapScope (not full JSON re-walk). Sub-100ms is not a hard SLA on every 1k+ tree.
108
121
 
109
- Tests: `python -m unittest discover tests` (stdlib only; 86+ cases including agent-scale edges).
122
+ Tests: `python -m unittest discover tests` (stdlib only; **125** cases including MapScope / index-first / dual-write).
110
123
 
111
124
  ## Commands
112
125
 
@@ -120,7 +133,8 @@ Tests: `python -m unittest discover tests` (stdlib only; 86+ cases including age
120
133
  | `wikifier mark-green <file>` | Mark wiki current + source content-hash baseline |
121
134
  | `wikifier record-deletion <file> "reason"` | Mark removed paths 🔴 + prune barrel refs |
122
135
  | `wikifier suggest-next` | Next actions (🔴/actionable 🟡 only; `--json` for `actions[]`) |
123
- | `wikifier update-maps [--directory=src/] [--max-files=N]` | Rebuild graph + `library.md` (warm 0-dirty is fast) |
136
+ | `wikifier update-maps [--directory=src/] [--max-files=N]` | Rebuild graph + `library.md` (warm 0-dirty is fast; honors `map_paths.txt`) |
137
+ | `wikifier cache-status` | SQLite/JSON backend, dual-write policy, coverage snapshot (no full pair load) |
124
138
  | `wikifier health [--summary\|--json]` | Health matrix (machine-friendly flags) |
125
139
  | `wikifier validate` | Missing wiki rows + ghost paths |
126
140
  | `wikifier cycles` | Circular deps + break hints |
@@ -2,16 +2,17 @@
2
2
 
3
3
  **Formerly "Wikifier Skills & Commands". This is the authoritative, versioned specification for agent behavior when using Wikifier.**
4
4
 
5
- **Version**: v0.6 (v4.2.0 real-pipeline + reliability update; package current **4.5.x**)
6
- **Date**: 2026-06-10 (package notes refreshed 2026-07-09; gap-closure contract 2026-07-09)
5
+ **Version**: v0.6 (v4.2.0 real-pipeline + reliability update; package current **4.6.x**, live `__version__` in `wikifier/__init__.py`)
6
+ **Date**: 2026-06-10 (package notes refreshed 2026-08-01; gap-closure contract 2026-07-09)
7
7
  **Status**: Active. Supersedes v0.5.
8
- **See also**: `README.md`, `Findings/gap-closure-report.md`, and the library in wikifier/.
8
+ **See also**: `README.md`, `Findings/gap-closure-report.md`, `Findings/gap-amendment-plan-2026-08-01.md`, and the library in wikifier/.
9
9
 
10
- **Package 4.5.x + gap-closure notes (additive; protocol still v0.6):**
10
+ **Package 4.6.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
- - **First-run:** `init` → `update-maps` → `health --summary` → `suggest_next_actions`. Map is automatic; wiki *prose* is agent-filled over time.
12
+ - **First-run:** `init` → lean `monitored_paths.txt` + `map_paths.txt` (default active root `src/`, not bare `.`) → `update-maps` → `health --summary` → `suggest_next_actions`. Map is automatic; wiki *prose* is agent-filled over time.
13
+ - **`session_bootstrap` readiness:** message embeds the readiness tier. `blocked` is not a crash — see **§ Readiness blocked**. Hard blockers = missing map/health (or severe ghosts). Bare `.` monitor alone → `map_ok_scope_risk` when map+health exist (scope warnings / fix_scope), not the same tier as missing map. Unattended ops require **`readiness=ready_for_daemon`**, not `health_score=Map Ready` alone.
13
14
  - **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 v1.3:** use `actionable_low_conf_edges` + `reason_code_counts` / `agent_signal` (`skip`|`investigate`). Do **not** thrash on raw `low_conf_edges` (includes external/bare scores ~0.48). Prefer `map_coverage.complete` / `files_remaining_dirty` after budgeted `update-maps` — `success: true` ≠ map complete.
15
+ - **Steady-state selective work:** only 🔴 and *actionable* 🟡 (never re-wiki 🟢; never bulk-wiki Initial/map stubs). **ACS v1.3:** use `actionable_low_conf_edges` + `reason_code_counts` / `agent_signal` (`skip`|`investigate`). Do **not** thrash on raw `low_conf_edges` (includes external/bare scores ~0.48). Prefer `map_coverage.complete` / `map_ready` / `files_remaining_dirty` after budgeted `update-maps` — `success: true` ≠ map complete.
15
16
  - **Warm maps:** SQLite primary; zero-dirty uses mtime index + meta. **Index-first:** re-list candidates only when fingerprint / map-scoped index / live count disagree (`index_first_dirty` / `candidates_reused` on `update_maps`). Prefer `--directory=` or `map_paths.txt` package roots (not bare `.`). Collect, live count, index filter, and prune share **MapScope** so full-tree→`map_paths` narrow does not thrash.
16
17
  - **map_paths vs monitored_paths:** `map_paths.txt` = import-map package roots; `monitored_paths.txt` = wiki/health thrash bound. Independent. Wiki-only monitored lists do **not** define the map.
17
18
  - **Cache ops:** `wikifier cache-status`. Dual-write JSON **deprecated default-off** (`WIKIFIER_CACHE_JSON=1` to opt in). Dual-read legacy JSON for migrate remains.
@@ -45,8 +46,9 @@ Do **not** open megamodules (`javascript.py`, `import_cache.py`, `bree.py`) to d
45
46
 
46
47
  | Need | Use |
47
48
  |------|-----|
48
- | First-run map | `init` → `update-maps` → `health --summary` → `suggest-next` |
49
- | **Session start (4.6+)** | `session_bootstrap` (or CLI `session-bootstrap`) — one shot: root, health, attention, `actions[]` |
49
+ | First-run map | `init` → lean paths (not bare `.`) → `update-maps` → `health --summary` → `suggest-next` |
50
+ | **Session start (4.6+)** | `session_bootstrap` (or CLI `session-bootstrap`) — one shot: root, health, attention, `actions[]`. If `readiness` is `blocked`, fix `actions[]` / `blockers[]` first (usually scope + maps). |
51
+ | **Unblock readiness** | Write lean `monitored_paths.txt` + `map_paths.txt` → `update_maps` (full once) → re-`session_bootstrap` until **`readiness=ready_for_daemon`** (not Map Ready alone) |
50
52
  | **Core tool list** | `list_core_tools` / bootstrap `core_surface` — prefer Core 6; advanced is non-core |
51
53
  | Steady-state | `check-changes` (content-honest) → edit 🔴/actionable 🟡 only → `record-change` → wiki → `mark-green` |
52
54
  | **Hash migration** | `seed_source_content_hashes` / CLI `seed-source-hashes` — baseline Greens without mass Yellow |
@@ -65,6 +67,74 @@ Do **not** open megamodules (`javascript.py`, `import_cache.py`, `bree.py`) to d
65
67
  **Scope:** `monitored_paths` = change detection; `exclude_patterns` + optional `--directory` = map walk.
66
68
  **Human HTML** is secondary observation only.
67
69
 
70
+ ## Readiness blocked (`session_bootstrap` / external projects)
71
+
72
+ When MCP `session_bootstrap` (or CLI `session-bootstrap`) returns:
73
+
74
+ ```text
75
+ readiness: "blocked"
76
+ scope.ok: false
77
+ blockers: [ ... ]
78
+ actions: [ { "action": "fix_scope", ... }, ... ]
79
+ ```
80
+
81
+ **this is expected on a project that has never been scoped/mapped**, not a Wikifier install failure.
82
+
83
+ ### Readiness tiers (code truth — `assess_autonomous_readiness`)
84
+
85
+ | Tier | When |
86
+ |------|------|
87
+ | `blocked` | Hard **blockers[]**: no import map and/or no file_health (or severe ghost flood). |
88
+ | `map_ok_scope_risk` | Map+health present but **scope warnings** (often bare `.` monitor or multi-repo parent) and clean red/actionable yellow. |
89
+ | `ready_for_daemon` | Map Ready/Good **and** not bare_dot_monitor — only safe unattended tier. |
90
+ | `ready_with_agent_wiki_work` | Needs Attention, red=0 — daemon OK but agents still have wiki work. |
91
+ | `not_ready` | Other (e.g. reds). |
92
+
93
+ Bare `.` is a **scope warning** (`scope.warnings` / `fix_scope`), not always a hard blocker. Missing map/health **are** hard blockers and emit priority-1 `update_maps` / `seed_health` in `actions[]`.
94
+
95
+ ### Why it happens (Grok-Bevy 2026-07 dogfood)
96
+
97
+ On **Grok-Bevy** (Rust workspace), first bootstrap reported:
98
+
99
+ | Signal | Field / tier | Meaning |
100
+ |--------|--------------|---------|
101
+ | `No import map (run update-maps first)` | `blockers[]` → `blocked` | No dependency/import map yet. Action: **`update_maps`**. |
102
+ | `No file_health` | `blockers[]` → `blocked` | Health matrix not seeded. Action: **`seed_health` / update-maps**. |
103
+ | `monitored_paths is bare '.'` | `scope.warnings` (often `map_ok_scope_risk` once map exists) | check-changes thrash risk. Action: **`fix_scope`**. |
104
+
105
+ Also: `bare_dot_monitor: true` and `scope.ok: false` until `monitored_paths.txt` lists **lean package roots**, not bare `.`.
106
+
107
+ ### Fix once per project (agents must do this, not ignore blockers)
108
+
109
+ 1. **Scope (wiki/health thrash bound)** — create/edit **`monitored_paths.txt`** at project root with *specific* dirs/files, e.g.:
110
+ ```text
111
+ crates/foo/src/
112
+ crates/bar/src/
113
+ README.md
114
+ docs/
115
+ ```
116
+ Never leave only `.` on non-tiny trees.
117
+ 2. **Map roots** — create/edit **`map_paths.txt`** (import-map package roots; independent of monitored):
118
+ ```text
119
+ crates/foo/src/
120
+ crates/bar/src/
121
+ ```
122
+ 3. **Build map + health** — from the target project (or with `project_root=` / `WIKIFIER_PROJECT_ROOT=`):
123
+ ```bash
124
+ wikifier update-maps --full # or MCP update_maps full=true
125
+ # re-run session_bootstrap until readiness == ready_for_daemon
126
+ ```
127
+ 4. **Do not** bulk-re-wiki 🟡 *Initial stubs* after maps land — stubs = map coverage only.
128
+
129
+ After a successful fix, bootstrap looks like: `scope.ok: true`, `blockers: []`, `health_score: Map Ready` (stubs OK), **`readiness: ready_for_daemon`**. Map Ready without `ready_for_daemon` is **not** unattended-ready.
130
+
131
+ ### Product note for Wikifier maintainers
132
+
133
+ - Default bare `.` is a footgun for real repos; agents must treat **`fix_scope` as P0** when scope warns bare-dot, and **update_maps/seed_health as P0** when readiness is `blocked`.
134
+ - **`wikifier init` (4.6.8+ / 4.6.9 templates)** seeds comment-guided lean templates with active default **`src/`** (bare `.` is commented opt-in for tiny toys only). Multi-crate / monorepo agents must replace with package roots before map-ready work.
135
+
136
+ See also: Findings note `Findings/readiness-blocked-bare-monitor-2026-07.md`.
137
+
68
138
  ## Mandatory New-Session Rule
69
139
 
70
140
  **Copy this exact block into the system prompt or the very first instruction of every new LLM session that uses Wikifier:**
@@ -74,7 +144,7 @@ You are now operating inside a Wikifier-managed codebase (Agent Protocol v0.6
74
144
 
75
145
  This is strictly an agent-to-agent wiki for token saving: map lookup (health + file wikis + deps) instead of full sources; selective wiki updates; not a human docs product or Jira.
76
146
 
77
- SELECTIVE WORK (mandatory): Only update/remove/re-wiki 🔴 Red and 🟡 Yellow files. Do not re-summarize 🟢 Green files or the whole tree. First-run builds the structural map; wiki depth is filled as you touch files. check-changes is content-honest (mtime-only thrash does not re-Yellow when source hash matches mark-green baseline).
147
+ SELECTIVE WORK (mandatory): Only update/remove/re-wiki 🔴 Red and *actionable* 🟡 Yellow files. Do **not** re-summarize 🟢 Green files, and do **not** bulk-wiki 🟡 Initial/map stubs (map coverage only — wiki a stub only when you edit that file). First-run builds the structural map; wiki depth is filled as you touch files. check-changes is content-honest (mtime-only thrash does not re-Yellow when source hash matches mark-green baseline).
78
148
 
79
149
  FIRST ACTIONS:
80
150
  1. Prefer MCP Core: session_bootstrap, check_changes, prepare_edit, suggest_next_actions (use actions[]), record_change, mark_green. Always pass project_root= for external projects.
@@ -131,10 +201,11 @@ However, if you are doing low-level direct writes to Wikifier state files (e.g.
131
201
  ### Limitations (Final M2-Rem-07 Assessment)
132
202
  - Locks are **advisory** — a broken or malicious process can ignore them.
133
203
  - Currently **project-level** (one lock for the entire project). This is the right tradeoff for current needs (including heavy multi-agent + monitor dogfooding) and keeps the implementation simple and fast.
134
- - Non-blocking / timeout queries and per-file locking are not implemented yet (advanced agents can use `wikifier.locking.is_project_locked()` for diagnostics).
204
+ - **Finite timeout** is supported: `file_lock(root, timeout=seconds)` raises `LockTimeoutError` if the lock is not free in time (`timeout=None` still blocks). `is_project_locked()` remains a diagnostic non-blocking probe.
205
+ - Per-file / sharded locking is a permanent non-goal unless extreme concurrency pressure appears.
135
206
  - Best-effort portability on non-Unix systems.
136
207
 
137
- The locking system (Python `file_lock` + shell `with_project_lock`) has received final polish. It is now considered production-ready for the M2 scope. Future extensions (fine-grained locking) can be added when real usage pressure appears on extremely large concurrent setups.
208
+ The locking system (Python `file_lock` + shell `with_project_lock`) is production-ready for the M2 scope.
138
209
 
139
210
  ## Protocol v0.4 Additions: I/O Contracts, Error Handling, Structured Output, Versioning
140
211
 
@@ -168,7 +239,7 @@ Pending/journal/health side effects are observable via the returned messages + d
168
239
  ### Error Handling Expectations
169
240
  - **Operational failures** (e.g., partial scan on huge tree, lock edge): return `{"success": False, "error": "...", "project_root": "...", ...partial_data }`. Agent must handle gracefully and log; continue where safe.
170
241
  - **Programming / contract errors** (bad types, missing required): raise (standard Python exceptions) — these indicate agent bug.
171
- - **Locking**: Mutators block on project lock (production default). Future non-blocking/timeout will be additive.
242
+ - **Locking**: Mutators block on project lock by default; optional finite `timeout=` raises on contention.
172
243
  - Never assume text output parsability. Always use structured returns + explicit format="json".
173
244
  - On external/packaged installs: discovery is robust; pass explicit project_root if cwd is ambiguous.
174
245
 
@@ -219,7 +290,7 @@ This project exposes a first-class MCP server (wikifier-mcp or python -m wikifie
219
290
 
220
291
  **M5.1+ reality (from dogfood on external 5k-79k+ projects)**: MCP tools are preferred when available (get_project_status, get_file_wiki, get_barrel_reports for deep BRC, check_changes, record_change, mark_green, suggest_next_actions, health equivalents, etc.). **Always pass project_root= (or use WIKIFIER_PROJECT_ROOT env)** for external/user projects (RecipeLab_alt BRC stress, ConsistencyHub, llvm subs, etc.).
221
292
 
222
- MCP timeouts are rare since v4.2.0 (the library workflow-call deadlock is fixed and barrel persistence is batched), but on very large/BRC-heavy targets they can still happen. In that case, **immediately fall back to CLI/library** (python -m wikifier health --summary or the library health(..., format="summary")) these are reliable and were the workhorse in M5 sustained/monitor work.
293
+ **CLI-at-scale policy (G15):** On barrel-heavy or large monorepos (BRC stress, multi-k source trees), treat **CLI/library as the primary reliability path** for `update_maps`, barrel reports, and long health/suggest loops. MCP subprocess tools use a ~60s cap and may still time out; that is **not** a product failure — **immediately fall back** to `python -m wikifier …` / library calls. Do not claim DoD2 (&lt;30s MCP on alt/Consistency/llama) closed without a fresh measured pack; default operator policy is CLI for scale.
223
294
 
224
295
  The server implements hardened external discovery (delegates to cli.py discover_project_root / _get_effective_root), 60s timeouts, actionable errors, and parity with library.
225
296
 
@@ -0,0 +1,238 @@
1
+ """Real-path tests for gap amendment plan G1–G13/G15 closed-when bars.
2
+
3
+ Drives shipped library functions (session_bootstrap, build_structured_actions,
4
+ file_lock, build_map_coverage / run_full_update, library ACS section, protocol
5
+ strings). No theater: asserts against live code outputs.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib
11
+ import os
12
+ import unittest
13
+
14
+ from tests._base import REPO_ROOT, TempProjectTestCase
15
+
16
+ from wikifier import __version__
17
+ from wikifier.agent_loop import build_structured_actions, session_bootstrap
18
+ from wikifier import cli
19
+ from wikifier import locking
20
+ from wikifier.library import _generate_acs_section
21
+
22
+ health_mod = importlib.import_module("wikifier.health")
23
+
24
+
25
+ class TestProtocolVersionPointers(unittest.TestCase):
26
+ """G1: operator package pointers match live major.minor."""
27
+
28
+ def test_version_is_4_6(self):
29
+ self.assertTrue(
30
+ __version__.startswith("4.6."),
31
+ f"expected 4.6.x package, got {__version__}",
32
+ )
33
+
34
+ def test_skills_run_header_not_4_5(self):
35
+ text = (REPO_ROOT / "skills" / "run.md").read_text(encoding="utf-8")
36
+ # Header / package-notes must not claim current 4.5.x
37
+ first_30 = "\n".join(text.splitlines()[:30])
38
+ self.assertNotIn("package current **4.5.x**", first_30)
39
+ self.assertIn("4.6.x", first_30)
40
+ # SELECTIVE WORK excludes Initial stubs (G3)
41
+ self.assertIn("actionable", text)
42
+ self.assertIn("Initial", text)
43
+ self.assertRegex(
44
+ text,
45
+ r"SELECTIVE WORK.*actionable.*Initial",
46
+ )
47
+ # G15 CLI-at-scale policy present
48
+ self.assertIn("CLI-at-scale policy", text)
49
+ # G8 ready_for_daemon not Map Ready alone
50
+ self.assertIn("ready_for_daemon", text)
51
+ self.assertIn("not Map Ready alone", text)
52
+
53
+ def test_claude_md_not_4_5(self):
54
+ for name in ("Claude.md", "CLAUDE.md"):
55
+ p = REPO_ROOT / name
56
+ if not p.is_file():
57
+ continue
58
+ t = p.read_text(encoding="utf-8")
59
+ self.assertNotIn("currently 4.5.x", t)
60
+ self.assertIn("4.6.x", t)
61
+
62
+
63
+ class TestBootstrapMessageAndActions(TempProjectTestCase):
64
+ """G2/G5/G6: bootstrap message + blocker actions."""
65
+
66
+ def test_bootstrap_message_blocked_never_says_ready(self):
67
+ # Fresh temp root: no cache, no health → blocked
68
+ boot = session_bootstrap(project_root=self.root)
69
+ self.assertTrue(boot.get("success"))
70
+ readiness = boot.get("readiness")
71
+ msg = str(boot.get("message") or "")
72
+ if readiness == "blocked":
73
+ self.assertIn("readiness=blocked", msg)
74
+ self.assertNotRegex(msg, r"^session_bootstrap ready")
75
+ self.assertNotIn("session_bootstrap ready —", msg)
76
+ # Always embeds readiness=
77
+ self.assertIn("readiness=", msg)
78
+
79
+ def test_blockers_produce_priority_leq_2_map_or_health(self):
80
+ acts = build_structured_actions(
81
+ blockers=[
82
+ "No import map (run update-maps first).",
83
+ "No file_health — run seed-health or update-maps.",
84
+ ],
85
+ scope_warnings=["monitored_paths is bare '.' — thrash risk"],
86
+ red=0,
87
+ actionable_yellow=0,
88
+ clean=True,
89
+ )
90
+ kinds = {a["action"] for a in acts}
91
+ self.assertIn("update_maps", kinds)
92
+ self.assertIn("seed_health", kinds)
93
+ self.assertIn("fix_scope", kinds)
94
+ for a in acts:
95
+ if a["action"] in ("update_maps", "seed_health"):
96
+ self.assertLessEqual(int(a["priority"]), 2, a)
97
+ if a["action"] == "fix_scope":
98
+ self.assertLessEqual(int(a["priority"]), 2, a)
99
+
100
+ def test_map_incomplete_action_priority(self):
101
+ acts = build_structured_actions(
102
+ map_coverage={
103
+ "complete": False,
104
+ "files_remaining_dirty": 12,
105
+ "files_skipped": 12,
106
+ "budget_max_files": 5,
107
+ },
108
+ red=0,
109
+ actionable_yellow=0,
110
+ )
111
+ incomplete = [a for a in acts if a["action"] == "update_maps_until_complete"]
112
+ self.assertTrue(incomplete)
113
+ self.assertLessEqual(int(incomplete[0]["priority"]), 2)
114
+ self.assertIn("success alone", incomplete[0]["reason"].lower())
115
+
116
+ def test_stub_only_map_first_ok_no_wiki_refresh(self):
117
+ acts = build_structured_actions(
118
+ stub_yellow=100,
119
+ actionable_yellow=0,
120
+ red=0,
121
+ actionable_yellow_files=[],
122
+ clean=False,
123
+ )
124
+ kinds = {a["action"] for a in acts}
125
+ self.assertIn("map_first_ok", kinds)
126
+ self.assertNotIn("wiki_refresh", kinds)
127
+
128
+
129
+ class TestMapCoverageHonesty(TempProjectTestCase):
130
+ """G5: update_maps surfaces map_complete / map_ready."""
131
+
132
+ def test_run_full_update_exposes_map_ready_flags(self):
133
+ self.write("pkg/a.py", "import os\n")
134
+ (self.root / "monitored_paths.txt").write_text("pkg/\n", encoding="utf-8")
135
+ (self.root / "map_paths.txt").write_text("pkg/\n", encoding="utf-8")
136
+ res = cli.run_full_update(root=self.root, directory="pkg/")
137
+ self.assertTrue(res.get("success"), res)
138
+ self.assertIn("map_coverage", res)
139
+ self.assertIn("map_complete", res)
140
+ self.assertIn("map_ready", res)
141
+ # map_ready must track complete + remaining dirty
142
+ mc = res["map_coverage"]
143
+ expected_ready = bool(mc.get("complete")) and int(
144
+ mc.get("files_remaining_dirty") or 0
145
+ ) == 0
146
+ self.assertEqual(res["map_ready"], expected_ready)
147
+ self.assertEqual(res["map_complete"], bool(mc.get("complete")))
148
+
149
+ def test_budgeted_max_files_incomplete_flags(self):
150
+ for i in range(6):
151
+ self.write(f"src/m{i}.py", f"x={i}\n")
152
+ (self.root / "monitored_paths.txt").write_text("src/\n", encoding="utf-8")
153
+ (self.root / "map_paths.txt").write_text("src/\n", encoding="utf-8")
154
+ res = cli.run_full_update(
155
+ root=self.root, directory="src/", max_files=2
156
+ )
157
+ self.assertTrue(res.get("success"), res)
158
+ self.assertIn("map_coverage", res)
159
+ # With budget, remaining dirty may be > 0
160
+ rem = int((res.get("map_coverage") or {}).get("files_remaining_dirty") or 0)
161
+ if rem > 0 or (res.get("map_coverage") or {}).get("complete") is False:
162
+ self.assertFalse(res.get("map_ready"))
163
+ acts = build_structured_actions(map_coverage=res["map_coverage"])
164
+ self.assertTrue(
165
+ any(a["action"] == "update_maps_until_complete" for a in acts)
166
+ )
167
+
168
+
169
+ class TestLockTimeout(TempProjectTestCase):
170
+ """G13: file_lock honors finite timeout."""
171
+
172
+ def test_timeout_raises_when_held(self):
173
+ """Hold exclusive lock on a second fd; finite timeout must raise (G13)."""
174
+ if locking.fcntl is None:
175
+ self.skipTest("fcntl not available")
176
+ lock_path = self.root / locking.LOCK_FILE_NAME
177
+ lock_path.parent.mkdir(parents=True, exist_ok=True)
178
+ holder_fd = os.open(str(lock_path), os.O_CREAT | os.O_RDWR, 0o644)
179
+ try:
180
+ locking.fcntl.flock(holder_fd, locking.fcntl.LOCK_EX)
181
+ contender_fd = os.open(str(lock_path), os.O_CREAT | os.O_RDWR, 0o644)
182
+ try:
183
+ with self.assertRaises(locking.LockTimeoutError):
184
+ locking._acquire_exclusive(contender_fd, 0.25)
185
+ finally:
186
+ os.close(contender_fd)
187
+ finally:
188
+ locking.fcntl.flock(holder_fd, locking.fcntl.LOCK_UN)
189
+ os.close(holder_fd)
190
+
191
+ def test_reentrant_still_works(self):
192
+ with locking.file_lock(self.root):
193
+ with locking.file_lock(self.root):
194
+ pass # nested re-entrant
195
+
196
+
197
+ class TestAcsLibrarySection(unittest.TestCase):
198
+ """G9: library ACS prefers actionable."""
199
+
200
+ def test_acs_section_prefers_actionable(self):
201
+ cache = {
202
+ "_acs_summary": {
203
+ "total_scored_edges": 100,
204
+ "avg_confidence": 0.7,
205
+ "low_conf_edges": 40,
206
+ "actionable_low_conf_edges": 5,
207
+ "low_conf_threshold": 0.65,
208
+ "reason_code_counts": {
209
+ "external_or_bare": 30,
210
+ "unresolved_project": 5,
211
+ },
212
+ "sample_low_conf_explanations": ["raw sample"],
213
+ }
214
+ }
215
+ lines = _generate_acs_section(cache)
216
+ text = "\n".join(lines)
217
+ self.assertIn("actionable_low_conf", text)
218
+ self.assertIn("reason_code_counts", text)
219
+ self.assertIn("thrash", text.lower())
220
+ self.assertIn("actionable_low_conf_edges", text)
221
+ self.assertIn("actionable", text.lower())
222
+
223
+
224
+ class TestReadinessTiersDocShape(TempProjectTestCase):
225
+ """G4: readiness tiers match code for missing map/health."""
226
+
227
+ def test_no_map_is_blocked(self):
228
+ ready = health_mod.assess_autonomous_readiness(self.root)
229
+ self.assertEqual(ready.get("readiness"), "blocked")
230
+ blockers = " ".join(ready.get("blockers") or [])
231
+ self.assertTrue(
232
+ "import map" in blockers.lower() or "file_health" in blockers.lower(),
233
+ blockers,
234
+ )
235
+
236
+
237
+ if __name__ == "__main__":
238
+ unittest.main()