+86
-3
@@ -8,7 +8,90 @@ # Changelog | ||
| ## [0.5.7] - 2026-08-06 | ||
| 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 '...'` and | ||
| `import '...'`, but not `import(...)`. 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, so `myrequire('stripe')` already false-matched. | ||
| Both now carry a negative lookbehind, which also excludes member calls such as | ||
| `obj.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 `sync` no longer looks like a successful one.** `sync` is 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 exited `0` and 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 next `mendapi scan` truthfully reports no impacted | ||
| usage — which is indistinguishable from a genuinely clean repository. Sync now | ||
| ends with an explicit verdict: `sync complete:` on success, or | ||
| `sync 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 stays `0` for a partial failure (the | ||
| database did get fresher, and one dead upstream feed should not fail CI) and | ||
| remains `1` when every feed fails. `mendapi sync --help` now 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) exited `0` and | ||
| reported on the current directory instead — the common shape being | ||
| `--repo $REPO` in CI with `REPO` unset. 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 exit `2` with | ||
| `Missing 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. | ||
| ## [0.5.6] - 2026-08-05 | ||
| Changes landed in the repository but not yet published to npm. The set of files | ||
| that differ from the published tarball is derived mechanically — see | ||
| `loop/release-drift.mjs` and the release-drift gate. | ||
| Two correctness fixes that could damage or misreport a user's repository. | ||
@@ -15,0 +98,0 @@ ### Fixed |
+19
-3
@@ -21,3 +21,3 @@ #!/usr/bin/env node | ||
| // node app/llmfix.js --from-report <impact.json> [--repo path] | ||
| // [--max N] [--out-dir dir] [--list] | ||
| // [--max <n>] [--out-dir dir] [--list] | ||
| // | ||
@@ -40,3 +40,3 @@ // --list print the candidate changes (those with a code-fixable knowledge | ||
| function usage() { | ||
| console.error('Usage: mendapi llmfix --from-report <impact.json> [--repo <path>] [--max N] [--out-dir <dir>] [--list]'); | ||
| console.error('Usage: mendapi llmfix --from-report <impact.json> [--repo <path>] [--max <n>] [--out-dir <dir>] [--list]'); | ||
| console.error('Requires MENDAPI_LLM_* config (BYO compute; see the BYO LLM docs at https://mendapi.com/docs/byo-llm.html). Emits DRAFT patches only; never modifies the repo.'); | ||
@@ -53,2 +53,8 @@ process.exit(2); | ||
| // flag, so anything not starting with `--` can only be a mistake. Fail loud. | ||
| // Flags that require a value. A flag listed here that arrives without one is a | ||
| // half-finished instruction (`--repo $REPO` where REPO is unset is the common | ||
| // shape), not an absent one -- so it must fail loud rather than fall back to a | ||
| // default and answer a different question than the one the user asked. | ||
| const VALUE_FLAGS = new Set(['repo', 'from-report', 'out-dir', 'max']); | ||
| function parseArgs(argv) { | ||
@@ -58,3 +64,13 @@ const args = {}; | ||
| const a = argv[i]; | ||
| if (a.startsWith('--')) { args[a.slice(2)] = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : true; continue; } | ||
| if (a.startsWith('--')) { | ||
| const name = a.slice(2); | ||
| const value = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : true; | ||
| if (value === true && VALUE_FLAGS.has(name)) { | ||
| console.error(`Missing value for --${name}`); | ||
| console.error(`Usage: --${name} <value>. Run with --help for usage.`); | ||
| process.exit(2); | ||
| } | ||
| args[name] = value; | ||
| continue; | ||
| } | ||
| console.error(`Unexpected argument: ${a}`); | ||
@@ -61,0 +77,0 @@ console.error('This command takes flags only (for example: --repo <path>). Run with --help for usage.'); |
+1
-1
| { | ||
| "name": "mendapi", | ||
| "version": "0.5.6", | ||
| "version": "0.5.7", | ||
| "license": "AGPL-3.0-only", | ||
@@ -5,0 +5,0 @@ "type": "module", |
+17
-1
@@ -34,2 +34,8 @@ #!/usr/bin/env node | ||
| // flag, so anything not starting with `--` can only be a mistake. Fail loud. | ||
| // Flags that require a value. A flag listed here that arrives without one is a | ||
| // half-finished instruction (`--repo $REPO` where REPO is unset is the common | ||
| // shape), not an absent one -- so it must fail loud rather than fall back to a | ||
| // default and answer a different question than the one the user asked. | ||
| const VALUE_FLAGS = new Set(['repo', 'migration', 'from-report', 'out-dir']); | ||
| function parseArgs(argv) { | ||
@@ -39,3 +45,13 @@ const args = {}; | ||
| const a = argv[i]; | ||
| if (a.startsWith('--')) { args[a.slice(2)] = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : true; continue; } | ||
| if (a.startsWith('--')) { | ||
| const name = a.slice(2); | ||
| const value = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : true; | ||
| if (value === true && VALUE_FLAGS.has(name)) { | ||
| console.error(`Missing value for --${name}`); | ||
| console.error(`Usage: --${name} <value>. Run with --help for usage.`); | ||
| process.exit(2); | ||
| } | ||
| args[name] = value; | ||
| continue; | ||
| } | ||
| console.error(`Unexpected argument: ${a}`); | ||
@@ -42,0 +58,0 @@ console.error('This command takes flags only (for example: --repo <path>). Run with --help for usage.'); |
+3
-1
@@ -15,2 +15,4 @@ # mendapi | ||
| That budget is measured, not aspirational: a 3,000-file repo is about where 30 seconds runs out. Large monorepos take proportionally longer. | ||
| `sync` pulls the upstream API change feed into a local SQLite database. It is the only command that touches the network, and you run it once (then whenever you want fresher data). | ||
@@ -168,3 +170,3 @@ | ||
| Published on npm as [`mendapi`](https://www.npmjs.com/package/mendapi) (v0.5.6). Early release — the change database and migration pack registry grow daily; interfaces may still shift before 1.0. | ||
| Published on npm as [`mendapi`](https://www.npmjs.com/package/mendapi) (v0.5.7). Early release — the change database and migration pack registry grow daily; interfaces may still shift before 1.0. | ||
@@ -171,0 +173,0 @@ ## License |
+64
-1
@@ -206,2 +206,47 @@ #!/usr/bin/env node | ||
| // syncVerdict — pure, importable, no I/O. Decides what a sync run means for the | ||
| // user, given how many feeds were attempted and how many failed. | ||
| // | ||
| // Why this exists (Loop 680): `sync` is the only networked command and it is the | ||
| // FIRST thing every user runs. Before this, only a TOTAL wipeout (errors > 0 && | ||
| // inserted === 0) exited non-zero. A PARTIAL failure — GitHub's anonymous rate | ||
| // limit is 60 req/h and we fetch 24 feeds, so a second run within the hour can | ||
| // fail most of them — exited 0 while quietly leaving the change database thin. | ||
| // `scan` then truthfully reports "no impacted usage", and the user concludes the | ||
| // product found nothing. That is the silent-wrong-answer family (Loop 665/675/676) | ||
| // landing on the one command whose failure mode is invisible by construction: | ||
| // a report of zero findings looks exactly like a clean repo. | ||
| // | ||
| // The verdict is deliberately not "any error is fatal". Upstream feeds go down | ||
| // individually all the time, and failing the whole run for one dead feed would | ||
| // make `sync` useless in CI. Three states: | ||
| // ok — every feed answered. | ||
| // degraded — some feeds failed. exit 0 (the DB did get fresher), but say so | ||
| // loudly and name the missing providers, because the next `scan` | ||
| // is now blind to exactly those. | ||
| // failed — every feed failed, or none succeeded. exit 1. | ||
| export function syncVerdict({ attempted, errors, inserted, failedProviders = [] }) { | ||
| if (attempted === 0) { | ||
| return { status: 'failed', exitCode: 1, coverage: 0, | ||
| message: 'sync attempted no feeds — nothing was fetched.' }; | ||
| } | ||
| const okFeeds = attempted - errors; | ||
| const coverage = Math.round((okFeeds / attempted) * 100); | ||
| const uniq = [...new Set(failedProviders)].sort(); | ||
| if (okFeeds === 0) { | ||
| return { status: 'failed', exitCode: 1, coverage, | ||
| message: `sync failed: all ${attempted} feeds errored. The change database was not updated.` + | ||
| ' Check network access, then re-run `mendapi sync`.' }; | ||
| } | ||
| if (errors > 0) { | ||
| return { status: 'degraded', exitCode: 0, coverage, | ||
| message: `sync incomplete: ${errors} of ${attempted} feeds failed (${coverage}% coverage).` + | ||
| ` Changes for these providers may be missing or stale: ${uniq.join(', ')}.` + | ||
| ' A scan run now can report no impact simply because those feeds were not fetched.' + | ||
| ' GitHub rate-limits anonymous feed requests; wait and re-run `mendapi sync`.' }; | ||
| } | ||
| return { status: 'ok', exitCode: 0, coverage, | ||
| message: `sync complete: ${attempted}/${attempted} feeds fetched, ${inserted} new change(s).` }; | ||
| } | ||
| async function main() { | ||
@@ -214,5 +259,8 @@ const db = openDb(); | ||
| let inserted = 0, errors = 0; | ||
| let attempted = 0; | ||
| const failedProviders = []; | ||
| const jobs = []; | ||
| for (const [provider, repos] of Object.entries(SOURCES)) { | ||
| for (const repo of repos) { | ||
| attempted++; | ||
| jobs.push( | ||
@@ -231,2 +279,3 @@ fetchFeed(repo) | ||
| errors++; | ||
| failedProviders.push(provider); | ||
| console.error(`error ${provider.padEnd(11)} ${repo}: ${err.message}`); | ||
@@ -244,2 +293,3 @@ }) | ||
| for (const url of urls) { | ||
| attempted++; | ||
| clJobs.push( | ||
@@ -258,2 +308,3 @@ fetchChangelog(url) | ||
| errors++; | ||
| failedProviders.push(provider); | ||
| console.error(`error ${provider.padEnd(11)} changelog ${url}: ${err.message}`); | ||
@@ -271,3 +322,10 @@ }) | ||
| db.close(); | ||
| if (errors > 0 && inserted === 0) process.exit(1); | ||
| // Verdict last, so it is the final thing the user sees. A degraded run must not | ||
| // be indistinguishable from a clean one: the next `scan` is blind to exactly the | ||
| // providers named here, and "no impact found" would look identical either way. | ||
| const verdict = syncVerdict({ attempted, errors, inserted, failedProviders }); | ||
| if (verdict.status === 'ok') console.log(verdict.message); | ||
| else console.error(verdict.message); | ||
| if (verdict.exitCode !== 0) process.exit(verdict.exitCode); | ||
| } | ||
@@ -282,2 +340,7 @@ | ||
| console.log('and it only runs when you invoke it without --help.'); | ||
| console.log(''); | ||
| console.log('Exit codes: 0 when every feed was fetched, or when some feeds failed'); | ||
| console.log('but others succeeded (a "sync incomplete:" line on stderr names the'); | ||
| console.log('providers left stale — a later scan is blind to exactly those).'); | ||
| console.log('1 when every feed failed and the database was not updated.'); | ||
| } | ||
@@ -284,0 +347,0 @@ |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
972659
2.93%15614
2.5%174
1.16%