From 575aee779fcd621f3c1f49eaf7eab33a10673afc Mon Sep 17 00:00:00 2001 From: Kris Wong Date: Wed, 16 Sep 2026 17:07:27 -0500 Subject: [PATCH 1/2] refactor(code-intel): remove the fork-only index archives (PLN-2027 PR 7) FR15. The fork added `lc code export` / `lc code import` in 8235fdaf, upstream never shipped them, and nothing consumes them. PLN-2027 section 11 decision 5 approved removing them without the export/re-import probe. Removed: both commands from gateway/cli/commands/code.py, the module-scope TIERS import they were the only consumer of, infra/code_intel/portable.py, tests/infra/code_intel/test_portable.py, and the `portable` optional- dependency extra (uv.lock regenerated with `uv lock`, never hand-edited). `_portable_repo_root` stays: `lc code clones` calls it, and it touches nothing in the deleted module. Beyond the plan's removal list, which named only open-code-intel-plan.md: phase-b-review-handoff.md, the clones command's own comment and the clones test's docstring all referenced the commands too. The plan's stated Exit (`lc code --help` lists neither command) passes with those surviving, so completeness is proven by grep instead. --- docs/planning/open-code-intel-plan.md | 57 +-- docs/planning/phase-b-review-handoff.md | 3 - pyproject.toml | 4 - src/lemoncrow/gateway/cli/commands/code.py | 68 +-- src/lemoncrow/infra/code_intel/portable.py | 560 --------------------- tests/infra/code_intel/test_clones.py | 2 +- tests/infra/code_intel/test_portable.py | 433 ---------------- uv.lock | 6 +- 8 files changed, 12 insertions(+), 1121 deletions(-) delete mode 100644 src/lemoncrow/infra/code_intel/portable.py delete mode 100644 tests/infra/code_intel/test_portable.py diff --git a/docs/planning/open-code-intel-plan.md b/docs/planning/open-code-intel-plan.md index e1266b0b9..8ff871798 100644 --- a/docs/planning/open-code-intel-plan.md +++ b/docs/planning/open-code-intel-plan.md @@ -444,54 +444,13 @@ that would read as "this code has no duplicates". ## F7. Index export / import -### Delivery - -New `src/lemoncrow/infra/code_intel/portable.py`, CLI subcommands under the -existing `lc code` group (`gateway/cli/commands/code.py`, live source): - -``` -lc code export [--out .lemoncrow/index.tar.zst] [--tier best|fast] -lc code import [--from .lemoncrow/index.tar.zst] -``` - -- `VACUUM INTO` each of the five DBs into a temp dir (compacts, drops WAL) -- tar + zstd; two tiers — `best` (zstd 9, drop derived indexes) on explicit - export, `fast` (zstd 3) for incremental refresh -- manifest: engine `index_version`, `indexer_semantics_version`, LemonCrow - version, repo HEAD sha, row counts, sidecar `schema_version` -- **import refuses on version mismatch** rather than producing a subtly wrong - graph. The engine owns those numbers; we cannot migrate its data. -- import bootstraps into an empty workspace, then the engine's normal - incremental pass fills the local diff - -`zstandard` is a new dependency — put it behind an extra, not the base install. - -### Tests - -`tests/infra/code_intel/test_portable.py` — round-trip fidelity (row counts and -a sampled query match), version-mismatch refusal, corrupt-archive handling. - -**Effort:** 5-8 days. **Risk:** low-medium. Deliberately does **not** commit the -artifact to git by default. - -**Shipped** as `8235fdaf` — `infra/code_intel/portable.py`, plus `lc code -export` / `lc code import`. Two deviations: - -> **`zstandard` is an accelerator, not a requirement.** It sits behind a new -> `portable` extra as planned, but export falls back to stdlib lzma when it is -> absent rather than failing. The manifest names the codec and import reads it, -> so the feature works on a base install and gets smaller archives with the -> extra. -> -> **The `best` tier does not drop derived indexes.** The engine's DDL is closed; -> an index dropped on export is one open code cannot recreate, so the import -> would hand back a database the engine expects to be complete. The two tiers -> differ by compression level only. - -One addition the plan did not call for: the archive is treated as untrusted -input. Members must be regular files whose names are on a fixed allow-list, so a -traversal path or a symlink is refused outright. A tar file is a format someone -else can write, and "a teammate sent it" is not provenance. +**Removed.** Shipped as `8235fdaf` — `infra/code_intel/portable.py` plus two +`lc code` subcommands that packed a workspace's index into an archive and +restored it — and removed again under PRD-739 FR15 (PLN-2027 PR 7): the fork +added the surface, upstream never shipped it, and nothing consumed it. The +delivery notes, the two deviations recorded against them, and the archive's +untrusted-input handling went with the code; `git show 8235fdaf` still carries +them. --- @@ -811,7 +770,7 @@ superseded generation just multiplies the blast radius of the defect. | F11 | `8ea1eb15` | `infra/code_intel/freshness.py` | | F2 | `f866112d` | `code_changes` tool | | F5 | `e832fc70` | `code_query` tool | -| F7 | `8235fdaf` | `lc code export` / `import` | +| F7 | `8235fdaf` | index export / import (removed, PRD-739 FR15) | | F12 | `67d56649` | routing + completeness contract (below) | Deviations are recorded against each item above. The whole `tests/gateway/` diff --git a/docs/planning/phase-b-review-handoff.md b/docs/planning/phase-b-review-handoff.md index 2f5f418ed..3e339e7d3 100644 --- a/docs/planning/phase-b-review-handoff.md +++ b/docs/planning/phase-b-review-handoff.md @@ -217,9 +217,6 @@ exclusive) and re-query — expect an `IndexRebuilding` error, not `[]`. any workspace without the optional tree-sitter `parsers` extra, and treating it as a rebuild made every code tool fail forever on a perfectly valid workspace. -5. **`zstandard` is not installed**, so `lc code export` uses an lzma fallback. - Functional, larger archives. Not a bug. - --- ## Also worth knowing diff --git a/pyproject.toml b/pyproject.toml index 67a822f47..4d18bbdf1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,10 +104,6 @@ semantic = [ parsers = [ "tree-sitter-languages>=1.10; python_version < '3.13'", ] -# Portable code-index archives (`lc code export` / `import`). zstd beats the -# stdlib codecs on both ratio and speed; without it export/import still work and -# fall back to lzma, so this is an accelerator, not a requirement. -portable = ["zstandard>=0.22"] rename = ["rope>=0.23"] ortools = ["ortools>=9.10"] litellm = ["litellm>=1.83.14"] diff --git a/src/lemoncrow/gateway/cli/commands/code.py b/src/lemoncrow/gateway/cli/commands/code.py index 76353585e..03fee0532 100644 --- a/src/lemoncrow/gateway/cli/commands/code.py +++ b/src/lemoncrow/gateway/cli/commands/code.py @@ -13,7 +13,6 @@ from lemoncrow.gateway.cli.commands._shared import _emit, require_pro from lemoncrow.gateway.integrations.openmemory_lifecycle import project_root as _project_root -from lemoncrow.infra.code_intel.portable import TIERS as _PORTABLE_TIERS @click.group("zoekt") @@ -831,68 +830,6 @@ def _portable_repo_root(repo_root: str | None) -> Path: return Path(resolve_workspace_root()).resolve() -@code_group.command("export") -@click.option( - "--out", default=None, type=click.Path(path_type=Path), help="Archive path (default: .lemoncrow/index.tar.*)." -) -@click.option( - "--tier", - type=click.Choice(sorted(_PORTABLE_TIERS)), - default="best", - show_default=True, - help="best: smaller archive, slower. fast: quicker, for incremental refresh.", -) -@click.option("--repo-root", default=None, help="Repository root (default: the resolved workspace root).") -@click.option("--json", "as_json", is_flag=True) -def code_export_cmd(out: Path | None, tier: str, repo_root: str | None, as_json: bool) -> None: - """Pack this workspace's code index into a portable archive. - - Compacts each database, records the engine's index and semantics versions - in a manifest, and compresses the bundle. The archive is deliberately not - committed anywhere by default -- it is a build artifact, not source. - """ - from lemoncrow.infra.code_intel.portable import PortableIndexError, export_index - - try: - result = export_index(repo_root=_portable_repo_root(repo_root), out=out, tier=tier) - except PortableIndexError as exc: - raise click.ClickException(str(exc)) from exc - if as_json: - _emit(result.to_dict(), as_json=True) - return - click.echo(f"Wrote {result.path} ({result.size_bytes:,} bytes, {result.codec}, tier={result.tier})") - click.echo(f" databases: {', '.join(result.databases)}") - click.echo(f" engine index_version: {result.manifest['engine_index_version']}") - - -@code_group.command("import") -@click.option("--from", "source", required=True, type=click.Path(path_type=Path), help="Archive to import.") -@click.option("--repo-root", default=None, help="Repository root (default: the resolved workspace root).") -@click.option("--force", is_flag=True, help="Replace an index this workspace already holds.") -@click.option("--json", "as_json", is_flag=True) -def code_import_cmd(source: Path, repo_root: str | None, force: bool, as_json: bool) -> None: - """Restore a code index from an archive built by `lc code export`. - - Refuses on an indexer-semantics mismatch. The engine owns that number and - open code cannot migrate its data, so importing across it would produce a - graph whose edges mean something else -- confident and wrong. Re-index - instead. --force overrides only the already-populated check. - """ - from lemoncrow.infra.code_intel.portable import PortableIndexError, import_index - - try: - result = import_index(archive=source, repo_root=_portable_repo_root(repo_root), force=force) - except PortableIndexError as exc: - raise click.ClickException(str(exc)) from exc - if as_json: - _emit(result.to_dict(), as_json=True) - return - click.echo(f"Restored {len(result.restored)} database(s) into {result.workspace}") - click.echo(f" from: {result.archive}") - click.echo(f" verified against: {result.verified_against}") - click.echo("Run `lc code index` to fill in the local diff.") - - @code_group.command("clones") @click.option( "--threshold", @@ -943,9 +880,8 @@ def code_clones_cmd( min_tokens=MIN_TOKENS if min_tokens is None else min_tokens, ) except (CodeIntelUnavailable, IndexRebuilding) as exc: - # Matches `lc code export` / `import` above: an unindexed workspace, or - # one mid-reindex, is a thing the user can act on, so it gets a one-line - # message rather than a traceback. + # An unindexed workspace, or one mid-reindex, is a thing the user can act + # on, so it gets a one-line message rather than a traceback. raise click.ClickException(str(exc)) from exc if as_json: _emit(report.as_dict(), as_json=True) diff --git a/src/lemoncrow/infra/code_intel/portable.py b/src/lemoncrow/infra/code_intel/portable.py deleted file mode 100644 index 207e9de1d..000000000 --- a/src/lemoncrow/infra/code_intel/portable.py +++ /dev/null @@ -1,560 +0,0 @@ -"""F7 -- move a built index between machines instead of rebuilding it. - -Indexing a large repository costs minutes of CPU that every teammate and every -CI runner pays again from scratch. The databases are already self-contained, so -the useful primitive is small: compact each one, tar them with a manifest, and -refuse to unpack the result anywhere it would produce a subtly wrong graph. - -That refusal is the load-bearing part. The closed engine owns ``index_version`` -and ``indexer_semantics_version``, and open code cannot migrate its data. An -archive built by a different generation of the extractor is not "slightly out of -date" -- it is a graph whose edges mean something else. Importing it would -produce answers that look right and are not, which is worse than not importing -at all. So a mismatch is an error, never a warning. - -The archive is treated as untrusted input on the way back in: every member is -checked against an allow-list of plain filenames, and every database is checked -against the digest recorded at export. A tar file is a file format an attacker -can write, and "it came from a teammate" is not provenance. -""" - -from __future__ import annotations - -import hashlib -import json -import shutil -import sqlite3 -import subprocess -import tarfile -import tempfile -from dataclasses import dataclass -from pathlib import Path -from typing import Any - -from lemoncrow.infra.code_intel.sidecar import SIDECAR_DB -from lemoncrow.infra.code_intel.store import ( - CODE_CONTEXT_DB, - FTS_DB, - INTEL_DB, - REPO_MAP_TAGS_DB, - VECTORS_DB, - workspace_dir, -) - -__all__ = [ - "ARCHIVE_FORMAT_VERSION", - "EXPORTABLE_DBS", - "MANIFEST_NAME", - "TIERS", - "ExportResult", - "ImportResult", - "PortableIndexError", - "available_codec", - "export_index", - "import_index", - "read_manifest", -] - -ARCHIVE_FORMAT_VERSION = 1 -MANIFEST_NAME = "manifest.json" - -#: Everything a workspace needs to answer queries without re-indexing. The five -#: engine databases plus the sidecar we own. Absent files are skipped, not -#: faked: ``vectors.sqlite`` only exists once embeddings have been built. -EXPORTABLE_DBS: tuple[str, ...] = ( - CODE_CONTEXT_DB, - INTEL_DB, - FTS_DB, - VECTORS_DB, - REPO_MAP_TAGS_DB, - SIDECAR_DB, -) - -#: ``best`` for a shared artifact, ``fast`` for an incremental refresh. -TIERS: dict[str, int] = {"best": 9, "fast": 3} - -_MEMBER_NAMES = frozenset({MANIFEST_NAME, *EXPORTABLE_DBS}) -_HASH_CHUNK = 1 << 20 -_GIT_TIMEOUT_S = 10.0 - - -class PortableIndexError(RuntimeError): - """The archive is unusable, or unsafe to unpack here.""" - - -@dataclass(frozen=True) -class ExportResult: - path: Path - codec: str - tier: str - size_bytes: int - databases: tuple[str, ...] - manifest: dict[str, Any] - - def to_dict(self) -> dict[str, Any]: - return { - "path": str(self.path), - "codec": self.codec, - "tier": self.tier, - "size_bytes": self.size_bytes, - "databases": list(self.databases), - "manifest": self.manifest, - } - - -@dataclass(frozen=True) -class ImportResult: - archive: Path - workspace: Path - restored: tuple[str, ...] - manifest: dict[str, Any] - verified_against: str - #: Databases cleared from the workspace that the archive did not replace. - #: Reported rather than silently dropped: their absence changes what the - #: workspace can answer until the engine's next pass rebuilds them. - removed: tuple[str, ...] = () - - def to_dict(self) -> dict[str, Any]: - return { - "archive": str(self.archive), - "workspace": str(self.workspace), - "restored": list(self.restored), - "removed": list(self.removed), - "verified_against": self.verified_against, - "manifest": self.manifest, - } - - -# --------------------------------------------------------------------------- # -# codec -# --------------------------------------------------------------------------- # - - -def available_codec() -> str: - """``"zstd"`` when :mod:`zstandard` is installed, else ``"xz"``. - - zstd is the better codec and lives behind the ``portable`` extra. Falling - back to stdlib lzma keeps export/import working on a base install rather - than making the feature depend on an optional wheel -- the archive names its - codec, and import reads that rather than assuming. - """ - try: - import zstandard # noqa: F401 - except ImportError: - return "xz" - return "zstd" - - -def _suffix(codec: str) -> str: - return ".tar.zst" if codec == "zstd" else ".tar.xz" - - -def _codec_for_archive(path: Path) -> str: - name = path.name.lower() - if name.endswith(".zst") or name.endswith(".zstd"): - return "zstd" - if name.endswith(".xz") or name.endswith(".lzma"): - return "xz" - raise PortableIndexError(f"cannot tell the codec from {path.name!r}; expected .tar.zst or .tar.xz") - - -def _compress(tar_path: Path, out_path: Path, codec: str, level: int) -> None: - if codec == "zstd": - import zstandard - - compressor = zstandard.ZstdCompressor(level=level) - with tar_path.open("rb") as source, out_path.open("wb") as target: - compressor.copy_stream(source, target) - return - import lzma - - with tar_path.open("rb") as source, lzma.open(out_path, "wb", preset=min(level, 9)) as target: - shutil.copyfileobj(source, target) - - -def _decompress(archive: Path, tar_path: Path, codec: str) -> None: - try: - if codec == "zstd": - try: - import zstandard - except ImportError as exc: - raise PortableIndexError( - "this archive is zstd-compressed; install the 'portable' extra (zstandard) to read it" - ) from exc - decompressor = zstandard.ZstdDecompressor() - with archive.open("rb") as source, tar_path.open("wb") as target: - decompressor.copy_stream(source, target) - return - import lzma - - with lzma.open(archive, "rb") as source, tar_path.open("wb") as target: - shutil.copyfileobj(source, target) - except PortableIndexError: - raise - except Exception as exc: - raise PortableIndexError(f"{archive.name} is not a readable {codec} stream: {exc}") from exc - - -# --------------------------------------------------------------------------- # -# helpers -# --------------------------------------------------------------------------- # - - -def _sha256(path: Path) -> str: - digest = hashlib.sha256() - with path.open("rb") as handle: - for chunk in iter(lambda: handle.read(_HASH_CHUNK), b""): - digest.update(chunk) - return digest.hexdigest() - - -def _compact_into(source: Path, target: Path) -> None: - """Copy *source* to *target* as a single compacted file, WAL folded in. - - ``VACUUM INTO`` is read-only with respect to the source and produces a - defragmented copy. Some builds refuse it against a read-only handle, so the - online backup API is the fallback: it costs a little archive size and is - otherwise equivalent. - """ - conn = sqlite3.connect(f"file:{source}?mode=ro", uri=True) - try: - try: - conn.execute("VACUUM INTO ?", (str(target),)) - return - except sqlite3.Error: - target.unlink(missing_ok=True) - backup = sqlite3.connect(target) - try: - conn.backup(backup) - finally: - backup.close() - except sqlite3.Error as exc: - raise PortableIndexError(f"cannot read {source.name}: {exc}") from exc - finally: - conn.close() - - -def _engine_versions(workspace: Path) -> tuple[int, int]: - db = workspace / CODE_CONTEXT_DB - if not db.exists(): - return (0, 0) - try: - conn = sqlite3.connect(f"file:{db}?mode=ro", uri=True) - except sqlite3.Error: # pragma: no cover - permission dependent - return (0, 0) - try: - rows = dict(conn.execute("SELECT key, value FROM engine_state").fetchall()) - except sqlite3.Error: - return (0, 0) - finally: - conn.close() - - def _as_int(key: str) -> int: - try: - return int(str(rows.get(key, 0))) - except ValueError: - return 0 - - return (_as_int("index_version"), _as_int("indexer_semantics_version")) - - -def _row_counts(workspace: Path) -> dict[str, int]: - counts: dict[str, int] = {} - for db, tables in ((CODE_CONTEXT_DB, ("files", "symbols", "imports")), (INTEL_DB, ("call_edges", "references"))): - path = workspace / db - if not path.exists(): - continue - try: - conn = sqlite3.connect(f"file:{path}?mode=ro", uri=True) - except sqlite3.Error: # pragma: no cover - permission dependent - continue - try: - for table in tables: - quoted = f'"{table}"' if table == "references" else table - try: - counts[table] = int(conn.execute(f"SELECT COUNT(*) FROM {quoted}").fetchone()[0]) - except sqlite3.Error: - continue - finally: - conn.close() - return counts - - -def _sidecar_schema_version(workspace: Path) -> int | None: - path = workspace / SIDECAR_DB - if not path.exists(): - return None - try: - conn = sqlite3.connect(f"file:{path}?mode=ro", uri=True) - except sqlite3.Error: # pragma: no cover - permission dependent - return None - try: - return int(conn.execute("PRAGMA user_version").fetchone()[0]) - except sqlite3.Error: # pragma: no cover - corrupt sidecar - return None - finally: - conn.close() - - -def _repo_head(repo_root: Path) -> str | None: - try: - result = subprocess.run( - ["git", "rev-parse", "HEAD"], - cwd=repo_root, - capture_output=True, - text=True, - check=False, - timeout=_GIT_TIMEOUT_S, - ) - except (OSError, subprocess.SubprocessError): - return None - head = result.stdout.strip() - return head if result.returncode == 0 and head else None - - -# --------------------------------------------------------------------------- # -# export -# --------------------------------------------------------------------------- # - - -def export_index( - repo_root: Path | str = ".", - out: Path | str | None = None, - tier: str = "best", -) -> ExportResult: - """Compact this workspace's index into a single portable archive. - - The two tiers differ only in compression level. The plan also called for - ``best`` to drop derived indexes; it does not, because the engine's DDL is - closed and an index we drop is one we cannot recreate -- the import would - hand back a database the engine expects to be complete. - """ - if tier not in TIERS: - raise PortableIndexError(f"unknown tier {tier!r}; expected one of {', '.join(sorted(TIERS))}") - root = Path(repo_root).resolve() - workspace = workspace_dir(root) - if not (workspace / CODE_CONTEXT_DB).exists(): - raise PortableIndexError(f"nothing to export: {workspace / CODE_CONTEXT_DB} does not exist") - - codec = available_codec() - destination = Path(out).resolve() if out is not None else workspace.parent / f"index{_suffix(codec)}" - destination.parent.mkdir(parents=True, exist_ok=True) - - from lemoncrow import __version__ as lemoncrow_version - - index_version, semantics_version = _engine_versions(workspace) - with tempfile.TemporaryDirectory(prefix="lemoncrow-index-export-") as tmp: - staging = Path(tmp) - present: list[str] = [] - databases: dict[str, dict[str, Any]] = {} - for name in EXPORTABLE_DBS: - source = workspace / name - if not source.exists(): - continue - compacted = staging / name - _compact_into(source, compacted) - present.append(name) - databases[name] = {"bytes": compacted.stat().st_size, "sha256": _sha256(compacted)} - - manifest: dict[str, Any] = { - "format_version": ARCHIVE_FORMAT_VERSION, - "lemoncrow_version": lemoncrow_version, - "engine_index_version": index_version, - "indexer_semantics_version": semantics_version, - "sidecar_schema_version": _sidecar_schema_version(workspace), - "repo_head": _repo_head(root), - "codec": codec, - "tier": tier, - "databases": databases, - "row_counts": _row_counts(workspace), - } - (staging / MANIFEST_NAME).write_text(json.dumps(manifest, indent=2, sort_keys=True), encoding="utf-8") - - tar_path = staging / "bundle.tar" - with tarfile.open(tar_path, "w") as tar: - for name in (MANIFEST_NAME, *present): - tar.add(staging / name, arcname=name) - _compress(tar_path, destination, codec, TIERS[tier]) - - return ExportResult( - path=destination, - codec=codec, - tier=tier, - size_bytes=destination.stat().st_size, - databases=tuple(present), - manifest=manifest, - ) - - -# --------------------------------------------------------------------------- # -# import -# --------------------------------------------------------------------------- # - - -def _safe_extract(tar: tarfile.TarFile, into: Path) -> list[str]: - """Extract only plain files whose names are on the allow-list. - - A tar archive can name ``../../etc/whatever``, or be a symlink pointing - anywhere on the filesystem. This index format has a fixed, flat membership, - so anything else is refused outright rather than sanitised -- there is no - legitimate archive that needs the general case. - """ - extracted: list[str] = [] - for member in tar.getmembers(): - if not member.isfile(): - raise PortableIndexError(f"archive member {member.name!r} is not a regular file") - if member.name not in _MEMBER_NAMES: - raise PortableIndexError(f"unexpected archive member {member.name!r}") - source = tar.extractfile(member) - if source is None: # pragma: no cover - unreachable for isfile() members - raise PortableIndexError(f"archive member {member.name!r} could not be read") - with source, (into / member.name).open("wb") as target: - shutil.copyfileobj(source, target) - extracted.append(member.name) - return extracted - - -def _open_archive(archive: Path, staging: Path) -> list[str]: - codec = _codec_for_archive(archive) - tar_path = staging / "bundle.tar" - _decompress(archive, tar_path, codec) - try: - with tarfile.open(tar_path, "r") as tar: - return _safe_extract(tar, staging) - except PortableIndexError: - raise - except tarfile.TarError as exc: - raise PortableIndexError(f"{archive.name} is not a readable tar archive: {exc}") from exc - - -def _load_manifest(staging: Path) -> dict[str, Any]: - path = staging / MANIFEST_NAME - if not path.exists(): - raise PortableIndexError(f"archive has no {MANIFEST_NAME}") - try: - manifest = json.loads(path.read_text(encoding="utf-8")) - except (OSError, ValueError) as exc: - raise PortableIndexError(f"{MANIFEST_NAME} is not readable JSON: {exc}") from exc - if not isinstance(manifest, dict): - raise PortableIndexError(f"{MANIFEST_NAME} is not an object") - if manifest.get("format_version") != ARCHIVE_FORMAT_VERSION: - raise PortableIndexError( - f"archive format version {manifest.get('format_version')!r} " - f"is not {ARCHIVE_FORMAT_VERSION}; this LemonCrow cannot read it" - ) - return manifest - - -def read_manifest(archive: Path | str) -> dict[str, Any]: - """The archive's manifest, without unpacking anything into a workspace.""" - path = Path(archive).resolve() - if not path.exists(): - raise PortableIndexError(f"{path} does not exist") - with tempfile.TemporaryDirectory(prefix="lemoncrow-index-manifest-") as tmp: - staging = Path(tmp) - _open_archive(path, staging) - return _load_manifest(staging) - - -def import_index( - archive: Path | str, - repo_root: Path | str = ".", - force: bool = False, -) -> ImportResult: - """Unpack *archive* into this workspace, or refuse and say why. - - Refuses when the target workspace already holds an index whose - ``indexer_semantics_version`` differs from the archive's. The engine owns - that number and open code cannot migrate its data, so a mismatched import - does not yield a stale graph -- it yields one whose edges mean something - else, answering confidently and wrongly. *force* overrides the - already-populated check, never the semantics check. - """ - path = Path(archive).resolve() - if not path.exists(): - raise PortableIndexError(f"{path} does not exist") - root = Path(repo_root).resolve() - - with tempfile.TemporaryDirectory(prefix="lemoncrow-index-import-") as tmp: - staging = Path(tmp) - members = _open_archive(path, staging) - manifest = _load_manifest(staging) - - databases = manifest.get("databases") - if not isinstance(databases, dict) or not databases: - raise PortableIndexError(f"{MANIFEST_NAME} lists no databases") - - for name, meta in databases.items(): - if name not in members: - raise PortableIndexError(f"{MANIFEST_NAME} lists {name!r} but the archive does not contain it") - expected = str(meta.get("sha256", "")) - if not expected: - # The manifest travels inside the archive it vouches for, so a - # missing digest is not a gap in the check -- it IS the attack, - # and bit-rot looks identical. Export always writes one, so an - # absent digest is never a legitimate archive from this codebase. - raise PortableIndexError( - f"{name} has no recorded digest in {MANIFEST_NAME}; refusing to import it unverified" - ) - actual = _sha256(staging / name) - if actual != expected: - raise PortableIndexError( - f"{name} failed its digest check (expected {expected[:12]}, got {actual[:12]}); " - "the archive is corrupt or was modified" - ) - - # Resolve the target only after the archive has passed every check. - # Resolving the store dir creates ``.lemoncrow/`` in the target (it - # self-ignores on first resolve), and a refused archive must leave the - # target untouched. - workspace = workspace_dir(root) - local_index_version, local_semantics = _engine_versions(workspace) - archive_semantics = int(manifest.get("indexer_semantics_version") or 0) - populated = (workspace / CODE_CONTEXT_DB).exists() - - if populated and local_semantics and archive_semantics and local_semantics != archive_semantics: - raise PortableIndexError( - f"indexer_semantics_version mismatch: archive {archive_semantics}, " - f"workspace {local_semantics}. The engine owns this number and open code cannot " - "migrate its data; re-index instead of importing." - ) - if populated and not force: - raise PortableIndexError( - f"{workspace} already holds an index (index_version {local_index_version}); " "pass force to replace it" - ) - verified_against = f"workspace indexer_semantics_version {local_semantics}" if populated else "empty workspace" - - workspace.mkdir(parents=True, exist_ok=True) - restored: list[str] = [] - removed: list[str] = [] - for name in EXPORTABLE_DBS: - target = workspace / name - # Clear every database first, including ones the archive does not - # carry. A source that never built embeddings or never ran the - # call-graph pass exports without vectors.sqlite / intel.sqlite, and - # leaving the local copies behind pairs a fresh code_context.sqlite - # with sidecars keyed to a superseded generation's symbol ids -- - # edges that mean something else, which is the exact failure the - # semantics check exists to prevent. It cannot catch this one: it - # reads only code_context.sqlite, and that file *is* replaced. - # - # WAL/SHM siblings describe the file being replaced, not the new one. - for sibling in (f"{name}-wal", f"{name}-shm"): - (workspace / sibling).unlink(missing_ok=True) - if name not in databases: - if target.exists(): - target.unlink() - removed.append(name) - continue - target.unlink(missing_ok=True) - shutil.move(str(staging / name), str(target)) - restored.append(name) - - return ImportResult( - archive=path, - workspace=workspace, - restored=tuple(restored), - removed=tuple(removed), - manifest=manifest, - verified_against=verified_against, - ) diff --git a/tests/infra/code_intel/test_clones.py b/tests/infra/code_intel/test_clones.py index 457c86d30..628a271a7 100644 --- a/tests/infra/code_intel/test_clones.py +++ b/tests/infra/code_intel/test_clones.py @@ -694,7 +694,7 @@ def test_the_mcp_tool_lets_stale_propagate_rather_than_calling_it_a_bad_argument def test_the_cli_reports_an_unindexed_workspace_without_a_traceback(tmp_path: Path) -> None: - """Matches `lc code export` / `import`, which sit directly above it.""" + """An unindexed workspace is a state the user can act on, so it gets a message, not a traceback.""" from click.testing import CliRunner from lemoncrow.gateway.cli.commands.code import code_group diff --git a/tests/infra/code_intel/test_portable.py b/tests/infra/code_intel/test_portable.py deleted file mode 100644 index a4732c059..000000000 --- a/tests/infra/code_intel/test_portable.py +++ /dev/null @@ -1,433 +0,0 @@ -"""Portable index archives: round-trip, refusal, and hostile input (F7). - -The interesting assertions are the refusals. An archive is a file format someone -else can write, and an index imported across an extractor-semantics boundary -does not produce a stale graph -- it produces one whose edges mean something -else, which answers confidently and wrongly. So a mismatch, a bad digest, and a -member named ``../../etc/passwd`` all have to be errors, not warnings. -""" - -from __future__ import annotations - -import io -import json -import lzma -import tarfile -from collections.abc import Callable, Mapping -from pathlib import Path - -import pytest - -from lemoncrow.infra.code_intel.portable import ( - ARCHIVE_FORMAT_VERSION, - MANIFEST_NAME, - PortableIndexError, - available_codec, - export_index, - import_index, - read_manifest, -) -from lemoncrow.infra.code_intel.store import ( - CODE_CONTEXT_DB, - INTEL_DB, - CodeIntelStore, - workspace_dir, -) - -WorkspaceFactory = Callable[..., Path] - -_FILES = [{"file_path": "src/a.py"}, {"file_path": "src/b.py"}] -_SYMBOLS = [ - {"file_path": "src/a.py", "symbol_name": "alpha", "kind": "function"}, - {"file_path": "src/a.py", "symbol_name": "beta", "kind": "function"}, - {"file_path": "src/b.py", "symbol_name": "Gamma", "kind": "class"}, -] -_IMPORTS = [{"source_file": "src/b.py", "raw_import": "a", "target_file": "src/a.py"}] -_EDGES = [{"caller_symbol_name": "Gamma", "caller_file_path": "src/b.py", "callee_name": "alpha"}] -_REFERENCES = [{"symbol_name": "alpha", "file_path": "src/b.py", "line": 4}] - - -@pytest.fixture -def source_repo(make_workspace: WorkspaceFactory) -> Path: - return make_workspace( - files=_FILES, - symbols=_SYMBOLS, - imports=_IMPORTS, - call_edges=_EDGES, - references=_REFERENCES, - index_version=31, - indexer_semantics_version=2, - name="source", - ) - - -def _snapshot(root: Path) -> dict[str, object]: - with CodeIntelStore(root) as store: - snapshot = store.snapshot() - return { - "files": snapshot.files, - "symbols": snapshot.symbols, - "imports": snapshot.imports, - "call_edges": snapshot.call_edges, - "references": snapshot.references, - "index_version": snapshot.index_version, - "names": sorted(row.symbol_name for row in store.symbols()), - } - - -def _archive_with(tmp_path: Path, members: Mapping[str, bytes], name: str = "crafted.tar.xz") -> Path: - """Hand-build an archive so the extractor's refusals can be exercised.""" - raw = io.BytesIO() - with tarfile.open(fileobj=raw, mode="w") as tar: - for arcname, payload in members.items(): - info = tarfile.TarInfo(arcname) - info.size = len(payload) - tar.addfile(info, io.BytesIO(payload)) - path = tmp_path / name - with lzma.open(path, "wb") as handle: - handle.write(raw.getvalue()) - return path - - -# --------------------------------------------------------------------------- # -# round trip -# --------------------------------------------------------------------------- # - - -def test_round_trip_preserves_row_counts_and_query_results( - source_repo: Path, - tmp_path: Path, -) -> None: - before = _snapshot(source_repo) - result = export_index(repo_root=source_repo) - assert result.path.exists() - assert result.size_bytes > 0 - assert CODE_CONTEXT_DB in result.databases - - clone = tmp_path / "clone" - imported = import_index(archive=result.path, repo_root=clone) - - assert CODE_CONTEXT_DB in imported.restored - assert imported.verified_against == "empty workspace" - assert _snapshot(clone) == before - - -def test_round_trip_survives_a_sampled_query(source_repo: Path, tmp_path: Path) -> None: - archive = export_index(repo_root=source_repo).path - clone = tmp_path / "clone" - import_index(archive=archive, repo_root=clone) - - with CodeIntelStore(clone) as store: - assert [row.symbol_name for row in store.symbols(kind="class")] == ["Gamma"] - assert [edge.caller_symbol_name for edge in store.call_edges(callee_name="alpha")] == ["Gamma"] - assert [ref.file_path for ref in store.references(symbol_name="alpha")] == ["src/b.py"] - - -def test_export_writes_beside_the_workspace_by_default(source_repo: Path) -> None: - result = export_index(repo_root=source_repo) - assert result.path.parent == workspace_dir(source_repo).parent - assert result.path.name.startswith("index.tar.") - - -def test_export_honours_an_explicit_out_path(source_repo: Path, tmp_path: Path) -> None: - destination = tmp_path / "nested" / f"custom.tar.{'zst' if available_codec() == 'zstd' else 'xz'}" - result = export_index(repo_root=source_repo, out=destination) - assert result.path == destination.resolve() - assert destination.exists() - - -def test_both_tiers_produce_importable_archives(source_repo: Path, tmp_path: Path) -> None: - suffix = "zst" if available_codec() == "zstd" else "xz" - for index, tier in enumerate(("best", "fast")): - archive = export_index(repo_root=source_repo, out=tmp_path / f"t{index}.tar.{suffix}", tier=tier) - assert archive.tier == tier - clone = tmp_path / f"clone{index}" - import_index(archive=archive.path, repo_root=clone) - assert _snapshot(clone)["symbols"] == 3 - - -# --------------------------------------------------------------------------- # -# manifest -# --------------------------------------------------------------------------- # - - -def test_manifest_records_the_provenance_an_importer_needs(source_repo: Path) -> None: - manifest = export_index(repo_root=source_repo).manifest - assert manifest["format_version"] == ARCHIVE_FORMAT_VERSION - assert manifest["engine_index_version"] == 31 - assert manifest["indexer_semantics_version"] == 2 - assert manifest["row_counts"]["symbols"] == 3 - assert manifest["row_counts"]["references"] == 1 - assert manifest["codec"] == available_codec() - assert set(manifest["databases"]) <= set(export_index(repo_root=source_repo).databases) - assert manifest["lemoncrow_version"] - - -def test_read_manifest_does_not_touch_a_workspace(source_repo: Path, tmp_path: Path) -> None: - archive = export_index(repo_root=source_repo).path - clone = tmp_path / "untouched" - manifest = read_manifest(archive) - assert manifest["engine_index_version"] == 31 - assert not clone.exists() - - -# --------------------------------------------------------------------------- # -# refusal -# --------------------------------------------------------------------------- # - - -def test_export_refuses_an_unindexed_workspace(tmp_path: Path) -> None: - with pytest.raises(PortableIndexError, match="nothing to export"): - export_index(repo_root=tmp_path / "never-indexed") - - -def test_export_refuses_an_unknown_tier(source_repo: Path) -> None: - with pytest.raises(PortableIndexError, match="unknown tier"): - export_index(repo_root=source_repo, tier="maximum") - - -def test_import_refuses_a_semantics_mismatch( - source_repo: Path, - make_workspace: WorkspaceFactory, -) -> None: - archive = export_index(repo_root=source_repo).path - target = make_workspace( - files=_FILES, - symbols=_SYMBOLS, - indexer_semantics_version=99, - name="target", - ) - with pytest.raises(PortableIndexError, match="indexer_semantics_version mismatch"): - import_index(archive=archive, repo_root=target) - - -def test_force_does_not_override_a_semantics_mismatch( - source_repo: Path, - make_workspace: WorkspaceFactory, -) -> None: - """force is for "replace what is here", never for "ignore what it means".""" - archive = export_index(repo_root=source_repo).path - target = make_workspace(files=_FILES, indexer_semantics_version=99, name="target") - with pytest.raises(PortableIndexError, match="indexer_semantics_version mismatch"): - import_index(archive=archive, repo_root=target, force=True) - - -def test_import_refuses_a_populated_workspace_without_force( - source_repo: Path, - make_workspace: WorkspaceFactory, -) -> None: - archive = export_index(repo_root=source_repo).path - target = make_workspace(files=_FILES, symbols=_SYMBOLS, indexer_semantics_version=2, name="target") - with pytest.raises(PortableIndexError, match="already holds an index"): - import_index(archive=archive, repo_root=target) - - -def test_force_replaces_a_matching_workspace( - source_repo: Path, - make_workspace: WorkspaceFactory, -) -> None: - archive = export_index(repo_root=source_repo).path - target = make_workspace( - files=[{"file_path": "only.py"}], - symbols=[{"file_path": "only.py", "symbol_name": "solo"}], - index_version=4, - indexer_semantics_version=2, - name="target", - ) - result = import_index(archive=archive, repo_root=target, force=True) - assert "indexer_semantics_version 2" in result.verified_against - assert _snapshot(target)["names"] == ["Gamma", "alpha", "beta"] - - -def test_import_refuses_a_missing_archive(tmp_path: Path) -> None: - with pytest.raises(PortableIndexError, match="does not exist"): - import_index(archive=tmp_path / "nope.tar.xz", repo_root=tmp_path) - - -def test_import_refuses_an_unrecognised_extension(source_repo: Path, tmp_path: Path) -> None: - archive = export_index(repo_root=source_repo).path - renamed = tmp_path / "index.tar.gz" - renamed.write_bytes(archive.read_bytes()) - with pytest.raises(PortableIndexError, match="cannot tell the codec"): - import_index(archive=renamed, repo_root=tmp_path / "clone") - - -# --------------------------------------------------------------------------- # -# corrupt and hostile input -# --------------------------------------------------------------------------- # - - -def test_truncated_archive_is_an_error(source_repo: Path, tmp_path: Path) -> None: - archive = export_index(repo_root=source_repo).path - broken = tmp_path / "broken.tar.xz" - broken.write_bytes(archive.read_bytes()[: max(1, archive.stat().st_size // 2)]) - with pytest.raises(PortableIndexError): - import_index(archive=broken, repo_root=tmp_path / "clone") - - -def test_garbage_bytes_are_an_error(tmp_path: Path) -> None: - garbage = tmp_path / "garbage.tar.xz" - garbage.write_bytes(b"this is not a compressed tar archive" * 10) - with pytest.raises(PortableIndexError, match="not a readable"): - import_index(archive=garbage, repo_root=tmp_path / "clone") - - -def test_missing_manifest_is_an_error(tmp_path: Path) -> None: - archive = _archive_with(tmp_path, {CODE_CONTEXT_DB: b"\x00" * 16}) - with pytest.raises(PortableIndexError, match=f"no {MANIFEST_NAME}"): - import_index(archive=archive, repo_root=tmp_path / "clone") - - -def test_unreadable_manifest_is_an_error(tmp_path: Path) -> None: - archive = _archive_with(tmp_path, {MANIFEST_NAME: b"{not json"}) - with pytest.raises(PortableIndexError, match="not readable JSON"): - import_index(archive=archive, repo_root=tmp_path / "clone") - - -def test_future_format_version_is_refused(tmp_path: Path) -> None: - manifest = json.dumps({"format_version": ARCHIVE_FORMAT_VERSION + 1}).encode() - archive = _archive_with(tmp_path, {MANIFEST_NAME: manifest}) - with pytest.raises(PortableIndexError, match="format version"): - import_index(archive=archive, repo_root=tmp_path / "clone") - - -def test_manifest_without_databases_is_refused(tmp_path: Path) -> None: - manifest = json.dumps({"format_version": ARCHIVE_FORMAT_VERSION, "databases": {}}).encode() - archive = _archive_with(tmp_path, {MANIFEST_NAME: manifest}) - with pytest.raises(PortableIndexError, match="lists no databases"): - import_index(archive=archive, repo_root=tmp_path / "clone") - - -def test_manifest_listing_an_absent_database_is_refused(tmp_path: Path) -> None: - manifest = json.dumps( - {"format_version": ARCHIVE_FORMAT_VERSION, "databases": {CODE_CONTEXT_DB: {"sha256": ""}}} - ).encode() - archive = _archive_with(tmp_path, {MANIFEST_NAME: manifest}) - with pytest.raises(PortableIndexError, match="does not contain it"): - import_index(archive=archive, repo_root=tmp_path / "clone") - - -def test_manifest_without_a_digest_is_refused(tmp_path: Path) -> None: - """The manifest travels inside the archive it vouches for. - - A missing digest is not a gap in the check, it IS the attack -- and bit-rot - looks identical. Export always writes one, so an absent digest is never a - legitimate archive from this codebase. - """ - for meta in ({"bytes": 32}, {"sha256": ""}): - manifest = json.dumps( - { - "format_version": ARCHIVE_FORMAT_VERSION, - "indexer_semantics_version": 2, - "databases": {CODE_CONTEXT_DB: meta}, - } - ).encode() - archive = _archive_with( - tmp_path, - {MANIFEST_NAME: manifest, CODE_CONTEXT_DB: b"\x00" * 32}, - name=f"nodigest{len(str(meta))}.tar.xz", - ) - with pytest.raises(PortableIndexError, match="no recorded digest"): - import_index(archive=archive, repo_root=tmp_path / "clone") - - -def test_tampered_database_fails_its_digest(tmp_path: Path) -> None: - payload = b"\x00" * 32 - manifest = json.dumps( - { - "format_version": ARCHIVE_FORMAT_VERSION, - "indexer_semantics_version": 2, - "databases": {CODE_CONTEXT_DB: {"sha256": "0" * 64}}, - } - ).encode() - archive = _archive_with(tmp_path, {MANIFEST_NAME: manifest, CODE_CONTEXT_DB: payload}) - with pytest.raises(PortableIndexError, match="failed its digest check"): - import_index(archive=archive, repo_root=tmp_path / "clone") - - -@pytest.mark.parametrize( - "hostile_name", - [ - "../../etc/passwd", - "/etc/passwd", - "nested/code_context.sqlite", - "code_context.sqlite.bak", - ], -) -def test_members_outside_the_allow_list_are_refused(tmp_path: Path, hostile_name: str) -> None: - """The format has a fixed, flat membership; anything else is refused outright.""" - archive = _archive_with(tmp_path, {hostile_name: b"payload"}, name="hostile.tar.xz") - with pytest.raises(PortableIndexError, match="unexpected archive member"): - import_index(archive=archive, repo_root=tmp_path / "clone") - assert not (tmp_path / "clone").exists() - - -def test_symlink_members_are_refused(tmp_path: Path) -> None: - raw = io.BytesIO() - with tarfile.open(fileobj=raw, mode="w") as tar: - info = tarfile.TarInfo(MANIFEST_NAME) - info.type = tarfile.SYMTYPE - info.linkname = "/etc/passwd" - tar.addfile(info) - archive = tmp_path / "symlink.tar.xz" - with lzma.open(archive, "wb") as handle: - handle.write(raw.getvalue()) - - with pytest.raises(PortableIndexError, match="not a regular file"): - import_index(archive=archive, repo_root=tmp_path / "clone") - - -def test_force_import_clears_databases_the_archive_does_not_carry( - make_workspace: WorkspaceFactory, - tmp_path: Path, -) -> None: - """A surviving sidecar is keyed to the generation it was built against. - - A source that never ran the call-graph pass exports without intel.sqlite. - Leaving the local copy behind pairs a fresh code_context.sqlite with call - edges pointing at another index's symbol ids -- edges that mean something - else, which is the failure the semantics check exists to prevent and cannot - catch here, because it reads only code_context.sqlite and that file IS - replaced. - """ - source = make_workspace( - files=_FILES, - symbols=_SYMBOLS, - indexer_semantics_version=2, - with_intel=False, - name="nointel", - ) - archive = export_index(repo_root=source, out=tmp_path / "nointel.tar.xz").path - assert INTEL_DB not in read_manifest(archive)["databases"] - - target = make_workspace( - files=_FILES, - symbols=_SYMBOLS, - call_edges=_EDGES, - indexer_semantics_version=2, - name="target", - ) - stale = workspace_dir(target) / INTEL_DB - assert stale.exists() - - result = import_index(archive=archive, repo_root=target, force=True) - - assert not stale.exists(), "a superseded sidecar survived the import" - assert INTEL_DB in result.removed - assert CODE_CONTEXT_DB in result.restored - - -def test_a_refused_import_leaves_the_target_untouched( - source_repo: Path, - make_workspace: WorkspaceFactory, -) -> None: - archive = export_index(repo_root=source_repo).path - target = make_workspace( - files=[{"file_path": "only.py"}], - symbols=[{"file_path": "only.py", "symbol_name": "solo"}], - indexer_semantics_version=99, - name="target", - ) - with pytest.raises(PortableIndexError): - import_index(archive=archive, repo_root=target, force=True) - assert _snapshot(target)["names"] == ["solo"] diff --git a/uv.lock b/uv.lock index 5606dc939..ec7d9dde6 100644 --- a/uv.lock +++ b/uv.lock @@ -2693,9 +2693,6 @@ ortools = [ parsers = [ { name = "tree-sitter-languages", marker = "python_full_version < '3.13' or (extra == 'extra-9-lemoncrow-langfuse' and extra == 'extra-9-lemoncrow-memory-server')" }, ] -portable = [ - { name = "zstandard" }, -] postgres = [ { name = "psycopg", extra = ["binary"] }, ] @@ -2799,9 +2796,8 @@ requires-dist = [ { name = "uvicorn", extras = ["standard"], specifier = ">=0.46.0" }, { name = "yarl", specifier = "==1.24.2" }, { name = "zipp", specifier = "==4.1.0" }, - { name = "zstandard", marker = "extra == 'portable'", specifier = ">=0.22" }, ] -provides-extras = ["mcp", "memory", "memory-server", "smart", "cloud", "postgres", "vector", "semantic", "parsers", "portable", "rename", "ortools", "litellm", "langfuse", "dev"] +provides-extras = ["mcp", "memory", "memory-server", "smart", "cloud", "postgres", "vector", "semantic", "parsers", "rename", "ortools", "litellm", "langfuse", "dev"] [package.metadata.requires-dev] dev = [ From 7d4ccc559496a38cb0b9d90c9d939dbe6412e7c3 Mon Sep 17 00:00:00 2001 From: Kris Wong Date: Wed, 16 Sep 2026 17:50:50 -0500 Subject: [PATCH 2/2] fix(code-intel): the removal's last two stale references (PLN-2027 PR 7) Review of PR 7 (cr C7, 1 verified finding, approved). `_portable_repo_root` -> `_resolve_repo_root`. 575aee77 argued the name could stay because `lc code clones` calls it, but the name is the one reference a prose edit cannot reach: it still asserts a portable-index concept the same commit deleted, and the hazard is a later grep-driven sweep of "portable" leftovers taking the helper with it and breaking `lc code clones`. Module-private, one call site at code.py:878, no test names it. F7's provenance clause pointed at a commit that never touched the file. 8235fdaf carries the code, untrusted-input handling included; the delivery notes and both deviations were added by 8d77d599 and removed here, so the recovery path is `git show 8d77d599:docs/planning/open-code-intel-plan.md`. --- docs/planning/open-code-intel-plan.md | 6 +++--- src/lemoncrow/gateway/cli/commands/code.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/planning/open-code-intel-plan.md b/docs/planning/open-code-intel-plan.md index 8ff871798..986e36974 100644 --- a/docs/planning/open-code-intel-plan.md +++ b/docs/planning/open-code-intel-plan.md @@ -448,9 +448,9 @@ that would read as "this code has no duplicates". `lc code` subcommands that packed a workspace's index into an archive and restored it — and removed again under PRD-739 FR15 (PLN-2027 PR 7): the fork added the surface, upstream never shipped it, and nothing consumed it. The -delivery notes, the two deviations recorded against them, and the archive's -untrusted-input handling went with the code; `git show 8235fdaf` still carries -them. +code, including the archive's untrusted-input handling, is at `8235fdaf`; the +delivery notes and the two deviations recorded against them are at +`git show 8d77d599:docs/planning/open-code-intel-plan.md`. --- diff --git a/src/lemoncrow/gateway/cli/commands/code.py b/src/lemoncrow/gateway/cli/commands/code.py index 03fee0532..a199e97ae 100644 --- a/src/lemoncrow/gateway/cli/commands/code.py +++ b/src/lemoncrow/gateway/cli/commands/code.py @@ -822,7 +822,7 @@ def _entry_age_days(entry: Path, now: float) -> float: return max(0.0, (now - newest) / 86_400.0) -def _portable_repo_root(repo_root: str | None) -> Path: +def _resolve_repo_root(repo_root: str | None) -> Path: if repo_root is not None: return Path(repo_root).expanduser().resolve() from lemoncrow.core.foundation.paths import resolve_workspace_root @@ -875,7 +875,7 @@ def code_clones_cmd( try: report = build_clones( - repo_root=_portable_repo_root(repo_root), + repo_root=_resolve_repo_root(repo_root), threshold=DEFAULT_THRESHOLD if threshold is None else threshold, min_tokens=MIN_TOKENS if min_tokens is None else min_tokens, )