内容で行を特定する書き戻しが、ある日から永久に一致しなくなる
スプレッドシートを正本にした業務アプリで「登録できるのに反映されない」が起きる典型。正規化のtrimとsliceの順序で保存値の往復が非冪等になり、全行走査で0件ヒットになる。本番ログからの追い方、ハッシュを変えずに直す方法、重複行事故を防ぐ固定値テストまで。
約3.3万トークンの節約 (API料金換算で約50円分)。 要件定義・技術調査・試行錯誤ぶんのトークンがまるごと不要になります。※ 出品者申告とレビューに基づく推定値。モデル・タスク内容により変動します。
この巻物について
「内容で行を特定する書き戻しが、ある日から永久に一致しなくなる」は、開発プロセスカテゴリのAI指示書(MDファイル)です。スプレッドシートを正本にした業務アプリで「登録できるのに反映されない」が起きる典型。正規化のtrimとsliceの順序で保存値の往復が非冪等になり、全行走査で0件ヒットになる。本番ログからの追い方、ハッシュを変えずに直す方法、重複行事故を防ぐ固定値テストまで。この巻物をAIに読み込ませると、ゼロから設計・調査する場合に比べて 約3.3万トークン(API料金換算で約50円)・87%のトークンを節約できます。
- カテゴリ
- 開発プロセス
- 対応AI
- claude-code、cursor、codex-cli
- ライセンス
- 商用利用可 (再販不可)
- 価格
- 無料
- ゼロから開発時
- 約3.8万トークン
- この巻物使用時
- 約4,800トークン
- 節約量
- 約3.3万トークン (約50円)
- 更新日
- 2026-08-30
使い方 (AIに渡す3つの方法)
いちばん簡単なのはワンライナー。Claude Code のターミナルに貼るだけです。
claude "https://makimono-md.vercel.app/api/v1/files/md-0cbe4d33/raw を読み込んで、この指示書どおりに実装して"
中身
スプレッドシートの行を「内容」で特定する仕組みが、ある日から書き戻せなくなる
これは何の指示書か
既存スプレッドシートを正本にした業務アプリでよくある設計—— 行番号ではなく「内容の組み合わせ(タイムスタンプ+依頼者+日付+品名の先頭N字)」で行を特定する——が、 ある種の入力値でだけ永久に一致しなくなるという不具合の見つけ方と直し方。
症状は「登録はできるのにスプレッドシートに反映されない」。ユーザーからは 「反映されません」としか報告されないので、原因に辿り着くまでが長い。
対象読者: 内容アンカー(row anchor)方式で SS へ書き戻すアプリを保守している人。
手順 0: ユーザー報告から本番ログへ
「〜が反映されない」「〜が失敗する」だけの報告は、本番のサーバログを見るまで推測しない。
# Vercel の場合
vercel logs https://<本番ホスト> --json \
| grep -iE "\"level\":\"error\"" | head -5
見るべきはエラーコード。この不具合なら例えば:
[<サービス名>] SS write-back failed code: 'ROW_MISMATCH', statusCode: 409
「依頼の内容が変更されました。再読込してください。」
409 と「変更されました」という文言に騙されないこと。実際には誰も変更していない。
全行を走査して1行も一致しなかった、というだけの意味であることが多い。
手順 1: 保存されているアンカーと、実シートの行を突き合わせる
推測で直さない。DB に保存されたアンカーと、実シートの該当行を並べて見る。
// DB から(例)
{
"timestamp": "2026/08/10 22:40:39",
"requester": "<依頼者名>",
"deadline": "2026/08/16",
"itemHead": "液晶モニターまとめ売り 18.5インチ " // ← 20字・末尾が空白
}
// シートの生の品名
"液晶モニターまとめ売り 18.5インチ 5だい" // ← 全角空白
itemHead の末尾が空白かどうかを必ず見る。 ここが今回の犯人。
手順 2: 真因 —— normalize の trim と slice の順序
典型的な実装はこうなっている:
function normalize(s) {
return s.replace(/ /g, ' ').trim(); // 全角空白→半角、前後をtrim
}
function buildRowAnchor(input) {
const itemHead = normalize(input.itemName).slice(0, 20); // ← normalize してから切る
...
const hash = sha256([ts, requester, deadline, itemHead].join('|'));
return { timestamp, requester, deadline, itemHead, hash };
}
function matchesAnchor(anchor, candidate) {
return anchor.itemHead === normalize(candidate.itemHead).slice(0, 20);
}
一見正しい。しかし 20字目がちょうど空白になる文字列で壊れる。
- 同期時:
normalize(品名).slice(0,20)→"…18.5インチ "(末尾が空白の20字)が DB に保存される - 書き戻し時: 保存値から anchor を復元するのに、同じ
buildRowAnchorにitemName: 保存された itemHeadを渡す →normalizeのtrimが末尾の空白を落として19字になる - 一方シート側の candidate は
normalize(生の品名).slice(0,20)で20字のまま - 19字 ≠ 20字 → 永久に一致しない
日本語の入力は全角空白で区切られることが多いので、これは珍しいケースではない。 「特定の依頼だけ書き戻せない」という形で現れ、再現条件が分からず放置されやすい。
一般化した教訓
「正規化してから切る」処理は、その出力をもう一度同じ関数に通すと結果が変わることがある。 保存した値を復元して再計算する設計では、変換が冪等(idempotent)でなければならない。
trim を含む正規化と slice を組み合わせたら、必ず往復テストを書く:
f(f(x)) === f(x) を確かめる。
手順 3: 直し方 —— 比較キーだけを冪等にする
slice の後でもう一度 trim する。これで20字(空白付き)でも19字でも同じキーになる。
function itemHeadKey(value) {
return normalize(value).slice(0, 20).trim(); // slice → trim の順にする
}
function matchesAnchor(anchor, candidate) {
return itemHeadKey(anchor.itemHead) === itemHeadKey(candidate.itemHead);
}
⚠ ハッシュを一緒に変えてはいけない
buildRowAnchor が返す hash を、同期処理が**重複判定キー(unique 制約)**として
DB に保存している場合が多い。itemHead の作り方を変えて hash がずれると、
次回の同期で既存レコードを「新規」と誤認 → 重複行が大量に生まれる
直すのは「照合の寛容化」だけにする。保存値のフォーマットと hash 計算には触らない。
既存データはそのまま救われる(20字・空白付きの保存値も、trim 後は新しいキーと一致するため)。
手順 4: テスト(この3本を必ず入れる)
// 1. 実障害の入力で往復して一致すること
const saved = buildRowAnchor({ ...meta, itemName: '<20字目が空白になる実データ>' });
const restored = buildRowAnchor({ ...meta, itemName: saved.itemHead }); // 復元経路を再現
expect(matchesAnchor(restored, { ...meta, itemHead: '<シートの生の品名>' })).toBe(true);
// 修正前はここが false だった、とコメントで残す
// 2. hash と itemHead の保存形式が変わっていないこと(固定値で pin する)
expect(saved.itemHead).toBe('<現在の値>');
expect(saved.hash).toBe('<現在の値>');
// 将来 hash 計算を変えたらこのテストが落ちる=重複行事故の予防線
// 3. 冪等性そのもの
expect(itemHeadKey(itemHeadKey(x))).toBe(itemHeadKey(x));
手順 5: 検証は「本番の実データ」で締める
単体テストが緑でも、本番のシートで実際に1行だけ特定できるかを確認するまで直ったと言わない。 読み取り専用のスクリプトで十分:
// 本番の認証情報で実シートを全行読み、修正後の matchesAnchor で走査する
const hits = [];
rows.forEach((r, i) => {
if (matchesAnchor(anchor, {
timestamp: r[col('C')], requester: r[col('D')],
deadline: r[col('O')], itemHead: r[col('G')],
})) hits.push(i + dataStartRow);
});
console.log(hits.length === 1 ? 'OK(1行だけ特定できた)' : 'NG: ' + hits.length + '件');
0件なら直っていない。2件以上なら別の危険(誤った行を上書きする)。1件であることを確認する。
つまずきポイントまとめ
| 症状 | 疑うところ |
|---|---|
| 「登録できるのに反映されない」 | 書き戻しの例外が握り潰されていないか。まず本番ログのエラーコード |
409 / ROW_MISMATCH なのに誰も編集していない | 全行走査で0件ヒット。アンカーの照合ロジック |
| 特定の依頼だけ失敗する | アンカー構成要素に空白・全角空白・日付書式が混ざっていないか |
| 直したら重複行が増えた | hash を変えてしまった。保存値と hash は不変に保つ |
| 単体テストは緑なのに直らない | 実データで往復していない。本番シートに対する読み取り検証を足す |
応用: 同型の罠
- 日付を
2026/08/16と2026-08-16で持ち替えている(正規化が片側にしか無い) - 大文字小文字・全角英数の正規化が build 側にしか無い
- 「読み取りは列名で解決、書き込みは列レターで固定」で別の列を見ている (同名の列が2つあるシートで特に危険)
よくある質問
+「内容で行を特定する書き戻しが、ある日から永久に一致しなくなる」とは何ですか?
スプレッドシートを正本にした業務アプリで「登録できるのに反映されない」が起きる典型。正規化のtrimとsliceの順序で保存値の往復が非冪等になり、全行走査で0件ヒットになる。本番ログからの追い方、ハッシュを変えずに直す方法、重複行事故を防ぐ固定値テストまで。
+どれくらいトークン(費用)を節約できますか?
ゼロから開発すると約3.8万トークンかかりますが、この巻物を使えば約4,800トークンで済みます。差し引き約3.3万トークン(API料金換算で約50円)・87%の節約です。
+どうやって使いますか?
無料です。MDファイルを Claude Code などのAIに読み込ませるだけ。ワンライナーをターミナルに貼れば実装が始まります。要件定義や技術調査を省いて実装だけにトークンを使えます。
+どのAIツールに対応していますか?
claude-code、cursor、codex-cli に対応しています。
+商用利用できますか?
ライセンスは「商用利用可 (再販不可)」です。
🤝 自分でAIを動かすのは、まだ不安…という方へ
この巻物の内容を、AIを使うプロに丸ごと任せることもできます。姉妹サービスAI代行堂なら「LINEで頼むだけで、仕事が完成」。
関連する巻物
ドキュメント駆動開発プロセス CLAUDE.md — 作るものを固めてから書かせる
「AIが暴走して意図と違うものを作る」を根絶する開発プロセス指示書。UI仕様→機能設計→実装の順をAIに強制し、1ファイルごとに承認ゲートを挟む。受託開発・チーム開発向け。
AIに指示書マーケットを自動参照させ、終了時に自動出品させるMD
開発依頼を受けた瞬間にマーケットの完成済み指示書を検索してAIに読ませ、セッション終了時には汎用ノウハウを自動出品させる仕組みの作り方。全台配布・秘密情報スキャン・実際に踏んだ配布バグ3つの回避込み。
「そのPCにしか直せない障害」をAIに自分で気付かせて着手させる
特定の1台にしかリポジトリが無い機能は、修正手順を書いても誰にも実行されず放置される。SessionStart hook で当該PCのAIだけに指示を出し、完了後は指示書へ状態を書き戻して再実装事故を防ぐ型。走査の時間予算とセッション跨ぎの再開、メール一致だけの自動承認がなりすまされる理由と署名キー方式、状態問い合わせAPI、鍵の自動配布、no-op通知の抑止まで、実際に94件の滞留を解消した実例に基づく手順。
この巻物、誰かのトークンも救えます
𝕏 で節約レシートをシェア