1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AI改修の「ここは変えないで」をビルド前に確かめる:Node.jsだけのファイル固定チェック

1
Last updated at Posted at 2026-10-30

AIに既存のアプリを改修してもらうとき、「今回の変更では、ここを触らないでほしい」という場所があります。

たとえば、投稿フォームや共通ナビゲーション。変更できる範囲を文章で伝えたうえで、指定したファイルが変わっていたら、ビルド前に止めて確認する仕組みも置いてみます。

必要なのは、ファイルのSHA-256を記録する処理と、保存した値に照合する処理です。Node.jsの標準機能だけで作れます。ただし、エラーが出るたびに基準を作り直すと、意図しない変更まで正解になってしまいます。普段の検査では基準を更新しないことが、このTipsの要点です。

やることは三つです。

  1. 正しい状態のファイルから、比較用のハッシュを記録する。
  2. ビルド前に現在のファイルと比較する。
  3. 変更や削除があれば、対象のパスを出して処理を止める。

自動で確認するのは、あくまで指定ファイルの内容が基準と一致するかです。UIの動作確認や、悪意ある変更へのアクセス制御は別に必要です。

改修してよい本文と、維持したい投稿フォーム

発想のきっかけは、個人開発のAfter 6 Chaosです。訪問者の要望をもとにAIがホームページを改修する実験で、「自由に変えてよい部分」と「次の要望を送るために維持したい部分」を分けたいと考えました。

本文の構成や機能は変えてよくても、投稿フォームや改修履歴まで毎回作り直したいわけではありません。本文が面白くなっても、次の投稿を送れなくなったら実験が続かないからです。

この記事では、その考え方のうち「ファイルの内容が変わっていないか」という確認を、記事用に作成した再現例で試します。本番ソースの転載や、公開サイトの現行実装を説明するものではありません。新しい作業用ディレクトリで、本文のコードだけから再現できます。

確認したい一つの条件を、コマンドへ移す

固定対象の現在のファイルと保存済みのハッシュを比較し、一致した場合はビルドへ進み、変更・削除・設定不備がある場合は停止して確認する。

図1:比較の基準は先に記録する。普段の検査では基準を書き換えない。

このチェックは、「固定対象のファイルが変わっていないか」を毎回目で探す代わりに、コマンドで確認するためのものです。

確認したいこと 確認手段
指定ファイルの内容が基準と一致するか 今回のハッシュチェック
投稿フォームが実際に送信できるか 単体・結合テスト、画面での確認
別のCSSによって見た目が崩れていないか スタイルの変更範囲の確認、画面テスト
その変更を採用してよいか 仕様や目的に照らしたレビュー

対象を絞るのがポイントです。リポジトリ全体を固定すると、普通の機能追加まで止まります。「今回の改修で変えてほしくないもの」だけを指定します。

Gitや既存コマンドでは足りないのか

Gitでも、基準コミットと対象パスを指定すれば同じ目的を満たせます。ここでは、維持したいパスと基準をJSONに記録し、普段のビルドから確認する形にします。

ハッシュの照合だけなら、GNU coreutilsの sha256sum -c や、macOS標準の shasum -a 256 -c でも行えます。Node.js版を作るのは、Node.js/npmがあるプロジェクトでOSごとの外部コマンドへの依存を増やさず、設定の検証、複数の問題の一覧表示、prebuild への接続をまとめるためです。

1. 基準を記録する

Node.js 22以上とnpmを使用します。追加パッケージは不要です。以下はNode.js v24.22.1、npm 11.9.0、Linuxで動作確認しています。macOSおよびWindowsでの実行は未検証です。

まず、新しい作業用ディレクトリに src と scripts を作ります。説明用の固定ファイルは src/stable-card.html とします。

<!doctype html>
<html lang="ja">
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>固定対象のUIサンプル</title>
  <style>
    body { margin: 3rem; background: #f4f6fb; color: #192232; font: 1rem/1.8 sans-serif; }
    article { max-width: 32rem; padding: 2rem; border-radius: 1rem; background: white; }
    h1 { margin-top: 0; }
    a { color: #1959b3; }
  </style>
  <article>
    <h1>このUIはいったん完成</h1>
    <p>固定対象として、ファイルの内容が変わっていないことを確認します。</p>
    <a href="#details">詳細を見る</a>
    <p id="details">記事用のサンプルです。実運用のコードではありません。</p>
  </article>
</html>

次に、以下を scripts/record-fixed-tools.mjs として保存します。ファイルパスはプロジェクトルートからの相対パスで渡します。

import { createHash } from 'node:crypto';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';

const root = fileURLToPath(new URL('../', import.meta.url));
const validPath = (path) => typeof path === 'string'
  && !/[:\\\0]/.test(path)
  && path.split('/').every((part) => part && part !== '.' && part !== '..');

async function main() {
  const paths = process.argv.slice(2).sort();
  if (!paths.length || !paths.every(validPath) || new Set(paths).size !== paths.length) {
    throw new Error('specify one or more unique repository-relative file paths');
  }
  const files = [];
  for (const path of paths) {
    const bytes = await readFile(join(root, path));
    files.push({ path, sha256: createHash('sha256').update(bytes).digest('hex') });
  }
  await mkdir(join(root, 'data'), { recursive: true });
  await writeFile(join(root, 'data/fixed-tools.json'),
    JSON.stringify({ version: 1, files }, null, 2) + '\n', { flag: 'wx' });
  console.log(`Recorded ${files.length} fixed file(s)`);
}

main().catch((error) => {
  console.error(error.code === 'EEXIST'
    ? 'Baseline already exists; review changes before intentionally replacing it.'
    : `RECORD ERROR: ${error.message}`);
  process.exitCode = 1;
});

プロジェクトルートで実行します。

node scripts/record-fixed-tools.mjs src/stable-card.html

data/fixed-tools.json に、対象パスとハッシュが保存されます。対象が複数なら、引数にファイルパスを追加します。

この処理では、wx フラグを指定して新規作成しています。既存の基準ファイルがある場合は上書きせず、失敗させます。 記録コマンドをうっかり再実行して、変更後の状態を正解にしてしまうのを避けるためです。

生成したJSONはソースと一緒にGitへコミットします。ビルドのたびに生成するファイルにはしません。

2. 現在のファイルと照合する

以下を scripts/check-fixed-tools.mjs として保存します。

import { createHash } from 'node:crypto';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';

const root = fileURLToPath(new URL('../', import.meta.url));
const manifestPath = join(root, 'data/fixed-tools.json');
const validPath = (path) => typeof path === 'string'
  && !/[:\\\0]/.test(path)
  && path.split('/').every((part) => part && part !== '.' && part !== '..');

async function main() {
  const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
  if (manifest?.version !== 1 || !Array.isArray(manifest.files) || !manifest.files.length) {
    throw new Error('version must be 1 and files must be a nonempty array');
  }
  const seen = new Set();
  for (const entry of manifest.files) {
    if (!entry || !validPath(entry.path) || typeof entry.sha256 !== 'string'
      || !/^[a-f0-9]{64}$/.test(entry.sha256)
      || seen.has(entry.path)) {
      throw new Error('each file needs a unique relative path and a SHA-256 hash');
    }
    seen.add(entry.path);
  }

  const failures = [];
  for (const { path, sha256 } of manifest.files) {
    try {
      const bytes = await readFile(join(root, path));
      const actual = createHash('sha256').update(bytes).digest('hex');
      if (actual !== sha256) failures.push(`CHANGED: ${path}`);
    } catch (error) {
      failures.push(`${error.code === 'ENOENT' ? 'MISSING' : 'UNREADABLE'}: ${path}`);
    }
  }
  if (failures.length) {
    console.error(failures.join('\n'));
    process.exitCode = 1;
  } else {
    console.log(`OK: ${manifest.files.length} fixed file(s) unchanged`);
  }
}

main().catch((error) => {
  console.error(`CONFIG ERROR: ${error.message}`);
  process.exitCode = 1;
});

検査を実行します。

node scripts/check-fixed-tools.mjs

ハッシュの計算には、Node.js標準の node:crypto を使っています。追加パッケージのインストールは不要です。

処理の中心は、ファイルのバイト列を読み、SHA-256を計算して、保存してある値と比較する部分です。この例では対象を順番に読み込み、一度の検査で変更や削除があったパスをまとめて表示します。

基準ファイルの欠落や不正なJSON、空の対象一覧もエラーにしています。設定が壊れた結果、何も検査せず成功する状態を避けます。

3. 普段使うビルドコマンドにつなぐ

単独で検査できても、毎回実行を思い出す必要があると使い忘れます。そこで、npmの prebuild に接続します。

以下は、動作確認用の package.json です。既存プロジェクトでは scripts に検査を追加し、元のビルド処理は維持してください。

すでに prebuild がある場合は、その処理を残して検査を統合します。

{
  "name": "fixed-tools-drift-demo",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "engines": {
    "node": ">=22"
  },
  "scripts": {
    "record:fixed": "node scripts/record-fixed-tools.mjs",
    "check:fixed": "node scripts/check-fixed-tools.mjs",
    "prebuild": "npm run check:fixed",
    "build": "node scripts/demo-build.mjs"
  }
}

この例の build は、検査を通過したことを表示するだけの仮の処理です。アプリのビルドは行いません。既存アプリで使う場合は、もともとの vite build などがそこに入ります。

再現例をそのまま動かす場合は、scripts/demo-build.mjs も作成します。

// Stand-in only: replace this script with the application's real build command.
console.log('DEMO_BUILD_EXECUTED: placeholder only; no application was compiled.');
npm run build

通常の npm run build では、prebuild、build の順に実行されます。前段の検査が失敗すると、後段のビルドへ進みません。

ただし、ビルドツールを直接起動した場合や、npm --ignore-scripts run build のように前後のスクリプトを無効化した場合は、prebuild を通りません。CIで確実に検査したいなら、検査コマンドを明示したステップを置き、成功した場合だけビルドを実行する構成にします。

4. わざと変えて、止まることを確かめる

ここまで用意したら、一度は固定対象を変更して確かめます。

src/stable-card.html の文字列を変更し、もう一度 npm run build を実行します。

CHANGED: src/stable-card.html

変更したパスが表示され、終了コードは 1 になります。後段のビルドメッセージは出ません。ファイルを元の内容へ戻せば、再び通るようになります。

一方、固定対象に含めていないファイルだけを変更しても、このチェックは通ります。自由に改修してよい部分はそのまま編集できます。

2026年10月31日(日本時間)に、本文のコードブロックを新しい一時ディレクトリへコピーし、Linux上で次の操作を確認しました。変更や削除の前に元のファイルをコピーしておき、各操作後に同じ内容へ戻して検査します。

操作 npm run build の終了コード 後段のデモ処理
記録した状態のまま実行 0 実行された
固定対象の内容を変更 1(CHANGED) 実行されなかった
内容を元に戻す 0 実行された
固定対象を削除 1(MISSING) 実行されなかった
元のファイルを復元 0 実行された

また、基準がある状態で記録コマンドを再実行すると、終了コード 1 で上書きを拒否することも確認しました。これらの確認は、本文に掲載したファイルとコマンドだけで再現できます。

ここでいうデモ処理は DEMO_BUILD_EXECUTED を表示する仮の処理です。アプリのコンパイル成功を意味するものではありません。

これは記事用コードの動作確認です。作業時間の削減率や、運用事故が何件減ったかは計測していません。

基準を更新してよいのは、仕様として変更するとき

固定した投稿フォームにも、いつか変更が必要になるかもしれません。その場合まで禁止する意図はありません。

大事なのは、エラーを消すための再記録と、仕様として認めた変更を分けることです。

  1. 固定対象の変更内容と理由を確認する。
  2. 動作確認を行い、新しい状態を基準にしてよいと判断する。
  3. 基準JSONを明示的に作り直す。
  4. 対象ファイルと基準JSONの差分を、一緒にレビューする。

特に、AIへ「テストが通るように直して」と依頼する場合は注意します。検査側の基準まで書き換えれば、元の制約を守らないまま成功にできます。通常の改修では、基準JSON、検査スクリプト、ビルドへの組み込みを変更対象から外す方針も伝えます。

小さなチェックだからこそ、限界を決めて使う

この方式は、同じリポジトリ内の比較用データを信頼する仕組みです。編集者が基準JSONや検査コードまで自由に変更できる環境では、強制力のある権限制御にはなりません。

また、固定対象の内容が変わっていなくても、依存先、親コンポーネント、グローバルCSS、設定の変更で動作や見た目が変わることはあります。ハッシュが一致しただけで「画面も安全」とは判断しません。

ファイルはバイト単位で比較するため、改行コードや整形だけの変更でも止まります。チームで使う場合は、改行・フォーマットの設定をそろえ、頻繁に整形し直すファイルを対象に含めるか検討します。

ハッシュを使うこと自体が、開発の品質を保証するわけではありません。基準JSONを更新するレビューと、画面や動作のテストを組み合わせて使います。

検査の呼び出しを外したり、基準と対象を同時に更新したりすると、何が起きるのでしょうか。関連記事「テストが通った」の中身をレビューする――AI改修を想定した6つの差分で検証したでは、6つの差分を使って、コマンドの成功だけでは判断できない点を確かめています。

AIに任せる範囲が広くなるほど、確認事項も増えます。そのうち「このファイルは前と同じ」という一つの条件を、実行できるチェックへ移せます。

最初から全部を守ろうとせず、次の改修で触ってほしくないファイルを一つ選び、変えたら止まるところまで確かめる。 小さく始めるなら、それで十分です。

記事のきっかけになったAfter 6 Chaosは、訪問者の要望をもとにAIがホームページを改修する実験サイトです。気になったら、投稿フォームからひと言送って試してみてください。この記事で確かめたのは、その発想を切り出した再現例です。

参考資料

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?