Skip to content

Commit 9c8bca9

Browse files
Bordaclaude[bot]codex
committed
fix(codemap): anchor paths at git root, add guards
- Anchor every codemap index and log path at the git toplevel instead of the process working directory, across the provider and all four consumer plugins: queries from a repository subdirectory previously compared subdirectory-relative paths against root-relative index entries (permanent stale, self-heal every call) or derived a different index path than the writer (false no_index), and log shards split across two directories so session joins returned nothing. Provider-side this covers the staleness git anchoring, the memoized single git SHA query, the stale_undetermined verdict when git fails inside a repository, and the log-root resolver; consumer-side it rewrites cc_develop's resolver/scanner/gate and skill bash blocks, cc_foundry's six agent files, and cc_oss's review-mode context block. - Report the index file a query actually loaded as index.index_path, captured at load time rather than recomputed at emission; the codex-rig adapter records it per query, tolerates absence as null for older providers, and reports disagreement with the probe's resolver-derived path as evidence under index_path_divergence instead of reconciling it (structural-context artifact schema version 3). - Compose coexisting codex-rig caveats into stale+degraded instead of letting a stale index mask a coverage gap, and let a targetless standard batch omit target-requiring queries so analysis without --target can report an honest status. - Add fail-closed cache freshness to cc_oss's codemap cache via an index_stamp field (size plus mtime), and read the index under a shared rwgate lease in check-index-currency while raising the helper size ceilings from 50 MB to the engine's 512 MB — the old ceiling reported no_index for any index above 50 MB, measured against this repository's own 131 MB index. - Canonicalize module names from the index instead of sed-based path guessing: resolve_centrality.py gains --modules-only and ordered_modules(), the cc_oss review and dispatch modes query it, and consumer project-name sanitization is dropped for the provider's raw-basename rule. - Add guard infrastructure: new check_codemap_guard.py (MANIFEST-managed vs registry-declared vs provider-CLI taxonomy, wired into audit_static.py and a new check-codemap-guard pre-commit hook), a canonical codemap-context snippet for cc_foundry, and check_cli_flag_drift.py extended to validate flags in a script's own docstring Usage block against its argparse surface with origin tracking. - Align the shared codemap-gates contract with reality: the build route is codemap-py index (the scan-index alias leases in-engine and is a deprecated shim, not ungated as four prose sites claimed), consumer wrappers drop their now-redundant override clauses, and the inject-preamble hook's stale ungated-scan comment and model-facing directive are corrected. - Consolidate the five codemap-py hooks onto a shared _hookutil module for project_name and session_key so path derivation cannot silently diverge between the writer and reader of a session sentinel. - Port setup_scan_env.sh to stdlib-only setup_scan_env.py (Windows-safe, no python3-on-PATH dependency, shared format_scan_args() quoting) with the .sh kept as a deprecated exec shim, and convert claude-skills dispatcher invocations to bare PATH-literal codemap-py form. - Gate new codemap-py code on complexity limits (C901, PLR0911/0912/0915) from the root pyproject.toml via a negated per-file-ignore, refactor check_currency under the gate, and enumerate the six pre-existing offenders as accepted debt. - Correct the codex usage-aggregation docstring to what the README literally claims and record the audit of all 401 captured real turns (exactly one usage event per turn, so max() and sum() are indistinguishable on real data), and add benchmarks/conftest.py so doctest collection can import _bench_common. - Record per-skill codemap route selection in the codex-rig contract with a drift test, and bump plugin versions (codemap-py 0.30.0, codex-rig 0.7.5, cc_foundry 0.46.1, cc_develop 0.22.1, cc_oss 0.28.1, cc_research) with matching CHANGELOG and README updates. --- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: OpenAI Codex <codex@openai.com>
1 parent 67b5006 commit 9c8bca9

106 files changed

Lines changed: 5832 additions & 957 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.pre-commit-config.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,13 @@ repos:
259259
pass_filenames: false
260260
always_run: true
261261

262+
- id: check-codemap-guard
263+
name: 🗺️ unmanaged codemap index-guard copy
264+
language: system
265+
entry: python3 plugins/cc_foundry/bin/check_codemap_guard.py
266+
pass_filenames: false
267+
always_run: true
268+
262269
- id: check-bash-persistence
263270
name: 🫙 shell var persistence across Bash blocks
264271
language: system

benchmarks/_bench_codex/runtime.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -187,13 +187,18 @@ def _ingest_usage(result: CodexParseResult, usage: Mapping[str, Any]) -> None:
187187
"""Fold one native usage event into the turn totals and count schema drift.
188188
189189
``max()`` rather than a running sum is deliberate: ``benchmarks/README.md``
190-
records that native Codex input usage is *cumulative within a turn*, so each
191-
usage event restates the turn total and summing would multiply the reported
192-
cost. That semantic is an assumption about the provider, pinned here only by
193-
a synthetic fixture in ``tests/test_codex_runtime.py``
194-
(``test_usage_events_are_treated_as_cumulative_not_additive``) — it has not
195-
been confirmed against a captured real stream. If a future CLI emits per-event
196-
deltas instead, that fixture is the contract to revisit before changing this.
190+
records that native Codex input usage is *cumulative within a turn* — its
191+
literal claim is only that cached input is a subset of gross input; the
192+
stronger reading that each usage event restates the turn total is this
193+
module's interpretation, not the README's assertion. An audit of every
194+
captured real stream (401 turns, 2026-08-13) found exactly one usage-bearing
195+
event per turn, always terminal — so ``max()``, ``sum()`` and last-wins are
196+
indistinguishable on real data and the cumulative property is unobservable
197+
there, while the subset claim held on all 401 events. The semantic stays
198+
pinned only by a synthetic fixture in ``tests/test_codex_runtime.py``
199+
(``test_usage_events_are_treated_as_cumulative_not_additive``). If a future
200+
CLI emits several usage events per turn, that fixture is the contract to
201+
revisit before changing this.
197202
"""
198203
result.raw_usage.update(dict(usage))
199204
for attribute, value in (

benchmarks/conftest.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""Make ``_bench_common`` and sibling top-level benchmark modules importable at collection.
2+
3+
``--doctest-modules`` (repo-root ``pyproject.toml``) imports every module under
4+
``benchmarks/`` during collection, and ``--import-mode=importlib`` deliberately does
5+
not put a module's parent directory on ``sys.path`` — so ``from _bench_common import
6+
presentation`` fails for any module collected outside ``benchmarks/tests/``, whose own
7+
conftest inserts the directory only for that subtree. This parent-level conftest runs
8+
first for the whole ``benchmarks/`` tree and applies the same insert once.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import sys
14+
from pathlib import Path
15+
16+
_BENCHMARKS_DIR = Path(__file__).resolve().parent
17+
if str(_BENCHMARKS_DIR) not in sys.path:
18+
sys.path.insert(0, str(_BENCHMARKS_DIR))

plugins/CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ Single-line sentinel read-back: `IFS= read -r VAR < "${TMPDIR:-/tmp}/<name>-${CS
9191
- Validate: after `claude plugin install`, all agents/skills/rules/hooks resolve without a local `plugins/` tree
9292
- **Bare `plugins/` path = only valid as final fallback** after cache-path resolution: `VAR="$(ls -td ~/.claude/plugins/cache/borda-ai-rig/<plugin>/*/skills/_shared 2>/dev/null | head -1)"; [ -z "$VAR" ] && VAR="plugins/<plugin>/skills/_shared"`. Never bare `plugins/` as primary path. Check C32.
9393
- **Background agents require health monitoring**: any skill spawning `Agent(..., run_in_background=true)` must implement CLAUDE.md §6 (sentinel + poll + cutoff) — reference `_FOUNDRY_SHARED/agent-spawn-protocol.md`, don't reproduce inline. Check C35.
94-
- **`bin/` executables are Python (`.py`), never shell (`.sh`)** — these plugins must run on Windows, where `.sh` does not execute. Call sites use `python "$PLUGIN_ROOT/bin/<name>.py"`, never `bash …/<name>.sh`. Python must itself stay portable: temp dir via `os.environ.get("TMPDIR") or tempfile.gettempdir()` (never hardcoded `/tmp` — absent on native Windows Python), session token via `os.environ.get("CSID") or os.environ.get("CLAUDE_CODE_SESSION_ID") or "shared"` (never `os.getppid()`), `pathlib` over string path concatenation. Three legacy `.sh` files remain tracked (`cc_research/bin/{git_slugs,resolve-quality-gates}.sh`, `codemap-py/bin/setup_scan_env.sh`) — **they are debt, not precedent**; never add to them or match them when authoring new scripts.
94+
- **`bin/` executables are Python (`.py`), never shell (`.sh`)** — these plugins must run on Windows, where `.sh` does not execute. Call sites use `python "$PLUGIN_ROOT/bin/<name>.py"`, never `bash …/<name>.sh`. Python must itself stay portable: temp dir via `os.environ.get("TMPDIR") or tempfile.gettempdir()` (never hardcoded `/tmp` — absent on native Windows Python), session token via `os.environ.get("CSID") or os.environ.get("CLAUDE_CODE_SESSION_ID") or "shared"` (never `os.getppid()`), `pathlib` over string path concatenation. Two legacy `.sh` files remain tracked (`cc_research/bin/{git_slugs,resolve-quality-gates}.sh`) — **they are debt, not precedent**; never add to them or match them when authoring new scripts. (`codemap-py/bin/setup_scan_env.sh` is no longer logic: it is a deprecated `exec` shim delegating to `setup_scan_env.py`, kept only for pre-existing call sites.)
9595

9696
## Worktree Base — verify before trusting agent output
9797

plugins/cc_develop/.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
"license": "Apache-2.0",
88
"name": "develop",
99
"repository": "https://github.com/Borda/AI-Rig",
10-
"version": "0.22.0"
10+
"version": "0.22.1"
1111
}

plugins/cc_develop/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -588,7 +588,7 @@ Available on: `feature`, `fix`, `refactor` (all work stays in the worktree); and
588588
/develop:refactor src/loader.py "extract batching" --worktree --team
589589
```
590590

591-
- **Codemap alignment**because the session CWD moves into the worktree, the codemap index resolves per-worktree (`<worktree>/.cache/codemap/…`). Each run owns its own ephemeral index, so any number of parallel `--worktree` runs never share or race one index; `.cache/` is gitignored so a worktree index never merges back. After you merge the branch, the main index is flagged stale on the next prompt and refreshes once.
591+
- **Codemap alignment**the index path is anchored to the git top-level (`<root>/.cache/codemap/<project>.json`), not to the session CWD, and a linked worktree is its own top-level, so the index still resolves per-worktree (`<worktree>/.cache/codemap/…`) — and resolves identically from any subdirectory inside it. Each run owns its own ephemeral index, so any number of parallel `--worktree` runs never share or race one index; `.cache/` is gitignored so a worktree index never merges back. After you merge the branch, the main index is flagged stale on the next prompt and refreshes once.
592592
- **Composes with `--team`** — the orchestrator worktree is the integration point; `--team` teammates keep their own per-agent isolation and merge into the orchestrator's worktree branch.
593593
- Not offered on `debug` (diagnosis handoff), `plan`, or `review` (read-only).
594594

plugins/cc_develop/bin/codemap_resolve.py

Lines changed: 132 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -2,123 +2,192 @@
22
"""Resolve CODEMAP_ENABLED from auto/strict/off to true/false.
33
44
Python (not shell) because plugins must run on Windows, where a `#!` shebang is
5-
not honoured — see plugins/CLAUDE.md §Installability. Behaviour is a 1:1 port of
6-
the former `bin/codemap-resolve` bash script; differential parity was verified
7-
across the full mode/index matrix before the shell version was removed.
5+
not honoured — see plugins/CLAUDE.md §Installability.
6+
7+
Index location mirrors codemap-py's own resolver (``codemap_py.index_paths.resolve_index``,
8+
consumed by ``query.find_index``) exactly:
9+
10+
* the project root is the **git toplevel**, or the CWD when outside a repository;
11+
* the index directory defaults to ``<root>/.cache/codemap`` — anchored to the root, never
12+
to the process CWD, so a skill invoked from a subdirectory still finds the index instead
13+
of reporting a false ``no_index``;
14+
* ``CODEMAP_INDEX_DIR`` overrides that directory as a flat ``<override>/<project>.json``;
15+
* the project name is the **raw** directory basename with no sanitization. The scanner
16+
writes that name verbatim, so stripping characters here made every repository whose
17+
directory contains a space, ``+`` or a non-ASCII character resolve to a filename the
18+
scanner never writes — a permanent, silent ``no_index``.
19+
20+
Also writes currency status to ``${TMPDIR}/<prefix>-${CSID}`` ("current", "stale", "off"
21+
or "no_index"), where ``<prefix>`` is supplied by the calling plugin via
22+
``--currency-prefix``.
23+
24+
This file is kept **byte-identical** across consuming plugins by
25+
``plugins/cc_foundry/bin/propagate_shared.py`` (MANIFEST). Every per-plugin difference
26+
must therefore arrive as an argument from that plugin's own wrapper — never as an edit
27+
here.
28+
29+
Usage:
30+
codemap_resolve.py [auto|strict|off|true|false] [--currency-prefix PREFIX]
831
9-
Also writes currency status to ${TMPDIR}/dev-codemap-currency-${CSID} when
10-
check-index-currency is available ("current", "stale", or "no_index").
11-
Usage: CODEMAP_ENABLED=$(python .../codemap_resolve.py "$CODEMAP_ENABLED") || exit 1
1232
Exits 0: prints "true" or "false" to stdout
1333
Exits 1: strict mode and binary/index missing — error to stderr
1434
"""
1535

36+
import argparse
37+
import json
1638
import os
17-
import re
1839
import shutil
1940
import subprocess
2041
import sys
2142
import tempfile
2243
from pathlib import Path
2344

24-
CURRENCY_PREFIX = "dev-codemap-currency"
25-
TOOL_LABEL = "codemap-py" # warn prefix
26-
QUERY_LABEL = "codemap-py" # binary name in the strict BREAKING message
45+
TOOL_LABEL = "codemap-py" # binary name, warn prefix, and plugin name — all one label
46+
DEFAULT_CURRENCY_PREFIX = "codemap-currency"
47+
_GIT_TIMEOUT_S = 5
48+
_CURRENCY_TIMEOUT_S = 15 # currency check may walk the tree (tier 2); bounded, never unbounded
2749

2850

29-
def _currency_file() -> Path:
51+
def _currency_file(prefix: str) -> Path:
52+
"""Return the session-scoped currency sentinel path for *prefix*."""
3053
csid = os.environ.get("CSID") or os.environ.get("CLAUDE_CODE_SESSION_ID") or "shared"
3154
tmp = os.environ.get("TMPDIR") or tempfile.gettempdir()
32-
return Path(tmp) / f"{CURRENCY_PREFIX}-{csid}"
55+
return Path(tmp) / f"{prefix}-{csid}"
3356

3457

35-
def _write_currency(value: str) -> None:
58+
def _write_currency(prefix: str, value: str) -> None:
59+
"""Record *value* in the currency sentinel, ignoring write failures."""
3660
try:
37-
_currency_file().write_text(value + "\n")
61+
_currency_file(prefix).write_text(value + "\n")
3862
except OSError:
3963
pass # currency status is advisory; never fail the gate on it
4064

4165

42-
def _project_name() -> str:
66+
def _canonical_root() -> Path:
67+
"""Return the project root — git toplevel, else CWD — symlink-collapsed.
68+
69+
Mirrors ``codemap_py.index_paths.canonical_root`` so consumer and provider agree on
70+
both the index directory and the project name.
71+
"""
4372
try:
4473
out = subprocess.run(
4574
["git", "rev-parse", "--show-toplevel"],
4675
capture_output=True,
4776
text=True,
48-
timeout=5,
77+
timeout=_GIT_TIMEOUT_S,
4978
check=False,
5079
)
51-
root = out.stdout.strip() if out.returncode == 0 and out.stdout.strip() else "default"
80+
if out.returncode == 0 and out.stdout.strip():
81+
return Path(out.stdout.strip()).resolve()
5282
except (OSError, subprocess.SubprocessError):
53-
root = "default"
54-
return re.sub(r"[^a-zA-Z0-9._-]", "", Path(root).name) or "default"
83+
pass
84+
return Path.cwd().resolve()
85+
5586

87+
def _index_path(root: Path) -> Path:
88+
"""Return the index file for *root*, honouring the flat ``CODEMAP_INDEX_DIR`` override."""
89+
override = os.environ.get("CODEMAP_INDEX_DIR")
90+
index_dir = Path(override).expanduser().resolve() if override else root / ".cache" / "codemap"
91+
return index_dir / f"{root.name}.json"
5692

57-
def _currency_field(cic: str, index: Path, field: str, fallback: str) -> str:
93+
94+
def _currency(cic: str, index: Path) -> tuple[str, str]:
95+
"""Return ``(status, reason)`` from a single, timed ``check-index-currency`` run.
96+
97+
``check-index-currency`` signals the verdict through its **exit code** (0 current,
98+
1 stale, 2 no_index) while always printing the full JSON result to stdout. Gating on
99+
``returncode == 0`` therefore discarded every stale verdict and substituted the
100+
"current" fallback, so the stale gate could never fire. The verdict is read from
101+
stdout and the exit code is deliberately ignored.
102+
103+
One run yields both fields; querying ``--field status`` and ``--field reason``
104+
separately spawned two interpreters, and neither call was time-bounded.
105+
"""
58106
try:
59107
out = subprocess.run(
60-
[sys.executable, cic, "--index-path", str(index), "--field", field],
108+
[sys.executable, cic, "--index-path", str(index)],
61109
capture_output=True,
62110
text=True,
111+
timeout=_CURRENCY_TIMEOUT_S,
63112
check=False,
64113
)
65-
return out.stdout.strip() if out.returncode == 0 else fallback
66-
except (OSError, subprocess.SubprocessError):
67-
return fallback
114+
data = json.loads(out.stdout.strip())
115+
except (OSError, subprocess.SubprocessError, ValueError):
116+
return "current", "" # fail open: an unreadable verdict must not block the gate
117+
if not isinstance(data, dict):
118+
return "current", ""
119+
status = data.get("status")
120+
reason = data.get("reason")
121+
return (status if isinstance(status, str) and status else "current"), (reason if isinstance(reason, str) else "")
122+
123+
124+
def _record_currency(prefix: str, index: Path) -> None:
125+
"""Probe index currency and record it, warning on stderr when the index is stale."""
126+
cic = shutil.which("check-index-currency")
127+
if not cic:
128+
_write_currency(prefix, "current")
129+
return
130+
currency, reason = _currency(cic, index)
131+
_write_currency(prefix, currency)
132+
if currency == "stale":
133+
print(
134+
f"⚠ {TOOL_LABEL}: index is stale — {reason}\n Run /codemap-py:scan-codebase to refresh it.",
135+
file=sys.stderr,
136+
)
137+
138+
139+
def _unavailable(prefix: str, mode: str, strict_detail: str, warning: str) -> int:
140+
"""Record ``no_index`` and emit the strict or soft outcome; returns the exit code."""
141+
_write_currency(prefix, "no_index")
142+
if mode == "strict":
143+
print(f"! BREAKING — --codemap strict: {strict_detail}", file=sys.stderr)
144+
return 1
145+
if warning:
146+
print(warning, file=sys.stderr)
147+
print("false")
148+
return 0
68149

69150

70-
def main() -> int:
71-
mode = sys.argv[1] if len(sys.argv) > 1 else "auto"
151+
def _parse_args(argv: list[str]) -> argparse.Namespace:
152+
"""Parse the resolver's mode and the caller-supplied currency prefix."""
153+
parser = argparse.ArgumentParser(description="Resolve CODEMAP_ENABLED to true/false.")
154+
parser.add_argument("mode", nargs="?", default="auto", help="auto | strict | off | true | false")
155+
parser.add_argument(
156+
"--currency-prefix",
157+
default=DEFAULT_CURRENCY_PREFIX,
158+
help="basename prefix of the currency sentinel; supplied by the calling plugin",
159+
)
160+
return parser.parse_args(argv)
161+
162+
163+
def main(argv: list[str] | None = None) -> int:
164+
"""Resolve the codemap mode, print true/false, and record index currency."""
165+
args = _parse_args(list(sys.argv[1:] if argv is None else argv))
166+
mode, prefix = args.mode, args.currency_prefix
72167

73168
if mode in ("off", "false"):
74169
print("false")
75-
_write_currency("off")
170+
_write_currency(prefix, "off")
76171
return 0
77172
if mode == "true":
78173
print("true")
79174
return 0
80175

81-
if not shutil.which("codemap-py"):
82-
_write_currency("no_index")
83-
if mode == "strict":
84-
print(
85-
f"! BREAKING — --codemap strict: {TOOL_LABEL} query not found. Install {TOOL_LABEL} plugin.",
86-
file=sys.stderr,
87-
)
88-
return 1
89-
print("false")
90-
return 0
91-
92-
index = Path(os.environ.get("CODEMAP_INDEX_DIR", ".cache/codemap")) / f"{_project_name()}.json"
176+
if not shutil.which(TOOL_LABEL):
177+
return _unavailable(prefix, mode, f"{TOOL_LABEL} query not found. Install {TOOL_LABEL} plugin.", "")
93178

179+
root = _canonical_root()
180+
index = _index_path(root)
94181
if not index.is_file():
95-
_write_currency("no_index")
96-
if mode == "strict":
97-
print(
98-
f"! BREAKING — --codemap strict: index {index} not found. Run /codemap-py:scan-codebase first.",
99-
file=sys.stderr,
100-
)
101-
return 1
102-
print(
103-
f"⚠ {TOOL_LABEL}: no index for project '{_project_name()}' at {index}\n"
182+
return _unavailable(
183+
prefix,
184+
mode,
185+
f"index {index} not found. Run /codemap-py:scan-codebase first.",
186+
f"⚠ {TOOL_LABEL}: no index for project '{root.name}' at {index}\n"
104187
" Run /codemap-py:scan-codebase to build it, then re-run this skill.",
105-
file=sys.stderr,
106188
)
107-
print("false")
108-
return 0
109189

110-
cic = shutil.which("check-index-currency")
111-
if cic:
112-
currency = _currency_field(cic, index, "status", "current")
113-
reason = _currency_field(cic, index, "reason", "")
114-
_write_currency(currency)
115-
if currency == "stale":
116-
print(
117-
f"⚠ {TOOL_LABEL}: index is stale — {reason}\n Run /codemap-py:scan-codebase to refresh it.",
118-
file=sys.stderr,
119-
)
120-
else:
121-
_write_currency("current")
190+
_record_currency(prefix, index)
122191
print("true")
123192
return 0
124193

plugins/cc_develop/bin/codemap_scan.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,13 @@ def _git_diff_files(timeout: int = 15) -> list[str]:
173173
return [line for line in out.splitlines() if line]
174174

175175

176-
def _git_project_name(timeout: int = 15) -> str:
177-
"""Return basename of git toplevel, falling back to current dir basename."""
176+
def _git_root(timeout: int = 15) -> Path:
177+
"""Return the git toplevel, falling back to the current directory.
178+
179+
Mirrors ``codemap_py.index_paths.canonical_root``: the index the scanner writes lives
180+
under the repository root, so a consumer that anchored on the process CWD reported a
181+
false ``no_index`` whenever a skill ran from a subdirectory.
182+
"""
178183
try:
179184
out = subprocess.check_output( # noqa: S603 — fixed argv, no shell.
180185
["git", "rev-parse", "--show-toplevel"],
@@ -183,10 +188,21 @@ def _git_project_name(timeout: int = 15) -> str:
183188
timeout=timeout,
184189
).strip()
185190
if out:
186-
return Path(out).name
191+
return Path(out).resolve()
187192
except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):
188193
pass
189-
return Path.cwd().name
194+
return Path.cwd().resolve()
195+
196+
197+
def _index_path(root: Path) -> Path:
198+
"""Return the index file for *root*, honouring the flat ``CODEMAP_INDEX_DIR`` override.
199+
200+
The project name is the raw directory basename — the scanner writes it verbatim, so any
201+
sanitization here would seek a filename that is never written.
202+
"""
203+
override = os.environ.get("CODEMAP_INDEX_DIR")
204+
index_dir = Path(override).expanduser().resolve() if override else root / ".cache" / "codemap"
205+
return index_dir / f"{root.name}.json"
190206

191207

192208
_MAX_FIND_FILES = 2000
@@ -290,9 +306,7 @@ def main(argv: list[str] | None = None) -> int:
290306
return 0
291307

292308
# Index file missing → silent exit 0.
293-
project = _git_project_name(timeout=args.timeout)
294-
_custom = os.environ.get("CODEMAP_INDEX_DIR")
295-
index_path = (Path(_custom) if _custom else Path(".cache") / "codemap") / f"{project}.json"
309+
index_path = _index_path(_git_root(timeout=args.timeout))
296310
if not index_path.is_file():
297311
return 0
298312

0 commit comments

Comments
 (0)