wikifier 4.6.8__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.
- {wikifier-4.6.8/wikifier.egg-info → wikifier-4.6.9}/PKG-INFO +1 -1
- {wikifier-4.6.8 → wikifier-4.6.9}/skills/run.md +35 -22
- wikifier-4.6.9/tests/test_gap_amendment_2026_08.py +238 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/tests/test_init_seed.py +10 -1
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/__init__.py +1 -1
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/agent_loop.py +101 -22
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/cli.py +9 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/library.py +38 -6
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/locking.py +63 -18
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/scripts/wikifier.sh +7 -10
- {wikifier-4.6.8 → wikifier-4.6.9/wikifier.egg-info}/PKG-INFO +1 -1
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier.egg-info/SOURCES.txt +1 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/CONTRIBUTING.md +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/LICENSE +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/MANIFEST.in +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/README.md +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/diagnostics.html +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/docs/spec.md +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/index.html +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/pyproject.toml +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/setup.cfg +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/tests/test_agent_loop.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/tests/test_agent_scale.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/tests/test_barrel_invalidation.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/tests/test_cache_store.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/tests/test_gap_closure.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/tests/test_health.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/tests/test_import_cache.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/tests/test_index_map_paths.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/tests/test_multi_lang_parsers.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/tests/test_parsers.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/tests/test_selftest_wrappers.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/tests/test_walk_coverage_resolvers.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/__main__.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/cache_store.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/candidates.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/contracts.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/daemon.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/diagnostics.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/health.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/import_cache.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/index.html +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/mcp/__init__.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/mcp/server.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/parsers/__init__.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/parsers/_edge.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/parsers/bree.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/parsers/c_cpp.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/parsers/cdia.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/parsers/csharp.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/parsers/go_lang.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/parsers/java.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/parsers/javascript.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/parsers/python.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/parsers/rust.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/project_root.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/resolution.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/scripts/wikifier.bat +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/scripts/wikifier.ps1 +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier/serve.py +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier.egg-info/dependency_links.txt +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier.egg-info/entry_points.txt +0 -0
- {wikifier-4.6.8 → wikifier-4.6.9}/wikifier.egg-info/requires.txt +0 -0
- {wikifier-4.6.8 → 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.
|
|
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
|
|
@@ -2,17 +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.
|
|
6
|
-
**Date**: 2026-06-10 (package notes refreshed 2026-
|
|
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.
|
|
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` → lean `monitored_paths.txt` + `map_paths.txt` (not bare `.`) → `update-maps` → `health --summary` → `suggest_next_actions`. Map is automatic; wiki *prose* is agent-filled over time.
|
|
13
|
-
- **`session_bootstrap` readiness `blocked
|
|
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.
|
|
14
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.
|
|
15
|
-
- **Steady-state selective work:** only 🔴 and *actionable* 🟡 (never re-wiki 🟢; never
|
|
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.
|
|
16
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.
|
|
17
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.
|
|
18
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.
|
|
@@ -48,7 +48,7 @@ Do **not** open megamodules (`javascript.py`, `import_cache.py`, `bree.py`) to d
|
|
|
48
48
|
|------|-----|
|
|
49
49
|
| First-run map | `init` → lean paths (not bare `.`) → `update-maps` → `health --summary` → `suggest-next` |
|
|
50
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
|
|
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) |
|
|
52
52
|
| **Core tool list** | `list_core_tools` / bootstrap `core_surface` — prefer Core 6; advanced is non-core |
|
|
53
53
|
| Steady-state | `check-changes` (content-honest) → edit 🔴/actionable 🟡 only → `record-change` → wiki → `mark-green` |
|
|
54
54
|
| **Hash migration** | `seed_source_content_hashes` / CLI `seed-source-hashes` — baseline Greens without mass Yellow |
|
|
@@ -80,15 +80,27 @@ actions: [ { "action": "fix_scope", ... }, ... ]
|
|
|
80
80
|
|
|
81
81
|
**this is expected on a project that has never been scoped/mapped**, not a Wikifier install failure.
|
|
82
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
|
+
|
|
83
95
|
### Why it happens (Grok-Bevy 2026-07 dogfood)
|
|
84
96
|
|
|
85
97
|
On **Grok-Bevy** (Rust workspace), first bootstrap reported:
|
|
86
98
|
|
|
87
|
-
|
|
|
88
|
-
|
|
89
|
-
| `
|
|
90
|
-
| `No
|
|
91
|
-
| `
|
|
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`**. |
|
|
92
104
|
|
|
93
105
|
Also: `bare_dot_monitor: true` and `scope.ok: false` until `monitored_paths.txt` lists **lean package roots**, not bare `.`.
|
|
94
106
|
|
|
@@ -110,16 +122,16 @@ Also: `bare_dot_monitor: true` and `scope.ok: false` until `monitored_paths.txt`
|
|
|
110
122
|
3. **Build map + health** — from the target project (or with `project_root=` / `WIKIFIER_PROJECT_ROOT=`):
|
|
111
123
|
```bash
|
|
112
124
|
wikifier update-maps --full # or MCP update_maps full=true
|
|
113
|
-
# re-run session_bootstrap until readiness
|
|
125
|
+
# re-run session_bootstrap until readiness == ready_for_daemon
|
|
114
126
|
```
|
|
115
127
|
4. **Do not** bulk-re-wiki 🟡 *Initial stubs* after maps land — stubs = map coverage only.
|
|
116
128
|
|
|
117
|
-
After a successful fix, bootstrap looks like: `scope.ok: true`, `blockers: []`, `health_score: Map Ready` (stubs OK),
|
|
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.
|
|
118
130
|
|
|
119
131
|
### Product note for Wikifier maintainers
|
|
120
132
|
|
|
121
|
-
- Default bare `.` is
|
|
122
|
-
- **`wikifier init` (4.6.8+)** seeds comment-guided lean templates
|
|
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.
|
|
123
135
|
|
|
124
136
|
See also: Findings note `Findings/readiness-blocked-bare-monitor-2026-07.md`.
|
|
125
137
|
|
|
@@ -132,7 +144,7 @@ You are now operating inside a Wikifier-managed codebase (Agent Protocol v0.6
|
|
|
132
144
|
|
|
133
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.
|
|
134
146
|
|
|
135
|
-
SELECTIVE WORK (mandatory): Only update/remove/re-wiki 🔴 Red and 🟡 Yellow files. Do not re-summarize 🟢 Green files
|
|
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).
|
|
136
148
|
|
|
137
149
|
FIRST ACTIONS:
|
|
138
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.
|
|
@@ -189,10 +201,11 @@ However, if you are doing low-level direct writes to Wikifier state files (e.g.
|
|
|
189
201
|
### Limitations (Final M2-Rem-07 Assessment)
|
|
190
202
|
- Locks are **advisory** — a broken or malicious process can ignore them.
|
|
191
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.
|
|
192
|
-
-
|
|
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.
|
|
193
206
|
- Best-effort portability on non-Unix systems.
|
|
194
207
|
|
|
195
|
-
The locking system (Python `file_lock` + shell `with_project_lock`)
|
|
208
|
+
The locking system (Python `file_lock` + shell `with_project_lock`) is production-ready for the M2 scope.
|
|
196
209
|
|
|
197
210
|
## Protocol v0.4 Additions: I/O Contracts, Error Handling, Structured Output, Versioning
|
|
198
211
|
|
|
@@ -226,7 +239,7 @@ Pending/journal/health side effects are observable via the returned messages + d
|
|
|
226
239
|
### Error Handling Expectations
|
|
227
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.
|
|
228
241
|
- **Programming / contract errors** (bad types, missing required): raise (standard Python exceptions) — these indicate agent bug.
|
|
229
|
-
- **Locking**: Mutators block on project lock
|
|
242
|
+
- **Locking**: Mutators block on project lock by default; optional finite `timeout=` raises on contention.
|
|
230
243
|
- Never assume text output parsability. Always use structured returns + explicit format="json".
|
|
231
244
|
- On external/packaged installs: discovery is robust; pass explicit project_root if cwd is ambiguous.
|
|
232
245
|
|
|
@@ -277,7 +290,7 @@ This project exposes a first-class MCP server (wikifier-mcp or python -m wikifie
|
|
|
277
290
|
|
|
278
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.).
|
|
279
292
|
|
|
280
|
-
|
|
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 (<30s MCP on alt/Consistency/llama) closed without a fresh measured pack; default operator policy is CLI for scale.
|
|
281
294
|
|
|
282
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.
|
|
283
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()
|
|
@@ -64,13 +64,22 @@ class TestInitSeedTemplates(TempProjectTestCase):
|
|
|
64
64
|
r"(?i)(lean|package root|thrash|map_paths)",
|
|
65
65
|
"monitored_paths.txt should mention lean roots / thrash / map_paths",
|
|
66
66
|
)
|
|
67
|
-
#
|
|
67
|
+
# G7: active default is lean src/ — bare "." is opt-in comment only.
|
|
68
68
|
active_mon = [
|
|
69
69
|
ln.strip()
|
|
70
70
|
for ln in mon_text.splitlines()
|
|
71
71
|
if ln.strip() and not ln.strip().startswith("#")
|
|
72
72
|
]
|
|
73
73
|
self.assertTrue(active_mon, "expected at least one non-comment monitored path")
|
|
74
|
+
self.assertNotIn(
|
|
75
|
+
".",
|
|
76
|
+
active_mon,
|
|
77
|
+
"monitored_paths must not activate bare '.' by default (opt-in comment only)",
|
|
78
|
+
)
|
|
79
|
+
self.assertTrue(
|
|
80
|
+
any(p == "src/" or p == "src" for p in active_mon),
|
|
81
|
+
f"expected lean src/ active default, got {active_mon}",
|
|
82
|
+
)
|
|
74
83
|
|
|
75
84
|
self.assertIn("#", map_text, "map_paths.txt must include guidance comments")
|
|
76
85
|
self.assertRegex(
|
|
@@ -174,6 +174,7 @@ def build_structured_actions(
|
|
|
174
174
|
red: int = 0,
|
|
175
175
|
acs_actionable: int = 0,
|
|
176
176
|
scope_warnings: Optional[List[str]] = None,
|
|
177
|
+
blockers: Optional[List[str]] = None,
|
|
177
178
|
clean: bool = False,
|
|
178
179
|
max_file_actions: int = 12,
|
|
179
180
|
map_coverage: Optional[Dict[str, Any]] = None,
|
|
@@ -181,18 +182,56 @@ def build_structured_actions(
|
|
|
181
182
|
"""Build dispatchable action objects for agents (not prose-only).
|
|
182
183
|
|
|
183
184
|
Each item: action, file?, priority (1=highest), reason, optional preflight.
|
|
185
|
+
Priority ≤2 is reserved for readiness blockers (scope + missing map/health).
|
|
184
186
|
"""
|
|
185
187
|
actions: List[Dict[str, Any]] = []
|
|
186
|
-
prio = 1
|
|
188
|
+
prio = 3 # file work starts after readiness fix actions (priority 1–2)
|
|
187
189
|
cov = map_coverage if isinstance(map_coverage, dict) else {}
|
|
188
190
|
remaining = int(cov.get("files_remaining_dirty") or 0)
|
|
189
191
|
complete = cov.get("complete")
|
|
190
|
-
|
|
192
|
+
blocker_list = [str(b) for b in (blockers or []) if b]
|
|
193
|
+
# G6: missing map / file_health always get priority ≤2
|
|
194
|
+
for b in blocker_list:
|
|
195
|
+
bl = b.lower()
|
|
196
|
+
# Prefer seed_health before generic update-maps (blocker text often mentions both)
|
|
197
|
+
if "file_health" in bl or "seed-health" in bl or "seed_health" in bl:
|
|
198
|
+
actions.append({
|
|
199
|
+
"action": "seed_health",
|
|
200
|
+
"file": None,
|
|
201
|
+
"priority": 1,
|
|
202
|
+
"reason": b,
|
|
203
|
+
"preflight": ["seed_health", "update_maps"],
|
|
204
|
+
})
|
|
205
|
+
elif "import map" in bl or "update-maps" in bl or "update_maps" in bl:
|
|
206
|
+
actions.append({
|
|
207
|
+
"action": "update_maps",
|
|
208
|
+
"file": None,
|
|
209
|
+
"priority": 1,
|
|
210
|
+
"reason": b,
|
|
211
|
+
"preflight": ["update_maps"],
|
|
212
|
+
})
|
|
213
|
+
else:
|
|
214
|
+
actions.append({
|
|
215
|
+
"action": "fix_blocker",
|
|
216
|
+
"file": None,
|
|
217
|
+
"priority": 2,
|
|
218
|
+
"reason": b,
|
|
219
|
+
"preflight": [],
|
|
220
|
+
})
|
|
221
|
+
for w in (scope_warnings or [])[:3]:
|
|
222
|
+
actions.append({
|
|
223
|
+
"action": "fix_scope",
|
|
224
|
+
"file": None,
|
|
225
|
+
"priority": 2,
|
|
226
|
+
"reason": str(w),
|
|
227
|
+
"preflight": [],
|
|
228
|
+
})
|
|
229
|
+
# Incomplete map under budget — agents must not treat success alone as done (G5)
|
|
191
230
|
if complete is False or remaining > 0:
|
|
192
231
|
actions.append({
|
|
193
232
|
"action": "update_maps_until_complete",
|
|
194
233
|
"file": None,
|
|
195
|
-
"priority":
|
|
234
|
+
"priority": 2,
|
|
196
235
|
"reason": (
|
|
197
236
|
f"Map incomplete: files_remaining_dirty={remaining}, complete={complete}. "
|
|
198
237
|
"Re-run update_maps (same directory/max_files) until map_coverage.complete=true; "
|
|
@@ -224,7 +263,7 @@ def build_structured_actions(
|
|
|
224
263
|
"preflight": ["prepare_edit", "why_file"],
|
|
225
264
|
})
|
|
226
265
|
prio = min(prio + 1, 9)
|
|
227
|
-
if red == 0 and actionable_yellow == 0 and clean and remaining == 0:
|
|
266
|
+
if red == 0 and actionable_yellow == 0 and clean and remaining == 0 and not blocker_list:
|
|
228
267
|
actions.append({
|
|
229
268
|
"action": "lookup_only",
|
|
230
269
|
"file": None,
|
|
@@ -232,12 +271,16 @@ def build_structured_actions(
|
|
|
232
271
|
"reason": "Health clean — do not re-summarize tree; use prepare_edit/get_file_wiki for lookup",
|
|
233
272
|
"preflight": [],
|
|
234
273
|
})
|
|
274
|
+
# G10: stub-only yellows are map coverage — never bulk wiki_refresh
|
|
235
275
|
if stub_yellow > 0 and actionable_yellow == 0 and red == 0:
|
|
236
276
|
actions.append({
|
|
237
277
|
"action": "map_first_ok",
|
|
238
278
|
"file": None,
|
|
239
279
|
"priority": 6,
|
|
240
|
-
"reason":
|
|
280
|
+
"reason": (
|
|
281
|
+
f"{stub_yellow} Initial stubs are map coverage only — not bulk wiki work. "
|
|
282
|
+
"Wiki a file only when you edit it, then mark-green."
|
|
283
|
+
),
|
|
241
284
|
"preflight": [],
|
|
242
285
|
})
|
|
243
286
|
if acs_actionable > 0:
|
|
@@ -251,16 +294,12 @@ def build_structured_actions(
|
|
|
251
294
|
),
|
|
252
295
|
"preflight": ["get_dependencies"],
|
|
253
296
|
})
|
|
254
|
-
for w in (scope_warnings or [])[:2]:
|
|
255
|
-
actions.append({
|
|
256
|
-
"action": "fix_scope",
|
|
257
|
-
"file": None,
|
|
258
|
-
"priority": 2,
|
|
259
|
-
"reason": str(w),
|
|
260
|
-
"preflight": [],
|
|
261
|
-
})
|
|
262
297
|
if not any(
|
|
263
|
-
a["action"] in (
|
|
298
|
+
a["action"] in (
|
|
299
|
+
"update_maps",
|
|
300
|
+
"update_maps_until_complete",
|
|
301
|
+
"update_maps_if_structure",
|
|
302
|
+
)
|
|
264
303
|
for a in actions
|
|
265
304
|
):
|
|
266
305
|
actions.append({
|
|
@@ -639,6 +678,7 @@ def session_bootstrap(
|
|
|
639
678
|
scope_warnings = list(out["scope"].get("warnings") or [])
|
|
640
679
|
|
|
641
680
|
map_cov = out.get("map_coverage") if isinstance(out.get("map_coverage"), dict) else {}
|
|
681
|
+
blockers = list(out.get("blockers") or [])
|
|
642
682
|
out["actions"] = build_structured_actions(
|
|
643
683
|
red_files=red_files,
|
|
644
684
|
actionable_yellow_files=actionable_yellow_files,
|
|
@@ -647,6 +687,7 @@ def session_bootstrap(
|
|
|
647
687
|
red=red_n,
|
|
648
688
|
acs_actionable=acs_actionable,
|
|
649
689
|
scope_warnings=scope_warnings,
|
|
690
|
+
blockers=blockers,
|
|
650
691
|
clean=clean,
|
|
651
692
|
map_coverage=map_cov,
|
|
652
693
|
)
|
|
@@ -654,13 +695,51 @@ def session_bootstrap(
|
|
|
654
695
|
out["selective_work"] = True
|
|
655
696
|
out["map_first"] = True
|
|
656
697
|
rem = int(map_cov.get("files_remaining_dirty") or 0) if map_cov else 0
|
|
657
|
-
out
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
698
|
+
readiness = out.get("readiness") or "unknown"
|
|
699
|
+
map_incomplete = rem > 0 or map_cov.get("complete") is False
|
|
700
|
+
# G2: never claim "ready" when readiness is blocked
|
|
701
|
+
if readiness == "blocked":
|
|
702
|
+
out["message"] = (
|
|
703
|
+
f"session_bootstrap readiness=blocked — fix actions[] / blockers[] first "
|
|
704
|
+
f"(typically fix_scope + update_maps / seed_health); do not treat project as map-ready."
|
|
705
|
+
+ (
|
|
706
|
+
f" MAP INCOMPLETE: files_remaining_dirty={rem}."
|
|
707
|
+
if map_incomplete
|
|
708
|
+
else ""
|
|
709
|
+
)
|
|
710
|
+
)
|
|
711
|
+
elif readiness == "map_ok_scope_risk":
|
|
712
|
+
out["message"] = (
|
|
713
|
+
f"session_bootstrap readiness=map_ok_scope_risk — map/health present but scope risk "
|
|
714
|
+
f"(often bare '.' monitor); fix_scope before unattended daemon. "
|
|
715
|
+
f"Use actions[] / attention; prepare_edit(file) before large edits; record_change after."
|
|
716
|
+
+ (
|
|
717
|
+
f" MAP INCOMPLETE: files_remaining_dirty={rem} — re-run update_maps until "
|
|
718
|
+
f"map_coverage.complete=true."
|
|
719
|
+
if map_incomplete
|
|
720
|
+
else ""
|
|
721
|
+
)
|
|
722
|
+
)
|
|
723
|
+
elif readiness == "ready_for_daemon":
|
|
724
|
+
out["message"] = (
|
|
725
|
+
"session_bootstrap readiness=ready_for_daemon — use actions[] / attention; "
|
|
726
|
+
"prepare_edit(file) before large edits; record_change after edits."
|
|
727
|
+
+ (
|
|
728
|
+
f" MAP INCOMPLETE: files_remaining_dirty={rem} — re-run update_maps until "
|
|
729
|
+
f"map_coverage.complete=true (success alone is not map-ready)."
|
|
730
|
+
if map_incomplete
|
|
731
|
+
else ""
|
|
732
|
+
)
|
|
733
|
+
)
|
|
734
|
+
else:
|
|
735
|
+
out["message"] = (
|
|
736
|
+
f"session_bootstrap readiness={readiness} — use actions[] / attention; "
|
|
737
|
+
"prepare_edit(file) before large edits; record_change after edits. "
|
|
738
|
+
"Unattended ops require readiness=ready_for_daemon (not Map Ready alone)."
|
|
739
|
+
+ (
|
|
740
|
+
f" MAP INCOMPLETE: files_remaining_dirty={rem} — re-run update_maps until complete."
|
|
741
|
+
if map_incomplete
|
|
742
|
+
else ""
|
|
743
|
+
)
|
|
664
744
|
)
|
|
665
|
-
)
|
|
666
745
|
return out
|
|
@@ -424,6 +424,11 @@ def run_full_update(
|
|
|
424
424
|
cache_backend=backend,
|
|
425
425
|
directory=directory,
|
|
426
426
|
)
|
|
427
|
+
_mc0 = result["map_coverage"] if isinstance(result["map_coverage"], dict) else {}
|
|
428
|
+
result["map_complete"] = bool(_mc0.get("complete"))
|
|
429
|
+
result["map_ready"] = bool(_mc0.get("complete")) and int(
|
|
430
|
+
_mc0.get("files_remaining_dirty") or 0
|
|
431
|
+
) == 0
|
|
427
432
|
try:
|
|
428
433
|
if cs is not None:
|
|
429
434
|
cs.save_meta_key(root, "_map_coverage", result["map_coverage"])
|
|
@@ -669,6 +674,10 @@ def run_full_update(
|
|
|
669
674
|
cache_backend=backend,
|
|
670
675
|
directory=directory,
|
|
671
676
|
)
|
|
677
|
+
# G5: success alone is not map-ready — surface complete flag at top level
|
|
678
|
+
_mc = result["map_coverage"] if isinstance(result["map_coverage"], dict) else {}
|
|
679
|
+
result["map_complete"] = bool(_mc.get("complete"))
|
|
680
|
+
result["map_ready"] = bool(_mc.get("complete")) and int(_mc.get("files_remaining_dirty") or 0) == 0
|
|
672
681
|
try:
|
|
673
682
|
cache["_map_coverage"] = result["map_coverage"]
|
|
674
683
|
if cands:
|
|
@@ -312,6 +312,7 @@ def _generate_cycles_section(cache: Dict[str, Any]) -> List[str]:
|
|
|
312
312
|
# ---------------------------------------------------------------------------
|
|
313
313
|
|
|
314
314
|
def _generate_acs_section(cache: Dict[str, Any]) -> List[str]:
|
|
315
|
+
"""ACS section for library.md — prefer actionable + reason codes (G9)."""
|
|
315
316
|
lines = ["## ACS Risk Snapshot", ""]
|
|
316
317
|
acs = cache.get("_acs_summary") or {}
|
|
317
318
|
if not isinstance(acs, dict) or not acs.get("total_scored_edges"):
|
|
@@ -319,17 +320,48 @@ def _generate_acs_section(cache: Dict[str, Any]) -> List[str]:
|
|
|
319
320
|
"to score edge confidence.)")
|
|
320
321
|
lines.append("")
|
|
321
322
|
return lines
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
323
|
+
actionable = acs.get("actionable_low_conf_edges")
|
|
324
|
+
if actionable is None:
|
|
325
|
+
actionable = acs.get("low_conf_edges", 0)
|
|
326
|
+
lines.append(
|
|
327
|
+
"**Scored edges**: {0} | **avg_confidence**: {1} | "
|
|
328
|
+
"**actionable_low_conf**: {2} | raw low-conf (telemetry): {3} "
|
|
329
|
+
"(threshold {4})".format(
|
|
330
|
+
acs.get("total_scored_edges"),
|
|
331
|
+
acs.get("avg_confidence"),
|
|
332
|
+
actionable,
|
|
333
|
+
acs.get("low_conf_edges", 0),
|
|
334
|
+
acs.get("low_conf_threshold", 0.65),
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
lines.append(
|
|
338
|
+
"**Agent work queue**: use `actionable_low_conf_edges` + "
|
|
339
|
+
"`reason_code_counts` / `agent_signal=investigate` only — "
|
|
340
|
+
"do **not** thrash on raw `low_conf_edges` (includes external/bare)."
|
|
341
|
+
)
|
|
342
|
+
rcc = acs.get("reason_code_counts") or {}
|
|
343
|
+
if isinstance(rcc, dict) and rcc:
|
|
344
|
+
lines.append(
|
|
345
|
+
"**reason_code_counts**: "
|
|
346
|
+
+ ", ".join("{0}:{1}".format(k, v) for k, v in list(rcc.items())[:8])
|
|
347
|
+
)
|
|
326
348
|
top_reasons = acs.get("top_risk_reasons") or {}
|
|
327
349
|
if top_reasons:
|
|
328
350
|
lines.append("**Top risk reasons**: " + ", ".join(
|
|
329
351
|
"{0}:{1}".format(k, v) for k, v in list(top_reasons.items())[:4]))
|
|
330
|
-
|
|
352
|
+
# Prefer actionable samples when present
|
|
353
|
+
samples = (
|
|
354
|
+
acs.get("sample_actionable_explanations")
|
|
355
|
+
or acs.get("sample_low_conf_explanations")
|
|
356
|
+
or []
|
|
357
|
+
)
|
|
331
358
|
if samples:
|
|
332
|
-
|
|
359
|
+
label = (
|
|
360
|
+
"Sample actionable edges"
|
|
361
|
+
if acs.get("sample_actionable_explanations")
|
|
362
|
+
else "Sample low-confidence edges (telemetry; prefer actionable)"
|
|
363
|
+
)
|
|
364
|
+
lines.append("**{0}**:".format(label))
|
|
333
365
|
for i, sample in enumerate(samples[:3], 1):
|
|
334
366
|
lines.append(" {0}. {1}".format(i, sample))
|
|
335
367
|
lines.append("")
|
|
@@ -47,14 +47,18 @@ Limitations (Honest Assessment)
|
|
|
47
47
|
-------------------------------
|
|
48
48
|
- Advisory only.
|
|
49
49
|
- Project-level (not per-file or per-agent yet).
|
|
50
|
-
-
|
|
51
|
-
|
|
50
|
+
- Finite ``timeout`` (seconds) is honored on Unix (non-blocking poll loop);
|
|
51
|
+
``timeout=None`` still blocks until free. Per-file / sharded locks remain
|
|
52
|
+
out of scope for M2 scale.
|
|
53
|
+
- Fine-grained (per-file) locking is a permanent non-goal unless extreme
|
|
54
|
+
concurrency pressure appears on very large multi-agent setups.
|
|
52
55
|
|
|
53
56
|
For the current scale (including heavy multi-agent dogfooding), project-level
|
|
54
57
|
advisory locking is sufficient, safe, and has been battle-tested.
|
|
55
58
|
"""
|
|
56
59
|
|
|
57
60
|
import os
|
|
61
|
+
import time
|
|
58
62
|
from contextlib import contextmanager
|
|
59
63
|
from pathlib import Path
|
|
60
64
|
from typing import Optional
|
|
@@ -79,6 +83,57 @@ LOCK_FILE_NAME = ".wikifier_staging/.lock"
|
|
|
79
83
|
_HELD_LOCKS: dict = {} # {resolved_root_str: {"fd": int, "depth": int}}
|
|
80
84
|
|
|
81
85
|
|
|
86
|
+
class LockTimeoutError(TimeoutError):
|
|
87
|
+
"""Raised when file_lock cannot acquire within the given timeout."""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _acquire_exclusive(lock_fd: int, timeout: Optional[float]) -> None:
|
|
91
|
+
"""Acquire exclusive lock; honor finite timeout on Unix (G13)."""
|
|
92
|
+
if fcntl is not None:
|
|
93
|
+
if timeout is None:
|
|
94
|
+
fcntl.flock(lock_fd, fcntl.LOCK_EX)
|
|
95
|
+
return
|
|
96
|
+
if timeout < 0:
|
|
97
|
+
raise ValueError("timeout must be None or >= 0")
|
|
98
|
+
deadline = time.monotonic() + float(timeout)
|
|
99
|
+
while True:
|
|
100
|
+
try:
|
|
101
|
+
fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
102
|
+
return
|
|
103
|
+
except (BlockingIOError, OSError):
|
|
104
|
+
if time.monotonic() >= deadline:
|
|
105
|
+
raise LockTimeoutError(
|
|
106
|
+
"project lock not acquired within {0}s".format(timeout)
|
|
107
|
+
)
|
|
108
|
+
# Short sleep to avoid busy-spin; remaining time may be < sleep.
|
|
109
|
+
remaining = deadline - time.monotonic()
|
|
110
|
+
time.sleep(min(0.05, max(0.001, remaining)))
|
|
111
|
+
elif msvcrt is not None:
|
|
112
|
+
# Windows: LK_LOCK retries ~10s then OSError; loop for blocking.
|
|
113
|
+
if timeout is None:
|
|
114
|
+
while True:
|
|
115
|
+
try:
|
|
116
|
+
os.lseek(lock_fd, 0, os.SEEK_SET)
|
|
117
|
+
msvcrt.locking(lock_fd, msvcrt.LK_LOCK, 1)
|
|
118
|
+
return
|
|
119
|
+
except OSError:
|
|
120
|
+
continue
|
|
121
|
+
deadline = time.monotonic() + float(timeout)
|
|
122
|
+
while True:
|
|
123
|
+
try:
|
|
124
|
+
os.lseek(lock_fd, 0, os.SEEK_SET)
|
|
125
|
+
msvcrt.locking(lock_fd, msvcrt.LK_NBLCK, 1)
|
|
126
|
+
return
|
|
127
|
+
except OSError:
|
|
128
|
+
if time.monotonic() >= deadline:
|
|
129
|
+
raise LockTimeoutError(
|
|
130
|
+
"project lock not acquired within {0}s".format(timeout)
|
|
131
|
+
)
|
|
132
|
+
remaining = deadline - time.monotonic()
|
|
133
|
+
time.sleep(min(0.05, max(0.001, remaining)))
|
|
134
|
+
# If neither primitive exists, proceed unlocked (advisory best-effort).
|
|
135
|
+
|
|
136
|
+
|
|
82
137
|
@contextmanager
|
|
83
138
|
def file_lock(root: Path, timeout: Optional[float] = None):
|
|
84
139
|
"""
|
|
@@ -86,13 +141,15 @@ def file_lock(root: Path, timeout: Optional[float] = None):
|
|
|
86
141
|
|
|
87
142
|
This is the primary locking primitive used by health.py and import_cache.py.
|
|
88
143
|
|
|
89
|
-
Behavior: blocking acquire
|
|
90
|
-
|
|
91
|
-
|
|
144
|
+
Behavior: blocking acquire when ``timeout is None``; with a finite
|
|
145
|
+
``timeout`` (seconds) raises ``LockTimeoutError`` if the lock is not free
|
|
146
|
+
in time. Re-entrant within the same process (nested acquires refcounted).
|
|
92
147
|
|
|
93
148
|
Usage (agents rarely need this directly):
|
|
94
149
|
with file_lock(Path(project_root)):
|
|
95
150
|
... critical section ...
|
|
151
|
+
with file_lock(Path(project_root), timeout=5.0):
|
|
152
|
+
... fail fast on contention ...
|
|
96
153
|
"""
|
|
97
154
|
lock_path = root / LOCK_FILE_NAME
|
|
98
155
|
lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -114,19 +171,7 @@ def file_lock(root: Path, timeout: Optional[float] = None):
|
|
|
114
171
|
lock_fd = None
|
|
115
172
|
try:
|
|
116
173
|
lock_fd = os.open(lock_path, os.O_CREAT | os.O_RDWR, 0o644)
|
|
117
|
-
|
|
118
|
-
fcntl.flock(lock_fd, fcntl.LOCK_EX)
|
|
119
|
-
elif msvcrt is not None:
|
|
120
|
-
# Windows: lock the first byte of the lock file. msvcrt LK_LOCK
|
|
121
|
-
# retries for ~10s then raises OSError, so loop until acquired
|
|
122
|
-
# to match the blocking semantics of flock.
|
|
123
|
-
while True:
|
|
124
|
-
try:
|
|
125
|
-
os.lseek(lock_fd, 0, os.SEEK_SET)
|
|
126
|
-
msvcrt.locking(lock_fd, msvcrt.LK_LOCK, 1)
|
|
127
|
-
break
|
|
128
|
-
except OSError:
|
|
129
|
-
continue
|
|
174
|
+
_acquire_exclusive(lock_fd, timeout)
|
|
130
175
|
# If neither primitive exists, proceed unlocked (advisory best-effort).
|
|
131
176
|
_HELD_LOCKS[key] = {"fd": lock_fd, "depth": 1}
|
|
132
177
|
yield
|
|
@@ -684,18 +684,15 @@ cmd_init() {
|
|
|
684
684
|
# node_modules, build/, dist/, caches, etc.
|
|
685
685
|
# This is NOT the import map — see map_paths.txt for package roots used by update-maps.
|
|
686
686
|
#
|
|
687
|
-
#
|
|
688
|
-
#
|
|
687
|
+
# Active default: src/ (lean). Multi-crate / monorepo: replace with package roots first.
|
|
688
|
+
# Tiny single-file toys only: uncomment bare "." below (explicit opt-in — not default).
|
|
689
|
+
src/
|
|
690
|
+
# .
|
|
689
691
|
# lib/
|
|
690
692
|
# crates/my_crate/src/
|
|
691
693
|
# packages/core/src/
|
|
692
694
|
# README.md
|
|
693
695
|
# docs/
|
|
694
|
-
#
|
|
695
|
-
# Tiny single-package toys only may keep bare "." below.
|
|
696
|
-
# Multi-crate / monorepo / anything with build output: REPLACE with lean roots first
|
|
697
|
-
# (session_bootstrap readiness "blocked" / fix_scope until you do).
|
|
698
|
-
.
|
|
699
696
|
EOT
|
|
700
697
|
fi
|
|
701
698
|
MAP_PATHS_FILE="$PROJECT_ROOT/map_paths.txt"
|
|
@@ -705,13 +702,13 @@ EOT
|
|
|
705
702
|
# List directories that contain parseable source for the dependency map.
|
|
706
703
|
# Do not use bare "." on large trees; do not list only wiki/docs files here.
|
|
707
704
|
#
|
|
708
|
-
#
|
|
709
|
-
|
|
705
|
+
# Active default: src/ (replace with this project's package roots).
|
|
706
|
+
src/
|
|
710
707
|
# crates/my_crate/src/
|
|
711
708
|
# packages/core/src/
|
|
712
709
|
#
|
|
713
710
|
# Leave empty (comments only) to fall back to monitored directory roots / full collect.
|
|
714
|
-
# After editing: wikifier update-maps (then session_bootstrap until ready_for_daemon)
|
|
711
|
+
# After editing: wikifier update-maps (then session_bootstrap until readiness=ready_for_daemon)
|
|
715
712
|
EOT
|
|
716
713
|
fi
|
|
717
714
|
[[ ! -f "$EXCLUDE_PATTERNS_FILE" ]] && cat > "$EXCLUDE_PATTERNS_FILE" << 'EOT'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wikifier
|
|
3
|
-
Version: 4.6.
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|