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 diff --git a/dev/design.md b/dev/design.md new file mode 100644 index 0000000..c9bad82 --- /dev/null +++ b/dev/design.md @@ -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 ` + +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. + +## 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. 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