From d5f0360b4c3a1af80e0d66499407a1fd4fc2a06a Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Mon, 24 Aug 2026 13:45:13 +0300 Subject: [PATCH 1/3] chore: pin Bun 1.4.0 and isolate tests Keep published engines.bun at >=1.0.31. Gather upgrade-packages evidence from bun pm diff, and fail check/CI when the lockfile is not deduped. --- .agents/skills/upgrade-packages/REFERENCE.md | 35 +- .agents/skills/upgrade-packages/SKILL.md | 25 +- .github/CONTRIBUTING.md | 2 +- .github/actions/setup/action.yml | 2 +- .github/workflows/ci.yml | 19 + bun.lock | 74 +- lint-staged.config.js | 9 +- package.json | 13 +- scripts/upgrade-packages/evidence.test.ts | 97 +++ scripts/upgrade-packages/evidence.ts | 677 +++++++++--------- .../upgrade-packages/tarball-delta.test.ts | 286 ++++++++ scripts/upgrade-packages/tarball-delta.ts | 459 ++++++++++++ 12 files changed, 1257 insertions(+), 441 deletions(-) create mode 100644 scripts/upgrade-packages/evidence.test.ts create mode 100644 scripts/upgrade-packages/tarball-delta.test.ts create mode 100644 scripts/upgrade-packages/tarball-delta.ts diff --git a/.agents/skills/upgrade-packages/REFERENCE.md b/.agents/skills/upgrade-packages/REFERENCE.md index a4a69d03..05009a36 100644 --- a/.agents/skills/upgrade-packages/REFERENCE.md +++ b/.agents/skills/upgrade-packages/REFERENCE.md @@ -1,6 +1,10 @@ # Reference — evidence artifact schema -The `upgrade-packages` skill runs `bun run upgrade-packages:evidence` (writes `scripts/upgrade-packages/artifact.json`) and reads the JSON artifact below. The agent never touches the registry, GitHub, or GHSA directly — every citation comes from artifact fields. The script requires network access (`gh api` + `bun pm view`); release/advisory data is cached to `scripts/upgrade-packages/.cache/` (1h TTL). +The skill runs `bun run upgrade-packages:evidence` (default `--out scripts/upgrade-packages/artifact.json`). Phase 1 of [`SKILL.md`](./SKILL.md) owns network, cache TTLs, and the never-touch-registry rule. + +Each outdated package gets **one** delta: the published tarball span `current → latest`. There is no per-tag GitHub release walk. + +Cutter caps (`tarball-delta.ts`): `PATCH_PATH_CAP` 40 paths on the second `bun pm diff`, `FILE_LIST_CAP` 80 files in the artifact, `HINT_CAP` 8 hints per bucket, `PATCH_MAX_CHARS` 8000, `RELEASE_NOTES_MAX_CHARS` 1200. A missing `*.d.ts` patch means it lost the cap, not that the API is unchanged. `source: "none"` sets `tarball: null`. ## Artifact shape @@ -14,7 +18,8 @@ The `upgrade-packages` skill runs `bun run upgrade-packages:evidence` (writes `s "outdated": [{ "pkg", "current", "latest", "bumpClass": "patch|minor|major|prerelease|no-op", - "coupledWith": [] // naive; agent confirms peer/dep coupling from deltas + "coupledWith": [], // naive; agent confirms peer/dep coupling from deltas + "dev" }], "audit": { "bunAudit": , @@ -29,13 +34,19 @@ The `upgrade-packages` skill runs `bun run upgrade-packages:evidence` (writes `s }, "deltas": { "": [{ - "version", "date", + "version", "date", // target; date from changelog hunk if present "breaking": [...], "deprecations": [...], "features": [...], - "security": [...], "peerEngine": [...], // best-effort regex hints — read releaseNotes for the authoritative text - "releaseNotes": "truncated body", - "diffUrl": "github.com///compare/...", - "changelogUrl": "github.com///releases/tag/", - "source": "github-release|none", + "security": [...], "peerEngine": [...], // notes + changelog/package.json hints + "releaseNotes": "changelog added-lines, truncated", + "changelogUrl": "npmjs.com/package//v/", + "tarball": { + "from", "to", + "notes": [...], // bun summary: engines, deps, install scripts, dangerous imports + "totals": { "files", "added", "deleted", "linesAdded", "linesRemoved", "formattingOnly" }, + "files": [{ "path", "status", "linesAdded", "linesRemoved", "formattingOnly", "patch" }] + // patch only for paths that won PATCH_PATH_CAP (named keep + symbol-boosted .d.ts + extras) + }, + "source": "bun-pm-diff|none", "error": null | "reason" }] }, @@ -52,8 +63,8 @@ The `upgrade-packages` skill runs `bun run upgrade-packages:evidence` (writes `s ## How to read it -- **Verdict a package**: read `outdated[].bumpClass` + `audit.ghsa[].verdict` + `deltas[][].breaking`/`security` + `usage[].importedSymbols`. Cite `diffUrl` or `changelogUrl` for every claim. -- **`features`/`breaking` arrays are hints** — when a hint is empty but the delta is minor/major, read `releaseNotes` before concluding "no changes". -- **`error` on a delta** means the script couldn't fetch that version's release notes — usually a gh secondary rate-limit during a large multi-repo run, or a monorepo squashed release. `changelogUrl` is still provided (the repo's releases page) — **deep-dive it per-package** when `releaseNotes` is null. Only mark the bump **blocked** if the deep-dive still can't cover the range. +- **Verdict a package**: read `outdated[].bumpClass` + `audit.ghsa[].verdict` + `tarball.notes` + `deltas[][].breaking`/`security` + `usage[].importedSymbols`. Cite `tarball.notes`, a kept `patch`, `changelogUrl`, or advisory `url`. +- **`features`/`breaking` arrays are hints** — when a hint is empty but the delta is minor/major, read `releaseNotes` and kept patches before concluding "no changes". +- **`error` on a delta** means `bun pm diff` failed for that span. `changelogUrl` is still the npm version page. Re-run evidence before marking **blocked**. - **`cleared-at-current`** = the GHSA advisory's fix already ships at the installed version — no bump needed, record the URL as evidence. -- **Citations are artifact fields** — `diffUrl`, `changelogUrl`, `url`. Do not invent URLs. +- **Citations are artifact fields** — `tarball.notes`, kept `tarball.files[].patch`, `changelogUrl`, `url`. Do not invent GitHub compare URLs. diff --git a/.agents/skills/upgrade-packages/SKILL.md b/.agents/skills/upgrade-packages/SKILL.md index b4982207..67dec334 100644 --- a/.agents/skills/upgrade-packages/SKILL.md +++ b/.agents/skills/upgrade-packages/SKILL.md @@ -6,13 +6,13 @@ description: >- # Upgrade packages -A script gathers every release delta, GHSA advisory, and codebase-usage site into one JSON artifact; you read it and judge. **Never touch the registry, GitHub, or GHSA directly** — every citation comes from the artifact (`diffUrl`, `changelogUrl`, advisory `url`), so no model priors sneak in. Sources of truth: the artifact, then the codebase. Cite where you read every claim. +A script gathers every tarball delta (`bun pm diff`), GHSA advisory, and codebase-usage site into one JSON artifact; you read it and judge. **Never touch the registry, GitHub, or GHSA directly** — every citation comes from the artifact (`tarball.notes`, kept `tarball.files[].patch`, `changelogUrl`, advisory `url`), so no model priors sneak in. Sources of truth: the artifact, then the codebase. Cite where you read every claim. This repo uses **bun** with **exact** pins in `package.json` — `bun update` alone bumps nothing. Lift an exact pin with `bun update @` (or `bun update --latest` per package) / a `package.json` edit. Never `bun update --latest` across the board. The `check-updates` script is inventory-only. ## Phase 1 — Gather evidence -Run `bun run upgrade-packages:evidence` (defaults to `--out scripts/upgrade-packages/artifact.json`; pass `--out ` to override). **Requires network access** — the script calls `gh api` (GitHub releases + GHSA advisories) and `bun pm view`; a sandboxed network allowlist that blocks `api.github.com` will make every gh call fail (the artifact degrades to `error` markers with `changelogUrl` deep-dive links). Release/advisory data is cached to `scripts/upgrade-packages/.cache/` (1h TTL) so re-runs are fast and don't re-trip rate limits. Read the artifact. Schema + how-to-read: [`REFERENCE.md`](./REFERENCE.md). +Run `bun run upgrade-packages:evidence` (defaults to `--out scripts/upgrade-packages/artifact.json`; pass `--out ` to override). **Requires network access** — the script calls `bun pm diff` (registry tarballs) and `gh api` (GHSA advisories only). A sandbox that blocks `api.github.com` fails GHSA (`check-failed`); deltas still land. Cache: `scripts/upgrade-packages/.cache/` (GHSA 1h, tarball diffs 7d). Read the artifact. Schema + how-to-read: [`REFERENCE.md`](./REFERENCE.md). **Done when:** the artifact exists and you've read `inventory`, `outdated`, `audit`, `deltas`, and `usage`. @@ -22,17 +22,17 @@ For each `outdated` package, produce a cited verdict + band: - **band** = `bumpClass` (patch/minor/major/prerelease). **Coupled deps:** if a patch bump's peer/dep requires a minor+ bump of another direct dep (check `deltas[][].peerEngine` + the other package's `bumpClass`), move the coupled set up a band. - **priority-bump** if `audit.ghsa` verdict is `priority-bump` — goes first within its band. -- **check-failed** if `audit.ghsa` verdict is `check-failed` (gh/parse/cache failure, `id:"error"`) — inconclusive, not a vuln; re-run evidence or deep-dive the `changelogUrl` before treating as blocked. -- **blocked** if any `deltas[][].error` leaves the range uncovered and the missing version can't be sourced via its `diffUrl`, OR a `breaking`/`security`/`peerEngine` delta is a break-risk you can't resolve (see Phase 3). +- **check-failed** if `audit.ghsa` verdict is `check-failed` (gh/parse/cache failure, `id:"error"`) — inconclusive, not a vuln; re-run evidence before treating as blocked. +- **blocked** if any `deltas[][].error` leaves the span uncovered (retry evidence; `changelogUrl` is the npm version page) OR a `breaking`/`security`/`peerEngine` delta is a break-risk you can't resolve (see Phase 3). - **deferred-major** for a major with an unresolved break — unless it clears a high/critical advisory, in which case surface the tradeoff to the user. -Cite the `diffUrl`/`changelogUrl`/advisory `url` for every verdict. +Cite `tarball.notes` / kept patches / `changelogUrl` / advisory `url` for every verdict. **Done when:** every outdated package has a cited verdict, a band, and a coupled-set tag; every `priority-bump` is flagged for Phase 3. ## Phase 3 — Fact-check break-risk against the codebase -For every **break-risk delta** (`breaking`, `deprecations`, `peerEngine`, or a behavior-changing fix in `security`/`features` — e.g. callback debounce, CVE patch altering semantics), cross-check `usage[]`. Use `callSites` (codemap) for **blast radius** — where the symbol is actually called, not just imported; `importedSymbols` + `typeOnlySymbols` for what's in scope; `sites` for import locations. Classify: **no usage** / **code-aligned** / **breaks** (needs a code change first). Cite `callSites`/`sites` (file:line). A `breaks` with no code change → the bump is **blocked** or **deferred**. +For every **break-risk delta** (`breaking`, `deprecations`, `peerEngine`, or a behavior-changing fix in `security`/`features` — e.g. callback debounce, CVE patch altering semantics), read `tarball.notes` plus kept patches (`package.json`, `*.d.ts`, changelog, top-churn extras) and cross-check `usage[]`. Use `callSites` (codemap) for **blast radius** — where the symbol is actually called, not just imported; `importedSymbols` + `typeOnlySymbols` for what's in scope; `sites` for import locations. Classify: **no usage** / **code-aligned** / **breaks** (needs a code change first). Cite `callSites`/`sites` (file:line). A `breaks` with no code change → the bump is **blocked** or **deferred**. **Done when:** every break-risk delta has a citation-backed `no usage | aligned | breaks` verdict; every `breaks` has a proposed code change. @@ -50,15 +50,15 @@ A bumped parser/resolver/CSS package can change extraction output — re-run `bu ## Phase 5 — Verify (local CI mirror) -Run what `.github/workflows/ci.yml` runs — `ci.yml` is the SSOT: +`package.json` `check` and `.github/workflows/ci.yml` are the SSOT. Do not restate their job lists here. -- `bun run check` → build + format:check + lint:ci + test + test:scripts + typecheck + test:golden + test:agent-eval (5 of 6 CI jobs: Format, Lint, Typecheck, Test, Build) -- `bun run build` → covered by `check`, but re-run explicitly if a bump only touched build tooling (tsdown/oxc) — **do not skip**; dep upgrades break the bundler/codegen far more often than types -- `bun audit` → CI's audit job blocks on **high/critical** (it greps `bun audit` output for `high:` / `critical:`); treat any high/critical advisory as **blocking-with-triage**, lower severities as documented +- `bun run check` +- `bun run build` — **do not skip** — dep upgrades break the bundler/codegen far more often than types +- `bun audit` — CI's audit job blocks on **high/critical**; treat a red high/critical audit as **blocking-with-triage**, lower severities as documented ## Phase 6 — Report -Produce: **security** (advisories → verdict, with GHSA id + URL + fixed-in), **consolidated changeset** (rolled up from `deltas` — per package `current → target`, bucketed breaking/deprecations/features/fixes/peer-engine, one line per delta), **adoption opportunities** (top ~5 `features` deltas the codebase isn't using — `usage` verdict + file:line + one-line why-adopt + follow-up; non-blocking), bumped packages (band → version → why-safe), deferred/blocked (cited reason + file:line), verification results. Every citation from the artifact. If the user asked to commit/PR, hand off to [`harden-pr`](../harden-pr/SKILL.md) full mode. +Produce: **security** (advisories → verdict, with GHSA id + URL + fixed-in), **consolidated changeset** (rolled up from `deltas` — per package `current → target` from the single tarball span, bucketed breaking/deprecations/features/fixes/peer-engine, one line per hint or note), **adoption opportunities** (top ~5 `features` deltas the codebase isn't using — `usage` verdict + file:line + one-line why-adopt + follow-up; non-blocking), bumped packages (band → version → why-safe), deferred/blocked (cited reason + file:line), verification results. Every citation from the artifact. If the user asked to commit/PR, hand off to [`harden-pr`](../harden-pr/SKILL.md) full mode. **Done when:** report accounts for every non-no-op package and advisory; every citation is real — no `possibly`, `likely`, or unstated assumptions. @@ -67,6 +67,9 @@ Produce: **security** (advisories → verdict, with GHSA id + URL + fixed-in), * - ❌ Touching the registry/GitHub/GHSA directly — run the script, read the artifact. - ❌ `bun update --latest` across the board — per-package `--latest`/`package.json` edit for exact pins. - ❌ Treating `bun audit` as pass/fail — every advisory needs a cited verdict; a high/critical audit is blocking-with-triage. +- ❌ `bun audit fix`. It treats exact pins as `^version` and rewrites `package.json`. +- ❌ Isolated linker. Do not set `linker = "isolated"`. Existing lockfiles stay hoisted. +- ❌ bunfig `[test]` for isolate. `--isolate` is CLI-only. `--parallel` implies isolate. - ❌ Skipping `bun run build` — bundler/codegen breakage beats type breakage for dep upgrades. ## Reference diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index f22761c7..b323e622 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -3,7 +3,7 @@ Codemap is in **bootstrap / extraction** phase. Before large PRs, please open an issue so we can align on: - **Core vs adapter** — core should stay small; language-specific logic belongs in **adapters** (see [docs/roadmap.md](../docs/roadmap.md)). -- **Runtimes** — **Node** `^20.19.0 || >=22.12.0` and **Bun** `>=1.0.0` (`package.json` **engines**); SQLite is **`better-sqlite3`** on Node and **`bun:sqlite`** on Bun ([docs/architecture.md](../docs/architecture.md), [docs/packaging.md § Node vs Bun](../docs/packaging.md#node-vs-bun)). +- **Runtimes** — **Node** `^20.19.0 || >=22.12.0` and **Bun** `>=1.0.0` (`package.json` **engines**); SQLite is **`better-sqlite3`** on Node and **`bun:sqlite`** on Bun ([docs/architecture.md](../docs/architecture.md), [docs/packaging.md § Node vs Bun](../docs/packaging.md#node-vs-bun)). Maintainers use **Bun 1.4.0** (`packageManager`). ## Dev workflow diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index b32a7e07..2052b300 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -12,7 +12,7 @@ runs: - name: Setup Bun uses: oven-sh/setup-bun@v2 with: - bun-version: latest + bun-version: 1.4.0 - name: Install packages shell: bash diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5644d7f4..f23737fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -242,6 +242,23 @@ jobs: - name: Audit docs site run: bun run docs:audit + dedupe: + name: 🧹 Dedupe + needs: skip-ci + if: needs['skip-ci'].outputs.skip != 'true' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false + + - name: Setup + uses: ./.github/actions/setup + + - name: bun dedupe --check + run: bun run dedupe:check + audit: # `bun audit` exits 0 regardless of severity — the scan below blocks on high/critical. # Transitive-dep CVE visibility; advisory-API outage doesn't block. @@ -314,6 +331,7 @@ jobs: check-pack, docs, audit, + dedupe, benchmark, ] if: always() @@ -330,6 +348,7 @@ jobs: needs['check-pack'].result != 'success' || needs.docs.result != 'success' || needs.audit.result != 'success' || + needs.dedupe.result != 'success' || needs.benchmark.result != 'success' ) run: exit 1 diff --git a/bun.lock b/bun.lock index 3c7cf869..1f6f82bd 100644 --- a/bun.lock +++ b/bun.lock @@ -21,7 +21,7 @@ "@changesets/changelog-github": "0.7.0", "@changesets/cli": "2.31.1", "@types/better-sqlite3": "7.6.13", - "@types/bun": "1.3.14", + "@types/bun": "1.4.0", "@types/node": "26.1.1", "@typescript/native-preview": "7.0.0-dev.20260707.2", "husky": "9.1.7", @@ -729,7 +729,7 @@ "@types/better-sqlite3": ["@types/better-sqlite3@7.6.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA=="], - "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + "@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="], "@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="], @@ -1015,7 +1015,7 @@ "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -2045,7 +2045,7 @@ "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], @@ -2309,15 +2309,15 @@ "vscode-json-languageservice": ["vscode-json-languageservice@4.1.8", "", { "dependencies": { "jsonc-parser": "^3.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", "vscode-nls": "^5.0.0", "vscode-uri": "^3.0.2" } }, "sha512-0vSpg6Xd9hfV+eZAaYN63xVVMOTmJ4GgHxXnkLCh+9RsQBkWKIghzLhW2B9ebfG+LQQg8uLtsQ2aUKjTgE+QOg=="], - "vscode-jsonrpc": ["vscode-jsonrpc@9.0.1", "", {}, "sha512-rfuA6T75H6m5EkbhtEPzre9pT0HPcDI2MMy4+nPFIBks5J8JBAUHD4tRYSgaBOijIEC7SRkC1kKyXTLqbmh9jw=="], + "vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="], "vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="], - "vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.18.2", "", { "dependencies": { "vscode-jsonrpc": "9.0.1", "vscode-languageserver-types": "3.18.0" } }, "sha512-XRyDbT0Pp3sSNti3JmxVEUMySWCSi1hhM+/KUlCy1hV1zmrqpM1OwO12EAki8blhmLuIMpaJrYbo0OzGVfK2Qg=="], + "vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="], "vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="], - "vscode-languageserver-types": ["vscode-languageserver-types@3.18.0", "", {}, "sha512-8TsGPNMIMiiBdkORgRSvLjuiEIiAFtO+KssmYWxQ+uSVvlf7RjK8YKCOjPzZ+YA04jXEV7+7LvkSmHkhpNS99g=="], + "vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="], "vscode-nls": ["vscode-nls@5.2.0", "", {}, "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng=="], @@ -2431,8 +2431,6 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@types/better-sqlite3/@types/node": ["@types/node@26.1.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw=="], - "@vercel/cli-config/zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="], "@vercel/nft/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], @@ -2453,8 +2451,6 @@ "astro/get-tsconfig": ["get-tsconfig@5.0.0-beta.4", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ=="], - "astro/obug": ["obug@2.1.3", "", {}, "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg=="], - "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "blume/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], @@ -2463,8 +2459,6 @@ "body-parser/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - "bun-types/@types/node": ["@types/node@26.1.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw=="], - "cli-highlight/yargs": ["yargs@16.2.2", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w=="], "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="], @@ -2511,8 +2505,6 @@ "parse5-htmlparser2-tree-adapter/parse5": ["parse5@6.0.1", "", {}, "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="], - "readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], - "rolldown-plugin-dts/get-tsconfig": ["get-tsconfig@5.0.0-beta.5", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-/6gFNr0N04nob252sTQxyFLi3eKFRqIg1I87YcqAMT1i6SQrSF6KujUEQrtrjMV0H/eejTCltLdDSTEMzHbnsQ=="], "shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg=="], @@ -2523,8 +2515,6 @@ "shiki/@shikijs/types": ["@shikijs/types@4.3.1", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g=="], - "string_decoder/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], - "strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "svgo/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], @@ -2539,16 +2529,10 @@ "unconfig-core/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="], - "unrun/rolldown": ["rolldown@1.1.4", "", { "dependencies": { "@oxc-project/types": "=0.138.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.4", "@rolldown/binding-darwin-arm64": "1.1.4", "@rolldown/binding-darwin-x64": "1.1.4", "@rolldown/binding-freebsd-x64": "1.1.4", "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", "@rolldown/binding-linux-arm64-gnu": "1.1.4", "@rolldown/binding-linux-arm64-musl": "1.1.4", "@rolldown/binding-linux-ppc64-gnu": "1.1.4", "@rolldown/binding-linux-s390x-gnu": "1.1.4", "@rolldown/binding-linux-x64-gnu": "1.1.4", "@rolldown/binding-linux-x64-musl": "1.1.4", "@rolldown/binding-openharmony-arm64": "1.1.4", "@rolldown/binding-wasm32-wasi": "1.1.4", "@rolldown/binding-win32-arm64-msvc": "1.1.4", "@rolldown/binding-win32-x64-msvc": "1.1.4" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA=="], - "vite/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], "vite/rolldown": ["rolldown@1.1.5", "", { "dependencies": { "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.5", "@rolldown/binding-darwin-arm64": "1.1.5", "@rolldown/binding-darwin-x64": "1.1.5", "@rolldown/binding-freebsd-x64": "1.1.5", "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", "@rolldown/binding-linux-arm64-gnu": "1.1.5", "@rolldown/binding-linux-arm64-musl": "1.1.5", "@rolldown/binding-linux-ppc64-gnu": "1.1.5", "@rolldown/binding-linux-s390x-gnu": "1.1.5", "@rolldown/binding-linux-x64-gnu": "1.1.5", "@rolldown/binding-linux-x64-musl": "1.1.5", "@rolldown/binding-openharmony-arm64": "1.1.5", "@rolldown/binding-wasm32-wasi": "1.1.5", "@rolldown/binding-win32-arm64-msvc": "1.1.5", "@rolldown/binding-win32-x64-msvc": "1.1.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA=="], - "vscode-css-languageservice/vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="], - - "vscode-languageserver/vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="], - "yaml-language-server/prettier": ["prettier@3.9.5", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg=="], "yaml-language-server/request-light": ["request-light@0.5.8", "", {}, "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg=="], @@ -2585,8 +2569,6 @@ "@vercel/routing-utils/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - "bl/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - "cli-highlight/yargs/cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], "cli-highlight/yargs/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], @@ -2613,40 +2595,6 @@ "svgo/css-select/domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], - "tar-stream/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - - "unrun/rolldown/@oxc-project/types": ["@oxc-project/types@0.138.0", "", {}, "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA=="], - - "unrun/rolldown/@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.4", "", { "os": "android", "cpu": "arm64" }, "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw=="], - - "unrun/rolldown/@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ=="], - - "unrun/rolldown/@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg=="], - - "unrun/rolldown/@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ=="], - - "unrun/rolldown/@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.4", "", { "os": "linux", "cpu": "arm" }, "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA=="], - - "unrun/rolldown/@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w=="], - - "unrun/rolldown/@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng=="], - - "unrun/rolldown/@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg=="], - - "unrun/rolldown/@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ=="], - - "unrun/rolldown/@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw=="], - - "unrun/rolldown/@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ=="], - - "unrun/rolldown/@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.4", "", { "os": "none", "cpu": "arm64" }, "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA=="], - - "unrun/rolldown/@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.4", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg=="], - - "unrun/rolldown/@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA=="], - - "unrun/rolldown/@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.4", "", { "os": "win32", "cpu": "x64" }, "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ=="], - "vite/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], "vite/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], @@ -2701,20 +2649,12 @@ "vite/rolldown/@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA=="], - "vscode-languageserver/vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="], - - "vscode-languageserver/vscode-languageserver-protocol/vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="], - "filelist/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "node-html-parser/css-select/domutils/dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], "svgo/css-select/domutils/dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], - "unrun/rolldown/@rolldown/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], - - "unrun/rolldown/@rolldown/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], - "vite/rolldown/@rolldown/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], "vite/rolldown/@rolldown/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], diff --git a/lint-staged.config.js b/lint-staged.config.js index 4e147d1a..653f13ab 100644 --- a/lint-staged.config.js +++ b/lint-staged.config.js @@ -60,7 +60,7 @@ function relatedTests(filenames) { if (tests.length === 0) { return "true"; } - return `bun test ${tests.join(" ")}`; + return `bun test --isolate ${tests.join(" ")}`; } /** @type {import('lint-staged').Configuration} */ @@ -70,7 +70,8 @@ export default { // (oxfmt collapses Blume `:::` fences). "*.{css,json,md,mdc,mdx,html,yaml,yml}": "bun run format:check", "*.{ts,tsx}": [typecheckStagedFiles, relatedTests], - "*.test.ts": "bun test", - "*.test.tsx": "bun test", - "scripts/**/*.test.mjs": "bun test", + "*.test.ts": "bun test --isolate", + "*.test.tsx": "bun test --isolate", + "scripts/**/*.test.mjs": "bun test --isolate", + "scripts/**/*.test.ts": "bun test --isolate", }; diff --git a/package.json b/package.json index eea5351e..6cd14167 100644 --- a/package.json +++ b/package.json @@ -52,12 +52,13 @@ "benchmark:query": "bun scripts/benchmark-query-output.ts", "build": "tsdown", "changeset": "changeset", - "check": "bun run build && bun run --parallel format:check lint:ci test test:scripts typecheck && bun run test:golden && bun run test:agent-eval", + "check": "bun run build && bun run --parallel format:check lint:ci test test:scripts typecheck dedupe:check && bun run test:golden && bun run test:agent-eval", "check-updates": "bun update -i --latest", "check:pack": "attw --pack . --profile esm-only && publint", "check:perf-baseline": "bun scripts/check-perf-baseline.ts", "check:perf-baseline:update": "bun scripts/check-perf-baseline.ts --update", "clean": "git clean -xdf -e .env -e docs/research/scratch", + "dedupe:check": "bun dedupe --check", "dev": "bun src/index.ts", "docs:api": "bun apps/docs/scripts/rewrite-api-links.ts --clean && typedoc && bun apps/docs/scripts/rewrite-api-links.ts", "docs:audit": "bun run --filter '@stainless-code/codemap-docs' audit", @@ -82,13 +83,13 @@ "prepublishOnly": "bun run check && bun run check:pack", "qa:external": "bun scripts/qa-external-repo.ts", "release": "changeset publish", - "test": "bun test ./src", - "test:agent-eval": "bun test scripts/agent-eval", + "test": "bun test --isolate ./src", + "test:agent-eval": "bun test --isolate scripts/agent-eval", "test:ci": "bun run test:coverage", "test:coverage": "bun test --coverage --coverage-threshold=0.75 ./src", "test:golden": "bun scripts/query-golden.ts", "test:golden:external": "bun scripts/query-golden.ts --corpus external", - "test:scripts": "bash -c 'files=$(find scripts -name \"*.test.mjs\"); if [ -z \"$files\" ]; then echo \"no scripts test files found\" >&2; exit 1; fi; exec bun test $files'", + "test:scripts": "bash -c 'files=$(find scripts -name \"*.test.mjs\"); if [ -z \"$files\" ]; then echo \"no scripts test files found\" >&2; exit 1; fi; exec bun test --isolate $files scripts/upgrade-packages'", "typecheck": "tsgo --noEmit", "upgrade-packages:evidence": "bun run scripts/upgrade-packages/evidence.ts", "version": "changeset version && bun run format CHANGELOG.md" @@ -110,7 +111,7 @@ "@changesets/changelog-github": "0.7.0", "@changesets/cli": "2.31.1", "@types/better-sqlite3": "7.6.13", - "@types/bun": "1.3.14", + "@types/bun": "1.4.0", "@types/node": "26.1.1", "@typescript/native-preview": "7.0.0-dev.20260707.2", "husky": "9.1.7", @@ -137,6 +138,6 @@ "bun": ">=1.0.31", "node": "^20.19.0 || >=22.12.0" }, - "packageManager": "bun@1.3.14", + "packageManager": "bun@1.4.0", "contributing": ".github/CONTRIBUTING.md" } diff --git a/scripts/upgrade-packages/evidence.test.ts b/scripts/upgrade-packages/evidence.test.ts new file mode 100644 index 00000000..cbcbf82d --- /dev/null +++ b/scripts/upgrade-packages/evidence.test.ts @@ -0,0 +1,97 @@ +import { describe, expect, it } from "bun:test"; + +import { ghsaVerdict, parseOutdatedTable, semverInRange } from "./evidence"; + +describe("semverInRange", () => { + it("matches GHSA AND/OR ranges", () => { + expect(semverInRange("8.0.0", ">= 7.0.0 < 9.0.6")).toBe(true); + expect(semverInRange("9.0.6", ">= 7.0.0 < 9.0.6")).toBe(false); + expect(semverInRange("3.3.18", ">=4.0.0 || >=3.0.0 <3.4.0")).toBe(true); + expect(semverInRange("3.4.0", ">=4.0.0 || >=3.0.0 <3.4.0")).toBe(false); + }); + + it("is false for a missing range", () => { + expect(semverInRange("1.0.0", null)).toBe(false); + }); + + it("does not treat an unparseable OR group as a match", () => { + expect(semverInRange("1.0.0", "not-a-range || also-bad")).toBe(false); + expect(semverInRange("1.0.0", "not-a-range < 2.0.0")).toBe(false); + }); +}); + +describe("ghsaVerdict", () => { + it("picks priority-bump when the target already ships the fix", () => { + expect(ghsaVerdict(true, "4.3.1", "4.3.1")).toBe("priority-bump"); + expect(ghsaVerdict(true, "4.3.1", "4.4.0")).toBe("priority-bump"); + }); + + it("needs a higher target when the fix is past latest", () => { + expect(ghsaVerdict(true, "5.0.0", "4.3.1")).toBe("needs-higher-target"); + }); + + it("clears when the installed version is outside the range", () => { + expect(ghsaVerdict(false, "4.3.1", "4.3.1")).toBe("cleared-at-current"); + }); + + it("is unpatched when in range with no fix", () => { + expect(ghsaVerdict(true, null, "4.3.1")).toBe("unpatched"); + }); +}); + +describe("parseOutdatedTable", () => { + it("parses bun outdated rows and strips (dev)", () => { + const stdout = [ + "| Package | Current | Update | Latest |", + "| --- | --- | --- | --- |", + "| immer | 10.0.0 | 10.1.0 | 10.1.1 |", + "| zod (dev) | 3.23.0 | 3.24.0 | 3.24.2 |", + "| already | 1.0.0 | 1.0.0 | 1.0.0 |", + ].join("\n"); + expect(parseOutdatedTable(stdout)).toEqual([ + { + pkg: "immer", + current: "10.0.0", + latest: "10.1.1", + bumpClass: "minor", + coupledWith: [], + dev: false, + }, + { + pkg: "zod", + current: "3.23.0", + latest: "3.24.2", + bumpClass: "minor", + coupledWith: [], + dev: true, + }, + ]); + }); + + it("parses workspace-filter rows and strips (peer)/(optional)", () => { + const stdout = [ + "| Package | Current | Update | Latest | Workspace |", + "| --- | --- | --- | --- | --- |", + "| react (peer) | 19.2.7 | 19.2.8 | 19.2.8 | @stainless-code/react-layers |", + "| alpinejs (optional) | 3.15.12 | 3.15.12 | 3.16.0 | @stainless-code/persist |", + ].join("\n"); + expect(parseOutdatedTable(stdout)).toEqual([ + { + pkg: "react", + current: "19.2.7", + latest: "19.2.8", + bumpClass: "patch", + coupledWith: [], + dev: false, + }, + { + pkg: "alpinejs", + current: "3.15.12", + latest: "3.16.0", + bumpClass: "minor", + coupledWith: [], + dev: false, + }, + ]); + }); +}); diff --git a/scripts/upgrade-packages/evidence.ts b/scripts/upgrade-packages/evidence.ts index e81bb35c..b3689420 100644 --- a/scripts/upgrade-packages/evidence.ts +++ b/scripts/upgrade-packages/evidence.ts @@ -1,22 +1,30 @@ /** - * upgrade-packages-evidence.ts + * evidence.ts * * Deterministic evidence gatherer for the `upgrade-packages` skill. * Emits a JSON artifact the AI agent reads and judges — the agent never - * touches the registry, GitHub, or GHSA directly. Citations (diffUrl, - * changelogUrl, advisory URL) are artifact fields, so model priors can't - * sneak in. + * touches the registry, GitHub, or GHSA directly. Citations (tarball notes + * and kept patches, changelogUrl, advisory URL) are artifact fields, so + * model priors can't sneak in. * * Usage: - * bun run scripts/upgrade-packages-evidence.ts [--out ] [--only ] - * bun run scripts/upgrade-packages-evidence.ts --only immer # tracer bullet + * bun run upgrade-packages:evidence [--out ] [--only ] + * bun run upgrade-packages:evidence --only immer # tracer bullet * - * Artifact schema: see `Evidence` type below. + * Defaults: --out scripts/upgrade-packages/artifact.json; cache colocated + * at scripts/upgrade-packages/.cache/ (GHSA 1h, bun pm diff 7d). Artifact + * schema: see `Evidence` type below. */ -// `export {}` keeps this file a Module (not a global Script) so its `main` -// doesn't collide with other script-mode files' `main` under tsgo. -export {}; +import { readdirSync } from "node:fs"; + +import type { BunPmDiffJson, Delta } from "./tarball-delta"; +import { + buildDelta, + failedDelta, + npmVersionUrl, + selectPatchPaths, +} from "./tarball-delta"; // ──────────────────────────────────────────────────────────────────────────── // Types — the artifact contract the AI agent reads @@ -30,6 +38,7 @@ interface OutdatedPkg { latest: string; bumpClass: BumpClass; coupledWith: string[]; // peer/dep that forces a higher band (filled naively here) + dev: boolean; } interface AdvisoryVuln { @@ -49,21 +58,6 @@ interface AdvisoryVuln { error?: string; } -interface Delta { - version: string; - date: string | null; - breaking: string[]; - deprecations: string[]; - features: string[]; - security: string[]; - peerEngine: string[]; - releaseNotes: string | null; // raw body, truncated - diffUrl: string | null; // github.com///compare/... - changelogUrl: string | null; - source: "github-release" | "none"; - error: string | null; -} - interface Usage { importedSymbols: string[]; typeOnlySymbols: string[]; @@ -159,16 +153,21 @@ async function ghGate() { const SCRIPT_DIR = import.meta.dir; const CACHE_DIR = `${SCRIPT_DIR}/.cache`; const DEFAULT_OUT = `${SCRIPT_DIR}/artifact.json`; -const CACHE_MAX_AGE_MS = 1000 * 60 * 60; // 1 hour +const CACHE_MAX_AGE_MS = 1000 * 60 * 60; // 1 hour — GHSA can change +const PMDIFF_CACHE_MS = 1000 * 60 * 60 * 24 * 7; // published tarball pair is immutable +const DIFF_CONCURRENCY = 4; function cachePath(key: string): string { return `${CACHE_DIR}/${key.replace(/[^a-z0-9._-]/gi, "_")}.json`; } -async function readCache(key: string): Promise { +async function readCache( + key: string, + maxAgeMs = CACHE_MAX_AGE_MS, +): Promise { const f = Bun.file(cachePath(key)); if (!(await f.exists())) return null; - if (Date.now() - f.lastModified > CACHE_MAX_AGE_MS) return null; + if (Date.now() - f.lastModified > maxAgeMs) return null; return await f.text(); } @@ -229,7 +228,7 @@ function bumpClass(current: string, latest: string): BumpClass { return lb !== cb ? "minor" : "patch"; } -function semverInRange(version: string, range: string | null): boolean { +export function semverInRange(version: string, range: string | null): boolean { if (!range) return false; // Naive but covers the GHSA `vulnerable_version_range` shapes we see: // comma- or space-separated comparators (AND), and `||` groups (OR). @@ -243,9 +242,14 @@ function semverInRange(version: string, range: string | null): boolean { .filter(Boolean); if (clauses.length === 0) continue; let groupOk = true; + let parsed = 0; for (const clause of clauses) { const m = clause.match(/^(>=|<=|>|<|=)?\s*(\d[^-+]*)/); - if (!m) continue; + if (!m) { + groupOk = false; + break; + } + parsed += 1; const [, op, ver] = m; const c = cmpVer(version, ver); if (op === ">=" && !(c >= 0)) groupOk = false; @@ -254,11 +258,26 @@ function semverInRange(version: string, range: string | null): boolean { if (op === "<" && !(c < 0)) groupOk = false; if ((!op || op === "=") && c !== 0) groupOk = false; } - if (groupOk) return true; + if (parsed > 0 && groupOk) return true; } return false; } +export function ghsaVerdict( + inRange: boolean, + fixedIn: string | null, + targetVer: string, +): AdvisoryVuln["verdict"] { + if (inRange && fixedIn && targetVer && cmpVer(fixedIn, targetVer) <= 0) { + return "priority-bump"; + } + if (inRange && fixedIn && targetVer && cmpVer(fixedIn, targetVer) > 0) { + return "needs-higher-target"; + } + if (!inRange) return "cleared-at-current"; + return "unpatched"; +} + // ──────────────────────────────────────────────────────────────────────────── // Gatherers // ──────────────────────────────────────────────────────────────────────────── @@ -274,20 +293,50 @@ const HIGH_RISK = [ "tsdown", ]; +function workspaceManifests(): string[] { + const manifests = ["package.json"]; + for (const dir of ["packages", "apps"]) { + try { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + if (entry.isDirectory()) { + manifests.push(`${dir}/${entry.name}/package.json`); + } + } + } catch { + // Directory may not exist in this repo. + } + } + return manifests; +} + async function parsePackageJson(): Promise { - const pkg = JSON.parse(await Bun.file("package.json").text()); const direct: Evidence["inventory"]["direct"] = []; const classify = (v: string): "exact" | "caret" | "tilde" => v.startsWith("^") ? "caret" : v.startsWith("~") ? "tilde" : "exact"; - for (const [name, version] of Object.entries( - pkg.dependencies ?? {}, - )) { - direct.push({ name, version, range: classify(version), dev: false }); - } - for (const [name, version] of Object.entries( - pkg.devDependencies ?? {}, - )) { - direct.push({ name, version, range: classify(version), dev: true }); + const seen = new Set(); + const add = (name: string, version: string, dev: boolean) => { + if (version.startsWith("workspace:")) return; + const key = `${name}@${version}:${dev}`; + if (seen.has(key)) return; + seen.add(key); + direct.push({ name, version, range: classify(version), dev }); + }; + for (const manifest of workspaceManifests()) { + const raw = await Bun.file(manifest) + .text() + .catch(() => ""); + if (!raw) continue; + const pkg = JSON.parse(raw); + for (const [name, version] of Object.entries( + pkg.dependencies ?? {}, + )) { + add(name, version, false); + } + for (const [name, version] of Object.entries( + pkg.devDependencies ?? {}, + )) { + add(name, version, true); + } } // Transitive duplicates: parse bun.lock for packages resolved at multiple versions // where one version is a direct dep (the signal the skill cares about). @@ -320,29 +369,55 @@ async function parsePackageJson(): Promise { return { direct, transitiveDuplicates }; } -async function parseBunOutdated(): Promise { - const { stdout } = await runSoft(["bun", "outdated"]); +export function parseOutdatedTable(stdout: string): OutdatedPkg[] { const out: OutdatedPkg[] = []; - // Table rows: | | | | | + const seen = new Set(); + // Root table: | Package | Current | Update | Latest | + // Workspace table (`bun outdated --filter '*'`): extra trailing Workspace column. + // Display suffixes: " (dev)" / " (peer)" / " (optional)". for (const line of stdout.split("\n")) { - const m = line.match( - /^\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|$/, - ); - if (!m) continue; - const [, pkg, current, , latest] = m.map((s) => s.trim()); - if (pkg === "Package" || pkg.startsWith("---")) continue; - if (current === latest) continue; + if (!line.trim().startsWith("|")) continue; + const cells = line + .split("|") + .slice(1, -1) + .map((s) => s.trim()); + if (cells.length < 4) continue; + const [pkgRaw, current, , latest] = cells; + if (!pkgRaw || pkgRaw === "Package" || pkgRaw.startsWith("---")) continue; + if (!current || !latest || current === latest) continue; + const isDev = /\s*\(dev\)\s*$/.test(pkgRaw); + const pkg = pkgRaw.replace(/\s*\((?:dev|peer|optional)\)\s*$/, ""); + const key = `${pkg}@${current}->${latest}`; + if (seen.has(key)) continue; + seen.add(key); out.push({ pkg, current, latest, bumpClass: bumpClass(current, latest), coupledWith: [], + dev: isDev, }); } return out; } +async function parseBunOutdated(): Promise { + // `--filter '*'` covers every workspace package (root-only `bun outdated` + // misses per-package deps). Adds a trailing Workspace column. + const { ok, stdout, code } = await runSoft([ + "bun", + "outdated", + "--filter", + "*", + ]); + const out = parseOutdatedTable(stdout); + if (!ok && out.length === 0) { + throw new Error(`bun outdated exited ${code} with no parseable rows`); + } + return out; +} + async function runBunAudit(): Promise { const { stdout } = await runSoft(["bun", "audit", "--json"]); const body = stripBunHeader(stdout); @@ -356,6 +431,97 @@ async function runBunAudit(): Promise { } } +async function fetchGhsaList(pkg: string): Promise { + return cachedParsed( + `ghsa:${pkg}`, + CACHE_MAX_AGE_MS, + () => + run( + [ + "gh", + "api", + "-X", + "GET", + "/advisories", + "-f", + "ecosystem=npm", + "-f", + `affects=${pkg}`, + ], + { retries: 2 }, + ), + (raw) => { + const list = JSON.parse(raw); + if (!Array.isArray(list)) { + throw new Error( + `non-array response from gh api advisories: ${String(list).slice(0, 120)}`, + ); + } + return list; + }, + ); +} + +function advisoryFromGhsa( + a: { + ghsa_id: string; + cve_id?: string | null; + severity?: string; + vulnerabilities?: { + package?: { name?: string }; + vulnerable_version_range?: string | null; + first_patched_version?: { identifier?: string } | string | null; + }[]; + }, + pkg: string, + installed: Map, + target: Map, +): AdvisoryVuln { + const vuln = + a.vulnerabilities?.find((v) => v.package?.name === pkg) ?? + a.vulnerabilities?.[0] ?? + {}; + const range = vuln.vulnerable_version_range ?? null; + const patched = vuln.first_patched_version; + const fixedIn = + typeof patched === "string" ? patched : (patched?.identifier ?? null); + const installedVer = installed.get(pkg) ?? ""; + const targetVer = target.get(pkg) ?? ""; + const inRange = installedVer ? semverInRange(installedVer, range) : false; + return { + id: a.ghsa_id, + cveId: a.cve_id ?? null, + severity: a.severity ?? "unknown", + vulnerableRange: range, + fixedIn, + installedInRange: inRange, + verdict: ghsaVerdict(inRange, fixedIn, targetVer), + url: `https://github.com/advisories/${a.ghsa_id}`, + }; +} + +function ghsaCheckFailed( + pkg: string, + e: unknown, +): Evidence["audit"]["ghsa"][number] { + return { + pkg, + advisories: [ + { + id: "error", + cveId: null, + severity: "unknown", + vulnerableRange: null, + fixedIn: null, + installedInRange: false, + verdict: "check-failed", + url: "", + error: e instanceof Error ? e.message : String(e), + }, + ], + }; +} + async function ghsaSpotCheck( pkgs: string[], installed: Map, @@ -364,300 +530,119 @@ async function ghsaSpotCheck( const out: Evidence["audit"]["ghsa"] = []; for (const pkg of pkgs) { try { - const cacheKey = `ghsa:${pkg}`; - const cached = await readCache(cacheKey); - const raw = - cached ?? - (await run( - [ - "gh", - "api", - "-X", - "GET", - "/advisories", - "-f", - "ecosystem=npm", - "-f", - `affects=${pkg}`, - ], - { retries: 2 }, - )); - const list = JSON.parse(raw); - if (!Array.isArray(list)) { - throw new Error( - `non-array response from gh api advisories: ${String(list).slice(0, 120)}`, - ); - } - if (!cached) await writeCache(cacheKey, JSON.stringify(list)); - const advisories: AdvisoryVuln[] = []; - for (const a of list) { - const vuln = - a.vulnerabilities?.find((v: any) => v.package?.name === pkg) ?? - a.vulnerabilities?.[0] ?? - {}; - const range: string | null = vuln.vulnerable_version_range ?? null; - const fixedIn: string | null = - vuln.first_patched_version?.identifier ?? - vuln.first_patched_version ?? - null; - const installedVer = installed.get(pkg) ?? ""; - const targetVer = target.get(pkg) ?? ""; - const inRange = installedVer - ? semverInRange(installedVer, range) - : false; - let verdict: AdvisoryVuln["verdict"] = "unpatched"; - if (inRange && fixedIn && targetVer && cmpVer(fixedIn, targetVer) <= 0) - verdict = "priority-bump"; - else if ( - inRange && - fixedIn && - targetVer && - cmpVer(fixedIn, targetVer) > 0 - ) - verdict = "needs-higher-target"; - else if (!inRange) verdict = "cleared-at-current"; - advisories.push({ - id: a.ghsa_id, - cveId: a.cve_id ?? null, - severity: a.severity ?? "unknown", - vulnerableRange: range, - fixedIn, - installedInRange: inRange, - verdict, - url: `https://github.com/advisories/${a.ghsa_id}`, - }); - } - out.push({ pkg, advisories }); - } catch (e) { + const list = await fetchGhsaList(pkg); out.push({ pkg, - advisories: [ - { - id: "error", - cveId: null, - severity: "unknown", - vulnerableRange: null, - fixedIn: null, - installedInRange: false, - verdict: "check-failed", - url: "", - error: e instanceof Error ? e.message : String(e), - }, - ], + advisories: list.map((a) => + advisoryFromGhsa( + a as Parameters[0], + pkg, + installed, + target, + ), + ), }); + } catch (e) { + out.push(ghsaCheckFailed(pkg, e)); } } return out; } -async function getRepoSlug( - pkg: string, -): Promise<{ owner: string; repo: string } | null> { - try { - const raw = stripBunHeader( - await run(["bun", "pm", "view", pkg, "repository", "--json"], { - retries: 2, - }), - ); - const data = JSON.parse(raw); - const url: string = data.url ?? ""; - // Preserve dots in repo names (e.g. mozilla/pdf.js); strip a trailing .git. - const m = url.match( - /github\.com[/:]([^/]+)\/([^/#?]+?)(?:\.git)?(?:[/?#].*)?$/, - ); - return m ? { owner: m[1], repo: m[2] } : null; - } catch { - return null; +async function cachedParsed( + key: string, + maxAgeMs: number, + fetchText: () => Promise, + parse: (raw: string) => T, +): Promise { + const cached = await readCache(key, maxAgeMs); + if (cached) return parse(cached); + const raw = await fetchText(); + const value = parse(raw); + await writeCache(key, raw); + return value; +} + +function parseDiffJson(raw: string, label: string): BunPmDiffJson { + const body = stripBunHeader(raw); + const data: unknown = JSON.parse(body); + if (!data || typeof data !== "object" || Array.isArray(data)) { + throw new Error(`${label}: expected a JSON object`); } -} - -async function getVersions(pkg: string): Promise { - try { - const raw = stripBunHeader( - await run(["bun", "pm", "view", pkg, "versions", "--json"], { - retries: 2, - }), - ); - const list = JSON.parse(raw); - return Array.isArray(list) ? list : []; - } catch { - return []; + const obj = data as Record; + if ("files" in obj && !Array.isArray(obj.files)) { + throw new Error(`${label}: files must be an array`); + } + if ("notes" in obj && !Array.isArray(obj.notes)) { + throw new Error(`${label}: notes must be an array`); } + return data as BunPmDiffJson; } -/** Walk the changelog between current and target. Naive categorization of release body. */ +/** One current→target tarball span via `bun pm diff`. Stat first, then patches for keep-paths. */ async function gatherDeltas( pkg: string, current: string, target: string, + importedSymbols: string[], ): Promise { - if (isPrerelease(target) || isPrerelease(current)) { - return [ - { - version: target, - date: null, - breaking: [], - deprecations: [], - features: [], - security: [], - peerEngine: [], - releaseNotes: null, - diffUrl: null, - changelogUrl: null, - source: "none", - error: - "prerelease/moving-target build — no per-version changelog; gate on same-major-line only", - }, - ]; - } - const slug = await getRepoSlug(pkg); - if (!slug) { + const changelogUrl = npmVersionUrl(pkg, target); + try { + const spec = `${pkg}@${current}`; + const range = `${pkg}@${current}..${target}`; + const stat = await cachedParsed( + `pmdiff-stat:${range}`, + PMDIFF_CACHE_MS, + () => + run(["bun", "pm", "diff", spec, target, "--json", "--stat"], { + retries: 1, + }), + (raw) => parseDiffJson(raw, `bun pm diff --stat ${range}`), + ); + const patchPaths = selectPatchPaths(stat.files ?? [], importedSymbols); + let patches: BunPmDiffJson | null = null; + if (patchPaths.length > 0) { + const patchKey = `pmdiff-patch:${range}:${Bun.hash(patchPaths.join("\0")).toString(16)}`; + patches = await cachedParsed( + patchKey, + PMDIFF_CACHE_MS, + () => + run(["bun", "pm", "diff", spec, target, "--json", ...patchPaths], { + retries: 1, + }), + (raw) => parseDiffJson(raw, `bun pm diff patches ${range}`), + ); + } + return [buildDelta({ target, changelogUrl, stat, patches })]; + } catch (e) { return [ - { - version: target, - date: null, - breaking: [], - deprecations: [], - features: [], - security: [], - peerEngine: [], - releaseNotes: null, - diffUrl: null, - changelogUrl: null, - source: "none", - error: "no github repository found", - }, - ]; - } - const { owner, repo } = slug; - const allVersions = await getVersions(pkg); - const inRange = allVersions - .filter( - (v) => - !isPrerelease(v) && cmpVer(v, current) > 0 && cmpVer(v, target) <= 0, - ) - .sort((a, b) => cmpVer(a, b)); - - // Fetch the repo's release list once and map version → actual tag name. - // Handles v, , @scope/pkg@, release-, etc. — whatever the repo uses. - const releases = await fetchReleaseMap(owner, repo); - const tagOf = (v: string): string | null => releases.get(v)?.tagName ?? null; - - const deltas: Delta[] = []; - let prevTag = tagOf(current) ?? `v${current}`; - for (const v of inRange) { - const rel = tagOf(v) ? releases.get(v) : null; - const tag = rel?.tagName ?? null; - const diffUrl = tag - ? `https://github.com/${owner}/${repo}/compare/${prevTag}...${tag}` - : null; - const body = rel?.body ?? null; - const date = rel?.publishedAt ?? null; - const source: Delta["source"] = rel ? "github-release" : "none"; - const error = rel - ? null - : `no github release for ${pkg}@${v} (${releases.size} releases scanned — likely a gh secondary rate-limit or monorepo squashed release; deep-dive via changelogUrl)`; - // Always provide a deep-dive link: the specific tag release page when known, - // else the repo's releases page (so the model can browse tags when the script couldn't). - const changelogUrl = tag - ? `https://github.com/${owner}/${repo}/releases/tag/${tag}` - : `https://github.com/${owner}/${repo}/releases`; - deltas.push({ - version: v, - date, - breaking: extractLines(body, /breaking|breaking change/i), - deprecations: extractLines(body, /deprecat|removed export/i), - features: extractLines(body, /^feat|feature|^add|^new/i), - security: extractLines( - body, - /security|cve|prototype pollution|vulnerabilit/i, + failedDelta( + target, + changelogUrl, + e instanceof Error ? e.message : String(e), ), - peerEngine: extractLines( - body, - /peer dep|engine|requires (node|bun|react)/i, - ), - releaseNotes: body, - diffUrl, - changelogUrl, - source, - error, - }); - if (tag) prevTag = tag; - } - if (deltas.length === 0) { - deltas.push({ - version: target, - date: null, - breaking: [], - deprecations: [], - features: [], - security: [], - peerEngine: [], - releaseNotes: null, - diffUrl: null, - changelogUrl: null, - source: "none", - error: "no versions found in range", - }); + ]; } - return deltas; } -/** Fetch a repo's releases (with bodies) in one paginated call. Maps version → release. */ -async function fetchReleaseMap( - owner: string, - repo: string, -): Promise< - Map< - string, - { tagName: string; publishedAt: string | null; body: string | null } - > -> { - const map = new Map< - string, - { tagName: string; publishedAt: string | null; body: string | null } - >(); - const cacheKey = `releases:${owner}/${repo}`; - try { - const cached = await readCache(cacheKey); - const raw = - cached ?? - (await run([ - "gh", - "api", - `repos/${owner}/${repo}/releases?per_page=100`, - ])); - const list = JSON.parse(raw); - if (!Array.isArray(list)) { - // gh returns a JSON object (e.g. {"message":"secondary rate limit"}) on rate-limit — not an array. - throw new Error( - `non-array response from gh api releases: ${String(raw).slice(0, 120)}`, - ); - } - if (!cached) await writeCache(cacheKey, JSON.stringify(list)); - for (const r of list) { - const m = (r.tag_name as string).match(/(\d+\.\d+\.\d+(?:-[\w.]+)?)/); - if (m) - map.set(m[1], { - tagName: r.tag_name, - publishedAt: r.published_at ?? null, - body: r.body ? String(r.body).slice(0, 1200) : null, - }); +async function mapPool( + items: T[], + concurrency: number, + fn: (item: T) => Promise, +): Promise { + const out: R[] = []; + let next = 0; + async function worker() { + for (;;) { + const i = next++; + if (i >= items.length) return; + out[i] = await fn(items[i]!); } - } catch { - // repo has no releases or gh failed (secondary rate-limit / not found) — empty map; caller records error per version. } - return map; -} - -function extractLines(body: string | null, re: RegExp): string[] { - if (!body) return []; - return body - .split("\n") - .map((l) => l.trim()) - .filter((l) => re.test(l) && l.length > 0) - .slice(0, 6) - .map((l) => l.replace(/^[#*\-\s]+/, "").slice(0, 160)); + const n = Math.min(concurrency, items.length); + if (n === 0) return out; + await Promise.all(Array.from({ length: n }, () => worker())); + return out; } // ──────────────────────────────────────────────────────────────────────────── @@ -875,27 +860,39 @@ async function main() { if (onlyPkg) outdated = outdated.filter((o) => o.pkg === onlyPkg); const target = new Map(outdated.map((o) => [o.pkg, o.latest])); - console.error("→ bun audit"); - const bunAudit = await runBunAudit(); - - console.error("→ ghsa spot-check"); + console.error("→ bun audit + ghsa"); const ghsaPkgs = (onlyPkg ? [onlyPkg] : HIGH_RISK).filter( (p) => installed.has(p) || target.has(p), ); - const ghsa = await ghsaSpotCheck(ghsaPkgs, installed, target); - - console.error("→ deltas"); - const deltas: Record = {}; - for (const o of outdated) { - console.error(` ${o.pkg} ${o.current} → ${o.latest}`); - deltas[o.pkg] = await gatherDeltas(o.pkg, o.current, o.latest); - } + const [bunAudit, ghsa] = await Promise.all([ + runBunAudit(), + ghsaSpotCheck(ghsaPkgs, installed, target), + ]); console.error("→ usage (batched codemap)"); const usageMap = await gatherAllUsageWithFallback(outdated.map((o) => o.pkg)); const usage: Record = {}; - for (const o of outdated) - usage[o.pkg] = usageMap.get(o.pkg) ?? (await grepUsage(o.pkg)); + for (const o of outdated) { + const u = usageMap.get(o.pkg); + if (!u) throw new Error(`usage missing for ${o.pkg}`); + usage[o.pkg] = u; + } + + console.error(`→ deltas (bun pm diff, ${DIFF_CONCURRENCY} at a time)`); + const deltaEntries = await mapPool(outdated, DIFF_CONCURRENCY, async (o) => { + console.error(` ${o.pkg} ${o.current} → ${o.latest}`); + const u = usage[o.pkg]; + const symbols = [ + ...(u?.importedSymbols ?? []), + ...(u?.typeOnlySymbols ?? []), + ]; + return [ + o.pkg, + await gatherDeltas(o.pkg, o.current, o.latest, symbols), + ] as const; + }); + const deltas: Record = {}; + for (const [pkg, list] of deltaEntries) deltas[pkg] = list; const evidence: Evidence = { generatedAt: new Date().toISOString(), @@ -915,7 +912,9 @@ async function main() { } } -main().catch((e) => { - console.error("fatal:", e); - process.exit(1); -}); +if (import.meta.main) { + main().catch((e) => { + console.error("fatal:", e); + process.exit(1); + }); +} diff --git a/scripts/upgrade-packages/tarball-delta.test.ts b/scripts/upgrade-packages/tarball-delta.test.ts new file mode 100644 index 00000000..09f436df --- /dev/null +++ b/scripts/upgrade-packages/tarball-delta.test.ts @@ -0,0 +1,286 @@ +import { describe, expect, it } from "bun:test"; + +import type { BunPmDiffFile } from "./tarball-delta"; +import { + FILE_LIST_CAP, + PATCH_MAX_CHARS, + PATCH_PATH_CAP, + addedPatchLines, + buildDelta, + classifyChangelogLines, + classifyNote, + extractDate, + failedDelta, + isKeepPath, + isSkipFile, + npmVersionUrl, + selectPatchPaths, + shouldKeepPatch, + slimFiles, +} from "./tarball-delta"; + +const patch = ( + path: string, + extra: Partial = {}, +): BunPmDiffFile => ({ + path, + status: "modified", + linesAdded: extra.linesAdded ?? 4, + linesRemoved: extra.linesRemoved ?? 1, + ...extra, +}); + +describe("isKeepPath / isSkipFile", () => { + it("keeps changelog, package.json, readme, and declaration files", () => { + expect(isKeepPath("CHANGELOG.md")).toBe(true); + expect(isKeepPath("package.json")).toBe(true); + expect(isKeepPath("README.md")).toBe(true); + expect(isKeepPath("dist/index.d.ts")).toBe(true); + expect(isKeepPath("dist/index.d.mts")).toBe(true); + expect(isKeepPath("src/index.js")).toBe(false); + }); + + it("skips source maps", () => { + expect(isSkipFile({ path: "dist/a.js.map" })).toBe(true); + expect(isSkipFile({ path: "dist/a.js", sourceMap: true })).toBe(true); + expect(isSkipFile({ path: "dist/a.js" })).toBe(false); + }); +}); + +describe("selectPatchPaths", () => { + it("puts named keep files first, then dts, then highest churn", () => { + const paths = selectPatchPaths([ + patch("dist/huge.js", { linesAdded: 200, linesRemoved: 50 }), + patch("package.json", { linesAdded: 2, linesRemoved: 2 }), + patch("dist/index.d.ts", { linesAdded: 10, linesRemoved: 1 }), + patch("CHANGELOG.md", { linesAdded: 8, linesRemoved: 0 }), + patch("dist/a.js.map", { linesAdded: 999, sourceMap: true }), + ]); + expect(paths[0]).toBe("package.json"); + expect(paths[1]).toBe("CHANGELOG.md"); + expect(paths[2]).toBe("dist/index.d.ts"); + expect(paths[3]).toBe("dist/huge.js"); + expect(paths).not.toContain("dist/a.js.map"); + expect( + selectPatchPaths([ + patch("dist/huge.production.mjs", { linesAdded: 400 }), + patch("src/mapset.ts", { linesAdded: 12 }), + ]), + ).toEqual(["src/mapset.ts"]); + }); + + it("boosts extras whose path mentions an imported symbol", () => { + expect( + selectPatchPaths( + [ + patch("src/unrelated.ts", { linesAdded: 80 }), + patch("src/produce.ts", { linesAdded: 4 }), + ], + ["produce"], + ), + ).toEqual(["src/produce.ts", "src/unrelated.ts"]); + }); + + it("does not let formatting-only dts eat PATCH_PATH_CAP", () => { + const files = [ + ...Array.from({ length: PATCH_PATH_CAP }, (_, i) => + patch(`dist/n${i}.d.ts`, { linesAdded: 1, formattingOnly: true }), + ), + patch("src/useLottie.ts", { linesAdded: 4 }), + ]; + expect(selectPatchPaths(files, ["useLottie"])).toEqual([ + "src/useLottie.ts", + ]); + }); + + it("boosts a low-churn .d.ts that mentions an imported symbol", () => { + const files = [ + ...Array.from({ length: PATCH_PATH_CAP }, (_, i) => + patch(`dist/n${i}.d.ts`, { linesAdded: 40 }), + ), + patch("build/useLottie.d.ts", { linesAdded: 7 }), + ]; + const paths = selectPatchPaths(files, ["useLottie"]); + expect(paths[0]).toBe("build/useLottie.d.ts"); + }); + + it("caps total paths", () => { + const files = Array.from({ length: PATCH_PATH_CAP + 20 }, (_, i) => + patch(`src/f${i}.js`, { linesAdded: i }), + ); + expect(selectPatchPaths(files)).toHaveLength(PATCH_PATH_CAP); + }); +}); + +describe("shouldKeepPatch", () => { + it("keeps any fetched non-minified patch", () => { + expect(shouldKeepPatch("package.json", "@@\n+x\n")).toBe(true); + expect(shouldKeepPatch("src/a.js", "function other() {}")).toBe(true); + expect(shouldKeepPatch("src/a.js", null)).toBe(false); + expect( + shouldKeepPatch("dist/immer.production.mjs", "function produce() {}"), + ).toBe(false); + }); +}); + +describe("classifyNote / addedPatchLines / extractDate", () => { + it("buckets bun summary notes", () => { + expect(classifyNote('engines changed: { "node": ">=0.10.0" }')).toBe( + "peerEngine", + ); + expect(classifyNote("dependencies immer: 11.0.0 → 11.1.0")).toBe( + "peerEngine", + ); + expect(classifyNote("new install script: postinstall")).toBe("security"); + expect(classifyNote("new import of child_process")).toBe("security"); + expect(classifyNote("entry point main changed")).toBe("features"); + expect(classifyNote("breaking: removed produceWithPatches")).toBe( + "breaking", + ); + expect(classifyNote("4 files changed")).toBe(null); + }); + + it("takes list items under changelog headings", () => { + const added = [ + "## 2.0.0", + "### Breaking Changes", + "* removed produceWithPatches", + "### Features", + "* new enableArrayMethods", + "### Bug Fixes", + "* timezone RangeError", + ].join("\n"); + const classified = classifyChangelogLines(added); + expect(classified.breaking).toEqual(["removed produceWithPatches"]); + expect(classified.features).toEqual(["new enableArrayMethods"]); + expect(classified.security).toEqual([]); + }); + + it("does not treat a dep named fs as a new fs import", () => { + expect(classifyNote("dependencies graceful-fs: 4.0.0 → 4.2.0")).toBe( + "peerEngine", + ); + }); + + it("takes added lines and ISO dates from a changelog hunk", () => { + const hunk = [ + "--- a/CHANGELOG.md", + "+++ b/CHANGELOG.md", + "@@ -1,3 +1,8 @@", + "+## [1.11.23](https://example.com) (2026-08-17)", + "+", + "+### Bug Fixes", + "+* timezone RangeError", + " ## 1.11.22", + ].join("\n"); + const added = addedPatchLines(hunk); + expect(added).toContain("## [1.11.23]"); + expect(added).not.toContain("## 1.11.22"); + expect(extractDate(added)).toBe("2026-08-17"); + }); +}); + +describe("slimFiles", () => { + it("keeps named files ahead of a dts flood when over FILE_LIST_CAP", () => { + const files = [ + patch("CHANGELOG.md", { linesAdded: 1 }), + patch("package.json", { linesAdded: 1 }), + ...Array.from({ length: FILE_LIST_CAP }, (_, i) => + patch(`dist/n${i}.d.ts`, { linesAdded: 2 }), + ), + ]; + const slim = slimFiles(files); + expect(slim).toHaveLength(FILE_LIST_CAP); + expect(slim[0]?.path).toBe("CHANGELOG.md"); + expect(slim[1]?.path).toBe("package.json"); + }); + + it("drops maps, truncates kept patches, and caps the list", () => { + const files: BunPmDiffFile[] = [ + patch("dist/a.js.map", { patch: "MAP", sourceMap: true }), + patch("package.json", { patch: "x".repeat(PATCH_MAX_CHARS + 50) }), + ...Array.from({ length: FILE_LIST_CAP + 5 }, (_, i) => + patch(`src/n${i}.js`, { linesAdded: i, patch: `fn${i}` }), + ), + ]; + const slim = slimFiles(files); + expect(slim.every((f) => f.path !== "dist/a.js.map")).toBe(true); + const pkg = slim.find((f) => f.path === "package.json"); + expect(pkg?.patch?.length).toBe(PATCH_MAX_CHARS); + expect(slim.length).toBe(FILE_LIST_CAP); + expect(slim.some((f) => f.path === "package.json")).toBe(true); + }); +}); + +describe("buildDelta / failedDelta / npmVersionUrl", () => { + it("builds a bun-pm-diff delta from notes + changelog patch", () => { + const changelog = [ + "@@ -1,1 +1,6 @@", + "+## 2.0.0 (2026-01-02)", + "+### Breaking Changes", + "+* removed produceWithPatches", + "+### Features", + "+* new enableArrayMethods", + ].join("\n"); + const delta = buildDelta({ + target: "2.0.0", + changelogUrl: npmVersionUrl("immer", "2.0.0"), + stat: { + from: "immer@1.0.0", + to: "immer@2.0.0", + notes: [ + "dependencies zod: 3.0.0 → 4.0.0", + "new import of child_process", + ], + totals: { + files: 3, + added: 0, + deleted: 0, + linesAdded: 10, + linesRemoved: 2, + formattingOnly: 0, + }, + files: [ + patch("CHANGELOG.md", { linesAdded: 6 }), + patch("src/produce.js", { linesAdded: 3 }), + ], + }, + patches: { + files: [ + { path: "CHANGELOG.md", patch: changelog }, + { + path: "src/produce.js", + patch: "@@\n+export function produce() {}\n", + }, + ], + }, + }); + expect(delta.source).toBe("bun-pm-diff"); + expect(delta.error).toBeNull(); + expect(delta.date).toBe("2026-01-02"); + expect(delta.peerEngine.some((l) => /zod/.test(l))).toBe(true); + expect(delta.security.some((l) => /child_process/.test(l))).toBe(true); + expect(delta.breaking.some((l) => /produceWithPatches/.test(l))).toBe(true); + expect(delta.releaseNotes).toContain("2.0.0"); + expect( + delta.tarball?.files.find((f) => f.path === "src/produce.js")?.patch, + ).toContain("produce"); + expect(delta.changelogUrl).toBe( + "https://www.npmjs.com/package/immer/v/2.0.0", + ); + }); + + it("keeps the npm URL on a failed gather", () => { + const delta = failedDelta( + "1.2.3", + npmVersionUrl("@scope/pkg", "1.2.3"), + "bun pm diff exited 1", + ); + expect(delta.source).toBe("none"); + expect(delta.tarball).toBeNull(); + expect(delta.changelogUrl).toBe( + "https://www.npmjs.com/package/@scope/pkg/v/1.2.3", + ); + expect(delta.error).toContain("exited 1"); + }); +}); diff --git a/scripts/upgrade-packages/tarball-delta.ts b/scripts/upgrade-packages/tarball-delta.ts new file mode 100644 index 00000000..e88435f4 --- /dev/null +++ b/scripts/upgrade-packages/tarball-delta.ts @@ -0,0 +1,459 @@ +/** + * Slim + classify a `bun pm diff --json` payload for the upgrade-packages + * artifact. Pure — no registry, no gh. evidence.ts fetches; this file judges + * what the agent is allowed to read. + */ + +export const PATCH_MAX_CHARS = 8_000; +const RELEASE_NOTES_MAX_CHARS = 1_200; +export const FILE_LIST_CAP = 80; +export const PATCH_PATH_CAP = 40; +const HINT_CAP = 8; + +export interface TarballFile { + path: string; + status: string; + linesAdded: number; + linesRemoved: number; + formattingOnly: boolean; + patch: string | null; +} + +export interface TarballDiff { + from: string; + to: string; + notes: string[]; + totals: { + files: number; + added: number; + deleted: number; + linesAdded: number; + linesRemoved: number; + formattingOnly: number; + }; + files: TarballFile[]; +} + +export interface Delta { + version: string; + date: string | null; + breaking: string[]; + deprecations: string[]; + features: string[]; + security: string[]; + peerEngine: string[]; + releaseNotes: string | null; + changelogUrl: string; + tarball: TarballDiff | null; + source: "bun-pm-diff" | "none"; + error: string | null; +} + +export interface BunPmDiffFile { + path: string; + status?: string; + sourceMap?: boolean; + formattingOnly?: boolean; + linesAdded?: number; + linesRemoved?: number; + patch?: string; +} + +export interface BunPmDiffJson { + from?: string; + to?: string; + notes?: unknown; + totals?: { + files?: number; + added?: number; + deleted?: number; + linesAdded?: number; + linesRemoved?: number; + formattingOnly?: number; + }; + files?: BunPmDiffFile[]; +} + +const KEEP_NAME = + /^(package\.json|changelog.*|history(?:\.(md|txt))?|news(?:\.(md|txt))?|changes(?:\.(md|txt))?|readme.*)$/i; +const DTS_EXT = /\.d\.[cm]?ts$/i; +const CHANGELOG_NAME = /^(changelog|history|news|changes)/i; + +function fileName(path: string): string { + const i = path.lastIndexOf("/"); + return i >= 0 ? path.slice(i + 1) : path; +} + +export function isSkipFile( + file: Pick, +): boolean { + return Boolean(file.sourceMap) || file.path.endsWith(".map"); +} + +function isMinifiedPath(path: string): boolean { + return /\.(min|production)\.(m|c)?js$/i.test(path); +} + +function isKeepName(path: string): boolean { + return KEEP_NAME.test(fileName(path)); +} + +function isDtsPath(path: string): boolean { + return DTS_EXT.test(path); +} + +export function isKeepPath(path: string): boolean { + return isKeepName(path) || isDtsPath(path); +} + +function isChangelogPath(path: string): boolean { + return CHANGELOG_NAME.test(fileName(path)); +} + +export function npmVersionUrl(pkg: string, version: string): string { + return `https://www.npmjs.com/package/${pkg}/v/${version}`; +} + +function churn( + file: Pick, +): number { + return (file.linesAdded ?? 0) + (file.linesRemoved ?? 0); +} + +function byChurnDesc(a: BunPmDiffFile, b: BunPmDiffFile): number { + return churn(b) - churn(a); +} + +function pathMentionsSymbol(path: string, symbols: string[]): boolean { + return symbols.some((s) => s.length >= 2 && path.includes(s)); +} + +/** Paths to request on the second `bun pm diff` (patches only). */ +export function selectPatchPaths( + files: BunPmDiffFile[], + importedSymbols: string[] = [], +): string[] { + const usable = files.filter( + (f) => f.path && !isSkipFile(f) && !f.formattingOnly, + ); + const named = usable.filter((f) => isKeepName(f.path)); + const namedSet = new Set(named); + const dts = usable.filter((f) => isDtsPath(f.path) && !namedSet.has(f)); + const namedOrDts = new Set([...named, ...dts]); + const extra = usable.filter( + (f) => !namedOrDts.has(f) && !isMinifiedPath(f.path), + ); + const bySymbolThenChurn = (a: BunPmDiffFile, b: BunPmDiffFile): number => { + const aHit = pathMentionsSymbol(a.path, importedSymbols) ? 1 : 0; + const bHit = pathMentionsSymbol(b.path, importedSymbols) ? 1 : 0; + if (aHit !== bHit) return bHit - aHit; + return byChurnDesc(a, b); + }; + extra.sort(bySymbolThenChurn); + return [ + ...named.map((f) => f.path), + ...[...dts].sort(bySymbolThenChurn).map((f) => f.path), + ...extra.map((f) => f.path), + ].slice(0, PATCH_PATH_CAP); +} + +export function addedPatchLines(patch: string): string { + return patch + .split("\n") + .filter((l) => l.startsWith("+") && !l.startsWith("+++")) + .map((l) => l.slice(1)) + .join("\n"); +} + +export function extractDate(text: string): string | null { + const m = text.match(/\b(20\d{2}-\d{2}-\d{2})\b/); + return m ? m[1] : null; +} + +function extractHintLines(body: string, re: RegExp): string[] { + return body + .split("\n") + .map((l) => l.trim()) + .filter((l) => l.length > 0 && re.test(l)) + .slice(0, HINT_CAP) + .map((l) => l.replace(/^[#*\-\s]+/, "").slice(0, 160)); +} + +const HEADING = /^(#{1,6}\s+|[A-Z][\w\s]{2,}:$)/; + +type HintKey = + | "breaking" + | "deprecations" + | "features" + | "security" + | "peerEngine"; + +interface HintBuckets { + breaking: string[]; + deprecations: string[]; + features: string[]; + security: string[]; + peerEngine: string[]; +} + +const HINT_KEYS: HintKey[] = [ + "breaking", + "deprecations", + "features", + "security", + "peerEngine", +]; + +const HEADING_BUCKET: [RegExp, HintKey][] = [ + [/breaking/i, "breaking"], + [/deprecat/i, "deprecations"], + [/security|advisory|\bcve\b/i, "security"], + [/\bfeat/i, "features"], + [/^#{1,6}\s+add/i, "features"], + [/peer|engine/i, "peerEngine"], +]; + +const DOC_HINTS: [HintKey, RegExp][] = [ + ["breaking", /breaking|breaking change/i], + ["deprecations", /deprecat|removed export/i], + ["features", /^feat|feature|^add|^new/i], + ["security", /security|cve|prototype pollution|vulnerabilit/i], + ["peerEngine", /peer dep|engine|requires (node|bun|react)/i], +]; + +function emptyHints(): HintBuckets { + return { + breaking: [], + deprecations: [], + features: [], + security: [], + peerEngine: [], + }; +} + +function headingBucket(line: string): HintKey | null { + for (const [re, key] of HEADING_BUCKET) { + if (re.test(line)) return key; + } + return null; +} + +export function classifyChangelogLines(added: string): HintBuckets { + const out = emptyHints(); + let section: HintKey | null = null; + for (const raw of added.split("\n")) { + const line = raw.trim(); + if (!line) continue; + if (HEADING.test(line)) { + section = headingBucket(line); + continue; + } + if (!section) continue; + const item = line.replace(/^[#*\-\s]+/, "").slice(0, 160); + if (item) out[section].push(item); + } + return out; +} + +export function classifyNote( + note: string, +): "security" | "peerEngine" | "features" | "breaking" | null { + if ( + /(preinstall|postinstall|preuninstall|install script|child_process|\bvm\b|eval\(|new Function|process\.env)/i.test( + note, + ) + ) { + return "security"; + } + // New `fs`/`net` imports are called out in notes; bare "fs" in a dep name is not. + if (/\b(fs|net|http)\b/.test(note) && /import/i.test(note)) return "security"; + if (/breaking/i.test(note)) return "breaking"; + if (/(engine|peer|dependenc)/i.test(note)) return "peerEngine"; + if (/(export|entry[- ]?point|binar|\bmain\b|\bmodule\b)/i.test(note)) { + return "features"; + } + return null; +} + +export function shouldKeepPatch(path: string, patch: string | null): boolean { + if (!patch) return false; + if (isMinifiedPath(path)) return false; + return true; +} + +function mergeDiffFiles( + stat: BunPmDiffJson, + patches: BunPmDiffJson | null, +): BunPmDiffFile[] { + const patchByPath = new Map(); + for (const f of patches?.files ?? []) { + if (f.path && typeof f.patch === "string") patchByPath.set(f.path, f.patch); + } + return (stat.files ?? []).map((f) => ({ + ...f, + patch: patchByPath.get(f.path) ?? f.patch, + })); +} + +export function slimFiles(files: BunPmDiffFile[]): TarballFile[] { + const mapped: TarballFile[] = []; + for (const f of files) { + if (!f.path || isSkipFile(f)) continue; + const rawPatch = typeof f.patch === "string" ? f.patch : null; + const keep = shouldKeepPatch(f.path, rawPatch); + mapped.push({ + path: f.path, + status: f.status ?? "modified", + linesAdded: f.linesAdded ?? 0, + linesRemoved: f.linesRemoved ?? 0, + formattingOnly: Boolean(f.formattingOnly), + patch: keep && rawPatch ? rawPatch.slice(0, PATCH_MAX_CHARS) : null, + }); + } + if (mapped.length <= FILE_LIST_CAP) return mapped; + const named = mapped.filter((f) => isKeepName(f.path)); + const namedSet = new Set(named); + const secondary = mapped.filter( + (f) => !namedSet.has(f) && (isDtsPath(f.path) || f.patch), + ); + const priority = [...named, ...secondary]; + const prioritySet = new Set(priority); + const rest = mapped + .filter((f) => !prioritySet.has(f)) + .sort( + (a, b) => b.linesAdded + b.linesRemoved - (a.linesAdded + a.linesRemoved), + ); + return [...priority, ...rest].slice(0, FILE_LIST_CAP); +} + +function uniqueHints(lines: string[]): string[] { + const seen = new Set(); + const out: string[] = []; + for (const l of lines) { + if (seen.has(l)) continue; + seen.add(l); + out.push(l); + if (out.length >= HINT_CAP) break; + } + return out; +} + +function notesOf(stat: BunPmDiffJson): string[] { + return Array.isArray(stat.notes) + ? stat.notes.filter((n): n is string => typeof n === "string") + : []; +} + +function appendNotes(out: HintBuckets, notes: string[]): void { + for (const note of notes) { + const bucket = classifyNote(note); + if (bucket) out[bucket].push(note); + } +} + +function appendDocHints(out: HintBuckets, added: string): void { + const fromSections = classifyChangelogLines(added); + for (const key of HINT_KEYS) out[key].push(...fromSections[key]); + for (const [key, re] of DOC_HINTS) { + out[key].push(...extractHintLines(added, re)); + } +} + +function collectFromFiles(files: TarballFile[]): { + hints: HintBuckets; + releaseNotes: string | null; + date: string | null; +} { + const hints = emptyHints(); + let releaseNotes: string | null = null; + let date: string | null = null; + for (const f of files) { + if (!f.patch) continue; + const added = addedPatchLines(f.patch); + if (isChangelogPath(f.path) || /^readme/i.test(fileName(f.path))) { + if (!releaseNotes && isChangelogPath(f.path)) { + releaseNotes = added.slice(0, RELEASE_NOTES_MAX_CHARS); + date = extractDate(added); + } + appendDocHints(hints, added); + } + if (fileName(f.path) === "package.json") { + hints.peerEngine.push( + ...extractHintLines(added, /peer|engine|dependenc/i), + ); + hints.security.push(...extractHintLines(added, /"(pre|post)?install"/i)); + } + } + return { hints, releaseNotes, date }; +} + +function tarballOf( + stat: BunPmDiffJson, + files: TarballFile[], + notes: string[], +): TarballDiff { + const totals = stat.totals ?? {}; + return { + from: stat.from ?? "", + to: stat.to ?? "", + notes, + totals: { + files: totals.files ?? files.length, + added: totals.added ?? 0, + deleted: totals.deleted ?? 0, + linesAdded: totals.linesAdded ?? 0, + linesRemoved: totals.linesRemoved ?? 0, + formattingOnly: totals.formattingOnly ?? 0, + }, + files, + }; +} + +export function failedDelta( + target: string, + changelogUrl: string, + error: string, +): Delta { + return { + version: target, + date: null, + breaking: [], + deprecations: [], + features: [], + security: [], + peerEngine: [], + releaseNotes: null, + changelogUrl, + tarball: null, + source: "none", + error, + }; +} + +export function buildDelta(args: { + target: string; + changelogUrl: string; + stat: BunPmDiffJson; + patches: BunPmDiffJson | null; +}): Delta { + const notes = notesOf(args.stat); + const files = slimFiles(mergeDiffFiles(args.stat, args.patches)); + const hints = emptyHints(); + appendNotes(hints, notes); + const fromFiles = collectFromFiles(files); + for (const key of HINT_KEYS) hints[key].push(...fromFiles.hints[key]); + return { + version: args.target, + date: fromFiles.date, + breaking: uniqueHints(hints.breaking), + deprecations: uniqueHints(hints.deprecations), + features: uniqueHints(hints.features), + security: uniqueHints(hints.security), + peerEngine: uniqueHints(hints.peerEngine), + releaseNotes: fromFiles.releaseNotes, + changelogUrl: args.changelogUrl, + tarball: tarballOf(args.stat, files, notes), + source: "bun-pm-diff", + error: null, + }; +} From 2eaf14cefa77e931cdcc009018924f9b810c7e1f Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Mon, 24 Aug 2026 13:50:44 +0300 Subject: [PATCH 2/3] fix: keep CI green on Bun 1.4 Force dim styles in the clack note test when there is no TTY, and pin transitive tar to 7.5.22 so audit no longer blocks on GHSA-r292-9mhp-454m. --- bun.lock | 3 ++- package.json | 3 ++- src/agents-init-interactive.test.ts | 7 +++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/bun.lock b/bun.lock index 1f6f82bd..5174fd7b 100644 --- a/bun.lock +++ b/bun.lock @@ -51,6 +51,7 @@ "nanoid": "3.3.18", "path-to-regexp": "6.3.0", "qs": ">=6.15.2", + "tar": "7.5.22", }, "packages": { "@ai-sdk/gateway": ["@ai-sdk/gateway@4.0.40", "", { "dependencies": { "@ai-sdk/provider": "4.0.5", "@ai-sdk/provider-utils": "5.0.20", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-poNySlk+zSe04M4v0wgJKt+mzY6Vk6abcQid58YZnFUBJYo6VwfsFUaW7oINoR1vYYB7Ne6eqY1doSxYKt0KDg=="], @@ -2153,7 +2154,7 @@ "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], - "tar": ["tar@7.5.20", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-9FcyK4PA6+WbzlTM9WhQm6vB5W7cP7dUiPsv1g7YDwEQnQ1CGpK3MGlKk/ITVWMk05kHZuBhmVhiv8LZoy/PFQ=="], + "tar": ["tar@7.5.22", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA=="], "tar-fs": ["tar-fs@2.1.5", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw=="], diff --git a/package.json b/package.json index 6cd14167..267b4378 100644 --- a/package.json +++ b/package.json @@ -132,7 +132,8 @@ "js-yaml": "4.3.1", "nanoid": "3.3.18", "path-to-regexp": "6.3.0", - "qs": ">=6.15.2" + "qs": ">=6.15.2", + "tar": "7.5.22" }, "engines": { "bun": ">=1.0.31", diff --git a/src/agents-init-interactive.test.ts b/src/agents-init-interactive.test.ts index a537621f..1b48bfac 100644 --- a/src/agents-init-interactive.test.ts +++ b/src/agents-init-interactive.test.ts @@ -15,7 +15,10 @@ describe("agents-init-interactive notes", () => { }); it("dims note body text like @clack/prompts 1.5", () => { - expect(styleText("dim", "hello")).toBe(styleText("dim", "hello")); - expect(styleText("dim", "hello")).not.toBe("hello"); + // Bun 1.4 `styleText` is a no-op without a TTY / when NO_COLOR is set + // (CI and this sandbox). Skip the stream check so we assert the dim codes. + const dimmed = styleText("dim", "hello", { validateStream: false }); + expect(dimmed).toBe(styleText("dim", "hello", { validateStream: false })); + expect(dimmed).not.toBe("hello"); }); }); From 4bd891fb54101d8bbbd4d6d8153a212640d4605c Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Mon, 24 Aug 2026 13:56:03 +0300 Subject: [PATCH 3/3] fix: address CodeRabbit facts on Bun engines and upgrade-packages CONTRIBUTING now matches engines.bun >=1.0.31. Deltas for the same package at two workspace versions no longer overwrite each other. --- .github/CONTRIBUTING.md | 2 +- .github/workflows/ci.yml | 1 + scripts/upgrade-packages/evidence.test.ts | 12 ++++++++++++ scripts/upgrade-packages/evidence.ts | 6 +++++- 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index b323e622..88f097d5 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -3,7 +3,7 @@ Codemap is in **bootstrap / extraction** phase. Before large PRs, please open an issue so we can align on: - **Core vs adapter** — core should stay small; language-specific logic belongs in **adapters** (see [docs/roadmap.md](../docs/roadmap.md)). -- **Runtimes** — **Node** `^20.19.0 || >=22.12.0` and **Bun** `>=1.0.0` (`package.json` **engines**); SQLite is **`better-sqlite3`** on Node and **`bun:sqlite`** on Bun ([docs/architecture.md](../docs/architecture.md), [docs/packaging.md § Node vs Bun](../docs/packaging.md#node-vs-bun)). Maintainers use **Bun 1.4.0** (`packageManager`). +- **Runtimes** — **Node** `^20.19.0 || >=22.12.0` and **Bun** `>=1.0.31` (`package.json` **engines**); SQLite is **`better-sqlite3`** on Node and **`bun:sqlite`** on Bun ([docs/architecture.md](../docs/architecture.md), [docs/packaging.md § Node vs Bun](../docs/packaging.md#node-vs-bun)). Maintainers use **Bun 1.4.0** (`packageManager`). ## Dev workflow diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f23737fa..84b271f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -243,6 +243,7 @@ jobs: run: bun run docs:audit dedupe: + # Fails when bun.lock still has duplicate versions (`bun dedupe --check`). name: 🧹 Dedupe needs: skip-ci if: needs['skip-ci'].outputs.skip != 'true' diff --git a/scripts/upgrade-packages/evidence.test.ts b/scripts/upgrade-packages/evidence.test.ts index cbcbf82d..468c2f8c 100644 --- a/scripts/upgrade-packages/evidence.test.ts +++ b/scripts/upgrade-packages/evidence.test.ts @@ -68,6 +68,18 @@ describe("parseOutdatedTable", () => { ]); }); + it("keeps the same package at two current versions", () => { + const stdout = [ + "| Package | Current | Update | Latest | Workspace |", + "| --- | --- | --- | --- | --- |", + "| react | 18.3.1 | 19.0.0 | 19.2.0 | @stainless-code/codemap |", + "| react | 19.1.0 | 19.2.0 | 19.2.0 | @stainless-code/codemap-docs |", + ].join("\n"); + const rows = parseOutdatedTable(stdout); + expect(rows).toHaveLength(2); + expect(rows.map((r) => r.current)).toEqual(["18.3.1", "19.1.0"]); + }); + it("parses workspace-filter rows and strips (peer)/(optional)", () => { const stdout = [ "| Package | Current | Update | Latest | Workspace |", diff --git a/scripts/upgrade-packages/evidence.ts b/scripts/upgrade-packages/evidence.ts index b3689420..f9b70914 100644 --- a/scripts/upgrade-packages/evidence.ts +++ b/scripts/upgrade-packages/evidence.ts @@ -859,6 +859,8 @@ async function main() { let outdated = await parseBunOutdated(); if (onlyPkg) outdated = outdated.filter((o) => o.pkg === onlyPkg); const target = new Map(outdated.map((o) => [o.pkg, o.latest])); + // Manifest inventory is a range floor; `bun outdated` reports the resolved version. + for (const o of outdated) installed.set(o.pkg, o.current); console.error("→ bun audit + ghsa"); const ghsaPkgs = (onlyPkg ? [onlyPkg] : HIGH_RISK).filter( @@ -892,7 +894,9 @@ async function main() { ] as const; }); const deltas: Record = {}; - for (const [pkg, list] of deltaEntries) deltas[pkg] = list; + for (const [pkg, list] of deltaEntries) { + deltas[pkg] = [...(deltas[pkg] ?? []), ...list]; + } const evidence: Evidence = { generatedAt: new Date().toISOString(),