Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 56 additions & 3 deletions .agents-docs/AGENTS-development-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,17 @@
| `npm test` | Runs the full Vitest suite once (`vitest run`). |
| `npm run test:watch` | Vitest in watch mode. |
| `npm run test:coverage` | Vitest with v8 coverage (`text` + `lcov`). |
| `npm run lint` | ESLint over `src/`. |
| `npm run format` | Prettier write over `src/**/*.ts` and `tests/**/*.ts`. |
| `npm run lint` | ESLint over `src/`, `tests/` and `scripts/`. |
| `npm run format` | Prettier write over `src/**/*.ts`, `tests/**/*.ts` and `scripts/**/*.mjs`. |
| `npm run format:check` | Prettier check (no writes) — used in CI. |
| `npm run smoke` | Builds, then runs the live local-ONNX provider test (`scripts/smoke-test.mjs`). Downloads models from HuggingFace, so it is local-only and not run in CI. |
| `npm run engines:check` | Runs the built `dist/` (ESM + CJS) and asserts real behaviour, to verify the `engines.node` floor. See CI below. |

Glob arguments in the `format` scripts must use **double** quotes. Single quotes are not stripped by `cmd`/PowerShell, so prettier receives the literal glob, matches nothing, and the script silently succeeds as a no-op on Windows.

`eslint.config.mjs` relaxes two rules for `tests/**` only: `no-explicit-any` is off (mocking `fetch`/`setTimeout`/provider responses legitimately needs `any`) and `no-unused-vars` honours the `^_` prefix for intentionally unused arguments. Neither relaxation applies to `src/`. `scripts/**/*.mjs` is plain ESM parsed by the default parser, so only core rules apply there.

`.gitattributes` pins the whole tree to `eol=lf` via `* text=auto eol=lf`. Files authored on Windows otherwise enter the index with CRLF, which produces a whole-file diff that buries the real change — this had already happened to three files. Prettier enforces LF too, but only for the extensions it covers, so the attribute is what protects `.yml`, `.json` and `.md`.

## Running a single test

Expand All @@ -32,4 +41,48 @@ npx vitest tests/clustering # watch a directory

- `dist/` is the published artifact; `files` in `package.json` ships `dist`, `README.md`, `LICENSE`.
- `@huggingface/transformers` is an **optional peer dependency** — present in `devDependencies` for local-provider tests but never required for consumers who don't use the local ONNX provider.
- Engines: Node `>=18`.
- Engines: Node `>=18`. The dev toolchain itself needs `>=20`, so 18 is verified against the built artifact only — see CI below.
- `package-lock.json` **is tracked**. Use `npm ci` for reproducible installs; do not re-ignore it.

## CI

`.github/workflows/ci.yml` runs on pushes to `master`/`develop`, on every pull request, and weekly on a schedule.

| Job | What it does |
|---|---|
| `verify` | `format:check`, `lint`, `typecheck`, `test`, `build` across Node 20, 22, 24. |
| `engines-floor` | Builds on Node 24, then runs `npm run engines:check` on Node 18. |
| `audit` | `npm audit --audit-level=high` against the committed lockfile. |

Notes for anyone editing the workflow:

- The `verify` matrix floor is **20, not 18**, because vitest 4 requires `^20 \|\| ^22 \|\| >=24` and eslint 10 requires `^20.19 \|\| ^22.13 \|\| >=24`. Node 18 cannot run the toolchain at all. `engines-floor` covers the `>=18` claim instead by exercising the built `dist/`, which has no runtime dependencies. If `engines.node` changes, update that job.
- Installs use `npm ci --ignore-scripts` to skip the `onnxruntime-node` and `sharp` postinstalls (hundreds of MB of native binaries). Nothing in CI needs them. If a future test does need real ONNX, drop `--ignore-scripts` for that job rather than adding it to `verify`.
- `audit` is gated at `high` so moderate findings in dev-only transitive dependencies don't block merges, and runs weekly so new advisories surface on their own schedule.

## Dependency overrides

`package.json` declares three `overrides`. All exist to escape advisories that the declaring package pins below the fixed version, with no upstream release available. **These are temporary and should be removed as soon as the condition in the last column is met** — each one forces a version outside a range some dependency declares, so none should outlive its reason.

| Override | Declared range it escapes | Advisory | Remove when |
|---|---|---|---|
| `sharp: ^0.35.3` | `^0.34.5`, by `@huggingface/transformers` | libvips CVEs (GHSA-f88m-g3jw-g9cj) | transformers depends on `sharp >=0.35.0` |
| `adm-zip: ^0.6.0` | `0.5.x`, via `onnxruntime-node@1.24.3` | GHSA-xcpc-8h2w-3j85 | `onnxruntime-node` depends on `adm-zip >=0.6.0` **and** transformers takes that version |
| `esbuild: 0.27.2` | `^0.27.0`, by `tsup` | GHSA-g7r4-m6w7-qqqr (affects `0.27.3` – `0.28.0`) | tsup widens to `^0.28.1` or later |

Two things to know before touching them:

- **`sharp` and `adm-zip` are not covered by `npm test`.** The unit suite mocks providers, so it will stay green even if these break the local ONNX provider. `npm run smoke` is the only guard — it exercises `onnxruntime-node` end-to-end across three models. Re-run it after any change here.
- **`esbuild` must be an override, not just a lockfile entry.** `^0.27.0` has no safe version above `0.27.2`, so anything that re-resolves the range (a fresh `npm install`, `npm update`, a dependency bump) would pull a vulnerable version back in. The pin is what makes it stick.

Check whether any of these can go with:

```bash
npm view @huggingface/transformers dependencies
npm view tsup dependencies.esbuild
```

## TypeScript version constraints

- The project is on **TypeScript 6.0.x**. Do not bump to TypeScript 7 yet: `@typescript-eslint/*` v8 declares `typescript: ">=4.8.4 <6.1.0"`, so `npm run lint` would run against an unsupported compiler.
- `tsconfig.json` sets `"ignoreDeprecations": "6.0"`. This is **required for `npm run build`**, not for our own config — tsup's dts pipeline hardcodes `baseUrl: compilerOptions.baseUrl || "."` (`node_modules/tsup/dist/rollup.js`), and TS 6 raises `TS5101` for deprecated `baseUrl`. Removing the flag breaks the DTS build. It can be dropped once tsup stops injecting `baseUrl`.
7 changes: 7 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Normalize text files to LF in the repository regardless of the authoring
# platform. Without this, files written on Windows land in the index with CRLF
# while the rest of the tree is LF, producing whole-file diffs that hide the
# real change. Prettier already enforces LF, but only for the files it covers.
* text=auto eol=lf

*.jpg binary
87 changes: 87 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: CI

on:
push:
branches: [master, develop]
pull_request:
schedule:
# Weekly, so new advisories surface on their own schedule instead of
# failing an unrelated pull request.
- cron: '0 6 * * 1'

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
verify:
name: verify (node ${{ matrix.node }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# The floor is 20, not the `engines.node` floor of 18, because the dev
# toolchain requires it: vitest 4 needs ^20 || ^22 || >=24 and eslint 10
# needs ^20.19 || ^22.13 || >=24. The `engines` claim is covered by the
# engines-floor job below instead.
node: ['20', '22', '24']
steps:
- uses: actions/checkout@v7

- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node }}
cache: npm

# --ignore-scripts skips the onnxruntime-node and sharp postinstalls,
# which download hundreds of MB of native binaries. Nothing below needs
# them: the suite mocks providers, and the local ONNX provider is only
# exercised by `npm run smoke`, which is deliberately not run here since
# it downloads models from HuggingFace.
- run: npm ci --ignore-scripts

- run: npm run format:check
- run: npm run lint
- run: npm run typecheck
- run: npm test
- run: npm run build

engines-floor:
name: engines floor (node 18)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

# Build on a modern Node, since the toolchain cannot run on 18...
- uses: actions/setup-node@v7
with:
node-version: '24'
cache: npm
- run: npm ci --ignore-scripts
- run: npm run build

# ...then run the built artifact on the oldest Node that engines.node
# claims to support. The package has no runtime dependencies, so this
# needs nothing installed. Keep this in step with engines.node.
- uses: actions/setup-node@v7
with:
node-version: '18'
- run: npm run engines:check

audit:
name: audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- uses: actions/setup-node@v7
with:
node-version: '24'
cache: npm

# Audits the committed lockfile. Fails on high and critical only; moderate
# and below in dev-only transitive dependencies should not block a merge.
- run: npm audit --audit-level=high
11 changes: 5 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
node_modules
package-lock.json
dist
coverage
specs
specs--completed
node_modules
dist
coverage
specs
specs--completed
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ The sections below are brief summaries. Each links to a detail file in `.agents-

## Development Commands

`npm run build` (tsup → dual ESM/CJS + dts), `npm test` (vitest run), `npm run test:watch`, `npm run test:coverage`, `npm run lint` (eslint), `npm run format` (prettier). Tests live in `tests/` mirroring `src/`.
`npm run build` (tsup → dual ESM/CJS + dts), `npm test` (vitest run), `npm run test:watch`, `npm run test:coverage`, `npm run lint` (eslint over `src/` + `tests/`), `npm run format` / `npm run format:check` (prettier), `npm run smoke` (live ONNX, local-only), `npm run engines:check`. Tests live in `tests/` mirroring `src/`. CI (`.github/workflows/ci.yml`) enforces all of these except `smoke`; `package-lock.json` is tracked, so use `npm ci`.

Three temporary `overrides` in `package.json` (`sharp`, `adm-zip`, `esbuild`) hold transitive dependencies at non-vulnerable versions, and TypeScript is held at 6.x. Both have removal conditions documented — read the detail file before changing either.

Details: [Development Commands](./.agents-docs/AGENTS-development-commands.md)

Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Maintenance

Tooling and dependency work only — **no changes to the public API or to runtime behavior**. Consumers are unaffected.

- **TypeScript 6:** upgraded from 5.9 to 6.0.3, the highest major currently supported by `@typescript-eslint` v8 (`>=4.8.4 <6.1.0`). TypeScript 7 is deliberately deferred until typescript-eslint supports it. `tsconfig.json` now sets `"ignoreDeprecations": "6.0"`, which TS 6 requires because tsup's dts pipeline injects the now-deprecated `baseUrl`.
- **All npm audit advisories resolved (5 → 0).** Added `overrides` for `sharp` (libvips CVEs), `adm-zip`, and `esbuild`, each of which was pinned below its fixed version by a dependency with no upstream fix available. All three sat under `@huggingface/transformers`, the optional peer dependency, so consumers not using the local ONNX provider were never exposed. Every override is documented with a condition for its removal.
- **`package-lock.json` is now tracked.** It was previously gitignored, so installs were not reproducible and the `overrides` above had no recorded resolution. Use `npm ci`.
- **Fixed `npm run format`, which was a silent no-op on Windows.** Its globs were single-quoted, which `cmd`/PowerShell do not strip, so Prettier matched zero files and exited successfully. As a result 90 files had been formatted at Prettier's defaults rather than this project's `.prettierrc` (100 columns, `es5` trailing commas); the configured style is now applied throughout. Added `npm run format:check`.
- **`npm run lint` now covers `tests/` as well as `src/`.** `eslint.config.mjs` had always declared `tests/**`, but the script never passed it. Cleared the dead code this surfaced — unused imports, a vestigial `EmbeddingStore`, and an abandoned serialization buffer — and relaxed `no-explicit-any` for test files only.
- **Added CI** (`.github/workflows/ci.yml`): format, lint, typecheck, test and build across Node 20/22/24, plus an `engines-floor` job that runs the built `dist/` on Node 18 to verify the `engines.node` claim (the dev toolchain itself now requires Node >=20), and a weekly `npm audit`.
- **`scripts/` is now linted and formatted**, and `.gitattributes` pins the tree to LF line endings so files authored on Windows can no longer enter the index with CRLF.

## [0.6.0] - 2026-08-12

### New Features
Expand Down
22 changes: 22 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,26 @@ export default [
...tseslint.configs.recommended.rules,
},
},
{
files: ['tests/**/*.ts'],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
},
},
{
// Plain ESM, parsed by the default parser rather than the TypeScript one,
// so only core rules apply here.
files: ['scripts/**/*.mjs'],
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module',
},
rules: {
'no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
},
},
];
Loading