第 5 章: パッケージ管理と静的解析¶
5.1 はじめに¶
前の章では、実験を再現するには乱数のシードとアルゴリズムに加えて、ライブラリと Node.js のバージョンを固定する必要があると述べました。この章では、それを担う npm の仕組みと、コードを実行せずに問題を見つける 静的解析、テストがコードのどこを通ったかを測る カバレッジ を整えます。
| 道具 | 役割 | 本章の節 |
|---|---|---|
npm(package.json・package-lock.json・.npmrc) |
依存ライブラリの管理 | 5.2 |
.nvmrc・engines |
Node.js のバージョンの指定 | 5.3 |
TypeScript のコンパイラ(tsc) |
型チェック | 5.4 |
| ESLint・typescript-eslint | コードの問題の検査 | 5.5 |
| Prettier | コードの整形 | 5.6 |
| @vitest/coverage-v8 | テストのカバレッジ計測 | 5.7 |
npm audit |
依存ライブラリの既知の脆弱性の検査 | 5.8 |
本章のバージョンは、執筆時点の apps/node/package.json に書かれたものです(TypeScript 6.0.3、ESLint 10.10.0、typescript-eslint 8.70.0、Prettier 3.9.7、Vitest 5.0.1)。選定の理由は ADR 003 を参照してください。
Python 版の第 5 章 の uv・Ruff・mypy・pytest-cov、Kotlin 版の第 5 章 の Gradle・ktlint・detekt・Kover に当たるものを、TypeScript 版では npm と、npm で入れる開発用のパッケージで組み立てます。
5.2 npm によるパッケージ管理¶
package.json¶
npm の設定の中心は apps/node/package.json です。第 7 章以降で使うライブラリまで入れた、執筆時点の内容です。
{
"name": "getting-started-ml",
"version": "0.1.0",
"description": "機械学習から始めるプログラミング入門(TypeScript 版)",
"private": true,
"type": "module",
"engines": {
"node": ">=22.13.0"
},
"scripts": {
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"lint": "eslint .",
"format": "prettier --write .",
"format:check": "prettier --check .",
"typecheck": "tsc --noEmit",
"check": "npm run format:check && npm run lint && npm run typecheck && npm test"
},
"devDependencies": {
"@types/node": "22.20.3",
"@vitest/coverage-v8": "5.0.1",
"eslint": "10.10.0",
"eslint-config-prettier": "10.1.8",
"prettier": "3.9.7",
"typescript": "6.0.3",
"typescript-eslint": "8.70.0",
"vitest": "5.0.1"
},
"dependencies": {
"@hono/node-server": "2.1.1",
"csv-parse": "7.0.2",
"hono": "4.13.8",
"ml-cart": "2.1.1",
"ml-confusion-matrix": "2.0.0",
"ml-cross-validation": "1.3.0",
"ml-kmeans": "7.0.1",
"ml-logistic-regression": "2.0.0",
"ml-matrix": "6.15.0",
"ml-pca": "4.1.1",
"ml-random-forest": "2.1.0",
"ml-regression-lasso": "0.1.2",
"ml-regression-multivariate-linear": "2.0.4",
"zod": "4.6.5"
}
}
| 項目 | 意味 |
|---|---|
private: true |
npm のレジストリに誤って公開しないようにする |
type: "module" |
.js・.ts のファイルを ES モジュール(import・export)として扱う |
engines |
動作する Node.js の版(5.3 節) |
scripts |
npm run <名前> で実行するコマンド(第 6 章) |
dependencies・devDependencies |
依存ライブラリ(次の項) |
本番依存と開発依存¶
依存ライブラリは、使いみちで 2 つに分けています。
| 区分 | 中身 | 判断の基準 |
|---|---|---|
dependencies |
csv-parse・ml.js 系・Hono・zod | src のコードが実行時に import するもの |
devDependencies |
TypeScript・Vitest・ESLint・Prettier・@types/node |
型チェック・テスト・整形など、開発のときだけ使うもの |
TypeScript そのものも devDependencies です。Node.js 22 は型の注釈を取り除いて .ts を直接実行するので、プログラムを動かすだけなら TypeScript のコンパイラは要りません。@types/node は Node.js の API(node:fs など)の型定義で、型チェックのときだけ使います。
npm ls --depth=0 で、直接の依存関係を一覧できます。
npm ls --depth=0
getting-started-ml@0.1.0 ...\apps\node
+-- @hono/node-server@2.1.1
+-- @types/node@22.20.3
+-- @vitest/coverage-v8@5.0.1
+-- csv-parse@7.0.2
+-- eslint-config-prettier@10.1.8
+-- eslint@10.10.0
+-- hono@4.13.8
+-- ml-cart@2.1.1
+-- ml-confusion-matrix@2.0.0
+-- ml-cross-validation@1.3.0
+-- ml-kmeans@7.0.1
+-- ml-logistic-regression@2.0.0
+-- ml-matrix@6.15.0
+-- ml-pca@4.1.1
+-- ml-random-forest@2.1.0
+-- ml-regression-lasso@0.1.2
+-- ml-regression-multivariate-linear@2.0.4
+-- prettier@3.9.7
+-- typescript-eslint@8.70.0
+-- typescript@6.0.3
+-- vitest@5.0.1
`-- zod@4.6.5
正確な版を書く — .npmrc¶
npm install でライブラリを追加すると、npm は既定で ^7.0.2 のような 範囲 を package.json に書きます。^7.0.2 は「7.0.2 以上 8.0.0 未満」の意味で、インストールした日によって入る版が変わりえます。本リポジトリでは、apps/node/.npmrc で正確な版を書くように設定しています。
# package.json に範囲ではなく正確な版を書く
save-exact=true
engine-strict=true
第 3 章で ml-cart を追加したときは、次のように実行しました。
npm install ml-cart@2.1.1
package.json の差分は次のとおりで、^ の付かない正確な版が書かれています。
"dependencies": {
- "csv-parse": "7.0.2"
+ "csv-parse": "7.0.2",
+ "ml-cart": "2.1.1"
}
開発用のライブラリは npm install --save-dev <名前>@<版> で devDependencies に追加します。
package-lock.json と npm ci¶
package.json に正確な版を書いても、依存ライブラリがさらに依存するライブラリ(推移的な依存)は範囲で指定されています。package-lock.json から ml-cart の記録を抜き出すと、次のようになっています。
{
"node_modules/ml-cart": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ml-cart/-/ml-cart-2.1.1.tgz",
"integrity": "sha512-f6rIj4EzbjqKLJa2Qmm5AjZ0WVgk+Y7J1N/+pQVaFr0d4oM1uZPLOh5h665LyH+bLBHTFEbvSR4OLKmJRQ8KfA==",
"license": "MIT",
"dependencies": {
"ml-array-mean": "^1.1.5",
"ml-matrix": "^6.8.2"
}
}
}
resolvedはダウンロード元、integrityはダウンロードしたファイルのハッシュです。ハッシュが一致しなければインストールは失敗するので、配布物が途中で差し替えられていないことを確かめられます- ml-cart は ml-matrix を
^6.8.2という範囲で求めています。どの版を入れたかは、package-lock.jsonのnode_modules/ml-matrixの記録で決まります
npm ls で調べると、ml-cart を含む 8 つのパッケージが求める ml-matrix は、すべて本リポジトリが直接入れた 6.15.0 にまとめられていました(deduped)。
npm ls ml-matrix
getting-started-ml@0.1.0 ...\apps\node
+-- ml-cart@2.1.1
| `-- ml-matrix@6.15.0 deduped
+-- ml-kmeans@7.0.1
| +-- ml-matrix@6.15.0 deduped
| `-- ml-spectra-processing@14.35.1
| `-- ml-matrix@6.15.0 deduped
+-- ml-logistic-regression@2.0.0
| `-- ml-matrix@6.15.0 deduped
+-- ml-matrix@6.15.0
+-- ml-pca@4.1.1
| `-- ml-matrix@6.15.0 deduped
+-- ml-random-forest@2.1.0
| `-- ml-matrix@6.15.0 deduped
+-- ml-regression-lasso@0.1.2
| `-- ml-matrix@6.15.0 deduped
`-- ml-regression-multivariate-linear@2.0.4
`-- ml-matrix@6.15.0 deduped
package-lock.json があれば、推移的な依存まで同じ版を入れられます。Python 版の uv.lock と同じ役割です。Kotlin 版では、依存関係の依存関係の版が公開済みの POM に書かれていて後から変わらないので、ロックファイルを使いませんでした。npm の依存関係は範囲で書かれるのが普通なので、TypeScript 版では package-lock.json をコミットします。
依存関係を入れるときは、npm install ではなく npm ci を使います。
| コマンド | 振る舞い |
|---|---|
npm install |
package.json の範囲に合う版を解決し、必要なら package-lock.json を書き換える |
npm ci |
node_modules を消してから、package-lock.json のとおりに入れる。package-lock.json は書き換えない |
npm ci は、package.json と package-lock.json が食い違っていると失敗します。試しに、スクラッチのディレクトリに package.json・package-lock.json・.npmrc を写し、package.json の csv-parse だけを 7.0.1 に書き換えて npm ci を実行しました。
npm error code EUSAGE
npm error
npm error `npm ci` can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync. Please update your lock file with `npm install` before continuing.
npm error
npm error Invalid: lock file's csv-parse@7.0.2 does not satisfy csv-parse@7.0.1
package.json だけを書き換えてコミットしてしまっても、CI の npm ci がこの食い違いを見つけます。
5.3 Node.js のバージョンを固定する¶
TypeScript のプログラムは Node.js の上で動きます。Python 版で .python-version を、Kotlin 版で JDK ツールチェーンを使ったのと同じように、Node.js の版も指定します。
.nvmrc と engines¶
| ファイル | 中身 | 使われる場面 |
|---|---|---|
apps/node/.nvmrc |
22 |
nvm などのバージョン管理ツールが、使う Node.js を選ぶ |
package.json の engines |
"node": ">=22.13.0" |
npm が、今の Node.js で動くかを確かめる |
ops/nix/environments/node/shell.nix |
nodejs_22 |
Nix の環境(CI を含む)に入れる Node.js(第 6 章) |
.nvmrc と engines は、どちらも「どの版を使うか」を宣言するだけで、npm は既定では engines に合わなくても警告を出すだけです。.npmrc の engine-strict=true を指定すると、警告ではなくエラーにしてインストールをやめます。
engine-strict が見つけた下限の誤り¶
第 1 章でプロジェクトを作ったときは、Vitest 5 の engines(^22.12.0 || ^24.0.0 || >=26.0.0)に合わせて、engines を >=22.12.0 にしていました。ところが、この章を書くために依存ライブラリの engines を確かめたところ、ESLint 10 とその関連パッケージは ^20.19.0 || ^22.13.0 || >=24 を求めていました。
実際に Node.js 22.12.0 で npm ci を実行すると、engine-strict=true のもとでは、ESLint の関連パッケージのところで失敗しました。
npm error code EBADENGINE
npm error engine Unsupported engine
npm error engine Not compatible with your version of node/npm: @eslint/config-array@0.23.5
npm error notsup Not compatible with your version of node/npm: @eslint/config-array@0.23.5
npm error notsup Required: {"node":"^20.19.0 || ^22.13.0 || >=24"}
npm error notsup Actual: {"npm":"10.9.3","node":"v22.12.0"}
package.json の engines は「22.12.0 で動く」と宣言していたのに、依存関係は 22.13.0 以上を求めていたわけです。package-lock.json に記録されたすべてのパッケージの engines を満たす 22 系の最小の版を調べると 22.13.0 だったので、engines を >=22.13.0 に上げました(package-lock.json の先頭にも同じ値が記録されるので、あわせて更新しています)。上げたあとで Node.js 22.12.0 を使うと、今度はプロジェクト自身の engines で止まります。
npm error code EBADENGINE
npm error engine Unsupported engine
npm error engine Not compatible with your version of node/npm: getting-started-ml@0.1.0
npm error notsup Not compatible with your version of node/npm: getting-started-ml@0.1.0
npm error notsup Required: {"node":">=22.13.0"}
npm error notsup Actual: {"npm":"10.9.3","node":"v22.12.0"}
Node.js 22.13.0 では npm ci が成功しました。どちらのエラーでもインストールは止まりますが、プロジェクトの engines で止まるほうが、何を入れればよいかが最初の 1 行で分かります。手元(22.18.0)と CI(22.21.1)はどちらも条件を満たしていたので、この誤りはこれまで表に出ていませんでした。
5.4 型チェック — TypeScript コンパイラ¶
tsconfig.json¶
Python 版では型ヒントを mypy で検査し、Kotlin 版ではコンパイラが型を検査しました。TypeScript 版では TypeScript のコンパイラ(tsc)が型を検査します。設定は apps/node/tsconfig.json です。
{
"compilerOptions": {
"target": "ES2023",
"lib": ["ES2023"],
"module": "nodenext",
"moduleResolution": "nodenext",
"types": ["node"],
"strict": true,
"noUncheckedIndexedAccess": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*.ts", "test/**/*.ts", "*.config.ts"]
}
設定は、型チェックを厳しくするものと、Node.js で .ts を直接実行するためのものに分かれます。
| 設定 | 役割 |
|---|---|
strict |
null・undefined の検査(strictNullChecks)や、暗黙の any の禁止(noImplicitAny)など、厳しい検査をまとめて有効にする |
noUncheckedIndexedAccess |
配列やオブジェクトの添字アクセスの結果に undefined を含める。strict には含まれない |
noEmit |
JavaScript を出力せず、型チェックだけをする |
allowImportingTsExtensions |
import ... from "./random.ts" のように .ts の拡張子で読み込めるようにする。Node.js は拡張子を補わないので必要 |
erasableSyntaxOnly |
型を消すだけでは JavaScript にならない構文(enum・コンストラクターの引数プロパティなど)を禁止する。Node.js の型除去はこれらを扱えない |
verbatimModuleSyntax |
型だけの読み込みに import type を書かせる。型除去で import ごと消してよいかを、書いたとおりに判断できるようにする |
noUncheckedIndexedAccess の効果を確かめます。配列の先頭を返すだけの関数を書いて tsc を実行すると、エラーになります。
export function first(values: number[]): number {
return values[0];
}
src/lint-demo.ts(2,3): error TS2322: Type 'number | undefined' is not assignable to type 'number'.
Type 'undefined' is not assignable to type 'number'.
空の配列なら values[0] は undefined です。この設定があると、要素があることを確かめるか、as number で「必ずある」と明示するまで、number として使えません。第 2 章の shuffle の result[j] as T は、添字が範囲内であることを人が保証した箇所です。
Vitest は型を検査しない¶
第 1 部で何度か見たとおり、Vitest は型を検査しません。Vitest は型の注釈を取り除いてから実行するだけなので、型が合わないコードもテストとしては実行されます。第 3 章では、コンストラクターに引数を追加し忘れたコードが、Vitest では「予測が違う」という失敗として現れ、tsc では「引数の数が違う」というエラーとして現れました。
test/chapter03/decision-tree.test.ts(94,36): error TS2554: Expected 0 arguments, but got 1.
そのため、テスト(npm test)と型チェック(npm run typecheck)を別々に実行し、第 6 章で npm run check にまとめています。
型定義を同梱しないパッケージ¶
ml.js のパッケージのうち、ml-cart・ml-logistic-regression・ml-cross-validation・ml-regression-lasso は型定義を同梱していません(ADR 003)。第 3 章で書いた型宣言 src/types/ml-cart.d.ts を一時的に外して tsc を実行すると、次のエラーになりました(パスの前半は省略しています)。
src/chapter03/ml-cart-adapter.ts(1,40): error TS7016: Could not find a declaration file for module 'ml-cart'. '.../apps/node/node_modules/ml-cart/cart.js' implicitly has an 'any' type.
Try `npm i --save-dev @types/ml-cart` if it exists or add a new declaration (.d.ts) file containing `declare module 'ml-cart';`
src/chapter03/ml-cart-adapter.ts(31,45): error TS7006: Parameter 'index' implicitly has an 'any' type.
test/chapter03/ml-cart-adapter.test.ts(1,40): error TS7016: Could not find a declaration file for module 'ml-cart'. '.../apps/node/node_modules/ml-cart/cart.js' implicitly has an 'any' type.
Try `npm i --save-dev @types/ml-cart` if it exists or add a new declaration (.d.ts) file containing `declare module 'ml-cart';`
エラーメッセージは 2 つの方法を示しています。
@types/ml-cartを入れる — コミュニティが型定義を公開していれば、それを使います。npm view @types/ml-cartを実行すると404で、公開されていませんでした- 型宣言のファイルを書く —
declare module 'ml-cart';とだけ書けばエラーは消えますが、ml-cart のすべてがanyになり、型チェックが効かなくなります
本リポジトリでは、使う範囲だけを宣言したファイルを書いています。
// ml-cart は型定義を同梱しないので、本リポジトリで使う範囲だけを宣言する
declare module "ml-cart" {
export interface DecisionTreeClassifierOptions {
/** 分割の基準。ml-cart 2.1.1 は "gini" だけ */
gainFunction?: "gini";
/** 境界の決め方。ml-cart 2.1.1 は隣り合う値の平均("mean")だけ */
splitFunction?: "mean";
/** 件数がこの値以下になったら分割せずに葉にする(既定値 3) */
minNumSamples?: number;
maxDepth?: number;
/** 利得がこの値以下なら分割しない(既定値 0.01) */
gainThreshold?: number;
}
export class DecisionTreeClassifier {
constructor(options?: DecisionTreeClassifierOptions);
/** 正解ラベルは 0 始まりの整数 */
train(trainingSet: number[][], trainingLabels: number[]): void;
predict(toPredict: number[][]): number[];
}
}
自分で書いた型宣言は、ライブラリの実際の振る舞いと合っている保証がありません。型が「正しい」と言っていても、実行時には違うことがありえます。第 3 章で、ml-cart のアダプターを学習用テストで確かめたのはそのためです。型宣言のコメントに書いた既定値や制約も、そのテストで確かめた事実です。
5.5 静的解析 — ESLint¶
ESLint の設定¶
コードの問題の検査には、ESLint と、ESLint で TypeScript を扱うための typescript-eslint を使います。設定は apps/node/eslint.config.mjs です。ESLint 9 以降の形式(flat config)で、設定を配列として並べます。
import { defineConfig } from "eslint/config";
import tseslint from "typescript-eslint";
import eslintConfigPrettier from "eslint-config-prettier";
export default defineConfig(
{ ignores: ["node_modules/", "coverage/", "dist/"] },
tseslint.configs.recommended,
{
rules: {
// 分割代入で一部のプロパティを取り除き、残りを使う書き方を許す
"@typescript-eslint/no-unused-vars": [
"error",
{ ignoreRestSiblings: true },
],
},
},
eslintConfigPrettier,
);
| 要素 | 役割 |
|---|---|
ignores |
検査しないディレクトリ |
tseslint.configs.recommended |
typescript-eslint の推奨ルール |
rules |
推奨ルールの一部を本リポジトリに合わせて変える |
eslintConfigPrettier |
書式に関するルールを無効にし、書式を Prettier に任せる。最後に置いて、それまでの設定を上書きする |
ESLint の実行¶
検査の効果を確かめるため、問題を 2 つ含むファイルを書いて検査しました。
export function mean(values: any[]): number {
const count = values.length;
let total = 0;
for (const value of values) total += value;
return total / values.length;
}
export function withoutLabel(row: { label: string; x: number }) {
const { label, ...rest } = row;
return rest;
}
npx eslint src/lint-demo.ts
先頭のファイルのパスの行は省略しています。
1:30 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
2:9 error 'count' is assigned a value but never used @typescript-eslint/no-unused-vars
✖ 2 problems (2 errors, 0 warnings)
anyは型チェックを無効にするので、no-explicit-anyが禁止します。型の分からない値にはunknownを使い、確かめてから使います- 使っていない変数
countは、消し忘れか、使い忘れのどちらかです
一方、withoutLabel の label は指摘されていません。label も使っていない変数ですが、分割代入で label を取り除き、残り(rest)を使うための書き方です。ignoreRestSiblings: true は、この書き方の変数を「使っていない」と見なさないようにする設定です。第 2 章でこの設定を加えるまでは、_target のような名前でも指摘されていました。
型チェックとの役割分担¶
tsc と ESLint は、どちらもコードを実行せずに問題を見つけますが、見るものが違います。
| 道具 | 見つけるもの | 例 |
|---|---|---|
tsc |
型が合わないこと | number \| undefined を number として返す、引数の数が違う |
| ESLint | 型は合っているが、望ましくない書き方 | any の使用、使っていない変数 |
Kotlin 版の ktlint と detekt の関係に近く、TypeScript 版ではコンパイラが型を、ESLint が書き方を受け持ちます。
5.6 整形 — Prettier¶
Prettier の設定¶
コードの書式は Prettier に任せます。設定ファイル apps/node/.prettierrc は {} で、既定の設定(1 行 80 文字、ダブルクォート、文末のセミコロンあり、改行コード LF など)をそのまま使います。整形しないファイルは apps/node/.prettierignore に書きます。
node_modules/
coverage/
dist/
package-lock.json
package-lock.json は npm が書き出すファイルなので、Prettier で書式を変えないようにしています。
Prettier の実行¶
セミコロンを省き、シングルクォートを使ったファイルを検査すると、書式の違いを報告します。
export function mean(values: number[]): number {
let total = 0
for (const value of values) total += value
return total / values.length
}
export const FEATURES = ['がく片長さ', 'がく片幅', '花弁長さ', '花弁幅'] as const;
npx prettier --check src/lint-demo.ts
Checking formatting...
[warn] src/lint-demo.ts
[warn] Code style issues found in the above file. Run Prettier with --write to fix.
npx prettier --write で整形すると、次のようになります。
export function mean(values: number[]): number {
let total = 0;
for (const value of values) total += value;
return total / values.length;
}
export const FEATURES = [
"がく片長さ",
"がく片幅",
"花弁長さ",
"花弁幅",
] as const;
FEATURES の行は、日本語の文字を 2 文字分の幅として数えるので 80 文字を超え、1 行に 1 つずつに折り返されました。第 2 章の FEATURES が縦に並んでいるのはこのためです。
Prettier は書式だけを扱い、ESLint のような「使っていない変数」の検査はしません。逆に ESLint の書式のルールは eslint-config-prettier で無効にしているので、2 つの道具が同じ箇所で違う指示を出すことはありません。
5.7 コードカバレッジ — @vitest/coverage-v8¶
テストがプロダクションコードのどこを実行したかを、@vitest/coverage-v8 で計測します。V8(Node.js の JavaScript エンジン)が実行中に記録する情報を使うので、コードに計測用の処理を埋め込む必要がありません。設定は apps/node/vitest.config.ts です。
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
include: ["test/**/*.test.ts"],
coverage: {
provider: "v8",
include: ["src/**/*.ts"],
reporter: ["text-summary"],
},
},
});
coverage.include で、計測の対象を src のコードに限っています。
npm run test:coverage
学習データを配置した環境での結果です。
=============================== Coverage summary ===============================
Statements : 96.92% ( 189/195 )
Branches : 87.5% ( 63/72 )
Functions : 100% ( 71/71 )
Lines : 96.66% ( 174/180 )
================================================================================
学習データが無い環境での結果です。
ML_DATA_DIR=/nonexistent npm run test:coverage
=============================== Coverage summary ===============================
Statements : 82.05% ( 160/195 )
Branches : 79.16% ( 57/72 )
Functions : 91.54% ( 65/71 )
Lines : 80.55% ( 145/180 )
================================================================================
データが無いと、実データのテストがスキップされ、各章の main 関数などが実行されないので、カバレッジが下がります。Python 版・Kotlin 版と同じく、カバレッジの数値を比べるときは、データのある環境で計測したものかを確認してください。
ファイルごとに、実行されなかった行を知りたいときは、レポーターを text に変えます。データのある環境での結果です。
npx vitest run --coverage --coverage.reporter=text
-------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------------|---------|----------|---------|---------|-------------------
All files | 96.92 | 87.5 | 100 | 96.66 |
src/chapter01 | 93.33 | 75 | 100 | 93.33 |
...o-takenoko.ts | 95.65 | 77.77 | 100 | 95.65 | 21
main.ts | 85.71 | 66.66 | 100 | 85.71 | 22
src/chapter02 | 98.41 | 90.9 | 100 | 98.3 |
main.ts | 93.33 | 66.66 | 100 | 93.33 | 43
src/chapter03 | 97 | 89.13 | 100 | 96.62 |
decision-tree.ts | 97.1 | 94.28 | 100 | 96.72 | 112-113
main.ts | 94.73 | 71.42 | 100 | 94.73 | 40
...rt-adapter.ts | 100 | 75 | 100 | 100 | 16
-------------------|---------|----------|---------|---------|-------------------
実行されなかった行を見ると、テストが足りないのではなく、実行されないことに意味がある行だと分かります。
| 行 | 内容 | 実行されない理由 |
|---|---|---|
kinoko-takenoko.ts の 21 行目 |
列が見つからないときの throw |
実データにも単体テストのデータにも、列の欠けた CSV が無い |
各章の main.ts の最後 |
if (import.meta.filename === process.argv[1]) の中の main() |
テストは main を直接呼ぶので、node src/chapter01/main.ts で実行したときだけ通る |
decision-tree.ts の 112〜113 行目 |
switch の default の never |
第 3 章で見たとおり、型チェックで到達しないことを保証した行 |
本リポジトリでは、カバレッジの下限(coverage.thresholds)を設けていません。CI には学習データを置けないので、CI で計測したカバレッジは手元より必ず低くなり、下限を決めても手元と CI で意味が変わってしまうためです。レポートは coverage/ に書き出され、第 4 章のとおり .gitignore で除外しています。
5.8 依存関係の脆弱性 — npm audit¶
依存ライブラリに既知の脆弱性が無いかは、npm audit で確かめます。npm のレジストリに登録された脆弱性の情報と、package-lock.json のすべてのパッケージの版を突き合わせます。
npm audit
found 0 vulnerabilities
執筆時点では、推移的な依存を含めて既知の脆弱性はありませんでした。npm install と npm ci も、終わりに同じ検査の結果(found 0 vulnerabilities)を表示します。脆弱性が見つかったら、npm audit の表示する版に上げ、テストが通ることを確かめてからコミットします。
ml.js の多くのパッケージは 2022〜2023 年から更新されていません(ADR 003)。更新の止まったパッケージに脆弱性が見つかると、修正版が出ない可能性があります。npm audit を定期的に実行し、必要なら代わりのパッケージや自作への切り替えを検討します。
5.9 まとめ¶
この章では、再現できる依存関係と、実行せずに問題を見つける仕組みを整えました。
- npm —
dependenciesとdevDependenciesで使いみちを分け、.npmrcのsave-exactで正確な版を書く。推移的な依存は範囲で指定されるので、package-lock.jsonをコミットし、npm ciで入れる - Node.js の版 —
.nvmrc・engines・Nix の環境で指定し、engine-strictで合わない版でのインストールを止める。enginesの下限は、依存関係のenginesをすべて満たす版にする - 型チェック —
tscのstrictとnoUncheckedIndexedAccessでundefinedを確かめさせる。Vitest は型を検査しない。型定義の無いパッケージには、使う範囲だけの型宣言を書き、学習用テストで裏付ける - ESLint と Prettier — ESLint が
anyや使っていない変数などの書き方を、Prettier が書式を受け持つ。eslint-config-prettierで両者がぶつからないようにする - カバレッジと脆弱性 — @vitest/coverage-v8 で計測し、データが無い環境では数値が下がることに注意する。
npm auditで依存ライブラリの既知の脆弱性を確かめる
次の章では、これらを npm scripts としてまとめ、GitHub Actions で自動実行します。