emendant
v0.1.2
Published
Watches API and SDK breaking changes, finds the affected code, and writes a patch it has proved against your own checks.
Maintainers
Readme
Emendant
Watches API and SDK breaking changes, finds the affected code in a repository, and writes a patch it has proved against that repository's own checks.
Dependabot bumps a version number in a manifest. It does not change the code that calls the changed API, so the bump lands, the build fails, and a human does the real work. The gap is the semantic change, not the version number.
Install
npx emendant scanThat is the whole of it. Node 20 or later, no account, no sign-in, and no
configuration: the change feed ships inside the package, so a scan makes no
network request unless you turn on automatic feed updates. npx fetches the
package and runs it, so there is no install step to do first. Install it properly when you want it in CI:
npm install --save-dev emendantThe first run at a terminal asks three short questions before it scans, and never asks them again: which model provider writes the few patches no transform can, whether to keep the feed up to date by itself, and whether to go ahead. Between the last two it lists the supported SDKs your repository has, which costs no scanning. Only providers whose credential is already in your environment are offered, the key is read when a request is made and never stored, and what is recorded is the provider name. All of it needs a terminal, so a scan in CI goes straight to the report.
What it does
emendant scan reads your manifests and lockfiles, walks the source it is
allowed to read, parses only the files that could possibly be relevant, and
reports the places where a breaking change described in the feed genuinely
applies to code you have. It follows a value across files to get there, so a
call site that never mentions the package by name is still found.
emendant fix turns the deterministic findings into patches and proves each one
before offering it. emendant explain <change-id> prints the feed entry behind a
finding, with its primary source. emendant feed update fetches a newer signed
feed, and emendant feed status says which feed a scan would use without making
a request. emendant scan --sarif prints the findings as one SARIF 2.1.0 log,
so a CI run can upload them to code scanning. emendant --version says which
build you have.
Coverage today is the AI SDKs: ai, openai, @anthropic-ai/sdk, langchain,
@langchain/core and @google/genai, for TypeScript and JavaScript. It grows
without a new release of the tool, because the feed is delivered separately.
A finding says which version it is about
openai (installed as legacy-openai) installed 4.104.0 → 5.0.0
2 changes affect this repository
────────────────────────────────────────────────────────────────
! deprecation Public core modules moved under openai/core
core-module-paths deterministic fix
src/legacy.ts:6:1
6 │ import { fileFromPath } from 'legacy-openai/uploads';
× breaking fileFromPath helper removed
filefrompath-removed assisted fix
src/legacy.ts:6:10
6 │ import { fileFromPath } from 'legacy-openai/uploads';
│ ^
src/legacy.ts:9:10
9 │ return fileFromPath(path);
│ ^
2 findings · 3 call sites in 1 file
→ emendant fix writes and proves a patch for 1 of these call sites
→ emendant explain openai-npm-5.0.0-core-module-paths the sources and the migration guidanceThree placements, and the report never mixes them up. pending: the installed
version is below the release, so the code works today and the upgrade is what
breaks it. adopted: the project is already on the release and the old call
site is still there; hidden unless you ask for it with --adopted. unknown:
the manifest declares a range that sits either side of the release and there is
no lockfile to settle it, so the finding is reported and a warning says we
could not place it. Guessing would select the wrong entries, and staying silent
would hand that repository a clean report it had not earned.
Exit codes are 0 nothing found, 1 findings present, 2 tool error. Exit 1 is
not failure; it means there is something to read.
A patch is proved before you are offered it
emendant fix rewrites the findings a named AST transform can rewrite, and
writes one diff per finding into .emendant/patches/. It is deliberately narrow:
the feed entry must carry a deterministic fix, the change must be curated at
high confidence, and the provenance chain behind the site must be complete
(every step traced, nothing assumed). Everything else is reported unfixed, with
the reason, because "not fixed because the chain has an untraced step" is a
sentence a person can act on and a silently missing finding is not.
Then it proves them. Each patch is applied in a copy of your repository outside
your working tree, moved first to the version the patch was written against, and
put through your own typecheck and test commands. Those commands are found
rather than configured: an ordinary repository with a typecheck or test
script needs no emendant.json to get a proved patch, and explicit
typecheckCommand and testCommand values still win over what was found. They
run behind the strongest boundary your machine has: a temporary home, a reduced
environment, no network, confined writes, and credential files masked by name.
A patch that does not pass is not written, and the finding says why. A patch that does pass names the commands that passed inside its own header, because the patch is the thing that gets mailed and pasted into a review.
There are four grades and only two of them are a green run. test verified and
typechecked mean your commands passed. structurally checked means the edits
were applied and the workspace matched again, with nothing compiled or run, which
is what a repository owning no checks gets. unchecked means even that was not
possible. A run that could not decide, because your suite was already red or the
install did not work, says so rather than blaming the patch.
A candidate is not a finding
emendant prefilter walks a repository and reports which files survive a plain
substring search. A survivor contains the right characters somewhere in its
text, including inside a comment, which is how the file implementing the
prefilter ends up as a candidate for a change to OpenAI's beta.chat namespace.
That is the stage working. It answers "could this file possibly be relevant?" and stops, so that the expensive stage only sees the 0.4 percent of files worth looking at.
emendant match is the expensive stage: it parses each survivor, resolves what
every import binds and what every receiver is, and reports only the places where
the change genuinely applies. It routinely discards half the candidate files it
is given, and every discarded one is a comment or a string literal that mentions
an API rather than code that uses it.
A match still is not a finding. emendant scan adds the question the other two
commands do not ask: is the installed version one this change applies to? That is
the difference between "this code calls a removed helper" and "this code will
break when you upgrade".
Why the feed is the product
Large language models write code well already, so the agent is not the moat. The moat is the curated, machine-readable record of what each breaking change actually breaks and what the fix looks like. Nobody publishes this in a usable form: changelogs are prose, release notes are inconsistent, and semantic version numbers do not tell you which call sites break.
Every entry cites a real source URL, pinned to a tag. If a change cannot be cited, it does not go in the feed.
Precision over recall
A tool that reports 40 things is ignored. A tool that reports 2 things that are both real gets installed everywhere. So: if a matcher cannot resolve a binding with confidence, it reports nothing. A missed finding costs one user interaction; a false finding costs the user.
This is why every entry ships with negative fixtures as well as positive ones, and why the negatives that the cheap substring gate cannot reject are the ones that matter. A local function sharing a removed export's name, a property chain on an unrelated object literal, a relative import containing the package path: each contains a trigger string, and each must still produce nothing.
The matcher declines wherever it would have to guess. A receiver is attributed to
a package only by an import, a constructor call, or an instanceof guard,
never by inference. An identifier bound twice in one file is dropped
entirely rather than resolved by assumption.
The matching documentation has the whole contract and its limits.
Precision applies to detection too
A version we are not sure of is worse than no version, because it selects the
wrong feed entries. So detection reports a manifest range as a range rather than
resolving it, and when a lockfile installs two versions of one package it says
so instead of picking one. Two managers' lockfiles in one directory are treated
the same way: nothing in a repository proves which of them was written last, not
a packageManager declaration and not a marker file, so both are read and a
disagreement between them is reported rather than resolved by filename. Naming
the manager yourself settles it, in emendant.json or with
--package-manager. The detection documentation has the
whole contract, including the limits.
It applies to what the tool says about itself, too. Every warning is tagged with
whether it describes a gap in coverage (something the repository has that we did
not check). Those warnings print by default, because a user who is not told reads a
clean report as though everything was looked at. Warnings that merely explain a
choice wait for --verbose, and are counted rather than silently dropped.
Security
These are hard constraints, and they are also why the tool is worth installing.
- It runs entirely on your machine.
scanmakes no network request by default: the feed ships inside the package. The only feed request there is fetches one whole signed snapshot of static JSON, and it happens when you ask for it:emendant feed update, or a scan after you have said yes to automatic updates and the cache is more than 24 hours old. Emendant downloads the whole feed rather than the entries for your dependencies precisely so that a request cannot name a package you depend on. Everything it fetches is verified against a trust root inside the installed package, so an origin decides what bytes arrive and never whether they are accepted. - Emendant never receives or stores your source. Not one line of it, ever.
scanis entirely local, and so are the transforms that write most patches: no network request at all. - A few changes need a model, and only if you say so. Some breaking changes
have no mechanical remedy, and for those
fixcan ask a provider you name on the command line to write the patch. It sends the matched block plus about twenty lines of context and the file's imports, from your machine straight to that provider, and nothing else about the repository. Without--model-providerthose findings are reported unfixed and nothing is sent anywhere. Emendant never picks a provider for you, never reads a key it was not pointed at, and never falls back from a subscription you have to a bill you did not agree to. We say it this precisely because "your source is never uploaded" stops being true the moment any hosted model sees an excerpt. - That provider can be your own.
--model-provider azureposts to one Azure AI Foundry resource in your tenant, and--model-endpointpointsopenaioranthropicat a gateway you run, so a network that permits a fixed list of hosts can still use assisted fixes. The line printed before the first request names the host that receives the excerpt, not the flag it was spelled after. - Verification asks your package manager for a version, and nothing else. To
prove a patch for an upgrade you have not made yet, the scratch copy has to be
moved to the new release, so
fixrunsnpm install(or pnpm, or yarn) in that copy. It reaches your registry for a package you were going to install anyway. It sends nothing about your repository anywhere. That step carries a short list of environment variables rather than your shell: package managers expand${NAME}in a repository's own.npmrc, so a repository could otherwise name any variable you have and have its value sent, as a token, to a registry it chose. Registry credentials are passed, anything else is refused, and--allow-registry-credential <name>is how you say otherwise. - It never writes to your source files or your git history.
scanopens files for reading and nothing else.fixwrites diffs into.emendant/and leaves applying them to you. It does not rungit addorgit commit, now or in the hosted phase, which opens pull requests rather than touching a working tree. - It never executes code from the repository it is scanning during a scan.
Only
fixruns anything, only in a copy outside your working tree, only your own typecheck and test commands, and only after saying what it selected and why. The copy leaves out.git, so nothing it runs can reach your history. - The
postinstallscript does nothing. People check. There is nopostinstall.
Licence and support
Emendant is proprietary software, licensed and not sold. You may install and run
it for any purpose, including a commercial one; you may not redistribute it or
the change feed inside it. The full terms are in LICENSE, and the terms of use
for the feed service are at https://emendant.com/terms/.
- Documentation: https://emendant.com/docs/
- Privacy notice: https://emendant.com/privacy/
- Report a vulnerability: https://emendant.com/security/
- Anything else: https://emendant.com/contact/
Include the output of emendant --version in a report.
