# エラーを出さずに壊れていた7つの事例：本物の抽選を捨てる・読めていない・失敗を握りつぶす

> エラーが出ないまま、本物のデータを捨てていた、読めていなかった7つの実例。正規表現、年のない日付、切れたAIの返事、t.co の中継、案内ページ、try? で捨てた失敗、fail-open。

- URL: https://koshinstudio.com/notes/silent-failures/
- 公開: 2026-10-08
- 書いた人: Koshin（https://koshinstudio.com/about/）
- サイト: Koshin Studio

  English version (https://koshinstudio.com/en/notes/silent-failures/)

トレカダッシュ 開発の記録 ／ 7 件のまとめ

  いちばん厄介な不具合は、エラーが出ないものです。処理は「成功」と言い、画面には何かが出ているのに、本物の抽選が黙って捨てられていたり、リンク先を一度も読めていなかったりします。ここには、そうした「静かに壊れていた」7 つの実例と、どうやって気づいたか、どう直したかを、起きた順にまとめました。

  それぞれの節は、もとは別々に公開していた記録です（2026 年 10 月から）。数字と出典は、その時の作業の記録のままです。テーマごとに読めるよう、1 つの記事にまとめ直しました（2026 年 11 月 7 日）。

## 本物の抽選を、静かに捨てていた 4 つの実例：正規表現・年のない日付・まとめて聞く・null

  2026 年 10 月 17 日の記録。

iOS アプリ「トレカダッシュ (https://koshinstudio.com/works/torecadash/)」の集める処理には、「これは対象外」と判断して捨てる場所がたくさんあります。捨てる判定は、安全側に見えます。間違えても、画面にゴミが出ないだけで、誰も困らない、ように見えるからです。

  ところが、この種の間違いは、気づくきっかけがありません。本物の抽選を捨てていても、画面には何も出ないからです。この記事では、そうやって本物を静かに捨てていた、または捨てかけた、4 つの実例を書きます。

  数字と日付は、2026 年 8 月 28 日〜9 月 20 日の作業の記録から書いています。

### 1. 「x.com」が、「e-starbox.com」に一致していた（8 月 31 日）

  応募のリンクが、SNS のリンクなら、お店のページではないので、採用しません。そのために、除外するサイトの名前を並べて、1 つの正規表現（文字列のパターン）にしていました。

  new RegExp(["x\\.com", "note\\.com", ...].join("|"))
  この書き方は、サイト名の途中にも一致します。x\.com は、e-starbox.com の後半に当たります。note\.com は mynote.com に当たります。

  実害は、抽選販売のサービス「抽選ロトボックス」の応募ページ（shoplottery.e-starbox.com）で出ていました。このサービスの応募リンクは、「SNS だから店のリンクではない」と判断されて、一度も採用されていませんでした。

  直し方は、ホスト名だけの項目は、「// の直後か、ドットの直後」から始まるときだけ一致させる（(?:^|//|\.) を前に付ける）ことでした。足したあとは、通したい URL と、弾きたい URL の両方で、13 例を試しました。除外のリストは安全側に見えるので、間違っていても、誰も疑いませんでした。

### 2. 年の書いていない日付を、6 年前と読んだ（8 月 28 日）

  まとめサイトや店の告知は、「8 月 30 日（日）」のように、年を書きません。この文字列を AI に読ませて、締切を取っていました。

  入荷情報のサイトの取り込みで、ポケモンカードの案件が、軒並み2020 年 8 月 30 日（6 年前）として返ってきました。締切が過去だと、入口の関門が弾きます。その結果、このサイト経由のポケモンカードの案件が、まるごと取り込まれていませんでした。ワンピースやドラゴンボールは、たまたま 2026 年と読まれていたので、気づきにくい状態でした。

  直し方は、AI に渡す指示に、今日の日付と、「今日より前の年を補ってはいけない」を入れることでした。読める案件は、0 件から 46 件になりました。「投稿日と矛盾しない年」とだけ書いても、投稿日が分からないときに、何年になるのかが決まらないので、足りません。

### 3. まとめて聞いた答えの、数がずれた（9 月 19 日）

  AI への問い合わせは、1 件ずつ送ると、回数分の費用がかかります。そこで、複数の投稿の「販売の形式」を、まとめて 1 回で聞く仕組みを入れました。複数行の投稿の本文を、「1. 本文」「2. 本文」と番号をつけて、改行でつないで渡していました。

  AI から見ると、投稿の中の改行と、投稿どうしの区切りが、区別できません。X の監視を再開した 1 日だけで、返ってきた答えの数が合わなかった回が 8 回ありました。数が合わないときは、その組を全部「対象外」にする作りだったので、その組の本物の抽選は、まとめて捨てられました。

  もっと厄介なのは、数が合った組でも、別の投稿の答えが当たっていたことです。遊戯王カードの抽選が、対象外として捨てられていました。

  直し方は、まとめて渡すときは、JSON の配列（[{no, text}]）か、本文に出てこない見出し（「【記事 N】」）で区切り、返事にも番号を含めさせることです。数が合わないときは、既定値の「対象外」に倒さず、1 件ずつ聞き直します。

### 4. 「決められない」を、「無い」と読んだ（9 月 20 日）

  ある関数は、投稿の文章から、トレカのジャンルを推定して返します。2 つ以上のジャンルが書かれていると、「決められない」ので null を返します。

  別の処理が、この関数を使って、「カードの語が無い（＝玩具だけの告知）」を判定していました。!関数(text) という書き方です。ここで、「決められない（null）」が「無い」と読まれました。

  ポケモンカード、ワンピース、遊戯王、ドラゴンボール、ガンプラ、と並べる、まとめの投稿は、複数のジャンルが書かれているので null になります。それが「カードの語ゼロ」と読まれ、さらに「ガンプラ」が見つかったので、玩具の告知と判定されました。

  もし、このまま動いていたら、毎朝 5:15 に動く一括の非表示の処理が、表示中の本物の抽選 129 件（30th CELEBRATION や、世界最強の戦士など、9 つの商品）を、伏せるところでした。止められたのは、一括の処理を動かす前に、件数だけを数える下見（ドライラン）をしたからです。配備の前に、129 件という数字が出て、気づきました。

  直し方は、「無い」を判定したいときは、見つかったジャンルを全部返す関数を使い、その長さが 0 かどうかで見ることでした。

### 分かったこと

> 捨てる判定の間違いは、苦情が出ない。だから自分で数える。

- 除外のリストも、間違える。安全側に見える判定ほど、疑われません。

- 通したいものでも試す。弾きたいものだけで試すと、通したいものまで弾いていても、気づきません。（1 の 13 例は、両方でした）

- AI には、前提を全部渡す。今日の日付も、区切りも、書かないと、AI は黙って別の前提で答えます。

- 数が合わないときに、「対象外」に倒さない。分からないときの既定値が「捨てる」だと、本物がまとめて消えます。

- 「無い」と「決められない」は、別の答え。戻り値の null を、無条件に否定しない。

- 毎日の一括の処理は、動かす前に、件数を数える。129 件が、事前に分かりました。

## 候補は見つかっていたのに、答えを読む所で毎回落ちていた：200 トークンで切れた URL と、ハイフンで 400 になる検索

  2026 年 10 月 17 日の記録。

iOS アプリ「トレカダッシュ (https://koshinstudio.com/works/torecadash/)」の、まとめサイト由来の記事は、お店の公式の告知のページへ、リンクを張り替えます。ところが、開発者モードの報告が、続けて 7 件、来ました。「正しい告知の URL は、これ。ちゃんとやって」。

  調べると、候補は見つかっていたのに、答えを読む所で、毎回落ちていました。この記事は、その真因と、同じ時期に見つけた、よく似た 1 件の記録です。

  数字は、2026 年 8 月 23 日と 24 日の作業の記録から書いています。

### 真因：AI の返事が、200 トークンで途中で切れていた

  公式の告知ページを選ぶ処理は、最後に AI に、「候補のどれが、そのお店の告知か」を答えさせます。その返事の長さの上限（max_output_tokens）を、200 にしていました。

  ところが、返事には、日本語をパーセント符号化した、長い URLが入ります。1 つで 200 文字を超えます。返事は途中で切れ、JSON として読めなくなり、Unterminated string in JSON という例外になっていました。

  WARN [応募リンク] 公式ページの判定に失敗 [お宝創庫・プレイズ / …]: Unterminated st…
→ 対象 10 件、すべて「見つからず」
  候補は、見つかっていたのです。答えを読むところで、毎回、落ちていました。上限を 800 に上げました。さらに、それでも切れたときに、全部を捨てず、"url": "… の部分だけを拾い直す救済も入れました。

### 直しても、採用が 0 件だった：AI が指示に忠実だった

  JSON を直しても、採用は 0 件でした。ログを見ると、候補はあるのに、「採用なし」です。原因は、AI が、指示に忠実だったことでした。お店の告知は、こういう書き方をします。

  「【アプリ会員様限定】再版／ポケモンカードゲーム 4 種 抽選販売に関して」
  個別の商品名を書いていません。指示には、「商品が違う場合は、選ぶな」とありました。AI は、それに従って、拒否していたのです。

  2 つ直しました。

- 商品名で絞った検索に加えて、いつも、site:店のドメイン 抽選 も検索して、候補にまとめる。（「空振りしたときだけ」では、無関係なページが 1 件返るだけで、発動しませんでした。）

- 指示に、「その店自身の告知のページが、複数の商品をまとめて案内していて、個別の名前を書いていないときは、選んでよい」を足す。ただし、ほかの店やまとめサイトは、まとめて案内していても、絶対に選ばない。

  結果は、10 件のうち、3 件が、お店の公式の告知のページへ張り替わりました。

  もう 1 つ、アプリ限定の店（アプリでしか、抽選の案内を出さない店）のために、「アプリでご確認ください」と出せる一覧を作りました。アプリが実在すると分かっている店だけです。推測で足しません。

### よく似た 1 件：楽天のキーワード検索が、ハイフンで丸ごと失敗していた（8 月 24 日）

  ログに、「楽天市場の個別弾の検索に失敗しました」が、遊戯王だけで 8 種以上、繰り返し出ていました。調べると、楽天の API は、検索のキーワードのハイフンの使い方によっては、リクエスト自体が 400 wrong_parameter で失敗しました。

    キーワードの形 | 結果

      "A - B"（ハイフンの前後に空白） | 400

      "A B "（末尾に空白だけ残る） | 400

      "A B-"（末尾がハイフンで終わる） | 400

      "AB-"（空白を挟まない単語 ＋ 末尾ハイフン） | 200（正常）

  遊戯王の商品名は、「LIMIT OVER COLLECTION - THE RIVALS -」のように、副題をハイフンで挟む書き方が多く、この形の弾は、個別弾の検索から、一度も拾えていませんでした。

  直し方は、2 段階です。(1) ハイフンの直後の空白を詰める。(2) 末尾に残るハイフンと空白を落とす。「LIMIT OVER COLLECTION - THE RIVALS -」は、「LIMIT OVER COLLECTION -THE RIVALS」になり、200 を確かめました。同じ問題は、楽天ブックスの API でも、起きることを確かめたので、同じ整形を、キーワードを渡す 4 つの関数に当てました。

### 分かったこと

> 候補が見つかっているのに、結果が 0 件のときは、答えを読む所を疑う。

- 返事の長さの上限は、返す中身から決める。パーセント符号化した URL は、長いです。

- 失敗は、全部捨てず、拾い直す。切れた JSON から、必要な部分だけ拾えます。

- AI は、指示に忠実。指示が、現実の書き方に合っていないと、正しく拒否します。

- 外の API の癖は、実測で切り分ける。ハイフンの前後で、400 と 200 が分かれました。

- 同じ癖は、同じ API を使う、ほかの場所にもある。楽天ブックスでも起きました。

## 「確認済み」の印が付いていたのに、リンク先を一度も読めていなかった：t.co の中継ページ

  2026 年 10 月 17 日の記録。

iOS アプリ「トレカダッシュ (https://koshinstudio.com/works/torecadash/)」は、お店の X（旧 Twitter）の告知から、トレカの抽選販売の情報を集めています。告知には、応募用のフォームへのリンクが付いています。アプリは、そのリンク先も読みに行って、締切の日時や、もう受付が終わっていないかを確かめています。

  ある日、「締切の日時が違う」という報告が来ました。調べると、締切の日時どころではなく、リンク先のページを、一度も読めていませんでした。しかも、読めていないのに、「確認済み」の印が付いていました。

  この記事は、2026 年 10 月 16 日の作業の記録（調査の内容と、数字）から書いています。

### 症状：閉じたフォームが、受付中のまま出ていた

  報告された抽選は、ゲームショップの「30th CELEBRATION カードセット 9 種」の X の告知でした。応募先の Google フォームを開くと、「回答の受け付けは終了しました」と出ます。ところが、アプリでは、この抽選が表示中のままで、締切の日時も空でした。

  おかしいのは、この記事に「受付が終わっていないか確認した」という印（日時）が付いていたことです。確認したのに、終わっていることに気づかなかった。確認が、中身を見ていなかったことになります。

### 原因：t.co は、ブラウザのふりをすると、別のページを返す

  X の投稿に貼られたリンクは、t.co という短縮 URL を通ります。普通は、開くと、本来のリンク先へ転送されます（HTTP の 301）。

  ところが、ブラウザの User-Agent（「私は Chrome です」という名乗り）を付けて開くと、転送されず、JavaScript で飛ばす、約 500 バイトの中継ページが返ってきます。アプリの読み取り処理は、2 か所とも、ブラウザの名乗りを付けて開いて、「転送に従う」設定にしていました。その結果、次のことが起きていました。

- 転送されないので、最後の URL は、t.co のままです。

- 読んだ本文は、応募フォームではなく、中継ページの HTML でした。

- つまり、応募フォームの中身を、一度も読んでいませんでした。

  ブラウザの名乗りを足したのは、8 月 6 日のことでした。今回の不具合は、その変更の副作用でした。

### 実際に、どれくらい読めていなかったか

  報告された 1 件だけの問題かを確かめるために、表示中の X の抽選・予約の記事のうち、t.co のリンクを持つものを全部調べました。

    調べたこと | 結果

      t.co を持つ、表示中の記事 | 220 件

      そのうち、中継ページが返ってきた記事 | 220 件（全部）

      そのうち、実は閉じたフォームなのに、表示中だった記事 | 9 件

  9 件は、複数の店にまたがっていました。1 つの店の問題ではなく、仕組みの問題です。

  もう 1 つ、見逃せない手がかりがありました。毎日動いている、「締切が空の記事を直す」処理が、「対象 126 件、更新 0 件」を、毎日繰り返していたことです。対象が 126 件あるのに、直せたのが 0 件。これは、「直すものが無かった」のではなく、「読めていないから、直せなかった」の結果でした。

### 直し方

  転送先を調べる処理を、fetchTcoTarget という 1 つの関数にまとめました。手順は、こうです。

- 名乗りを付けずに、t.co を開く。転送には従わず（redirect: "manual"）、最初の転送先だけを受け取る。

- 転送先が返ってきたら、それを使う。

- 中継ページが返ってきたら、その中の自動転送の指定（<meta http-equiv="refresh">）を読んで、本当の転送先を取り出す。

- その転送先を、今度はブラウザの名乗りを付けて開く。

  名乗りを付けるか付けないかで、返ってくるものが違う。だから、「短縮 URL を解く」ときは名乗りなし、「本来のページを読む」ときはブラウザの名乗りあり、と使い分けました。同じ失敗を繰り返さないよう、テストも足しています。

### すでに付いていた、「確認済み」の印をどうするか

  直しただけでは、足りません。誤って付いた「確認済み」の印が、記事に残っているからです。印がある記事は、「もう確認した」と見なされて、再確認の対象から外れます。コードを直しても、既存の記事は直りません。

  そこで、条件に合う 195 件（X の抽選・予約で、投稿へのリンクを持ち、t.co を持つもの）から、印を外しました。そのうえで、受付終了の確認と、締切の再抽出を、手で実行しました。

    処理 | 直す前 | 直した後

      締切の再抽出（対象になった件数／更新できた件数） | 対象 126 件・更新 0 件（毎日） | 対象 123 件・更新 14 件

  同じ日に、受付終了の確認の頻度も見直しました。締切の無い記事は 6 時間おき（締切のある記事は 24 時間おき）、1 回に見る件数は 40 件から 80 件、1 回の上限は 7 分です。t.co が読めるようになったので、閉じたフォームを丸 1 日出し続けないようにするためです。

### 分かったこと

> 「確認した」の印は、読めたときだけ付ける。

- 印は、成功したときだけ書く。読めなかったのに「確認した」と書くと、直した後も、その記事は二度と見直されません。

- 外部のページを読んだら、目的のページかを確かめる。最後の URL が、短縮 URL のままではないか。本文が、期待した種類のページか。

- 「更新 0 件」が続くのは、異常かもしれない。対象があるのに、毎日 0 件。「ない」のか「できない」のかを、区別します。

- 通信の設定を変えたら、それを使う全部の通信を見直す。8 月に足したブラウザの名乗りが、2 か月後に、t.co の読み取りを壊していました。

## 「取れた」と読んでいたのは、ただの案内ページだった：3日間、商品が0件

  2026 年 10 月 8 日の記録。

iOS アプリ「トレカダッシュ」は、通販サイトの販売ページを定期的に見に行って、抽選や先着の情報を集めています。ある日、ポケモンセンターオンラインの取得が、3 日間ずっと、商品 0 件になっていたことが分かりました。エラーは出ていません。「取れた」と判定されていたからです。何が起きていたのか、コミットの記録で書きます。

  このサイトには、トレカダッシュの作品ページはまだありません。ここに書くのは、開発の記録です。

### 「取れた」の判定が、甘かった

  ページの取得は、外部のサービス（Scrape.do）を通して行っています。取得の設定には、安いものから高いものまであって、安い設定で取れなければ、強い設定に切り替える（昇格する）作りです（取得の費用の記事 (https://koshinstudio.com/notes/ai-cost-cutting/#s3)と同じ仕組みです）。

  この「取れたかどうか」の判定が、問題でした。ポケモンセンターのページは、安い設定で取ると、2,410 バイトの、「JavaScript を有効にしてください」という案内ページが返ってきます。商品の一覧ではなく、ただの案内です。ところが、これを「取れた」と読んで、成功と扱っていました。成功なら、強い設定への切り替えも行われません。

  こうして、9 月 4 日から 3 日間、取得は「成功」し続けて、商品は 0 件のまま、でした。

### 同じページを、3つの設定で叩いて測った

  原因を推測で決めないために、ポケモンセンターの検索ページを、取得の設定を変えて実際に取り直しました。安い設定は 1 クレジット、強い設定（super と render を併用）は 25 クレジットかかります。

    取得の設定 | 応答 | 中身

      安い設定（1 クレジット） | 200 | 2,410 バイト。「このサイトでは JavaScript を有効にする必要があります」だけ

      super + render（25 クレジット） | 200 | 278,322 バイト。商品 39 件

  応答コードはどちらも 200 です。通信の成功だけを見ていたら、この 2 つは区別できません。違いは中身の大きさだけで、約 115 倍ありました。

### 守りはあったのに、2 つとも寸前で通り抜けていた

  「取れたかどうか」を見る条件は、もともと何もなかったわけではありません。ページに次の 2 つの確認がありました。

- 本文が 2,000 文字未満なら、取れていないとみなす。案内ページは 2,410 バイトで、わずかに超えていました。

- 本文に「JavaScript が無効」という文言があれば、取れていないとみなす。実際の文言は「JavaScript を有効にする必要があります」で、この文言には一致しませんでした。

  どちらも、想定した形の少しだけ外側で、通り抜けています。確認の条件は、想定した 1 つの形にしか合っていませんでした。実際に来るページの形は、いくつもあります。

  通ってしまった後は、次のことが起きます。

- 強い設定に切り替え直さない。商品は 0 件のまま。

- 「安い設定で取れる」と覚えて、強い設定への切り替えを解除する。

- 以後ずっと 0 件。エラーが 1 件も出ないので、ログにも何も残らない。

### 3 日間の数字

    日時 | ポケモンセンターの取得件数

      9 月 4 日 12:45 まで | 10 件か 8 件（安定していた）

      9 月 4 日 12:45 から 9 月 7 日 | 0 件が 323 回連続。エラーは 1 件もなし

      直した後に手で実行 | 10 件に戻った

### 直し方

- 「取れた」の判定を、1 か所に集める。それまで、判定が複数の場所に散らばっていました。ブロックされたページかどうかを見る関数（scrapeDoResultLooksBlocked）を 1 本にして、全部がそれを使うようにしました。

- 通った設定を、覚える。一度成功した設定を覚えておき、次からは、そこから試します。これで、1 回の取得に使うクレジットが、36 から 25 に減りました。

### 新しい判定は、何を「取れていない」と読むか

  scrapeDoResultLooksBlocked は、次のどれかに当てはまると「取れていない」と答え、その理由の文字列も返します。理由はログに必ず残るので、あとから「なぜ強い設定に上がったか」が読めます。

- 本文が短すぎる。

- 順番待ちのページ（Queue-it）である。

- 中身が小さいのに、JavaScript を求める案内がある。

  3 つ目には、落とし穴があります。「JavaScript が無効になっています」という一文は、まっとうなページにも、noscript という要素として常に入っていることがあります。実際に、別の店（ヤマダ）のページで一度、取れているページを「取れていない」と誤診しました。そこで、この判定は8,000 バイト未満のときだけ当てる形にしました。大きなページは、noscript があっても本物とみなします。

  この関門は、安い設定、super、render と super の併用の、3 つの段階すべてで同じものを通します。

### 通った設定を、覚えるようにした

  昇格のしくみ自体にも、無駄がありました。それまでは、取得の関数が自分の中で設定を上げたときだけ、通った設定を覚えていました。呼び出し側が設定を切り替えて上げた分は、覚えていなかったのです。その結果、毎回 1 + 10 + 25 の36 クレジットを払っていました。覚えるようにしたので、次回からは 25 クレジットで始まります。安い設定も、定期的に試し直します。

  書き込みは、設定が変わったときだけにしています。毎回書くと、今度はデータベースの書き込み費用が増えるからです。

### 同じ日に見つかった、もう一つの汚れ

  同じ点検で、別の問題も見つかりました。商品名の「別表記」の食い違いを判定する処理が、照合の入口と、お掃除の側で、別々に書かれていました。お掃除の側にだけ、「商品の形が食い違う」という判定が無かったのです。その結果、トライアルデッキの別表記が、ブースターパックの商品に残ったままになり、毎日、誤った照合を呼び続けていました。判定を共通の関数にして、27 商品の 46 件を除去しました。

  あわせて、「更新する文書が無い」というエラー（7 日間で 64 件）も解消しました。商品は、キャッシュが古いときに読み直す。記事は、消えていたら見送って、残りの重複処理を続ける、という形にしています。

### 気づいた経緯と、ほかの「0 件」4 つの確認

  気づいたきっかけは、作業の最初に必ず見る定型の点検です。健全性のチェックに、「取得 0 件が 48 時間続いている」という警告が並びます。それまでは、そこに並んだ名前を「掲載が無いだけ」と流していました。この回は、流さずに中身を確かめました。

  同じ警告に並んでいたほかの 4 つも、1 つずつ見ました。全部、まっとうな 0 件でした。なぜ 0 件でよいのかを書き残しておかないと、次に見た人が同じ確認をやり直すことになります。

    情報源 | 確かめた中身

      三洋堂 | ページに「現在受付中の抽選販売はありません」と書いてある。取得も成功している（56KB）。

      コナミスタイル | 16 件取れるが、バッジが全部「遊戯王カードゲームポイント交換専用」。現金では買えない景品なので、設計どおり除外している。

      楽天市場 | もともと 1〜2 件が普通の情報源。14 日間で 0 件が 1,860 回、1 件が 2,345 回あった。

      ミニストップオンライン | どの設定でも取れない（20 秒の待ちを 2 回ずつ、3 設定すべてで応答なし）。抽選受付のときだけページが出る作りなので、休止の仕組みに任せている。

  同じ「0 件」でも、掲載が無い 0 件、設計どおり除く 0 件、取れていない 0 件は別のものです。件数だけでは区別できないので、中身を見て、理由を書き残しています。

### 分かったこと

> 「成功」と「中身が取れた」は、別のもの。

- 成功の判定は、中身で見る。通信が通ったことと、欲しいものが取れたことは、別です。「取れたものが 0 件」が続いたら、それ自体を異常として見張る必要があります。

- 判定は、1 か所に。同じ判定が 2 か所にあると、片方だけが古くなります。照合の入口とお掃除のように。

- エラーが出ないことは、うまくいっていることの証拠にならない。

  この記事は、2026 年 9 月 7 日のコミットと、その日の作業の記録から書きました。数字（2,410 バイト、278,322 バイト、323 回、36 と 25 クレジット）は、その記録に残っていた実測値です。

## 表示中の記事のリンク先を、実際に開いて確かめた：約 60 件の間違いと、最初の点検が間違えた 3 つのこと

  2026 年 10 月 17 日の記録。

iOS アプリ「トレカダッシュ (https://koshinstudio.com/works/torecadash/)」の記事には、お店や通販サイトのリンク先が付いています。記事の題名や値段が正しくても、リンク先のページが、閉じていたり、別の商品だったりすれば、利用者には役に立ちません。

  2026 年 9 月 15 日の夜、「遷移先の内容まで見て、問題ないか調べてほしい」という依頼を受けて、表示中の記事のリンク先を、実際に開いて、中身を記事と突き合わせました。商品、店、受付中か、販売の形式、締切、値段です。約 60 件の間違いが見つかりました。

  数字は、2026 年 9 月 15 日の作業の記録から書いています。点検は、書き込みをしない形で行い、見つかったものを別に直しました。

### どう調べたか

- X の投稿（361 件）：削除されたものは 0 件でした。本文は、以前の点検で突き合わせ済みです。

- X 以外（Yahoo! を除く 703 件）：直接、または取得サービス（日本の IP を指定）でリンク先を読み、AI で判定しました。Amazon は、必ず取得サービス経由です。

  最初の版の点検は、3 つ間違えていました。

- Amazon を、シンガポールから開いた。価格がシンガポールドルで出て、別の出品者が見えたので、それを「誤り」と判定していました。

- 楽天を、文字化けしたまま読んだ。文字コード（EUC-JP）を判定していませんでした。

- Yahoo! を、6 並列で 501 件開いた。本番と同じ IP が、HTTP 489 で拒否されました（別の記事 (https://koshinstudio.com/notes/data-repair/#s6)に書いています）。Yahoo! は、途中で打ち切りました。

  点検の道具が間違っていると、「間違い」の数が増えます。文字コードの判定、Amazon を日本から開くこと、「この商品そのもの」の在庫だけで判定すること、を直してから、流し直しました。

### 見つかった間違い（約 60 件）

    中身 | 件数 | 直し

      Amazon の「招待リクエスト」なのに、予約・先着になっていた | 10 | 抽選に直す

      Amazon の「予約注文」なのに、先着になっていた | 7 | 予約に直す

      楽天ブックスで「予約受付中」なのに、先着 | 3 | 予約に直す

      休店中の出店者（Yahoo!） | 6 | 伏せる

      売り切れ、受付終了なのに、表示されていた | 多数 | 伏せる

      リンク先が、別の店、または終わった抽選だった | 3 | 店の X の投稿へ戻す

      リンク先が、別の商品だった | 2 | 伏せる

      値段の食い違い | 約 10 | 直す

      店名が空 | 1 | 補う

  特に多かったのは、販売の形式の取り違えです。Amazon は、先着でも予約でもなく、招待を受けて買える「招待リクエスト」があります。これを「先着」と扱うと、利用者は、急いでも買えません。それまでの処理は、招待の印を付けるだけで、販売の形式を直していませんでした。

  「売り切れ」の判定語に当たらなかったものもありました。Yahoo! の売り切れのページは、画面には「在庫なし」と出ていますが、判定語のどれにも当たっていませんでした。そこで、ページに埋め込まれている、その商品自身の在庫データ（"stock":{"isAvailable":false）で判定します。画面の文言より、データのほうが確かです。

### 「応募リンクを開くと、サイトのトップへ飛ぶ」

  同じ日の別の点検で、見つかったものです。ある店の抽選の、告知画像の QR コードが指す URL が、実際に開くと、そのサイトのトップページに転送されていました。応募のページではありません。

  応募のリンクを採用する、唯一の関門に、判定を足しました。実際に開いて、もとの URL はトップ以外のパスなのに、転送先のパスが空（または、トップらしい名前のファイル）なら、採用しません。読めないとき（403、タイムアウト）は、採用のままにします。ボット対策をしている店が多いので、読めないことは、悪いことの印にしません。同じ URL は、6 時間控えます。

### 直さなかったもの、直せなかったもの

  見つかったものを、全部「間違い」と数えなかったのも、大事なところです。確かめて、直していないものがあります。

- 楽天市場の「売り切れ」：ページのデータ上は在庫あり（在庫数を出さない設定）。AI の読み違いでした。

- Amazon の「他の購入オプションを見る」：ほかの出品者がいるので、売り切れとは限りません。

- Amazon の英語の題名：自動翻訳された名前で、商品は合っていました。

- 読めなかった 85 件：楽天市場（広告経由のリンク）、会員サイト、LINE、Google フォームなど。

  点検の結果は、直す、直さない、読めなかったの 3 つに分けて、理由とともに残しています。「直さない」にも、理由が要ります。

### 分かったこと

> 題名と値段が合っていても、リンク先まで開かないと、分からない。

- リンク先を、実際に開く。記事の中身だけを見ても、リンク先が閉じていることは分かりません。

- 点検の道具を、先に点検する。国、文字コード、並列数。道具の間違いが、「間違い」を増やします。

- 画面の文言より、埋め込まれたデータ。売り切れは、文言ではなく、在庫のデータで判定します。

- 見つけたものを、全部「間違い」にしない。直さないものにも、理由を残します。

- 読めなかったことを、悪い印にしない。ボット対策で読めないのは、普通のことです。

## 失敗を捨てると、画面は嘘をつく：「○件登録しました」と出て 1 件も入っていなかったカレンダー、固まる文字認識、店名を積み忘れた通知

  2026 年 10 月 17 日の記録。

iOS アプリ「トレカダッシュ (https://koshinstudio.com/works/torecadash/)」で、「失敗を、握りつぶしたせいで、嘘の成功を表示していた」という不具合が、続けて見つかりました。この記事は、そのうちの 3 つの記録です。

  この記事は、2026 年 8 月 25 日の作業の記録から書いています。

### 1. カレンダーに 1 件も入っていないのに、「○件登録しました」

  アプリには、抽選の締切を、iPhone のカレンダーに書き出す機能があります。ビルドの警告（result of 'try?' is unused）から、この処理を見直しました。try? は、Swift で、失敗を捨てる書き方です。この機能は、保存の処理を try? store.save(...) と try? store.commit() で呼び、失敗を捨てていました。

  その結果、こうなっていました。

- 1 件もカレンダーに入っていないのに、「○件登録しました」と表示される。

- 「書き出し済み」の記録だけが残る。

- もう一度押すと、「新しく登録できる案件はありませんでした」と出て、永久に登録できなくなる。

  直し方は、失敗を呼び出し元に返して、成功した件数だけ数えることでした。既定のカレンダーが無い端末は、先に検出して知らせます。案内の文も、理由ごとに分けました。それまでは、どんな失敗でも、「アクセスが許可されていません」と出していました。

### 2. 文字認識が失敗すると、画面が固まる

  応募の画像から文字を読み取る機能（OCR）も、try? handler.perform([request]) で、失敗を捨てていました。文字認識が失敗すると、コールバック（終わったときの呼び出し）が、一度も呼ばれません。読み取り中の「ぐるぐる」が止まらず、画面を閉じる以外に、抜けられなくなっていました。

  直し方は、失敗したときも、「認識し終わった（見つからなかった）」状態に戻すことです。

  ビルドの警告、result of 'try?' is unused、は、握りつぶしの目印です。ビルドを流して、警告を定期的に見ます。

### 3. 通知に、店名が出ない：バッファに積み忘れた

  別の話ですが、同じ形です。「通知に、店名がない」という報告が来ました。記事には、店名（sellerName）が、正しく入っています。それなのに、通知には出ていませんでした。

  抽選と予約の通知は、いったんバッファ（一時置き場）に積んでから、まとめて送る設計です（サイレントな時間帯の保留と、送る回数の制限のため）。ところが、店名を、バッファに引き継いでいませんでした。送るときに、店名を探しても、もう情報が無く、カテゴリ（「ライヴポケット」）しか残っていません。

  この症状は、前の報告の直し方が表に出した、という面もありました。「通知が、ライヴポケットという名前で来る」を直したとき、店名が分からない場合は、null を返すようにしました。この積み忘れが、「間違った店名」から、「店名なし」に、症状を変えて表に出たのです。

  2 種類のバッファ（ジャンル別と、商品別）の両方に、同じ抜けがあったので、両方を直しました。すでに積まれていた 5 件は、記事から店名を補って、送るときに店名が出ることを確かめました。

  同じ抜けは、8 月 17 日にも、別の項目（商品名）で起きていました。教訓は、バッファに積むときは、送るときに使う項目を、全部持たせることです。組み立て済みの文言をそのまま保存する、ほかの 2 つのバッファでは、この問題は起きません。

### 分かったこと

> 失敗を捨てると、画面は、嘘をつく。

- 失敗は、呼び出し元に返す。捨てると、成功の表示と、実際が食い違います。

- 失敗したときも、「終わった」状態に戻す。コールバックが呼ばれないと、画面が固まります。

- 成功の記録は、成功したときだけ。失敗しても残ると、再試行ができなくなります。

- ビルドの警告を読む。try? の警告は、握りつぶしの目印です。

- バッファには、送るときに使う項目を、全部積む。後から、別の項目で、同じ抜けが出ます。

## 確認できなかったときに、通していた：「fail-open」が転売品を招いた話

  2026 年 10 月 8 日の記録。

iOS アプリ「トレカダッシュ」で、「Amazon に転売の価格がたくさん入ってきている」という報告が、アプリ内の不具合報告から 3 件届きました。うち 1 件は、¥79,800 の、明らかな転売品でした。原因は、わざと入れていた「確認できなかったら通す」という設計の、盲点でした。HANDOFF とコミット（2026年7月30日）の記録で書きます。

  このサイトには、トレカダッシュの作品ページはまだありません。ここに書くのは、開発の記録です。

### 価格の関門と、「通す」設計

  アプリは、通販サイト（楽天市場、Yahoo!ショッピング、Amazon など）の出品を拾って、フィードに出します。そのとき、出品価格が、商品の定価から見て妥当かを調べる「価格の関門」があります。定価を、AI（Web 検索つき）に調べさせて、極端に高い転売品を弾く仕組みです。

  この関門には、「定価を確認できなかった場合は、審査せずに通す」という動きがありました。これは意図した設計です。AI が本当に定価を知らない商品を、確認できないだけで除外してしまうと、本物の出品まで消してしまうからです。いわゆる fail-open（失敗したら開く＝通す）です。

### 盲点：検索の上限に達した場合も、同じ道を通っていた

  AI の Web 検索には、1 日 300 回の上限があります（想定外の費用を防ぐための回路遮断器）。ところが、関門は、定価を確認できなかった理由を、区別していませんでした。

- AI が「この商品の定価は知らない」と答えた。

- 1 日の上限に達して、確認すること自体ができなかった。

  この二つが、どちらも「定価不明 → 通す」の同じ道を通っていました。前者は意図どおりです。後者は、「調べる前に、道が閉まっていた」だけなのに、関門が、素通しになっていたのです。

### 穴が、実害になった理由

  この穴は、前の変更（Amazon の対象を広げた回）をきっかけに表に出ました。対象を広げたことで、新しく価格を確認する必要がある商品が、一気に増えました。その結果、1 日の上限に、頻繁に達するようになった。上限に達した間は、関門が素通しになって、転売品が大量に混ざりました。

  さらに、二次被害がありました。定価が分からなかった結果は、理由を問わず24 時間キャッシュされる設計でした。そのため、一度上限に当たった商品は、上限が解除されたあとも、最大 24 時間、素通しのままになります。

### 直し方：理由を区別する

  専用の目印（QUOTA_EXCEEDED）を作って、二つを区別できるようにしました。

    状況 | 関門の動き

    AI が本当に定価を知らなかった（null） | これまでどおり通す（fail-open のまま）

    1 日の上限で、確認自体ができなかった（QUOTA_EXCEEDED） | 除外する（fail-close）

  後者は、24 時間のキャッシュにも入れません。次に呼ばれたときに、あらためて確認を試みます。この関門は、楽天市場、Yahoo!ショッピング、Amazon、AI による出品の発見、楽天ブックスの先着など、価格の審査を使うすべての機能に共通なので、一回の修正が、広い範囲に効きます（コミットの記録では、18 の関数を配備しています）。

  報告のあった 3 件は、手作業で非表示にしました。そのあとは、5 分おきに動く再審査のバッチが、同じ見逃しを拾い直す想定です。

  確認できていないこと：記録には、「上限のリセット後に、転売価格の報告が来なくなったかを、次のセッションで確認する」と書いてあります。その結果は、この記事を書いた時点で、記録にありません。

### ほかの判定にも、同じ穴は無いか

  その後の点検（2026年7月30日）で、販売形式の判定や商品の照合など、ほかの AI による判定も確認しました。いずれも、失敗したときは、安全側（除外、または null）に倒れる設計で、新しい fail-open の穴は見つかりませんでした。

### 分かったこと

> 「確認できなかった」には、二種類ある。知らなかったのと、調べられなかったのと。

- 通す設計は、「通してよい理由」を、はっきりさせてから使う。「分からないから通す」と「調べられないから通す」は、別のものです。

- 失敗の結果を、理由を問わず、キャッシュしない。一時的な失敗を、24 時間、固定してしまいました。

- 前の変更が、別の場所の穴を、実害にする。Amazon の対象を広げたことが、上限の穴を、表に出しました。

  この記事は、HANDOFF（第182回）とコミット（2026年7月30日）の記録から書きました。

## それぞれの要点

- 本物の抽選を、静かに捨てていた 4 つの実例：「x.com」が「e-starbox.com」に一致して応募リンクが一度も採用されなかった、年のない日付が 6 年前と読まれてポケカ案件が 0 件だった、まとめ判定の数がずれた、「決められない」が「無い」と読まれ 129 件を伏せかけた。捨てる判定の失敗の記録。

- 候補は見つかっていたのに、答えを読む所で毎回落ちていた：公式の告知ページを選ぶ AI の返事が 200 トークンで途中で切れて毎回失敗していた。直しても 0 件だったのは、店の告知が個別の商品名を書かないため、AI が指示に忠実に拒否していたから。楽天の検索は、ハイフンの位置で 400 になった。

- 「確認済み」の印が付いていたのに、リンク先を一度も読めていなかった：X の告知の応募リンクを、ブラウザの名乗りを付けて開くと、t.co は転送せず中継ページを返した。表示中の 220 件が全部読めておらず、9 件は閉じたフォームだった。直し方と、誤った「確認済み」の印の外し方の記録。

- 「取れた」と読んでいたのは、ただの案内ページだった：ポケモンセンターの取得が3日間ずっと商品0件だった。安い設定が返した2,410バイトの「JavaScriptを有効に」ページを、成功と読んでいた。判定を1か所に集めた記録。

- 表示中の記事のリンク先を、実際に開いて確かめた：表示中の記事のリンク先 1,000 件超を開いて、記事と突き合わせた。Amazon の招待リクエストが先着になっていた、休店中・売り切れが表示中、リンク先が別の店・別の商品、応募リンクがサイトのトップへ転送。最初の点検自体も 3 つ間違えていた。

- 失敗を捨てると、画面は嘘をつく：try? で失敗を捨てていたため、カレンダーに 0 件なのに「登録しました」と出て二度と登録できなくなった。文字認識の失敗で画面が固まった。通知バッファに店名を積み忘れて「店名なし」になった。

- 確認できなかったときに、通していた：「定価を確認できなかったら通す」という設計に、検索の1日上限という盲点があった。「知らなかった」と「調べられなかった」を区別して直した記録。

  ほかの記録は、Notes の一覧 (https://koshinstudio.com/notes/)から、テーマごとに読めます。トレカダッシュそのものは、作品ページ (https://koshinstudio.com/works/torecadash/)に書いています。
