From bcc5702508c494b46c0e8f50129d1bcb5c1ce5b0 Mon Sep 17 00:00:00 2001 From: Harendra Kumar Date: Wed, 29 Apr 2026 19:34:18 +0530 Subject: [PATCH 1/3] Make minor edits in README --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2c8d88a..0629211 100644 --- a/README.md +++ b/README.md @@ -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 From 0c28ad0865afd760295c015cdbe10f9dd0db479f Mon Sep 17 00:00:00 2001 From: Harendra Kumar Date: Wed, 29 Apr 2026 19:37:12 +0530 Subject: [PATCH 2/3] Add a design doc --- dev/design.md | 161 +++++++++++++++++++++++++++++++++++++++++++++++++ packdiff.cabal | 5 +- 2 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 dev/design.md diff --git a/dev/design.md b/dev/design.md new file mode 100644 index 0000000..97300e9 --- /dev/null +++ b/dev/design.md @@ -0,0 +1,161 @@ +# 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 ` + +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:` — 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 ` + +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 ` | Output format: `text` (default), `json`, `markdown`, `github`. The `github` format emits GitHub Actions annotations (`::warning` / `::error`). | +| `--color ` | 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 ` | Show only these change types. Repeatable. Values: `added`, `removed`, `changed`, `deprecated`. Mutually exclusive with `--hide`. | +| `--hide ` | Hide these change types. Repeatable. Mutually exclusive with `--show`. | +| `--breaking-only` | Shorthand for `--show removed,changed`. | +| `--module ` | Narrow the diff to a specific module. Repeatable. | +| `--ignore-module ` | Exclude a module entirely. Repeatable. Useful for suppressing known re-export false positives. | + +### CI and exit codes + +| Flag | Description | +|------|-------------| +| `--fail-on ` | 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 ` | 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 ` to suppress known false-positive +modules from CI output, and `--warn-on removals` to flag removals as +warnings rather than hard failures until reviewed. diff --git a/packdiff.cabal b/packdiff.cabal index 16c0ec2..e040fc5 100644 --- a/packdiff.cabal +++ b/packdiff.cabal @@ -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 From 81bf648fb8d227f9cf4aeb3b5ee728899a5a9098 Mon Sep 17 00:00:00 2001 From: Harendra Kumar Date: Sun, 3 May 2026 09:06:36 +0530 Subject: [PATCH 3/3] Add notes about what can be added to this tool --- dev/design.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/dev/design.md b/dev/design.md index 97300e9..c9bad82 100644 --- a/dev/design.md +++ b/dev/design.md @@ -159,3 +159,17 @@ this is manageable with human review in the loop. Mitigation: use `--ignore-module ` 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.