Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ build-backend = "maturin"
# distribution costs users an uninstall/reinstall for no benefit.
# Distribution name only: the import is unchanged, `import intentumdiff`.
name = "intentumdiff-python"
version = "0.0.1"
version = "0.0.2b1"
description = "Semantic code review: detect intent, moves, refactorings, and style changes"
readme = "README.md"
requires-python = ">=3.12"
Expand Down
11 changes: 10 additions & 1 deletion scripts/provision_build_inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,16 @@

REPO_ROOT = Path(__file__).resolve().parents[1]
CORE_REPO = "https://github.com/buchochelliq-labs/intentumdiff-core"
CORE_REF = "main" # pin to a tag once intentumdiff-core cuts releases
# Which intentumdiff-core to build against.
#
# NOT "main". `main` only moves when a release is cut, so a consumer pinned to it cannot verify
# against an unreleased engine change — which is precisely what a release candidate exists to
# allow. That gap is not theoretical: the provenance tests here assert facts added to the core
# and failed in CI while passing locally, because CI was building an engine that predated them.
#
# Tracking a branch does make the build unreproducible, so this becomes a TAG the moment core
# cuts one. Override for a one-off build without editing the file.
CORE_REF = os.environ.get("INTENTUMDIFF_CORE_REF", "release/v0.0.2-rc")
CORE_DEST = REPO_ROOT / "build" / "intentumdiff-core"
WASM_DEST = REPO_ROOT / "src" / "intentumdiff" / "wasm"

Expand Down
171 changes: 171 additions & 0 deletions scripts/smoke_published_wheel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
"""Install the PUBLISHED wheel into a clean venv and use it as a user would.

# Why this exists

IntentumDiff 0.0.1 shipped broken and had to be pulled. Every invocation printed ~69
"Failed to catalog parser plugin" errors, because the distribution name
(`intentumdiff-python`) was missing from the package's own first-party trust list, so all
69 built-in parsers were refused as untrusted third-party code.

**No test in the repo could have caught it.** In a source checkout the distribution resolves
differently, so the trust check passed. The bug only exists in an installed wheel — and
nothing in CI ever installed one and ran it.

That is the gap this closes: CI proves the code builds; this proves the ARTEFACT works.

# What it checks

Everything a new user does in their first five minutes, in order:

1. `pip install` from the index into an empty venv
2. `import intentumdiff`
3. the console script runs
4. `python -m intentumdiff` works
5. a real diff produces a real result
6. **stderr is CLEAN** — the check that would have caught 0.0.1
7. every URL in the error catalogue actually resolves

Usage:
python scripts/smoke_published_wheel.py # from PyPI
python scripts/smoke_published_wheel.py --wheel dist/x.whl # a local build, pre-publish
"""

from __future__ import annotations

import argparse
import shutil
import subprocess
import sys
import tempfile
import venv
from pathlib import Path

# A diff with an obvious right answer: a guard clause is added, so exactly one change.
OLD_SRC = "def greet(name):\n return 'hi ' + name\n"
NEW_SRC = "def greet(name):\n if not name:\n return None\n return 'hi ' + name\n"

USE_SCRIPT = """
from intentumdiff import SemanticDiffer
old = {old!r}
new = {new!r}
diff = SemanticDiffer().diff_strings(old, new, "example.py")
print("CHANGES", len(diff.changes))
"""


class Smoke:
def __init__(self, root: Path) -> None:
self.root = root
self.py = (
root / ".venv" / ("Scripts" if sys.platform == "win32" else "bin")
/ ("python.exe" if sys.platform == "win32" else "python")
)
self.failures: list[str] = []

def check(self, name: str, ok: bool, detail: str = "") -> None:
print(f" {'PASS' if ok else 'FAIL'} {name}")
if not ok:
if detail:
print(f" {detail.strip()[:400]}")
self.failures.append(name)

def run(self, *args: str, **kw) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[str(self.py), *args], capture_output=True, text=True,
encoding="utf-8", errors="replace", cwd=self.root, **kw
)


def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--wheel", help="local wheel or sdist; defaults to installing from PyPI")
ap.add_argument("--package", default="intentumdiff-python")
args = ap.parse_args()

root = Path(tempfile.mkdtemp(prefix="intentumdiff-smoke-"))
print(f" clean environment: {root}\n")
try:
venv.create(root / ".venv", with_pip=True)
s = Smoke(root)

# 1. Install exactly as a user would.
target = args.wheel or args.package
r = s.run("-m", "pip", "install", "--quiet", target)
s.check(f"pip install {target}", r.returncode == 0, r.stderr)
if r.returncode != 0:
return report(s)

# 2. Import.
r = s.run("-c", "import intentumdiff; print(intentumdiff.__version__)")
s.check("import intentumdiff", r.returncode == 0, r.stderr)
version = r.stdout.strip()

# 3. Console script — the documented entry point.
exe = s.py.parent / ("intentumdiff.exe" if sys.platform == "win32" else "intentumdiff")
r2 = subprocess.run([str(exe), "--version"], capture_output=True, text=True,
encoding="utf-8", errors="replace")
s.check("console script `intentumdiff --version`", r2.returncode == 0, r2.stderr)

# 4. `python -m` — READMEs reach for this constantly, so it must work.
r = s.run("-m", "intentumdiff", "--version")
s.check("python -m intentumdiff", r.returncode == 0, r.stderr)

# 5. A real diff with a known-correct answer.
script = root / "use.py"
script.write_text(USE_SCRIPT.format(old=OLD_SRC, new=NEW_SRC), encoding="utf-8")
r = s.run(str(script))
s.check("SemanticDiffer produces a diff", "CHANGES" in r.stdout, r.stderr)

# 6. THE ONE THAT MATTERS. 0.0.1 emitted ~69 plugin-catalogue errors on every
# invocation while still returning a result, so exit code alone said "fine".
noise = [
ln for ln in r.stderr.splitlines()
if ln.strip() and "Failed to catalog" in ln
]
s.check(
"no plugin-catalogue errors on stderr",
not noise,
f"{len(noise)} error line(s), first: {noise[0] if noise else ''}",
)

# 7. Every URL the package tells users to visit must resolve. 0.0.1 pointed at
# docs.intentumdiff.dev, which does not exist.
import re
import urllib.error
import urllib.request

urls = sorted(set(re.findall(r"https?://[^\s'\"<>)\]]+", r.stderr)))
dead = []
for u in urls:
try:
req = urllib.request.Request(u, method="HEAD",
headers={"User-Agent": "intentumdiff-smoke"})
with urllib.request.urlopen(req, timeout=10) as resp:
if resp.status >= 400:
dead.append(f"{u} -> {resp.status}")
except urllib.error.HTTPError as e:
dead.append(f"{u} -> {e.code}")
except Exception as e: # DNS failure counts: the domain does not exist
dead.append(f"{u} -> {type(e).__name__}")
s.check(
f"all {len(urls)} URL(s) in output resolve",
not dead,
"; ".join(dead[:3]),
)

print(f"\n version under test: {version or '(unknown)'}")
return report(s)
finally:
shutil.rmtree(root, ignore_errors=True)


def report(s: Smoke) -> int:
if s.failures:
print(f"\n SMOKE FAILED: {len(s.failures)} check(s) — {', '.join(s.failures)}")
return 1
print("\n SMOKE PASSED")
return 0


if __name__ == "__main__":
sys.exit(main())
12 changes: 12 additions & 0 deletions src/intentumdiff/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""Enable `python -m intentumdiff`.

The console script alone is not enough: `python -m` is what people reach for when the
script is not on PATH (a venv they have not activated, CI, a container), and READMEs use
it constantly. 0.0.1 shipped without this module, so the invocation failed with an
unhelpful "cannot be directly executed".
"""

from intentumdiff.cli import main

if __name__ == "__main__":
raise SystemExit(main())
6 changes: 4 additions & 2 deletions src/intentumdiff/cli/_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,14 +347,16 @@ def _render_terminal(diff: SemanticDiff) -> None:
)
)
return
scope_label = (diff.staging_status or "working tree").replace("_", " ")
header = Table.grid(padding=(0, 2))
header.add_column(style="bold")
header.add_column()
header.add_row("Old", diff.old_filename or "<unknown>")
header.add_row("New", diff.new_filename or "<unknown>")
header.add_row("Language", diff.language or "unknown")
header.add_row("Scope", scope_label)
# Only a git-backed diff has a staging scope. Defaulting to "working tree" told
# someone comparing two local files that a working tree was involved when none was.
if diff.staging_status:
header.add_row("Scope", diff.staging_status.replace("_", " "))
header.add_row("Changes", str(len(diff.changes)))
_console.print(
Panel(
Expand Down
5 changes: 5 additions & 0 deletions src/intentumdiff/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ class NodeFacts(BaseModel, frozen=True):
without revealing code. The host carries this through unchanged.
"""

#: Which derivation passes produced these facts, e.g. ``"cst,enrich(derived=14,added=5)"``.
#: Populated only when ``INTENTUMDIFF_TRACE_FACTS=1``; ``None`` in normal operation.
#: Diagnostic provenance, never source — safe to paste into a bug report.
facts_trace: str | None = None

param_count: int | None = None
#: ``"none"`` | ``"value"`` | ``"literal"`` (constant) | ``"unknown"``.
returns: str | None = None
Expand Down
9 changes: 8 additions & 1 deletion src/intentumdiff/differ.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,13 +491,20 @@ def diff(self, source: Source) -> SemanticDiff:
profiler = self._new_profiler()
with profiler.phase("source_loading"):
old_content, new_content, filename, language_hint = source.get_content()
return self._run_pipeline(
diff = self._run_pipeline(
old_content,
new_content,
filename,
language_hint,
_profiler=profiler,
)
# The pipeline works from the single filename language detection needs. A source
# comparing two differently named files knows both, so restore them here rather
# than threading a second name through every construction site.
names = source.display_names()
if names is not None:
diff = diff.model_copy(update={"old_filename": names[0], "new_filename": names[1]})
return diff

def diff_strings(
self,
Expand Down
33 changes: 32 additions & 1 deletion src/intentumdiff/live_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1047,9 +1047,40 @@ def _handle_control_request(
if op == "cancel":
send_fn(self._cancel_request(request, seq))
return True
send_fn(self._error_response(seq, "unknown_op", f"unknown op: {op!r}", op=str(op)))
if op == "asset_diff":
send_fn(self._asset_diff_request(request, seq))
return True
# `invalid_op`, not `unknown_op`: the native server has always answered an unrouted op
# with that code, and it is the spelling the extension lists as non-retryable — so the
# python server's own spelling made the extension auto-retry a request that can never
# succeed. Two servers on one protocol have to name the same failure the same way.
send_fn(self._error_response(seq, "invalid_op", f"unknown op: {op!r}", op=str(op)))
return True

def _asset_diff_request(self, request: dict[str, Any], seq: int) -> dict[str, Any]:
"""Perceptual image diff — the engine does the pixel work, this only marshals the request.

The extension previously synthesised an asset "review" and never asked for artifacts,
so the panel always reported PERCEPTUAL DIFF PENDING while the engine sat there able
to answer. This is the missing call.

Two request shapes, both engine-served:

* ``path`` (+ optional ``ref``) — one tracked image against a git ref. This is what a
reviewing editor has: a repo-relative path and a base. The *before* bytes are in the
object store, and materialising them is git work, so the engine does it.
* ``before_path`` + ``after_path`` — two files that already exist on disk.

Request parsing, the path-containment guard and the self-ignoring artifact cache live in
the core's ``live_handle_asset_diff``, so this server and the native live-server binary
answer from ONE implementation. A second copy here would be a second place for the
containment rule to drift — and the rule is what stops a protocol method that accepts
file paths from becoming an arbitrary-file reader.
"""
from intentumdiff import rust_core

return rust_core.live_handle_asset_diff(self._repo_path, self._ref, request, seq)

def _process_review_request(
self,
request: dict[str, Any],
Expand Down
24 changes: 19 additions & 5 deletions src/intentumdiff/plugins/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
)

logger = logging.getLogger(__name__)

# Emitted once per process, not once per plugin load: there are 78 plugins.
_OVERRIDE_WARNED: bool = False
_MAX_TELEMETRY_RECORDS = 128

# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -404,13 +407,24 @@ def _check_osv_cache_or_block(
# Stamp is fresh but no cache file: the last fetch failed.
override = os.environ.get("INTENTUMDIFF_ALLOW_VULNERABLE_WASMTIME", "").strip()
if override in ("1", "true", "yes"):
logger.warning(
"INTENTUMDIFF_ALLOW_VULNERABLE_WASMTIME: loading plugins with "
"unverified OSV status — last advisory fetch failed."
)
# Worth saying — a safety check has been deliberately disabled — but worth
# saying ONCE. Emitting it per plugin load repeated it up to 78 times a run.
global _OVERRIDE_WARNED
if not _OVERRIDE_WARNED:
_OVERRIDE_WARNED = True
logger.warning(
"INTENTUMDIFF_ALLOW_VULNERABLE_WASMTIME: loading plugins with "
"unverified OSV status — last advisory fetch failed."
)
return
if trusted or _is_trusted_wasm_path(wasm_path):
logger.warning(
# DEBUG, not WARNING. This fires on every ordinary run whenever the machine
# is offline or the advisory cache has expired, and there is nothing for the
# user to do about it: first-party plugins ship inside the wheel and
# third-party ones stay blocked either way. Emitting it at warning level put
# alarming text — naming a "allow vulnerable" override — on stderr beside
# correct results, which is precisely how 0.0.1 came to look broken.
logger.debug(
"Loading first-party trusted Wasm plugin with unverified OSV "
"status because the last advisory fetch failed. Third-party "
"plugins remain blocked without INTENTUMDIFF_ALLOW_VULNERABLE_WASMTIME=1."
Expand Down
14 changes: 13 additions & 1 deletion src/intentumdiff/plugins/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@
# trusted plugin metadata.
_TRUSTED_PLUGIN_PACKAGE_NAMES: frozenset[str] = frozenset(
{
# The DISTRIBUTION name, which is not the import name. The wheel is published as
# `intentumdiff-python` (the polyglot-binding convention) while the import package
# is `intentumdiff`, and entry points report the DISTRIBUTION.
#
# Omitting it made the package fail its own first-party check: all 69 built-in
# parsers were treated as untrusted third-party plugins and refused with a security
# warning naming the package itself as the culprit. Every invocation printed ~69
# error lines before producing output. Shipped in 0.0.1 and only found by installing
# the published wheel — no test in the repo could see it, because in a source
# checkout the distribution resolves differently.
"intentumdiff-python",
"intentumdiff_python",
"intentumdiff",
"intentumdiff-dbt",
"intentumdiff_dbt",
Expand Down Expand Up @@ -275,7 +287,7 @@ def _wasm_path_from_ep(ep: importlib.metadata.EntryPoint) -> str:
"execute arbitrary code before Wasm sandboxing is in effect. "
f"The plugin author must add '{_WASM_PATH_METADATA_FIELD}: <relative/path.wasm>' "
"to their package metadata. "
"See: https://docs.intentumdiff.dev/plugins/metadata"
"See: https://github.com/buchochelliq-labs/intentumdiff-python/blob/main/docs/PLUGIN_GUIDE.md"
)

# Locate the file via importlib.metadata without importing anything.
Expand Down
Loading
Loading