実装学習記事全11節Build
入力チェック・保存・状態更新をつなぎ、保存前後を照合する
この記事で身につくこと入力チェック、データ変換、保存、読出し、更新の各段階と、CSV・JSON・データベースへの保存の違いを説明できる。
記事内目次(22項目)
この記事の会社・人物・データはすべて架空の設定です。実在する会社や案件とは関係ありません。本文のコードは
examples/08-02-storage-check/の正本と同じもので、実行結果は2026-09-20にローカル環境で実際に実行して記録したものです(第6〜7節、実行記録は第9節)。教材演習用のJSONファイル1つだけを使い、実際のデータベースや外部サービスには接続していません。第5.3節の指示文と第6.1節の初稿は説明用の例で、実際のAI出力を観測したものではありません。
この記事は何の話か。依頼の作成(必須項目チェック)と保存、状態の更新を小さい単位で実装し、保存前後の内容を照合する話です。保存した依頼に対して、08-01で作った承認可否の判定関数を呼び、前工程の成果物が実際につながることも確かめます。
なぜ必要か。入力を確認せずに保存すると、必須項目が欠けたデータが残ってしまいます。状態を変更しても履歴が残らなければ、REQ-007(状態変更と履歴追加の一体性)を満たせません。また、存在しない依頼や、提出できない状態の依頼を「提出」してしまう処理は、05-02の状態遷移表に反します。AIに保存処理を頼むと、これらの確認が抜けた「動くコード」が返ってくることがあります。
この記事を読むとできるようになること。
- 入力チェック、データ変換、保存、読出し、更新の各段階と、保存形式の違いを説明できる
- 依頼の作成・保存・状態更新を実装し、対象の存在と状態を確かめてから保存し、保存前後の内容を照合できる
- 状態変更と履歴追加を一まとまりで扱う方法を保存先の種類ごとに比較し、1つのファイルへの1回の書き込みで保証できることとできないことを言える
読む順路。この記事は3つの深さで読めます。全部を順に読む必要はありません。
- 基本だけ理解する:第1節→第3節(保存形式の違い、保存と判定の分担)→第4節(判断すること)→第5.2節の「役割分担」の表→第6節の照合表→第9節。コードを読まなくても、「保存する部品」と「保存してよいかを判断する部品」を分ける理由が分かります。
- 作って確かめる:上に加えて第5.2節(保存処理・ガード・実行コード)→第5.3節(AIに頼む:渡す情報と指示文)→第6節(初稿の不備、修正後の実行結果)→第7節。
- 設計判断を深める:上に加えて第8節(保存先の種類ごとの比較と、1回の書き込みで保証できる範囲)。
1. 今回の業務課題と完成するもの
この記事でやることは、08-01の最小の関数に、実際に保存する処理をつなげ、依頼の作成(必須項目チェック含む)・読出し・状態の更新を実装し、保存前後の内容を照合することです。
08-01では承認可否の判定だけを扱いましたが、判定に使う「状態」「割当承認者か」「本人か」は、実際には保存された依頼から読み出す値です。この記事では、依頼を保存し、保存した依頼から読み出した値を08-01の関数に渡すところまでをつなぎます。
この記事を読み終えると、次のものが手元にできます。
- 保存処理・提出前のガード・実行コード(
examples/08-02-storage-check/) - 処理コード・保存確認記録(処理、入力、期待される保存内容、実際の保存内容、照合結果、未検証)
2. 前提知識と読む順路
この記事で使う言葉を先に説明します。
入力チェック
一言でいうと、保存する前に、必須項目がそろっているかなどを確認することです。
データ変換
一言でいうと、入力された値を、保存に適した形に変えることです。
保存・読出し・更新
一言でいうと、データを書き込む(保存)、取り出す(読出し)、書き換える(更新)という3つの操作です。
これがないと、必須項目が欠けたデータがそのまま残ったり、保存した内容を後から正しく取り出せなかったりします。
ガード
一言でいうと、処理を実行する前に「この対象に、この操作をしてよいか」を確かめて、だめなら止める部品です。
たとえば、「提出」の前に、その依頼が存在するか、状態が「下書き」か「差戻し」かを確かめるのがガードです。
これがないと、存在しない依頼を提出しようとして処理が途中で止まったり、提出済みの依頼をもう一度提出して履歴が二重に残ったりします。
システム開発では、この「保存する前に確かめる」判断を、保存先ではなく処理側に置くのが基本です(00-01の「判定をどこで行うか」)。08-03のサーバーも同じ形です。
読む順路:最小の処理は08-01、データ項目表は05-01、状態遷移表は05-02が該当します。この記事の処理は、08-03(画面・処理・外部機能をつなぐ)、09-01・記事B(テスト)で使います。
3. 理解する:保存形式の違いと、保存と判定の分担
3.1 CSV・JSON・データベースへの保存の違い(00-02の続き)
00-02でCSVとJSONの違いを見ましたが、この記事の教材演習ではJSON形式のファイル1つに、依頼(requests)と履歴(history)をまとめて保存します。実際の企業でデータベース(03-03で選ぶ技術)を使う場合も、「状態変更と履歴追加を一まとまりで扱う」という考え方は同じです。ただし、ファイル1つへの書き込みとデータベースのトランザクションでは、保証できる範囲が違います(第8.1節)。
3.2 保存する部品と、保存してよいかを判断する部品を分ける
結論から言うと、この記事のコードは「保存する」(storage.js)と「保存してよいかを確かめる」(submit_guard.js)を別の部品にしています。
みらい業務サービスの例で言うと、佐藤さんの依頼を「提出」するとき、確かめることは2つあります。その依頼が本当に存在するか。状態が「下書き」か「差戻し」か(05-02の状態遷移表で、提出できるのはこの2つの状態からだけ)。これを保存処理の中に混ぜると、保存処理が業務ルールを抱え込み、08-03のサーバーがもう一度同じ判断をするときに二重になります。この記事では、storage.js は保存に専念し、判断はガードが行います。
| 部品 | 役割 | 確かめること | 確かめないこと |
|---|---|---|---|
storage.js | 保存・読出し・更新 | 作成時の必須項目(件名・内容・承認者) | 提出時の対象の存在、状態 |
submit_guard.js | 提出してよいかの判断 | 対象の存在、状態が「下書き」「差戻し」か | — |
approval.js(08-01のコピー) | 承認してよいかの判断 | 状態・割当・本人(入力は保存した依頼から読む) | 保存そのもの |
3.3 よくある誤解
「保存できれば入力チェックは省略してよい」。必須項目が欠けたまま保存すると、後で読み出したときに扱いに困ります(第6節の手順1で確認します)。
「状態を変更したら、履歴はあとで追加すればよい」。状態変更と履歴追加を別々のタイミングで行うと、片方だけが完了する状況(REQ-007が禁止する状況)が起こりやすくなります。この記事では、同じ保存処理の中で1回の書き込みにまとめます。ただし、それで「絶対に片方だけにならない」わけではありません(第8.1節)。
「保存処理が動けば、提出も正しく動く」。storage.js の提出処理は保存するだけで、対象が存在するか、状態が提出できる状態かは確かめません。ガードを通さずに呼ぶと、存在しない依頼で例外になります(第6節の手順7)。
4. 判断すること
保存処理を作るときに人が判断することは、次の5つです。
| 判断すること | なぜ判断が必要か | 仮置きしてよいか | 間違えた場合の影響 |
|---|---|---|---|
| どの条件を保存の前に確かめるか(必須項目、対象の存在、状態) | 確かめる条件は05-01・05-02の設計から来る。AIは「動く」コードは書けるが、どの条件が業務上必須かは渡さないと入らない | 不可。設計にある条件を減らさず、無い条件を足さない | 存在しない依頼の提出で処理が落ちる。提出済みをもう一度提出して履歴が二重になる |
| 確かめる判断を、保存処理の中に置くか、別の部品(ガード)に置くか | 置き場所で、後工程(08-03のサーバー)との役割分担が決まる(第3.2節) | 可(この記事はガードに置く。08-03のサーバーが同じ判断を持つのは、画面を通らない操作を止めるため) | 保存処理が業務ルールを抱え込み、変更のたびに保存処理を直すことになる |
| 状態変更と履歴追加を、どこまで「一まとまり」として保証するか | 1回の書き込みで保証できるのは「呼び出し側が2回に分けるより隙が小さい」までで、途中失敗や同時書き込みには効かない(第8.1節) | 可(教材演習は1回の書き込み。本番相当は記事Bのトランザクション) | 「1回で書くから安全」と信じて、08-04の部分失敗や09-03の同時更新を検討しない |
| 日時と文字コードの形式 | 形式が決まっていないと、10-02の移行や00-02の読み違えが起きる | 可(この記事はISO 8601(toISOString())とUTF-8。正式には05-01) | 日付が文字列として扱われ、並べ替えや比較ができない |
| 実行結果をどこまで載せるか(全文か抜粋か) | 省略した出力を「実測」と書くと、読者は載っていない部分も確認済みと受け取る | 不可。全文を載せるか、「抜粋」と明記する | 記事の出力と実際の出力が食い違い、読者が再現できない |
勝手に決めてはいけないこと:05-01のデータ項目表にない項目を、この記事で追加しません。05-02の状態遷移表にない遷移(例:承認済みからの提出)を、この記事で許可しません。日時の形式は05-01で「教材上の仮定」とした形式(タイムゾーンが分かる形式)を踏襲します。
AIに任せる範囲:保存・読出し・更新のコードの下書き、実行コードの下書き、実行して出力をそのまま返すことはAIに任せられます。任せてはいけないのは、確かめる条件を決めること(第4節1行目)、初稿の出力を設計と照合して不足を見つけること(第6.2節)、「一まとまり」の保証範囲を判断すること(第8.1節)です。
5. 作って確かめる:入力チェック・保存・更新を実装する
5.1 準備
08-01の最小の関数(approval.js。このフォルダにコピーを置いています)、05-01のデータ項目表、05-02の状態遷移表(提出できるのは「下書き」「差戻し」から)を用意します。
5.2 保存処理・ガード・実行コード
保存処理です(examples/08-02-storage-check/storage.js)。保存・読出し・更新だけを担当し、作成時の必須項目だけを確かめます。提出時の対象の存在や状態は確かめません(第3.2節)。
// examples/08-02-storage-check/storage.js
// 教材演習用:架空データをJSONファイル1つに保存する最小の保存処理。
// requestsとhistoryを同じファイル・同じ書き込みでまとめて更新し、
// 「状態変更だけ完了して履歴が残らない」状況(REQ-007)を避ける。
const fs = require("node:fs");
const path = require("node:path");
const DB_FILE = path.join(__dirname, "practice-db.json");
function loadDb() {
if (!fs.existsSync(DB_FILE)) return { requests: [], history: [] };
return JSON.parse(fs.readFileSync(DB_FILE, "utf8"));
}
function saveDb(db) {
fs.writeFileSync(DB_FILE, JSON.stringify(db, null, 2)); // 状態変更と履歴を1回の書き込みでまとめて保存
}
function createRequest({ id, requester, title, content, approver }) {
if (!title || !content || !approver) {
throw new Error("必須項目が不足しています(件名・内容・承認者)");
}
const db = loadDb();
db.requests.push({ id, requester, title, content, state: "下書き", approver });
db.history.push({ requestId: id, action: "作成", actor: requester, at: new Date().toISOString() });
saveDb(db);
return db;
}
function submitRequest(id, actor) {
const db = loadDb();
const req = db.requests.find((r) => r.id === id);
req.state = "提出済み";
db.history.push({ requestId: id, action: "提出", actor, at: new Date().toISOString() });
saveDb(db);
return db;
}
module.exports = { createRequest, submitRequest, loadDb, DB_FILE };
日付・時刻の扱い:new Date().toISOString()により、タイムゾーンが分かる形式(例:2026-09-20T14:31:48.230Z)で記録しています。これは05-01で「教材上の仮定」とした方針に沿ったものです。文字コードは、Node.jsの標準の読み書き(UTF-8)をそのまま使っています。
提出前のガードです(examples/08-02-storage-check/submit_guard.js)。対象の存在と状態を確かめてから、storage.js の提出処理を呼びます。
// 教材演習用:保存処理(storage.js)を呼ぶ前に、対象の存在と状態を確かめる「ガード」。
// storage.js の submitRequest は保存するだけで状態を確かめない(保存先は保存に専念し、判定は呼ぶ側が行う)。
// 提出できるのは 05-02 の状態遷移表のとおり「下書き」「差戻し」のときだけ。
const { loadDb, submitRequest } = require("./storage.js");
const SUBMITTABLE = ["下書き", "差戻し"];
function submitWithGuard(id, actor) {
const req = loadDb().requests.find((r) => r.id === id);
if (!req) {
throw new Error(`対象の依頼が見つかりません(${id})`);
}
if (!SUBMITTABLE.includes(req.state)) {
throw new Error(`状態が「${req.state}」のため提出できません(提出できるのは ${SUBMITTABLE.join("・")} のときだけ)`);
}
return submitRequest(id, actor);
}
module.exports = { submitWithGuard, SUBMITTABLE };
「入力→判断・処理→出力→失敗時」の対応:
| 段階 | 内容 |
|---|---|
| 入力 | 依頼のID、操作者 |
| 判断・処理 | 保存先から依頼を読み出し、存在するか、状態が「下書き」「差戻し」かを確かめる。通れば storage.js の提出処理で状態変更と履歴追加を1回で書く |
| 出力 | 更新後の保存内容 |
| 失敗時 | 存在しない、または状態が合わなければ例外で止め、保存先は変えない |
実行コードです(examples/08-02-storage-check/run.js)。手順1〜7を順に動かし、手順3・5で保存した依頼から読み出した値を08-01の canApprove に渡します。
// 教材演習用:入力チェック・保存・状態の更新を順に動かし、保存前後の内容を照合する。
// 保存は storage.js、提出の可否判断は submit_guard.js、承認の可否判断は approval.js(08-01のコピー)が担当する。
const { createRequest, loadDb, DB_FILE } = require("./storage.js");
const { submitWithGuard } = require("./submit_guard.js");
const { canApprove } = require("./approval.js");
const fs = require("node:fs");
if (fs.existsSync(DB_FILE)) fs.unlinkSync(DB_FILE); // 演習用データを初期化
console.log("=== 1. 必須項目が欠けた入力(件名なし) ===");
try {
createRequest({ id: "REQ-0001", requester: "佐藤", title: "", content: "会議室の予約", approver: "鈴木" });
} catch (e) {
console.log("拒否されました: " + e.message);
}
console.log("保存された依頼の件数:", loadDb().requests.length);
console.log("=== 2. 正常な入力の保存 ===");
createRequest({ id: "REQ-0001", requester: "佐藤", title: "会議室の予約", content: "9/25 10時から", approver: "鈴木" });
const saved = loadDb();
console.log("保存後のrequests:", JSON.stringify(saved.requests));
console.log("保存後のhistory:", JSON.stringify(saved.history));
console.log("=== 3. 保存した依頼に対して、08-01の判定関数を呼ぶ(提出前) ===");
const before = loadDb().requests.find((r) => r.id === "REQ-0001");
console.log(`鈴木が承認できるか: canApprove("${before.state}", 割当=${before.approver === "鈴木"}, 本人=${before.requester === "鈴木"}) => ${canApprove(before.state, before.approver === "鈴木", before.requester === "鈴木")}`);
console.log("=== 4. 状態の更新(提出):ガードを通して保存する ===");
submitWithGuard("REQ-0001", "佐藤");
const after = loadDb();
console.log("更新後のrequests:", JSON.stringify(after.requests));
console.log("更新後のhistory件数:", after.history.length, "最後の履歴:", JSON.stringify(after.history[after.history.length - 1]));
console.log("=== 5. 保存した依頼に対して、08-01の判定関数を呼ぶ(提出後) ===");
const now = loadDb().requests.find((r) => r.id === "REQ-0001");
console.log(`鈴木が承認できるか: canApprove("${now.state}", 割当=${now.approver === "鈴木"}, 本人=${now.requester === "鈴木"}) => ${canApprove(now.state, now.approver === "鈴木", now.requester === "鈴木")}`);
console.log("=== 6. ガードが止める場合:存在しない依頼、提出済みの依頼をもう一度提出 ===");
for (const [id, actor] of [["REQ-9999", "佐藤"], ["REQ-0001", "佐藤"]]) {
try {
submitWithGuard(id, actor);
console.log(`${id}: 提出できてしまった(想定外)`);
} catch (e) {
console.log(`${id}: 拒否されました: ${e.message}`);
}
}
const final = loadDb();
console.log("拒否のあとのrequestsの状態:", final.requests[0].state, "/ history件数:", final.history.length, "(変わっていない)");
console.log("=== 7. 参考:ガードを通さずに storage.js を直接呼ぶと(存在しない依頼) ===");
try {
require("./storage.js").submitRequest("REQ-9999", "佐藤");
console.log("保存できてしまった(想定外)");
} catch (e) {
console.log(`例外で止まります: ${e.constructor.name}: ${e.message}`);
console.log("→ storage.js は対象の存在を確かめない。呼ぶ側(ガード)で確かめる設計にしている");
}
5.3 AIに保存処理を頼む
この記事の保存処理は、AIに書かせる想定です。渡す情報と渡さない情報を先に決めます。
| 渡す情報 | 内容 | 渡さない情報と理由 |
|---|---|---|
| 目的と完成条件 | 依頼の作成(必須項目チェック)・読出し・提出。作成時は「下書き」で保存し履歴「作成」を残す。提出は状態を「提出済み」に変え履歴「提出」を残す | — |
| 設計資料の該当部分 | 05-01のデータ項目(id・requester・title・content・state・approver、履歴は requestId・action・actor・at)、05-02の「下書き→提出済み」「差戻し→提出済み」の行 | 承認・差戻しの遷移(この記事の範囲外。渡すとAIが承認処理まで書く) |
| 保存先と形式 | JSONファイル1つ、requests と history を同じファイルに、日時はISO 8601、UTF-8 | 実データ(架空の入力だけを使う) |
| 前工程の成果物 | 08-01の approval.js(保存した依頼から読み出した値で呼ぶこと) | — |
指示文は次のとおりです。07-03の指示文と同じ構造です。これは説明のために書いた例で、実際にAIへ送信した記録ではありません。
依頼の作成・読出し・提出を、JavaScript(Node.js 標準機能のみ)で実装してください。
渡す資料:
- 05-01 データ項目表(依頼:id, requester, title, content, state, approver/履歴:requestId, action, actor, at)
- 05-02 状態遷移表のうち「下書き → 提出済み(提出)」「差戻し → 提出済み(再提出)」の行
- 08-01 の approval.js(承認可否の判定関数。保存した依頼から読み出した値で呼べること)
完成条件:
- 作成時に件名・内容・承認者のいずれかが空なら保存せず例外にする(REQ-004)
- 作成時は状態「下書き」で保存し、履歴「作成」を同時に残す(REQ-005)
- 提出時は状態を「提出済み」に変え、履歴「提出」を同時に残す。状態変更と履歴追加を別々に書かない(REQ-007)
- 提出は、対象の依頼が存在し、状態が「下書き」または「差戻し」のときだけ行う(05-02)。それ以外は保存せず例外にする
- 日時は toISOString()(タイムゾーンが分かる形式)、文字コードは UTF-8
返し方:
- 保存処理(storage.js)と、手順を順に実行して結果を表示する run.js を返してください。
- 「できました」ではなく、run.js を実行した出力をそのまま貼ってください。省略しないでください。
してはいけないこと:
- 承認・差戻しの処理は書かないでください。
- 状態遷移表に無い遷移を許可しないでください。
- 実データを使わないでください。
6. 初稿・失敗例・修正
6.1 説明用の初稿(AIの初稿という設定)
第5.3節の指示に対して、次のような初稿が返ってきた、という設定にします(実際のAI出力を観測したものではなく、教えるために不備を入れた説明用の例です)。この初稿は、この記事の初版(Phase 5B-2B より前)に載せていたコードと出力そのものです。
- 提出処理が、対象の依頼を探してそのまま
req.state = "提出済み"と書き換えていた。対象が無い場合と、状態が「提出済み」「承認済み」の場合の扱いが無い - 実行結果に
"state": "提出済み", ...のような...が含まれ、省略した出力を「実行結果」として載せていた - 手順が「必須項目の拒否」「保存」「提出」の3つだけで、保存した依頼を08-01の関数に渡す場面が無く、「08-01につなげる」と書きながら実際にはつながっていなかった
- 説明に「1回の書き込みで一まとまりの保存を実現」とあり、途中失敗や同時書き込みへの保証があるかのように読めた
6.2 見つけ方と直し方
見るところは3つです。完成条件の各行が、コードのどこに対応するか指せるか。出力に ... や「以下略」が無いか。「つなげる」と書いた前工程の成果物が、コードの中で実際に呼ばれているか。
| 問題点 | なぜ問題か | どう見つけるか | 誰が判断するか | どう直したか |
|---|---|---|---|---|
| 提出時に対象の存在と状態を確かめない | 存在しない依頼で TypeError になる(第6.3節の手順7)。提出済みをもう一度提出すると履歴「提出」が二重に残る。05-02の遷移に無い操作が通る | 完成条件「存在し、状態が下書きまたは差戻しのときだけ」に対応する行をコードで指す。指せなければ抜け | 実装を確認する人(架空:開発担当) | submit_guard.js を足し、存在と状態を確かめてから storage.js を呼ぶ形にした。storage.js は変えていない |
出力に ... がある | 省略された部分が実際にどうなったかを読者が確認できない。「実測」と書いた出力の一部が実測でない | 出力ブロックの中の ...「略」を探す。あれば全文か「抜粋」明記に直す | 開発担当 | run.js の出力を1行のJSONに変え、全文を載せた(第6.3節) |
| 08-01の関数を呼んでいない | 「前工程の成果物を使う」が文章だけになっている | 記事が「つなげる」と書いた成果物が、require されて呼ばれているかを見る | 開発担当 | 手順3・5で、保存した依頼から読み出した値を canApprove に渡し、提出前は false、提出後は true になることを確かめた |
| 「1回の書き込みで一まとまり」 | writeFileSync 1回は、書き込み途中の失敗や2人の同時書き込みに対して何も保証しない。読者が「トランザクションと同じ」と誤解する | 「一まとまり」「保証」と書いた箇所に、何に対する保証かが添えてあるかを見る | 開発担当と、設計の責任者 | 第8.1節を「保証できること・できないこと」に書き直し、できない部分を08-04・記事Bへ委ねた |
AIへの修正指示は次のとおりです(説明用の例)。
指摘:
1. 提出処理の前に、対象の依頼が存在すること、状態が「下書き」または「差戻し」であることを確かめてください。満たさなければ保存せず例外にしてください。保存処理(storage.js)は変えず、確かめる部品を別ファイルにしてください。
2. 実行結果を省略しないでください。出力を1行のJSONにして全文を貼ってください。
3. 保存した依頼から読み出した値を 08-01 の canApprove に渡す手順を、提出の前後に加えてください。
4. 「1回の書き込みで一まとまり」という説明を、実際に保証できる範囲(2回に分けて書くより片方だけ残る隙が小さい)に直してください。
変えないこと:
- storage.js の保存の形式(1ファイル、requests と history を同時に書く)は変えないでください。
- 05-02 に無い遷移を足さないでください。
6.3 修正後の実行結果
examples/08-02-storage-check/ で実行した stdout と終了コードです(整形・省略なし。stderr への出力はありませんでした。履歴の at は実行のたびに変わります)。
$ node run.js
=== 1. 必須項目が欠けた入力(件名なし) ===
拒否されました: 必須項目が不足しています(件名・内容・承認者)
保存された依頼の件数: 0
=== 2. 正常な入力の保存 ===
保存後のrequests: [{"id":"REQ-0001","requester":"佐藤","title":"会議室の予約","content":"9/25 10時から","state":"下書き","approver":"鈴木"}]
保存後のhistory: [{"requestId":"REQ-0001","action":"作成","actor":"佐藤","at":"2026-09-20T14:31:48.230Z"}]
=== 3. 保存した依頼に対して、08-01の判定関数を呼ぶ(提出前) ===
鈴木が承認できるか: canApprove("下書き", 割当=true, 本人=false) => false
=== 4. 状態の更新(提出):ガードを通して保存する ===
更新後のrequests: [{"id":"REQ-0001","requester":"佐藤","title":"会議室の予約","content":"9/25 10時から","state":"提出済み","approver":"鈴木"}]
更新後のhistory件数: 2 最後の履歴: {"requestId":"REQ-0001","action":"提出","actor":"佐藤","at":"2026-09-20T14:31:48.231Z"}
=== 5. 保存した依頼に対して、08-01の判定関数を呼ぶ(提出後) ===
鈴木が承認できるか: canApprove("提出済み", 割当=true, 本人=false) => true
=== 6. ガードが止める場合:存在しない依頼、提出済みの依頼をもう一度提出 ===
REQ-9999: 拒否されました: 対象の依頼が見つかりません(REQ-9999)
REQ-0001: 拒否されました: 状態が「提出済み」のため提出できません(提出できるのは 下書き・差戻し のときだけ)
拒否のあとのrequestsの状態: 提出済み / history件数: 2 (変わっていない)
=== 7. 参考:ガードを通さずに storage.js を直接呼ぶと(存在しない依頼) ===
例外で止まります: TypeError: Cannot set properties of undefined (setting 'state')
→ storage.js は対象の存在を確かめない。呼ぶ側(ガード)で確かめる設計にしている
終了コード: 0
| 確認項目 | 期待される内容 | 実際の内容 | 照合結果 |
|---|---|---|---|
| 1. 必須項目が欠けた入力 | 拒否され、保存されない | 「拒否されました」と表示され、件数0 | 一致 |
| 2. 正常な保存 | requestsに1件(下書き)、historyに1件(作成) | requests 1件・state 下書き、history 1件・作成 | 一致 |
| 3. 提出前の判定 | 下書きなので承認できない(false) | canApprove("下書き", true, false) => false | 一致 |
| 4. 状態の更新 | 状態が「提出済み」、historyが2件(作成+提出) | state 提出済み、history 2件、最後は「提出」 | 一致 |
| 5. 提出後の判定 | 提出済み・割当・本人でないので承認できる(true) | canApprove("提出済み", true, false) => true | 一致 |
| 6. ガードの拒否 | 存在しない依頼、提出済みの再提出は拒否され、保存先は変わらない | 2件とも拒否。状態 提出済み・history 2件のまま | 一致 |
| 7. ガードなしの直接呼び出し | 存在しない依頼で例外になる(設計上の限界の確認) | TypeError で止まる | 一致(限界を確認) |
7. 確認結果
実行環境:macOS 26.6.2、Node.js v24.18.0(node:fs・node:pathのみ使用)、2026-09-20に実行。教材演習用のフォルダ(examples/08-02-storage-check/)の中のJSONファイル1つだけを使い、実際のデータベース・外部サービスには接続していません。stdout・stderr・終了コードをそのまま記録した実行記録は第9節の run_log.md にあります。
完了条件との照合:
| 完了条件 | 確認結果 |
|---|---|
| 必須項目が欠けた入力が拒否される記録がある | 第6.3節の手順1に記載(件数0まで確認) |
| 保存前後の照合結果がある | 第6.3節の表に記載(手順2〜6) |
| 日付・時刻と文字コードの扱いが記録されている | 第5.2節に記載 |
この記事で確かめたこと:作成時の必須項目チェック、下書きでの保存と履歴、ガード付きの提出と履歴、存在しない依頼・提出済みの再提出の拒否、保存した依頼から読み出した値で08-01の判定が動くこと。
この記事で確かめていないこと:
| 範囲 | 理由 | どこで確かめるか |
|---|---|---|
| 書き込み途中の失敗(状態だけ書けて履歴が書けない) | 1回の writeFileSync では起こしにくいが、起きないとは言えない。この記事では起こしていない | 08-04(部分失敗を起こして扱いを決める)、記事B(トランザクション) |
| 複数の利用者が同時に保存する場合 | 1つのプロセスで順に実行しているだけ | 09-03 |
| 差戻し状態からの再提出 | 差戻しの処理をこの記事では作っていない(ガードは「差戻し」を通す設計だが、通す動作は未確認) | 差戻し処理を実装した段階 |
| ガードを通さない呼び出しが実際のアプリで起きないか | この記事は「ガードを通す」約束の上で動かしている | 08-03(処理側で確かめる)、09-01(ガードはテスト対象外と明記) |
残る制限:この保存処理は、教材演習用のJSONファイル1つを対象にしたものです。第5.3節の指示文と第6.1節の初稿は説明用の例であり、実際にAIへ送信・観測した記録ではありません。この記事の本文はClaude Codeが作成したもので、業務担当者による内容確認は行っていません。
8. 設計を判断する
8.1 状態変更と履歴追加を「一まとまり」で扱う方法と、保証できる範囲
| 保存先の種類 | 一まとまりで扱う方法 | 保証できること | 保証できないこと |
|---|---|---|---|
| 1つのファイル(この記事の演習) | 状態変更と履歴追加を同じデータに入れ、1回の writeFileSync で書く | 呼び出し側が「状態を書いてから履歴を書く」と2回に分けるより、片方だけ残る隙が小さい | 書き込みの途中で失敗したとき(ファイルが壊れる、古い内容と新しい内容が混ざる)の扱い。2人が同時に書いたときの上書き。失敗したときに元に戻すこと |
| データベース(03-03で選ぶ想定) | トランザクション(複数の変更を「全部成功」か「全部取り消し」のどちらかにする仕組み) | 途中で失敗したら状態も履歴も元に戻る。同時アクセスの制御 | 具体的な実装は技術に依存する。設定を誤ると効かない(記事Bの「対応しない接続設定」) |
この記事の「1回の書き込み」は、REQ-007(片方だけの結果を正常として扱わない)を守るための第一歩であって、解決ではありません。あえて2回に分けて書いたときに何が起きるかは08-04で実際に起こし、「全部成功か全部取り消しか」を保証する形は記事Bで扱います。
8.2 適用限界
この保存処理は、教材演習用の小規模なデータ(依頼1件程度)を前提にしています。実際の利用者数・データ量に応じた性能や、同時アクセスへの対応は、この記事では確認していません。
9. 持ち帰る成果物
| 成果物 | 空欄版 | 記入例 |
|---|---|---|
| 処理コード・保存確認記録ひな形(処理、入力、期待される保存内容、実際の保存内容、照合結果、未検証) | templates/08-02-storage-check_template.md | templates/08-02-storage-check_example.md |
| 保存処理コード一式 | examples/08-02-storage-check/(storage.js、submit_guard.js、approval.js=08-01のコピー、run.js) | 同上 |
| 実行記録(stdout・stderr・終了コード、整形なし) | — | evidence_public/phase5b2b-2026-09-20/08-02/run_log.md |
自分の業務に使うときの手順は次のとおりです。
- 前工程(データ項目表、状態遷移表、最小の関数)を用意し、保存の前に確かめる条件を設計から書き出す(第4節)
- 保存する部品と、確かめる部品を分けて実装する(AIに頼むなら第5.3節の指示文の形で)
- 初稿に、確かめる条件がすべて入っているか、出力が省略されていないかを照合する(第6.2節)
- 保存前後の内容を照合し、拒否したときに保存先が変わっていないことも見る
- 日付・時刻・文字コードの扱いと、保証できない範囲(途中失敗、同時書き込み)を記録する
10. 次工程への受渡し
次へ渡すもの:保存処理のコード(storage.js)、提出前のガード、保存確認記録、保証できない範囲の一覧(第7節)。
次工程での使われ方:08-03(画面・処理・外部機能をつなぐ)では、storage.js のコピーを処理側(サーバー)から呼び、対象の存在と状態の確認をサーバー側で行います(この記事のガードと同じ判断を、画面を通らない操作に対しても行うため)。08-04では、状態と履歴を2回に分けて書いたときの部分失敗を扱います。09-01は08-01の判定関数だけを対象にし、この記事の保存・ガードは対象外です。記事B(09-02)では、保存先がトランザクションで守る形を確かめます。
前工程へ戻る条件:この保存処理が05-01・05-02の設計と食い違う場合は、それぞれの記事に戻って確認します。
11. 根拠と更新情報
- この記事の会社・人物・データはすべて架空の設定です。
- データ項目・状態遷移は、05-01・05-02の設計に基づきます。提出できる状態を「下書き」「差戻し」に限る条件は、記事Aの状態表と同じです。
- 第5.3節の指示文は、この記事のために書いた指示文の例であり、実際にAIへ送信した記録ではありません。第6.1節の初稿は説明用の例であり、実際のAI出力を観測した記録ではありません(内容はこの記事の初版のコードと出力に基づきます)。
- 制作の来歴:本文はClaude Codeが作成しました。第5〜7節のコードは
examples/08-02-storage-check/を正本とし、2026-09-20にmacOS 26.6.2・Node.js v24.18.0の環境で実際に実行して、stdout・stderr・終了コードをevidence_public/phase5b2b-2026-09-20/08-02/run_log.mdに記録しています。人(ユーザー)による実読・内容確認は未実施です。 - 2026-09-20(Phase 5B-2B):受入監査で「08-01との接続が弱い」「状態ガード不足」「対象が存在しない場合の扱いが弱い」「JSON出力の省略表記が実測と曖昧」「1回の
writeFileSyncを『一まとまり』と表現」と指摘された。submit_guard.js(存在と状態の確認)とapproval.js(08-01のコピー)を加え、run.jsを手順1〜7に改めて実測に置き換え、第8.1節を保証できる範囲の表に直した。storage.jsは変更していない(08-03・10-01のコピーと同一のまま)。あわせて読む順路、第4節の判断項目、第5.3節「AIに保存処理を頼む」、第6節の初稿→レビュー→修正、記事間の参照を追加した。 - 未確認事項:書き込み途中の失敗、複数の利用者が同時にアクセスする場合の動作、差戻しからの再提出、実際のデータベースを使う場合の実装は、この記事の時点では確認していません(第7節)。
