Skip to content

Commit cb30b10

Browse files
Bordaclaude[bot]codexclaude
committed
refine(codemap): dual-runtime slice, RW gate, CLI
- bin/_rwgate.py: writer-preferred cross-process index gate — registry.lock mutex (fcntl/msvcrt), reader-token drain, writer intent, bounded IndexBusy, fork-safe PID reset, liveness-only crash recovery, recycle-safe intent release - bin/_index_identity.py: canonical-root index resolver; root-keyed CODEMAP_INDEX_DIR override (sha256 root key), legacy-flat read-only compat, index_root_collision diagnostics - bin/_runtime_log.py: claude/codex/direct log-subtree isolation under .cache/codemap/logs, CODEMAP_LOG_DIR override keeps runtime component - scripts/codemap_py_entry.py + codemap_py_cli.py: codemap-py {index,query,doctor} dispatcher; interpreter probe CPython >=3.11,<3.15 with exit-127 rejection and hard-fail CODEMAP_PYTHON; index/query wrapped in the RW gate (index_busy / index_coordination_unavailable) - bin/codemap-py + bin/codemap-py.cmd launchers; space-safe CODEMAP_PYTHON quoting - scripts/build_package.py: deterministic candidate builder with sha256 package-manifest.json, --check byte-compare; manifests carry description/author (claude plugin validate --strict passes) - scripts/probe_codex_install.py + probe_claude_install.py: non-interactive install probes into disposable CODEX_HOME / CLAUDE_CONFIG_DIR - tests: gate core, delegation no-rebuild oracle, runtime logging, CLI gate wiring, interpreter contract, candidate determinism/closure, frozen-grammar corpus (suite: 958 passed, 1 named skip) - ci-tests.yml: python matrix gains 3.11 and 3.13 - pyproject.toml: ruff exclude for post-3.11 grammar corpus fixtures - plugin version 0.24.1 --- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: OpenAI Codex <codex@openai.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent fb34769 commit cb30b10

24 files changed

Lines changed: 4040 additions & 3 deletions

.github/workflows/ci-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
fail-fast: false
1818
matrix:
1919
os: ["ubuntu-latest", "macos-latest", "windows-latest"]
20-
python-version: ["3.10", "3.12"]
20+
python-version: ["3.10", "3.11", "3.12", "3.13"]
2121
timeout-minutes: 10
2222

2323
steps:

plugins/codemap/.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@
1717
"license": "Apache-2.0",
1818
"name": "codemap",
1919
"repository": "https://github.com/Borda/AI-Rig",
20-
"version": "0.24.0"
20+
"version": "0.24.1"
2121
}
Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
#!/usr/bin/env python
2+
"""_index_identity.py — canonical project-root resolver and codemap index identity.
3+
4+
Every codemap entrypoint (launchers, skills, both runtimes) resolves the shared
5+
project index through this module so Claude Code, Codex, and direct CLI use on the
6+
same real project root land on one runtime-neutral index. Runtime/session identity
7+
is never part of the resolved path — only the logging subtree is runtime-scoped
8+
(see ``_runtime_log``).
9+
10+
Resolution rules (plan §4.4 "Shared project index"):
11+
12+
- the canonical root is the git repository root (else the current directory),
13+
with symlinks/case aliases collapsed so an alias of the same project resolves
14+
to the same identity;
15+
- the default index is ``<canonical-root>/.cache/codemap/<project>.json`` with a
16+
sibling ``.index-rw/`` coordination directory;
17+
- ``CODEMAP_INDEX_DIR`` is a product-wide base override (never a runtime override).
18+
Its target is ``<override>/<root-key>/<project>.json`` where ``<root-key>`` is the
19+
full lowercase SHA-256 of the normalized canonical-root identity, so equal-basename
20+
projects get distinct reusable indexes;
21+
- a legacy flat override at ``<override>/<project>.json`` is a read-only compatibility
22+
candidate only when its stored ``scan_root`` matches the canonical root; a mismatch
23+
is ignored with an ``index_root_collision`` diagnostic and never blocks the
24+
root-keyed target;
25+
- ``split_index_roots`` is reported when two environments resolve different index paths.
26+
"""
27+
28+
from __future__ import annotations
29+
30+
import hashlib
31+
import json
32+
import os
33+
import shutil
34+
import subprocess
35+
from dataclasses import dataclass, field
36+
from pathlib import Path
37+
38+
_GIT_TIMEOUT_S = 5
39+
INDEX_SUBDIR = Path(".cache", "codemap")
40+
COORDINATION_DIRNAME = ".index-rw"
41+
42+
INDEX_ROOT_COLLISION = "index_root_collision"
43+
SPLIT_INDEX_ROOTS = "split_index_roots"
44+
45+
_UNSET = object()
46+
47+
48+
@dataclass(frozen=True)
49+
class Diagnostic:
50+
"""A bounded, machine-readable resolver diagnostic.
51+
52+
Attributes:
53+
code: Stable diagnostic code (e.g. ``index_root_collision``).
54+
message: Human-readable one-line summary.
55+
detail: Structured supporting fields; never contains secrets.
56+
"""
57+
58+
code: str
59+
message: str
60+
detail: dict = field(default_factory=dict)
61+
62+
63+
@dataclass(frozen=True)
64+
class IndexIdentity:
65+
"""Resolved codemap index identity for one canonical project root.
66+
67+
Attributes:
68+
project: Canonical-root basename.
69+
root: Canonical (symlink-collapsed) project root.
70+
root_key: Full lowercase SHA-256 of the normalized root identity.
71+
index_dir: Directory holding the resolved index and coordination subtree.
72+
index_path: Resolved ``<project>.json`` index path.
73+
coordination_dir: Sibling ``.index-rw/`` directory beside the index.
74+
override: ``True`` when ``CODEMAP_INDEX_DIR`` selected the base.
75+
legacy_candidate: Read-only legacy flat index path when it matches this
76+
root, else ``None``.
77+
diagnostics: Any diagnostics raised while resolving (e.g. collisions).
78+
"""
79+
80+
project: str
81+
root: Path
82+
root_key: str
83+
index_dir: Path
84+
index_path: Path
85+
coordination_dir: Path
86+
override: bool
87+
legacy_candidate: Path | None
88+
diagnostics: tuple[Diagnostic, ...]
89+
90+
91+
def _real(path: Path) -> Path:
92+
"""Return *path* with symlinks and ``..`` collapsed (best-effort)."""
93+
try:
94+
return path.resolve()
95+
except OSError:
96+
return Path(os.path.abspath(path))
97+
98+
99+
def canonical_root(cwd: Path | str | None = None) -> Path:
100+
"""Return the canonical project root for *cwd*.
101+
102+
The root is the git repository top-level when *cwd* is inside a repository,
103+
otherwise *cwd* itself; either way the result is symlink-collapsed so an alias
104+
of the same project resolves to the same identity.
105+
106+
Args:
107+
cwd: Working directory to resolve from (defaults to the process CWD).
108+
109+
Returns:
110+
Absolute, symlink-collapsed canonical root path.
111+
112+
Examples:
113+
>>> isinstance(canonical_root(), Path) # doctest: +SKIP
114+
True
115+
"""
116+
work = Path(cwd) if cwd is not None else Path.cwd()
117+
git = shutil.which("git")
118+
if git:
119+
try:
120+
out = subprocess.run(
121+
[git, "rev-parse", "--show-toplevel"],
122+
capture_output=True,
123+
text=True,
124+
cwd=str(work),
125+
check=True,
126+
timeout=_GIT_TIMEOUT_S,
127+
).stdout.strip()
128+
if out:
129+
return _real(Path(out))
130+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, OSError):
131+
pass
132+
return _real(work)
133+
134+
135+
def normalize_identity(root: Path | str, *, windows: bool | None = None) -> str:
136+
"""Return the normalized string identity used to key an index for *root*.
137+
138+
On Windows the identity casefolds and normalizes separators so drive-letter
139+
form and case differences collapse to one identity; on POSIX the resolved
140+
path string is already the identity.
141+
142+
Args:
143+
root: Canonical root path (should already be symlink-collapsed).
144+
windows: Force Windows normalization; defaults to the host platform.
145+
146+
Returns:
147+
The normalized identity string.
148+
149+
Examples:
150+
>>> normalize_identity("/Repo/Proj", windows=False)
151+
'/Repo/Proj'
152+
>>> normalize_identity("C:/Repo/Proj", windows=True)
153+
'c:\\\\repo\\\\proj'
154+
"""
155+
if windows is None:
156+
windows = os.name == "nt"
157+
raw = str(root)
158+
if windows:
159+
raw = raw.replace("/", "\\").casefold()
160+
return raw
161+
162+
163+
def root_key(root: Path | str, *, windows: bool | None = None) -> str:
164+
"""Return the full lowercase SHA-256 of the normalized identity of *root*.
165+
166+
Args:
167+
root: Canonical root path.
168+
windows: Force Windows normalization; defaults to the host platform.
169+
170+
Returns:
171+
64-character lowercase hex digest, stable and free of any raw path.
172+
173+
Examples:
174+
>>> len(root_key("/repo/proj", windows=False))
175+
64
176+
>>> root_key("/a", windows=False) == root_key("/a", windows=False)
177+
True
178+
"""
179+
identity = normalize_identity(root, windows=windows)
180+
return hashlib.sha256(identity.encode("utf-8")).hexdigest()
181+
182+
183+
def _read_scan_root(path: Path) -> str | None:
184+
"""Return the ``scan_root`` field stored in *path*, or ``None`` if unreadable."""
185+
try:
186+
with path.open(encoding="utf-8") as fh:
187+
data = json.load(fh)
188+
except (OSError, ValueError):
189+
return None
190+
value = data.get("scan_root") if isinstance(data, dict) else None
191+
return value if isinstance(value, str) else None
192+
193+
194+
def _legacy_candidate(path: Path, root: Path, diagnostics: list[Diagnostic]) -> Path | None:
195+
"""Return *path* when it is a valid read-only legacy candidate for *root*.
196+
197+
A legacy flat override index is reusable only when its stored ``scan_root``
198+
normalizes to the same identity as *root*; a mismatch appends an
199+
``index_root_collision`` diagnostic and is never returned.
200+
"""
201+
if not path.is_file():
202+
return None
203+
stored = _read_scan_root(path)
204+
if stored is not None and normalize_identity(_real(Path(stored))) == normalize_identity(root):
205+
return path
206+
diagnostics.append(
207+
Diagnostic(
208+
INDEX_ROOT_COLLISION,
209+
"legacy flat index does not match the canonical root; ignoring it",
210+
{"legacy_path": str(path), "stored_scan_root": stored, "canonical_root": str(root)},
211+
)
212+
)
213+
return None
214+
215+
216+
def resolve_index(
217+
cwd: Path | str | None = None,
218+
*,
219+
root: Path | str | None = None,
220+
index_dir_override: object = _UNSET,
221+
) -> IndexIdentity:
222+
"""Resolve the canonical codemap index identity.
223+
224+
Args:
225+
cwd: Working directory used to derive the canonical root (ignored when
226+
*root* is given).
227+
root: Explicit canonical root; when omitted it is derived from *cwd*.
228+
index_dir_override: Base override path. ``_UNSET`` (default) reads
229+
``CODEMAP_INDEX_DIR``; pass ``None`` to force the default layout even
230+
when the environment variable is set.
231+
232+
Returns:
233+
An :class:`IndexIdentity` describing the resolved paths and diagnostics.
234+
235+
Examples:
236+
>>> ident = resolve_index() # doctest: +SKIP
237+
>>> ident.index_path.name.endswith(".json") # doctest: +SKIP
238+
True
239+
"""
240+
base_root = _real(Path(root)) if root is not None else canonical_root(cwd)
241+
project = base_root.name
242+
rk = root_key(base_root)
243+
override_raw = os.environ.get("CODEMAP_INDEX_DIR") if index_dir_override is _UNSET else index_dir_override
244+
diagnostics: list[Diagnostic] = []
245+
246+
if override_raw:
247+
override_base = Path(str(override_raw)).expanduser().resolve()
248+
index_dir = override_base / rk
249+
legacy = _legacy_candidate(override_base / f"{project}.json", base_root, diagnostics)
250+
override = True
251+
else:
252+
index_dir = base_root / INDEX_SUBDIR
253+
legacy = None
254+
override = False
255+
256+
index_path = index_dir / f"{project}.json"
257+
return IndexIdentity(
258+
project=project,
259+
root=base_root,
260+
root_key=rk,
261+
index_dir=index_dir,
262+
index_path=index_path,
263+
coordination_dir=index_dir / COORDINATION_DIRNAME,
264+
override=override,
265+
legacy_candidate=legacy,
266+
diagnostics=tuple(diagnostics),
267+
)
268+
269+
270+
def diagnose_split_index_roots(path_a: Path, path_b: Path) -> Diagnostic | None:
271+
"""Return a ``split_index_roots`` diagnostic when two paths disagree.
272+
273+
``codemap-py integrate check`` uses this to report — never to reconcile —
274+
when two runtime environments resolve different index paths.
275+
276+
Args:
277+
path_a: Index path resolved in the first environment.
278+
path_b: Index path resolved in the second environment.
279+
280+
Returns:
281+
A :class:`Diagnostic` when the paths differ, else ``None``.
282+
283+
Examples:
284+
>>> diagnose_split_index_roots(Path("/a/i.json"), Path("/a/i.json")) is None
285+
True
286+
>>> diagnose_split_index_roots(Path("/a/i.json"), Path("/b/i.json")).code
287+
'split_index_roots'
288+
"""
289+
if path_a == path_b:
290+
return None
291+
return Diagnostic(
292+
SPLIT_INDEX_ROOTS,
293+
"runtime environments resolve different index paths; not reconciling",
294+
{"path_a": str(path_a), "path_b": str(path_b)},
295+
)

0 commit comments

Comments
 (0)