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
53 changes: 53 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Changelog

All notable changes to diffly are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed

- Interactive mode no longer breaks imports on platforms without POSIX terminal support; it now falls back to the standard report.
- Running bare `diffly` in non-interactive environments (CI, pipes) prints help instead of hanging on prompts.
- Pressing Escape alone in the interactive selector no longer blocks waiting for arrow-key bytes.
- `CHECKS_PENDING` risk-flag evidence now lists the actual pending check names instead of the literal state string.
- Diff parsing ignores `\ No newline at end of file` markers when computing changed line numbers, fixing off-by-one symbol attribution.
- GitHub Action comment publishing distinguishes listing 404s (no comments yet) from publish failures via a typed error instead of string matching.

### Added

- **Local mode (`diffly local`):** triage git changes in any folder on disk — uncommitted working-tree edits by default, or a branch comparison with `--base main`. Works fully offline for private, archived, or removed repositories; untracked files are included and CI-check rules are skipped as not applicable.
- Generated explanations now render in the interactive review screen as a toggleable section, including clear setup guidance when no LLM key is configured.
- Loading spinners with progress messaging while pull-request data is fetched.
- Centered terminal composition across the wizard, setup walkthrough, interactive review, doctor, and version screens.
- The guided wizard prompts for a **Repository URL** with explicit format hints, confirms when deterministic-only mode was chosen, and accepts pasted pull-request URLs without asking for the number again.
- `-V` / `--version` flag on the root command and a `python -m diffly_cli` entry point.
- Guided wizard arguments are built from the real parser defaults so new options cannot drift between the wizard and the CLI.
- The repository argument now accepts full pull-request URLs (`diffly pr https://github.com/owner/repo/pull/12`), inferring the number; the guided wizard skips the number prompt when a URL is pasted.
- `CONTRIBUTING.md`, `SECURITY.md`, and this changelog.

### Changed

- Expanded LICENSE from the bare MIT template to a documented four-part license that keeps the standard MIT grant and adds definitions, contribution terms, verdict disclaimers, an extended warranty/liability statement, and general terms.
- Usage examples in the README and `--help` epilog point at live public pull requests.

## [0.2.1] - 2026-08-21

### Changed

- Renamed the all-clear verdict from `SHIP` to `PASS`; `SHIP` remains accepted as a legacy alias and `legacy_verdict` preserves it in JSON output.
- `diffly` is now the primary executable name with `diffly-cli` kept as a compatibility alias.
- Consolidated the interactive UX: bare `diffly` opens the guided PR wizard, plus new `setup`, `doctor`, `help`, and `version` subcommands.
- Pending checks are quarantined explicitly (`CHECKS_PENDING`) rather than folded into unknown-check handling.

## [0.2.0] - 2026-08-20

### Added

- Hardened triage for large and incomplete GitHub responses: paginated files, commits, check runs, statuses, and repository tree; raw-diff fallback when the unified diff exceeds GitHub's size limit.

## [0.1.0] - 2026-08-18

### Added

- Phase 1 deterministic triage: blast-radius map, fixed risk rules, one-page Markdown report, stable JSON output, and the bundled GitHub Action.
- Phase 2 optional literate-diff explainer with strict output validation, secret redaction, and fail-closed behavior.
39 changes: 39 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Contributing to diffly

Thanks for helping make pull-request triage deterministic. This guide covers what you need to get productive quickly.

## Development setup

```bash
git clone https://github.com/VIVAAN-DHAWAN/diffly-cli.git
cd diffly-cli
python -m venv .venv
. .venv/bin/activate
python -m pip install -e '.[dev]'
pytest -q
```

The CI matrix runs the suite on Python 3.10 through 3.13.

## Ground rules

- **Determinism is the product.** The same PR data must always produce the same flags and verdict. Never let heuristics, network state, or LLM prose change a verdict.
- **Fail closed.** When evidence is missing (truncated tree, unavailable checks), quarantine rather than assume success.
- **Keep the LLM sandboxed.** Generated explanations are clearly labeled, validated against a strict contract, redacted, and cannot influence `PASS`, `QUARANTINE`, or `BLOCK`. See [`docs/phase-2-contract.md`](docs/phase-2-contract.md).
- **Focused changes.** One behavior change per pull request, with regression tests.

## Pull-request checklist

1. Add or update tests for any behavior change.
2. Run `pytest -q` locally.
3. Update [`CHANGELOG.md`](CHANGELOG.md) under **Unreleased**.
4. If you changed user-facing behavior, update the README section that documents it.
5. Verify any new example commands against live public repositories — broken examples are bugs.

## Reporting bugs

Open an issue with the command you ran, the full output, your OS and Python version, and the repository/PR analyzed (public ones only). Run `diffly doctor` and include its output for environment problems.

## Security

Do not open public issues for security reports. Use [GitHub's security advisory form](https://github.com/VIVAAN-DHAWAN/diffly-cli/security/advisories/new). See [`SECURITY.md`](SECURITY.md).
98 changes: 92 additions & 6 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
MIT License
MIT License — diffly

Copyright (c) 2026 Vivaan Dhawan

================================================================================
PART 1 — THE LICENSE GRANT (standard MIT text)
================================================================================

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
Expand All @@ -12,10 +16,92 @@ furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

================================================================================
PART 2 — DEFINITIONS AND CLARIFICATIONS
================================================================================

These clarifications are part of this license. Where Part 1 and Part 2 could
be read differently, Part 1 controls; Part 2 explains intent.

1. "Software" means the diffly source code, its bundled GitHub Action, its
install scripts, its documentation, and any object code or distributions
built from them, in whole or in part.

2. "Contribution" means any work submitted to the project — including pull
requests, patches, and documentation — that is intentionally offered for
inclusion in the Software. By submitting a Contribution you agree it is
licensed under this same license, with no additional terms or conditions,
to the maximum extent permitted by law. You confirm you have the right to
license what you submit.

3. Verdicts are opinions, not guarantees. The PASS, QUARANTINE, BLOCK, and
related outputs produced by the Software are deterministic heuristics
applied at a point in time against data available at that moment. They are
review aids only. They do not certify correctness, security, safety,
fitness for production, compliance with any law, regulation, or standard,
or the absence of defects in any analyzed code, repository, or change.
You remain solely responsible for all decisions about the software you
review, merge, ship, or operate.

4. Network services. The Software may contact third-party services you direct
it to (for example the GitHub REST API) and, only when explicitly enabled
by you, an OpenAI-compatible model endpoint you configure. Those services
are governed by their own terms. The project authors neither operate those
services nor are party to your relationship with them.

5. Trademarks. This license grants no rights in the names "diffly",
"diffly-cli", or any related marks, logos, or trade dress, except as
strictly required for reasonable attribution or nominative fair use.

================================================================================
PART 3 — WARRANTY AND LIABILITY (extended statement)
================================================================================

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
FITNESS FOR A PARTICULAR PURPOSE, TITLE, NON-INFRINGEMENT, ACCURACY OF OUTPUT,
AND UNINTERRUPTED OR ERROR-FREE OPERATION. SOME JURISDICTIONS DO NOT ALLOW THE
EXCLUSION OF CERTAIN IMPLIED WARRANTIES, SO SOME OF THE ABOVE MAY NOT APPLY TO
YOU TO THE EXTENT LAW REQUIRES IT.

IN NO EVENT AND UNDER NO LEGAL THEORY — WHETHER IN CONTRACT, TORT (INCLUDING
NEGLIGENCE), STRICT LIABILITY, STATUTE, OR OTHERWISE — SHALL THE AUTHORS,
COPYRIGHT HOLDERS, OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, PUNITIVE, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, PROFITS, GOODWILL, OR REPUTATION; BUSINESS INTERRUPTION; SECURITY
INCIDENTS; BUILD FAILURES; OR RELEASED DEFECTS) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, ARISING IN ANY WAY OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR ITS USE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, THE TOTAL AGGREGATE
LIABILITY OF THE AUTHORS AND COPYRIGHT HOLDERS FOR ALL CLAIMS RELATING TO THE
SOFTWARE SHALL BE LIMITED TO THE GREATER OF ONE HUNDRED UNITED STATES DOLLARS
(USD 100) OR THE AMOUNT YOU PAID FOR THE SOFTWARE, WHICH BY DEFAULT IS ZERO.

================================================================================
PART 4 — GENERAL TERMS
================================================================================

1. Compliance with law. Your use of the Software must comply with applicable
export-control, sanctions, privacy, and other laws.

2. Severability. If any provision of this license is held unenforceable, it
will be modified to the minimum extent necessary, and the remaining
provisions stay in force.

3. No waiver. Failure to enforce any provision is not a waiver of it.

4. Termination. The licenses granted here terminate automatically if you
breach this license. They otherwise survive as long as the copyright law
of your jurisdiction requires, and are perpetual where permitted.

5. Entire agreement. This license is the entire agreement between you and the
copyright holders concerning the Software, superseding any other terms
communicated alongside copies of it.

6. Language. This license is written in English; translations are for
convenience only, and the English text controls.

THIS LICENSE FOLLOWS THE MIT LICENSE TEMPLATE AND ADDS CLARIFYING MATERIAL.
THE GRANT IN PART 1 REMAINS THE GOVERNING PERMISSION.
28 changes: 22 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ This release includes **Phase 1 deterministic triage** plus the optional **Phase
- **CI ready:** run it as a CLI, consume stable JSON, or add the bundled GitHub Action.
- **LLM optional:** generated explanation is isolated from the authoritative verdict and fails closed to deterministic output.
- **No GitHub CLI dependency:** it talks directly to the GitHub REST API.
- **Works offline:** `diffly local` triages git changes in any folder on disk — private, archived, or removed repositories included.
- **Interactive terminal UI:** use arrow keys and the space bar to focus the report on what matters.

## Why it exists
Expand Down Expand Up @@ -84,10 +85,13 @@ The default model is `gpt-5-mini`; override it with `DIFFLY_LLM_MODEL` or `--llm
Examples:

```bash
diffly pr pallets/urllib3 3456
diffly pr pallets/urllib3 3456 --interactive
diffly pr pallets/urllib3 3456 --output triage.md
diffly pr pallets/urllib3 3456 --json
diffly pr astral-sh/ruff 27808
diffly pr https://github.com/astral-sh/ruff/pull/27808
diffly pr astral-sh/ruff 27808 --interactive
diffly pr astral-sh/ruff 27808 --output triage.md
diffly pr astral-sh/ruff 27808 --json
diffly local ~/code/my-project --base main
diffly --version
diffly help
diffly setup
diffly doctor
Expand All @@ -96,7 +100,19 @@ diffly version

New to Diffly? Run `diffly setup` for a short guided tutorial covering PR analysis, interactive controls, automation, troubleshooting, credentials, and privacy. The final step can launch the real PR wizard so you can immediately try what you learned.

The command prints clean terminal Markdown by default. `--json` is provided for CI integration and `--output` saves the Markdown report to a file.
The command prints clean terminal Markdown by default. `--json` is provided for CI integration and `--output` saves the Markdown report to a file. The repository argument accepts `owner/repo`, any GitHub URL, or a full pull-request URL; with a pull-request URL the number is inferred.

## Local mode

Analyze git changes on your own disk — no GitHub access, token, or network required:

```bash
diffly local # triage uncommitted working-tree changes in the current folder
diffly local ~/code/private-repo # analyze any checkout, including private or deleted repositories
diffly local --base main # compare your branch against main instead of the working tree
```

Local mode runs the identical deterministic pipeline: changed-file inventory, Tree-sitter symbol detection, blast-radius map, and risk rules. CI checks do not exist locally, so check-derived flags are skipped; everything else (auth/secrets, database changes, new dependencies, missing test coverage) behaves exactly as it does for pull requests. Untracked files are included, so brand-new work is never silently ignored.

Run `diffly --help` or `diffly pr --help` for the complete option reference. `--interactive` opens a keyboard-driven selector: use up/down arrows to move, space to toggle sections, Enter to render, and `q` to quit. Pull-request numbers must be positive integers. For automation, prefer `--json`; operational errors exit with status `2` and successful triage exits with status `0` regardless of the verdict, so callers should inspect the `verdict` field when enforcing policy.

Expand Down Expand Up @@ -190,7 +206,7 @@ python -m pip install -e '.[dev]'
pytest -q
```

Keep changes focused and include regression tests for behavior changes. The CI matrix runs the suite on Python 3.10 through 3.13.
Keep changes focused and include regression tests for behavior changes. The CI matrix runs the suite on Python 3.10 through 3.13. See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the full guide, and record user-facing changes in [`CHANGELOG.md`](CHANGELOG.md).

## Security and privacy

Expand Down
20 changes: 20 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Security Policy

## Supported versions

| Version | Supported |
| --- | --- |
| latest release | yes |
| older releases | no |

## Reporting a vulnerability

Report security issues privately through [GitHub's security advisory form](https://github.com/VIVAAN-DHAWAN/diffly-cli/security/advisories/new), not a public issue. Include reproduction steps, affected versions, and any relevant logs with secrets redacted.

## What matters most in this codebase

- **Token handling:** `diffly` reads `GITHUB_TOKEN` from the environment or the Action context. Tokens are never logged, echoed, or included in reports.
- **LLM boundary:** deterministic mode sends no code anywhere. With `--explain`, bounded, secret-redacted context is sent to the configured OpenAI-compatible endpoint. Model output is strictly validated and cannot change verdicts. See [`docs/phase-2-contract.md`](docs/phase-2-contract.md).
- **Untrusted input:** PR titles, bodies, diffs, and check names are treated as data, never instructions.

Please review [`docs/phase-2-contract.md`](docs/phase-2-contract.md) before enabling `--explain` for sensitive repositories.
45 changes: 30 additions & 15 deletions action/post_comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@
API_VERSION = "2022-11-28"


class GitHubApiError(RuntimeError):
def __init__(self, code: int, detail: str) -> None:
super().__init__(f"GitHub API {code}: {detail}")
self.code = code


def request(url: str, token: str, method: str = "GET", payload: dict | None = None):
body = json.dumps(payload).encode() if payload is not None else None
headers = {
Expand All @@ -27,10 +33,27 @@ def request(url: str, token: str, method: str = "GET", payload: dict | None = No
raw = response.read().decode()
return json.loads(raw) if raw else {}
except urllib.error.HTTPError as exc:
if exc.code == 404 and method == "GET":
return []
detail = exc.read().decode(errors="replace")
raise SystemExit(f"GitHub API {exc.code}: {detail[:500]}") from exc
raise GitHubApiError(exc.code, detail[:500]) from exc


def existing_comments(base: str, token: str) -> list:
"""List prior PR comments; a 404 means none are readable yet."""
comments = []
page = 1
while True:
try:
batch = request(f"{base}?per_page=100&page={page}", token)
except GitHubApiError as exc:
if exc.code == 404 and page == 1:
return []
raise
if not isinstance(batch, list):
raise SystemExit(f"GitHub returned unexpected comment listing: {type(batch).__name__}")
comments.extend(batch)
if len(batch) < 100:
return comments
page += 1


def main() -> int:
Expand All @@ -43,25 +66,17 @@ def main() -> int:
with open(comment_file, encoding="utf-8") as handle:
body = handle.read()
base = f"https://api.github.com/repos/{repository}/issues/{number}/comments"
page = 1
existing = []
while True:
batch = request(f"{base}?per_page=100&page={page}", token)
existing.extend(batch)
if len(batch) < 100:
break
page += 1
match = next((item for item in existing if MARKER in (item.get("body") or "")), None)
match = next((item for item in existing_comments(base, token) if MARKER in (item.get("body") or "")), None)
try:
if match:
request(f"{base}/{match['id']}", token, "PATCH", {"body": body})
print(f"Updated Diffly comment {match['id']}")
else:
created = request(base, token, "POST", {"body": body})
print(f"Created Diffly comment {created.get('id', 'unknown')}")
except SystemExit as exc:
if "GitHub API 404" in str(exc):
print(f"Warning: Diffly could not publish its PR comment (GitHub returned 404; check pull-request write permissions).")
except GitHubApiError as exc:
if exc.code == 404:
print("Warning: Diffly could not publish its PR comment (GitHub returned 404; check pull-request write permissions).")
else:
raise
return 0
Expand Down
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "diffly-cli"
version = "0.2.1"
version = "0.3.0"
description = "Deterministic triage for large GitHub pull requests"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
CAPTURE_DIR="$ROOT/assets/real-captures"
mkdir -p "$CAPTURE_DIR"

Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion src/diffly_cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Deterministic triage for large GitHub pull requests."""

__version__ = "0.2.1"
__version__ = "0.3.0"
6 changes: 6 additions & 0 deletions src/diffly_cli/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import sys

from diffly_cli.cli import main

if __name__ == "__main__":
sys.exit(main())
Loading
Loading