Release notes
Every published release and what changed in it. This page is rendered from CHANGELOG.md inside the npm package itself — the same file you get with npm install mendapi — so it cannot describe a release npm never served.
The latest version published to npm is 0.5.7 (2026-08-06). Install or upgrade with npm install mendapi@latest, or run it without installing: npx mendapi scan --help.
Unreleased
Added
scannow reports the repairs it can actually perform, not only the change records it matched. The scanner joins the upstream change feed against your code; the fixer carries a separate corpus of 47 migration packs. The two only overlap where a pack declares change ids the feed also holds — and the three SDK-major packs (openai v3→v4, stripe v7→v8, aws-sdk v2→v3) declare none. A repo could therefore be fully broken by one of them and be told it was clean: measured on openai/openai-quickstart-node at itsopenai ^3.1.0commit,fix --migration openai-v3-to-v4rewrote a file thatscannever named among its seven findings. Reports now carryrepairs_available[]({ pack, provider, title, reference, files[], rules[] }) and the terminal output prints a "repairs available" section with the exactfix --migrationcommand per pack. Membership is proved by replaying each pack over the repo's bytes and keeping only packs whose rewrite changes something — not by adetectpre-filter, which claimed a cloudflare path-param migration on a plainstripe.customers.list()call. The section renders on the zero-impact path too, which is where the gap did its damage.- Each offered repair now states how much of the report it retires.
repairs_available[]entries carrymends_impacts[]— the change ids from the same report that the pack's authored coverage claims — plusdeclares_coverage, which isfalsefor the three SDK-major packs that make no claim about the feed at all. The terminal block printsmends N of the M impacts above, or names the honest alternative when a pack repairs code the feed never flagged. Previously a report could print "1762 potential impacts" and "1 repair available" with nothing connecting them, leaving the reader unable to tell whether running the repair addressed the findings or something unrelated. Measured on a repo readingpayment_method_options.us_bank_account.preferred_settlement_speed: the one offered pack retires 40 of the 1762 findings, including all 6 high-confidence ones. - A pack that can never join the feed now says so, instead of going silent. The three SDK-major packs (openai v3→v4, stripe v7→v8, aws-sdk v2→v3) declare no coverage for a structural reason, not an editorial one: they migrate code to a version older than the oldest release the feed holds for that repo — measured, the feed's earliest openai/openai-node release is v6.40.0, stripe/ stripe-node v22.2.3, aws/aws-sdk-js-v3 v3.1086.0, against pack targets of v4.0.0 / v8.0.0 / v3.0.0. Until now the repair block printed nothing at all for them, which reads identically to "this repair is unrelated to your findings". Entries now carry
coverage_boundary({ repo, to_version, feed_oldest, predates_feed }), recomputed from the changes table at scan time rather than asserted by the pack, and the terminal block explains the gap as a corpus limit. Packs that do declare coverage are unaffected and print theirmendsline as before.
Changed
- A specification format name is no longer treated as a named API surface.
OpenAPI,GraphQL,OpenTelemetryandWebMCPpass the PascalCase branch of the substantive-symbol test, so a generated-SDK release whose entire body is "regenerated from the OpenAPI spec" scored as having named a surface — the strongest half of the high-confidence signal — on a sentence that names no field, no method and no endpoint. Measured across the corpus: 17 records rest on nothing else, 4 of them breaking or deprecation. The filter is an exact list rather than a shape rule, and that choice is load-bearing: the highest-frequency PascalCase tokens in the corpus are real Twilio path and query parameters (AccountSid,ServiceSid,PageToken), and any heuristic broad enough to catchOpenAPIby shape deletes them. Confidence band only — the token stays inmatched_symbolsand in--json. - A dotted token lifted out of a link is no longer treated as a named API surface. The symbol extractor is deliberately greedy, so
https://docs.slack.dev/changelogyields the "member path"docs.slack.dev, which passes the substantive-shape test and counts as "the release named a surface". The obvious fix — blacklisting hostname-shaped suffixes — was measured against the whole 84112-record corpus and rejected: 105 distinct substantive symbols end in a TLD-shaped segment and nearly all are real fields (payment_method.id,payment_method.link,creator.via.app). The discriminator shipped instead is provenance: a token is discounted only when *every* occurrence in the record sits inside an http(s) URL, so a field named in both a docs link and a sentence still counts. Corpus-wide this touches 5 records and 12 tokens. Confidence band only — the token stays inmatched_symbolsand in--json. review --llmnow asks one question per repair, not one per change record. A spec is versioned per endpoint, so a single upstream edit is filed once for every endpoint that embeds the changed object. Measured on stripe-samples/accept-a-payment: 214 medium-confidence findings that are 41 distinct repairs, the four largest being 25/23/23/23 records of one property removal. Every one of those was previously sent to the LLM as its own request, so users paid for 214 completions to answer 41 questions — and the same repair could come back confirmed on one endpoint and unlikely on the next. The review now groups by repair (scoped per provider), asks the highest-ranked member, and applies that verdict to the whole family: 41 calls instead of 214, with all 214 findings still receiving their own audit entry.review --pendinggains arepair_familyfield but is deliberately NOT collapsed — it is a published output contract, and dropping entries would break existing parsers.- The published repo-size ceiling for the "30 seconds" promise drops from ~3,000 to ~2,000 files. The ceiling was measured on an idle machine. It is now measured with every core busy, which is the case a user actually hits on a working laptop or a shared CI runner: there the 30-second budget is exhausted at ~2,051-2,347 files, below the number we were publishing. The claim was outrunning the measurement in exactly the condition where a first-time user would notice. The README, home page, how-it-works page and docs index now state the lower bound, which still sits below the lowest loaded observation rather than at its average.
Fixed
- Reading
process.envno longer counts as using an SDK's changed surface. A release is promoted to the top of the report when it names a concrete surface and your code matches one. That test was shape-based — a dotted member path, a PascalCase type, a snake_case field — and JavaScript platform builtins satisfy those shapes exactly. Measured on cloudflare/templates (422 files): all three high-confidence findings were cloudflare-typescript majors resting on nothing butprocess.env(env-var reads in a deploy script) andRequestInit(a type annotation in a test helper), while every concrete surface those releases name went unmatched. Language and runtime globals now carry no surface information. The tokens still appear inmatched_symbolsand in--json; they simply stop promoting a finding on their own. Provider surfaces are untouched, including builtin-adjacent names likeRequestOptions, and eleven previously measured real repos rescan byte-for-byte identically. - Paths that are not spelled in lowercase are no longer truncated before matching. Several APIs write resources in PascalCase and version some paths by date: Twilio's spec names
/v1/LinkShortening/Domains/{DomainSid}/Configand/2010-04-01/Accounts/{AccountSid}/Calls/FeedbackSummary.json. The path extractor accepted lowercase segments only, so those paths were cut down to their first lowercase run (/v1/a2p) before any evidence check ran — the surface a change actually named never reached the matcher. Date-stamped version prefixes are now dropped like/v1/, response format suffixes (.json) are not treated as part of a resource name, and the SDK's own lowerCamel spelling (client.linkShortening.domains(...).config()) counts as usage evidence, including when instance-id calls sit between chain links. A nested call between links still does not match, so the chain cannot drift. - Endpoint-level changes are now evidenced through SDK calls, not only literal URLs. A spec-diff change names a request path (
/v1/checkout/sessions/{session}), but a repo using the vendor SDK never writes that string — it writesstripe.checkout.sessions.create(...). Requiring the literal path therefore proved only that a repo speaks raw HTTP, and measured on two real public repos that both import the SDK, zero of 1,747 and zero of 1,755 endpoint-level changes carried any evidence: every one was silently floored to low confidence whatever surface it touched. The scanner now also reads the change path's resource chain as member access, in wire spelling (payment_intents) or the SDK's camelCase (paymentIntents), and reports the matching file and line. Deliberately narrow: source files only, code lines only, the chain must be reached through a member access rather than a bare identifier, and a single-word generic resource never carries a finding on its own. - A symbol that only appears in a comment no longer counts as usage. A changed API surface mentioned in a doc comment —
// at https://dashboard.stripe.com/settings/payment_methods.— was matched exactly like a call site, promoting the finding to high confidence and pointing the reader at a line with no code on it. Measured against public sample repos: of 222 high-confidence findings instripe-samples/accept-a-payment, 68 rested entirely on comment lines. Those findings now fall back to the weaker import-level evidence, and code lines are cited before comments for the ones that survive. A symbol used in real code keeps its high verdict even when a nearby comment also mentions it. - A pipenv repo no longer scans as having no dependencies at all.
Pipfileis the fourth place a Python project declares its dependencies, and the only one of the four with no file extension — so it was skipped before matching ever began, at the point where the walker decides whether a file is worth reading. A repo whose sole declaration was[packages]withstripe = ">=11.0.0"scanned as zero files, zero providers and zero impacts: the report said the code was unaffected for a project that installs the SDK on everypipenv install. Pins under[packages]and[dev-packages]are now read, in the plain, inline-table and quoted-key spellings, and attributed to the Python ecosystem so the declared-range gates apply to them.Pipfile.lockis deliberately still ignored: it is generated output whose keys mirror thePipfile, so reading both would count the same declaration twice. - A tilde range is now read in the ecosystem's own grammar. RubyGems' pessimistic
~> 7.0, PEP 440's~= 7.0and Composer's~14.2all free the component above the last one stated, while npm's~14.2.0frees only the patch level — a difference of a whole major version. Every one of them was read as npm, sogem 'twilio-ruby', '~> 7.0', the most common way a Gemfile pins an SDK, got a 7.1.0 ceiling instead of 8.0.0. Measured onstripe~=15.3against the stripe-python corpus: 6 real breaking releases, including the stable v15.4.0, were demoted to low and flagged out of the declared range — reported to the user as "your code is unaffected" for releases the repo installs by default, with the report otherwise looking perfectly normal. npm's own reading is unchanged. - A dependency declared twice no longer inherits the stricter declaration's version range. A repo that lists the SDK in both
dependenciesanddevDependencies— one open (">=1.0.0", a wildcard, an alias), one capped ("^1.0.0") — can install whatever the open declaration admits. The bound readers dropped declarations that stated no bound, so the capped sibling decided the whole ecosystem on its own: measured on that pair, 7 real findings were demoted to low and flagged out-of-range, identical to a repo that really is pinned, while the same repo with only the open declaration reported them. A declaration stating no bound is now the most permissive one and clears its ecosystem outright, on both the floor and the ceiling side. A passing mention of the package name in prose ("name": "my-stripe-app", a package blurb) is not a declaration and still cannot clear a genuine cap. - An aliased dependency no longer inherits its alias target's version range. An alias names the package twice —
"stripe": "npm:@scope/stripe@^14.0.0"— and the second name sits inside the value. The version read began at the last occurrence of the name, so it started past thenpm:prefix and the check for values that resolve elsewhere never saw the protocol: the alias target's range was reported as this package's own declared cap, muting 7 real findings to low and flagging them out-of-range. The read now starts at the last occurrence that actually begins a declaration, which leaves aliased andgit:/file:values recognised as stating no version at this site while keeping a minified manifest resolved to its own entry rather than a neighbour's. - A comment beside a dependency is no longer read as part of its version range. Every manifest grammar here allows a trailing comment on the declaration line, and authors routinely put version numbers in them. Both bound readers scanned the whole line, so the prose became the constraint:
stripe>=1.0.0 # do not go <2.0.0 yetwas capped at a 2.0.0 the file never states, muting 8 real findings to low and flagging them out-of-range, andstripe # pinned >=9.0.0 in prodinvented a floor on a declaration that states none. Comments are now stripped (#in requirements.txt, setup.cfg, Gemfile and TOML;//in go.mod) after the check for values that resolve elsewhere, so a#egg=fragment or anhttps://URL is still recognised rather than truncated. A real bound written next to a comment still wins. - A dependency that excludes one version is no longer treated as pinned to it.
!=rules a single release out; it states no upper bound. That operator was missing from the ceiling reader, sostripe!=5.0.0fell through to the exact-pin branch and was capped at 5.0.0 — the same verdict asstripe==5.0.0. Measured on a repo with that one line, 8 real findings were muted to low and flagged out-of-range while the report still looked normal. Floor reading has always refused!=; only the ceiling side read an exclusion as a cap. - A
pyproject.tomldependency no longer inherits the next entry's version range. PEP 621 states each dependency as one quoted string holding the name and the range together (dependencies = ["stripe>=11.0.0", "zod<3.0.0"]). That value was read past its own closing quote, so stripe took zod's<3.0.0as its ceiling and every real finding above 3.0.0 was silently dropped to low. A bare entry (["stripe", "zod>=3.0.0"]) was worse: with no range of its own it took the *next array element* as its constraint. Both now stop at the entry's own closing quote. - A range union written in descending order no longer mutes releases the repo can install.
"stripe": "^15 || ^14"admits all of 14.x, but the floor was read from the first alternative only, so 14.x findings were dropped to low — while the identical"^14 || ^15"reported them. The floor is now the lowest alternative the union states, and a union containing an open-ended alternative states no floor at all. - A version range spelled across more than one token is now read whole. Two grammars state a range without an operator in front of every bound, and both were misread in the false-negative direction. npm's hyphen range (
"stripe": "14.0.0 - 24.0.0") has no leading operator at all, so it fell through to the exact-pin branch and the range's *floor* came back as an inclusive ceiling — every finding in a range the repo genuinely installs was muted to low, while the identical>=14.0.0 <24.0.0reported normally. Bundler's multi-argument form (gem 'twilio-ruby', '>= 6.0', '< 7.0') read only the first constraint and threw the upper bound away. Both now parse to the bounds they state; option arguments (require: false,group: 'x',:source => '...') are never absorbed as a version bound. - A neighbouring dependency's version range no longer bleeds into yours. The declared-range gates anchored their read at the provider's own name but never bounded where that entry ends, so a minified manifest —
{"stripe":">=14.0.0","zod":"<3.0.0"}— handed stripe the neighbour's<3.0.0ceiling and silently dropped every real high-confidence stripe finding to low. This is the false-negative direction: the report told you your code was fine because of an unrelated line. Each manifest grammar now reads only its own value (JSON quoted values,gemtuples, TOML plain and inline-table values, one-entry-per-line text formats), and a value that is an object or a protocol (catalog:,latest) states no version rather than borrowing the next one's. - An SDK release ABOVE the version range your manifest declares no longer reports as a high-confidence impact either. The floor was closed first; this is the other half, and it is the half a first scan is more likely to hit. A repo pinning
"stripe": "^14.0.0"was shown stripe-node v22.3.1 at high confidence, at the very top of the report — a major it cannot install without changing the pin. Caret, tilde, pessimistic (~>), compatible-release (~=), explicit</<=bounds and exact pins now all state a ceiling, and a release above it is capped to low and flaggedbelow_declared_rangein--json. Anything that states no usable ceiling — a floor-only pin, a range union (^14 || ^15), a wildcard, acatalog:/workspace:/git reference, or a go.modrequireline, which states a minimum rather than a bound — leaves confidence untouched, so the fail-safe direction remains "report it". - Both version gates now read a
setup.pydeclaration.setup.pyis a dependency declaration site that is deliberately not treated as a manifest (it is also Python source), so a setuptools project declaring its pins only there had no declared range at all and every release stayed promotable however far outside that range it was. - An SDK release older than the version range your manifest declares no longer reports as a high-confidence impact. Every manifest matcher already proved the value it read had the shape of a version range, then discarded the value. So a repo pinning
"stripe": "^22.9.0"was still told that stripe-node v22.3.1 breaks it — a release it cannot install, ranked at the top of the report because its symbols genuinely matched. Declared floors are now read per ecosystem across every manifest that declares the provider (the lowest floor wins, since that is what the repo may install), and a release strictly below that floor is capped to low confidence and flaggedbelow_declared_rangein--json. Values that resolve elsewhere (catalog:,workspace:, a git URL), upper-bound-only constraints (<23), and PEP 508 environment markers state no floor and change nothing, so an unreadable declaration still reports the change rather than hiding it. - A dependency nested one level below the section that declares it is no longer invisible. Scoping the manifest matchers to dependency sections left three spellings unreadable, each of which removed the provider from the report entirely rather than lowering its confidence: pnpm's scoped override form (
"overrides": { "foo>bar": { "stripe": "1.2.3" } }), PEP 621 extras ([project.optional-dependencies]withdev = ["stripe>=11"]), and PEP 735 dependency groups ([dependency-groups]). Thepackage.jsonsection walk can now descend into nested objects while still being unable to climb out of the section it started in, and the pyproject matcher scopes extras and groups by their table header. Every false-positive control from the previous fix stays at zero, including a same-namedconfigkey nested inside an object. - A key that merely shares a name with an SDK no longer invents a whole provider. The
package.jsonandpyproject.tomlmatchers read the name anywhere in the file, so"config": { "stripe": "^14.0.0" }— a setting that installs nothing — reported stripe as a detected provider with its full impact set, on a repo that does not depend on stripe at all. Both are now scoped to the sections that actually declare a dependency: forpackage.json,dependencies/devDependencies/peerDependencies/optionalDependencies/resolutions/overrides; forpyproject.toml, adependencies = [...]list or an open Poetry/PEP 621 dependency table. Real declarations in every one of those spellings still resolve unchanged. This is the mirror image of the packaging-metadata misses below — a first scan that opens with impacts nobody owns costs a user's trust faster than one that reports too few. - A dependency declared only in packaging metadata is no longer invisible. A library repo often spells its runtime dependencies exactly once, and not in the manifest the scanner knew about: a gem declares them in
<name>.gemspec(its Gemfile is then a one-linegemspecdirective naming nothing), and a setuptools project that predates pyproject declares them under[options] install_requiresinsetup.cfgor ininstall_requires=[...]insidesetup.py. Each of those repos scanned asproviders_detected: []— the whole provider left the report rather than being reported at lower confidence, so the scan read as "your code is unaffected". All three declaration sites are now read.setup.pykeeps being scanned as ordinary Python source at the same time, so a setup.py that also imports the SDK yields both findings. Precision is unchanged: a commented-out declaration (what an upgrade leaves behind) declares nothing, development-only dependencies (add_development_dependency,extras_require,tests_require) are not product SDK calls and are excluded, the distribution name insetup()'s own metadata is not a dependency, andstripe-mockstill does not count asstripe. - A dependency declared in
requirements.txtorpyproject.tomlis no longer invisible. Python's manifests are the same blind spot as the Gemfile andpackage.jsonone ecosystem over: a service that pinsstripe==11.4.0and reaches the SDK through a wrapper reportedproviders_detected: [], so every breaking change for that provider was dropped rather than reported at lower confidence. Both manifests are now read, in all the spellings that declare a dependency — a requirements line with extras and any specifier, a PEP 621dependencies = [...]entry, and a Poetryname = "^1.2"table key — and distribution names are compared per PEP 503, soslack_sdkandslack-sdkare recognised as the same project. Precision is unchanged: a commented-out pin (what an upgrade leaves behind) declares nothing,stripe-mockdoes not count asstripe, and the manifest matchers never read ordinary source files. - A dependency declared in
package.jsonbut only used through a wrapper is no longer invisible."stripe": "^14.0.0"in a manifest is the strongest proof a repo depends on an SDK, but the scanner only read files with a source extension, so a service that declares its dependencies and touches them through an internal wrapper module reportedproviders_detected: []— every breaking change for that provider was dropped, not downgraded. The Gemfile had already been fixed; npm's manifest is the far more common spelling of the same defect. Only real dependency entries count: ascriptsentry with a matching name declares no dependency and is ignored, and the manifest matcher never reads ordinary source files. - A Python or Ruby import wrapped across lines with a backslash is no longer invisible. A backslash immediately before a newline is the idiomatic way to wrap a long
import a, b, clist, and it is how Ruby continues a longrequire. A backslash is not a whitespace character, so the\sseparator in the import matchers stopped dead at it: inimport os, \-newline-openai, every module past the wrap was never seen. The consequence is not a lower-confidence verdict — the provider drops out of the report entirely, and the scan reads as "your code is unaffected". This is the same silent underreport family as the multi-name list fix in 0.5.7, one syntax layer down: that one lost modules in a later position on a single line, this one loses every module past a wrapped line. The Python and Ruby import matchers now treat a line continuation as a legal separator wherever an import statement may be wrapped. Precision is unchanged: a continuation cannot make a lookalike name (openai_helper) into an import, and a continued import inside a comment stays silent. - A Ruby
require('gem')written with parentheses is no longer invisible.requireis an ordinary method in Ruby, so the parenthesised call is as valid asrequire 'gem', and generated or vendored code writes it that way. The Ruby matcher only accepted the whitespace spelling. Gems whose name happens to equal the npm package name (stripe, openai, plaid) appeared to work, but only by accident — the language-agnostic JavaScriptrequire(...)alternative caught them. The four gems named differently from their npm counterpart (twilio-ruby,shopify_api,slack-ruby-client,sendgrid-ruby) had no such backstop and produced zero impacts: the provider left the report entirely, so the repository read as unaffected. That is the same silent underreport family as the wrapped-import fix above. Precision is unchanged:require_relative('gem'), an identifier ending in the verb (myrequire('gem')), a local path spelled like the gem (require('./gem')) and a commented-out require all stay silent. - A Ruby
Kernel.require 'gem'is no longer invisible.requireis a private method onKernel, so an explicit receiver —Kernel.require 'gem'orKernel::require('gem')— is equally valid Ruby, and it is what code writes when a file defines its ownrequireor when a generator emits fully qualified calls. The matcher anchored on the bare verb at a statement boundary, so the receiver form was rejected outright and the provider left the report entirely: measured at 0 impacts against 36 for the identical bare spelling. Same silent underreport family as the parenthesised-require fix above, one call shape further out. Precision is unchanged: a lookalike receiver (MyKernel.require), a chained receiver (obj.kernel.require),Kernel.require_relativeand a commented-out call all stay silent.
Version 0.5.7
Four scanner fixes, all shipping together. The headline one is the worst class of
bug this tool can have: a repository that used a common import spelling was
scanned, found clean, and told it had nothing to worry about.
Fixed
- A repository that loads an SDK dynamically is no longer scanned as clean. The JavaScript import matcher enumerated
require(,from '...'andimport '...', but notimport(...). A repo that reaches for a provider SDK through a dynamic import was therefore never considered for that provider at all: the scan finished, found nothing, and reported no impacted usage. That is a silent false negative, not a low-confidence verdict — and it is the worst possible failure for a tool whose entire job is to tell you whether an upstream breaking change touches your code. Measured on the same dependency: 0 impacts for the dynamic spelling versus 1762 for the static and CommonJS spellings. Two further spellings measured at zero for the same reason. A module specifier may be a template literal — `import(stripe)is legal and common in lazy-loading code — but the quote class held only'and". And a call may span a newline: the matcher accepts it, but the per-line pass that attributes a line number could not re-match a multiline hit, so a confirmed whole-file match was dropped. Line attribution now runs as a single whole-text pass, so a multi-line usage survives even when the same file also contains a single-line one. Adding the dynamic form exposed a pre-existing twin: neither parenthesised form carried a left boundary, somyrequire('stripe')already false-matched. Both now carry a negative lookbehind, which also excludes member calls such asobj.import(...)`. Evidence requirements are unchanged — the path-level evidence bar is not relaxed, and a substituted template specifier is still treated as a variable reference that a text matcher must never claim to resolve. - A partly failed
syncno longer looks like a successful one.syncis the only networked command and the first thing every user runs. Previously only a total wipeout (every feed erroring) exited non-zero; if some feeds failed and others succeeded, sync exited0and said nothing. Because GitHub rate-limits anonymous feed requests (60/hour, and sync fetches 27 feeds), a second run inside the same hour could leave most providers unfetched. The resulting change database is thin, and the nextmendapi scantruthfully reports no impacted usage — which is indistinguishable from a genuinely clean repository. Sync now ends with an explicit verdict:sync complete:on success, orsync incomplete:on stderr naming every provider left stale plus the coverage percentage, and warning that a scan run now can report no impact simply because those feeds were never fetched. Exit code stays0for a partial failure (the database did get fresher, and one dead upstream feed should not fail CI) and remains1when every feed fails.mendapi sync --helpnow documents both exit codes. - A value-taking flag given without its value no longer falls back to a default.
mendapi scan --repo(flag present, value missing) exited0and reported on the current directory instead — the common shape being--repo $REPOin CI withREPOunset. The user gets a complete, plausible report about a tree they did not ask about, with no warning and no non-zero exit. All five flag-parsing subcommands (scan,fix,llmfix,pr,deps) now declare which flags require a value and exit2withMissing value for --<flag>when one is missing. Boolean flags (--json,--apply,--push,--run-checks,--match, ...) are unaffected. This completes the fix started in 0.5.6, which covered discarded positional arguments but not half-given flags. - Endpoint-level evidence is now held to a path-level bar, not a host-level one. An impact used to qualify on the strength of the host alone, so any usage of a provider's domain could carry a finding about one specific endpoint. Evidence must now match at path level for the endpoint it claims.
Changed
- Changelog wording for 0.5.6 corrected. The published 0.5.6 tarball ships a preamble describing the release as "landed in the repository but not yet published to npm" — text written while the release was still pending and left in place when it went out. It now states what the release contains. No code behaviour changed; this entry exists so the correction reaches users with the next publish rather than silently.
- The "30 seconds" claim now states the repo size it holds for. Every surface that made the claim said "in under 30 seconds" with no qualification. Measured wall-clock scan time is roughly 8 ms per file, so the budget is exhausted at around 3,200-3,900 files depending on machine load — true for a typical service repository, false for a large monorepo, and nothing said which was which. The README, home page, how-it-works page and docs index now publish a conservative ceiling (~3,000 files, below the lowest measurement observed) and state in prose that large monorepos take proportionally longer. The ceiling is a single source in the build and is asserted against the live measurement by the test suite, so the published number cannot outrun what the scanner actually does.
Version 0.5.6
Two correctness fixes that could damage or misreport a user's repository.
Fixed
scan,fix,deps,llmfixandprno longer scan the wrong directory. A positional path (mendapi deps ./my-repo— the most natural thing to type) was silently discarded, and--repofell back to the current working directory. The command then produced a complete, well-formed, confidently worded report about a tree the user never asked about, and exited 0. Stray positional arguments are now a usage error: the offending argument is named, the message points at--help, and the process exits 2. Affectsdeps.js,fixer.js,llmfix.js,pr.js,scanner.js.prno longer commits or deletes files you own. The commit was staged withgit add -A, which swept in every artifact the clean-worktree check had just exempted as yours — yourimpact.jsonand the entire.mendapi/directory, change database included — and then removedimpact.jsonfrom the working tree when it checked back out to the base branch. Staging is now limited to the files the fixer actually rewrote.prworks whennodeis not onPATH. The child fixer was spawned as a barenode, which crashes under an agent, a CI runner, or an npx shim with a differentPATH. It now re-enters throughprocess.execPath.prno longer collides with other users through a shared temp path. Its probe run wrote to a hardcoded/tmp/mendapi-probeand left the directory behind; each run now gets a private temp directory and cleans up after itself.pr --from-reportresolves the report path against your shell's working directory, not against wherever git checkouts happen to have moved the process mid-run. Argument errors are also reported before worktree state, so a mistyped filename no longer surfaces as a confusing "dirty worktree" complaint naming the report you asked for.fix --from-report --jsonemits run-levelverification. The aggregate output omitted the block that the per-migration output carries, so anything consuming the documented JSON pipeline — including the CI recipe in the docs — hit aTypeErroron its first run.scan's closing hint names a real file. It always printed the placeholder<report>, leaving the user to guess. It now reflects actual state: with--out, it prints the real filename; without it, it tells you to add--outfirst.
Changed
- README quickstart starts with
sync. The first block told a new user to runscanagainst a change database that does not exist yet on a cold install, so the documented first command exited 2. The three-step path (sync→scan --out→fix --from-report) is now explicit, and the one command that touches the network is named as such.
Version 0.5.5
Fixed
mendapi <subcommand> -hwas not normalized to--helpbefore dispatch, so the short flag failed on every subcommand.
Version 0.5.4
Added
--versionflag, read frompackage.jsonas the single source of truth.
Fixed
sync --helpis offline-safe: it prints usage instead of triggering a network fetch.- The CLI fails loudly on Node older than 22.13 (the
node:sqlitefloor) instead of failing obscurely later.
Version 0.5.3
Fixed
- Default
--out-diris<cwd>/.mendapirather than a path inside the development tree. - Database path resolution is single-sourced through
dbpath.js, so npm consumers persistsentinel.dbunder<cwd>/.mendapiinstead of insidenode_modules.
Version 0.5.2
Fixed
astlite.jsis included in the published package. Without it,npx mendapi fixwas broken in 0.5.2's initial upload.
Version 0.5.1
Added
mcpNameand MCP-related keywords, for the Model Context Protocol registry.
Version 0.5.0
First public release on npm.
Added
sync— fetch the upstream API change feed into a local SQLite database. The only command that touches the network.scan— find upstream breaking changes that hit your code, with file, line, symbol and a confidence score.fix— draft the migration as a reviewable diff; nothing is written until you pass--apply.review,deps,revalidate,llmfix,prsubcommands.mendapi mcp— a zero-dependency stdio MCP server exposing the toolset to any MCP client.- Agent skill for Claude Code, Cursor and other agent runtimes.
Documentation generated from the mendapi codebase. Command output and pack listings reflect the shipped CLI exactly.