Skip to content
Open
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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ https://github.com/composewell/streamly.
Packdiff uses the hoogle file created by haddock to generate and compare the
difference between multiple versions of a package

1. The API for modules in the `other-modules` sections is not generated or
compared.
2. The re-exported module is just considered a re-exported module. The API of
the re-exported module isn't merged with the module that re-exports it.
1. The API for modules in the `other-modules` (unexposed modules)
sections is not generated or compared.
2. The API of a re-exported module isn't merged with the module that
re-exports it.

The 2nd limitation might end up falsely reporting a diff even if the diff does
not exist. In our use-case where we have manual intervention this isn't a
Expand Down
175 changes: 175 additions & 0 deletions dev/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# packdiff CLI Design

> Find API changes between different versions of a Haskell package.

---

## Overview

packdiff compares two versions of a Haskell package and reports API
changes: added, removed, changed, and deprecated symbols. It works by
diffing the hoogle files generated by haddock, and only covers exposed
modules — unexposed modules in `other-modules` are excluded by design.

---

## Commands

There are two commands. Everything else is flags.

### `packdiff diff <ref1> <ref2>`

The primary command. Compares two refs and prints the API diff. This
output also serves as the API changelog — there is no separate log
command.

A ref is one of:

- A git commit SHA or tag (e.g. `v1.2.0`, `abc1234`)
- `hackage:<version>` — a specific published Hackage release
- `hackage` — the latest published release (no version specified)
- `.` or omitted — the current working directory, including unstaged changes

```sh
# two git tags
packdiff diff v1.2.0 v1.3.0

# local HEAD vs a tag
packdiff diff v1.2.0 HEAD

# working directory vs HEAD (second arg defaults to .)
packdiff diff HEAD

# HEAD vs latest published Hackage release
packdiff diff HEAD hackage

# two Hackage versions
packdiff diff hackage:1.1.0 hackage:1.2.0

# CI: fail if breaking changes detected
packdiff diff HEAD hackage --fail-on breaking
```

### `packdiff check <ref1> <ref2>`

Inspects the diff and suggests the appropriate semver bump. Accepts the
same ref syntax as `diff`.

```sh
packdiff check v1.2.0 HEAD
-> breaking changes detected: bump major (2.0.0)

packdiff check v1.2.0 HEAD
-> new symbols added: bump minor (1.3.0)

packdiff check v1.2.0 HEAD
-> no API changes detected: bump patch (1.2.1)
```

---

## Output Format

Default output uses annotation sigils. Each changed module is listed as
a top-level entry, with affected symbols nested below.

```
---------------------------------
API Annotations
---------------------------------
[A] : Added
[R] : Removed
[C] : Changed
[O] : Old definition
[N] : New definition
[D] : Deprecated
---------------------------------
API diff
---------------------------------
[C] Streamly.Data.Stream.Prelude
[A] useAcquire :: AcquireIO -> Config -> Config
[D] parEval :: MonadAsync m => (Config -> Config) -> Stream m a -> Stream m a
[C] Streamly.Data.Fold.Prelude
[C] toHashMapIO
[O] toHashMapIO :: (MonadIO m, Hashable k, Ord k) => (a -> k) -> Fold m a b -> Fold m a (HashMap k b)
[N] toHashMapIO :: (MonadIO m, Hashable k) => (a -> k) -> Fold m a b -> Fold m a (HashMap k b)
```

The module-level annotation reflects the worst change inside it — a
module containing only additions is annotated `[A]`, not `[C]`. This
makes module-level filtering meaningful.

The legend is shown in `text` format only. It is omitted in `markdown`,
`json`, and `github` output.

---

## Flags

### Output and formatting

| Flag | Description |
|------|-------------|
| `--format <fmt>` | Output format: `text` (default), `json`, `markdown`, `github`. The `github` format emits GitHub Actions annotations (`::warning` / `::error`). |
| `--color <mode>` | Color control: `auto` (default), `always`, `never`. |
| `-q / --quiet` | Print a one-line summary only, no symbol detail. |
| `--modules-only` | Collapse output to module-level entries, no symbol detail. |

### Filtering

| Flag | Description |
|------|-------------|
| `--show <types>` | Show only these change types. Repeatable. Values: `added`, `removed`, `changed`, `deprecated`. Mutually exclusive with `--hide`. |
| `--hide <types>` | Hide these change types. Repeatable. Mutually exclusive with `--show`. |
| `--breaking-only` | Shorthand for `--show removed,changed`. |
| `--module <M>` | Narrow the diff to a specific module. Repeatable. |
| `--ignore-module <M>` | Exclude a module entirely. Repeatable. Useful for suppressing known re-export false positives. |

### CI and exit codes

| Flag | Description |
|------|-------------|
| `--fail-on <severity>` | Exit 1 if changes at this severity or above are detected. Values: `breaking` (removed or changed), `any`. |
| `--warn-on removals` | Emit warnings for removals without triggering `--fail-on`. Useful when re-export false positives are possible. |
| `--cabal-file <path>` | Explicit path to the .cabal file. Defaults to auto-discovery. |

Exit codes:

- `0` — no diff detected
- `1` — diff detected and matches `--fail-on` threshold
- `2` — usage or configuration error

---

## Known Limitations

packdiff uses the hoogle file produced by haddock. Two limitations follow from this.

### 1. Unexposed modules are excluded

Modules listed in `other-modules` in the .cabal file are never generated
or compared. packdiff only covers the public API surface.

### 2. Re-exported symbols may appear as false removals

When a module re-exports symbols from another module, packdiff cannot
merge the two and may report a removal that does not exist. In practice
this is manageable with human review in the loop.

Mitigation: use `--ignore-module <M>` to suppress known false-positive
modules from CI output, and `--warn-on removals` to flag removals as
warnings rather than hard failures until reviewed.

## More things to do

Additionally we should be able to:
* specify a hoogle file instead of a package for the diff
* specify an installed package for the diff
* show the API summary for any rev, hackage version
* show API summary for an installed package in the current ghc environment
* show the doc of an api "package:module:definition".
* show reverse deps of a package and which version are they using, show
maintainer email -- send mails about how to migrate.

We can use a scope specifier to specify the source type e.g. hackage:, git:,
github:, installed:, file: etc.
5 changes: 4 additions & 1 deletion packdiff.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ tested-with:
, GHC==9.10.3
, GHC==9.8.4
, GHC==9.6.3
extra-doc-files: README.md, CHANGELOG.md
extra-doc-files:
README.md
, CHANGELOG.md
, dev/design.md

source-repository head
type: git
Expand Down
Loading