AIに既存のアプリを改修してもらうとき、「今回の変更では、ここを触らないでほしい」という場所があります。
たとえば、投稿フォームや共通ナビゲーション。変更できる範囲を文章で伝えたうえで、指定したファイルが変わっていたら、ビルド前に止めて確認する仕組みも置いてみます。
必要なのは、ファイルのSHA-256を記録する処理と、保存した値に照合する処理です。Node.jsの標準機能だけで作れます。ただし、エラーが出るたびに基準を作り直すと、意図しない変更まで正解になってしまいます。普段の検査では基準を更新しないことが、このTipsの要点です。
やることは三つです。
- 正しい状態のファイルから、比較用のハッシュを記録する。
- ビルド前に現在のファイルと比較する。
- 変更や削除があれば、対象のパスを出して処理を止める。
自動で確認するのは、あくまで指定ファイルの内容が基準と一致するかです。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 を表示する仮の処理です。アプリのコンパイル成功を意味するものではありません。
これは記事用コードの動作確認です。作業時間の削減率や、運用事故が何件減ったかは計測していません。
基準を更新してよいのは、仕様として変更するとき
固定した投稿フォームにも、いつか変更が必要になるかもしれません。その場合まで禁止する意図はありません。
大事なのは、エラーを消すための再記録と、仕様として認めた変更を分けることです。
- 固定対象の変更内容と理由を確認する。
- 動作確認を行い、新しい状態を基準にしてよいと判断する。
- 基準JSONを明示的に作り直す。
- 対象ファイルと基準JSONの差分を、一緒にレビューする。
特に、AIへ「テストが通るように直して」と依頼する場合は注意します。検査側の基準まで書き換えれば、元の制約を守らないまま成功にできます。通常の改修では、基準JSON、検査スクリプト、ビルドへの組み込みを変更対象から外す方針も伝えます。
小さなチェックだからこそ、限界を決めて使う
この方式は、同じリポジトリ内の比較用データを信頼する仕組みです。編集者が基準JSONや検査コードまで自由に変更できる環境では、強制力のある権限制御にはなりません。
また、固定対象の内容が変わっていなくても、依存先、親コンポーネント、グローバルCSS、設定の変更で動作や見た目が変わることはあります。ハッシュが一致しただけで「画面も安全」とは判断しません。
ファイルはバイト単位で比較するため、改行コードや整形だけの変更でも止まります。チームで使う場合は、改行・フォーマットの設定をそろえ、頻繁に整形し直すファイルを対象に含めるか検討します。
ハッシュを使うこと自体が、開発の品質を保証するわけではありません。基準JSONを更新するレビューと、画面や動作のテストを組み合わせて使います。
検査の呼び出しを外したり、基準と対象を同時に更新したりすると、何が起きるのでしょうか。関連記事「テストが通った」の中身をレビューする――AI改修を想定した6つの差分で検証したでは、6つの差分を使って、コマンドの成功だけでは判断できない点を確かめています。
AIに任せる範囲が広くなるほど、確認事項も増えます。そのうち「このファイルは前と同じ」という一つの条件を、実行できるチェックへ移せます。
最初から全部を守ろうとせず、次の改修で触ってほしくないファイルを一つ選び、変えたら止まるところまで確かめる。 小さく始めるなら、それで十分です。
記事のきっかけになったAfter 6 Chaosは、訪問者の要望をもとにAIがホームページを改修する実験サイトです。気になったら、投稿フォームからひと言送って試してみてください。この記事で確かめたのは、その発想を切り出した再現例です。
