2026年8月14日 金曜日
AI時短ラボ
活用· 約28

Object.entries(true) は空配列を返す──設定JSONの型違いがエラーも警告も出さずに通り、起承転結インジケーターが4本分まるごと点灯しなかった

JavaScriptのObject.entries()はプリミティブを渡しても例外を投げず、真偽値や数値なら空配列を返す。動画ビルドの設定JSONで {"ki":[0,1],...} を期待している箇所に true と書いても、エラーもログも出ないまま起承転結インジケーターが一度も点灯しない。2026年8月14日にmacOS 15.1.1・Node.js v25.9.0で Object.entries(true) を実行して [] を確認し、Remotion 4.0.474で書き出し済みのQAフレーム10枚すべてで4つの丸が全部消灯していることも実物で確認した。台本JSON 64本のうち4本が、この記事を書いている時点でもtrueのままである。

執筆・編集:
目次

Object.entries() はプリミティブを渡しても例外を投げない。真偽値や数値を渡すと空配列を返して、そのまま次の行へ進む。だから設定JSONで {"ki":[0,1], "sho":[2,3], ...} というオブジェクトを期待している箇所に true と書いても、パースは通り、バリデーションも通り、レンダも最後まで完走し、その機能だけが黙って無効化される。直し方は消費側ではなく検査側にある。設定を読む前段に「dictであること」を明示的に検査する工程を1つ足し、そこでビルドを止める。当サイトの動画ビルドでは isinstance(ktk, dict) のHARDチェックを台本lintに足して、違反時に終了コード1で build.sh を停止させる形にした(build.sh の先頭に set -euo pipefail があるため、lintが1で落ちると後続の工程は走らない)。

  1. Object.entries(true)Object.entries(42)[]。例外は出ない。 例外が出るのは nullundefined だけで、そのとき出るのは TypeError: Cannot convert undefined or null to object である(2026年8月14日・macOS 15.1.1・Node.js v25.9.0で実行して確認)
  2. 型違いは「表示が消える」のではなく「表示されたまま何も起きない」形で出る。 当サイトの起承転結インジケーターは if (!ktk) return null; で守られているが、true は真値なので通過し、4つの丸は描画されたまま1つも点灯しない。書き出し済みのQAフレーム10枚(2動画×5枚)すべてで、丸は灰色の輪郭のままだった
  3. 同じ設定を「配列で書く」なら大声で落ちる。 {"ki": true} のように中身を間違えると TypeError: chs.includes is not a function が出て止まる。静かに壊れるのは、外側の型を間違えたときである

症状:エラーが1行も出ない

この事故に「エラーメッセージ」は無い。それが症状である。

  • JSONのパースは通る(true は正当なJSON値)
  • レンダは最後まで完走する(今回は674.3秒の動画が1本、15分05秒の動画が1本、13分46秒の動画が1本)
  • コンソールに警告も出ない
  • 出来上がった動画には、インジケーターがちゃんと表示されている。ただし4つとも消灯している

検索で辿り着けるように、周辺で実際に出る文字列だけ先に置いておく。これらが出ているうちはまだ幸運で、今回の型違いではどれも出ない。

TypeError: Cannot convert undefined or null to object
TypeError: chs.includes is not a function

2つめのメッセージに出る chs は、V8がそのコードで書かれている変数名をそのまま入れるために出る名前である。当サイトの src/Main.jsx は24行目で const chs = entries[i][1]; と受けているので chs になる。同じ処理を entries[i][1].includes(...) と直接書いた場合は TypeError: entries[i][1].includes is not a function になる(2026年8月14日・Node.js v25.9.0で両方の書き方を実行して確認)。検索するときは変数名の部分が自分のコードに合わせて変わる。

当サイトが最終的に出るようにした文字列は、実行時エラーではなくlintの出力である。

❌ HARD違反 1件(ビルド停止・修正必須):
  kishoutenketsu が dict でない(例: {"ki":[0,1],"sho":[2,3],...})。true だとインジケーターが全編点灯しない

原因:ToObject は真偽値を「中身の無いオブジェクト」に変える

MDNの Object.entries() のページ(2025年7月20日最終更新・2026年8月14日確認)は、プリミティブを渡したときの挙動を専用の節で説明している。

Non-object arguments are coerced to objects. undefined and null cannot be coerced to objects and throw a TypeError upfront. Only strings may have own enumerable properties, while all other primitives return an empty array.

(オブジェクトでない引数はオブジェクトへ強制変換される。undefinednull はオブジェクトへ変換できず、その場で TypeError を投げる。自前の列挙可能プロパティを持ちうるのは文字列だけで、それ以外のプリミティブはすべて空配列を返す)

同ページのサンプルコードも同じことを示している。

console.log(Object.entries("foo")); // [ ['0', 'f'], ['1', 'o'], ['2', 'o'] ]
// Other primitives except undefined and null have no own properties
console.log(Object.entries(100)); // []

仕様側はもっと短い。ECMAScript® 2027 Language Specification(tc39.esの編集中ドラフト、last-modified は2026年8月13日22時52分51秒GMT・2026年8月14日取得)の 20.1.2.5 Object.entries ( obj ) は3ステップしかない。

  1. Let coerced be ? ToObject(obj).
  2. Let entryList be ? EnumerableOwnProperties(coerced, key+value).
  3. Return CreateArrayFromList(entryList).

(1. coerced を ToObject(obj) とする。2. entryList を EnumerableOwnProperties(coerced, key+value) とする。3. entryList から配列を作って返す)

肝心なのは1ステップ目の ToObject である。同仕様 7.1.19 ToObject ( arg ) はこう書いている。

If arg is either undefined or null, throw a TypeError exception. If arg is a Boolean, return a new Boolean object whose [[BooleanData]] internal slot is set to arg. If arg is a Number, return a new Number object whose [[NumberData]] internal slot is set to arg. If arg is a String, return a new String object whose [[StringData]] internal slot is set to arg.

arg が undefined か null なら TypeError を投げる/真偽値なら [[BooleanData]] 内部スロットに arg を入れた新しいBooleanオブジェクトを返す/数値なら [[NumberData]] 内部スロットに入れた新しいNumberオブジェクトを返す/文字列なら [[StringData]] 内部スロットに入れた新しいStringオブジェクトを返す)

真偽値も数値も、値は内部スロットに入る。内部スロットは列挙可能な自前プロパティではないので、次の EnumerableOwnProperties が拾うものが何も無い。結果が [] になる。文字列だけが例外なのは、文字列オブジェクトがインデックス番号を自前プロパティとして持つからである。

2026年8月14日に手元で実行した結果が次である(macOS 15.1.1/ビルド24B91、Node.js v25.9.0)。

true         -> []
false        -> []
42           -> []
"ab"         -> [["0","a"],["1","b"]]
{"ki":[0,1],"sho":[2,3]} -> [["ki",[0,1]],["sho",[2,3]]]
null         -> TypeError: Cannot convert undefined or null to object
undefined    -> TypeError: Cannot convert undefined or null to object

消費側は「真値かどうか」しか見ていなかった

当サイトの動画ビルド(Remotion 4.0.474/React 18.3.1)で、起承転結インジケーターを描いているのは src/Main.jsx の22行目を含むループである。

const KishoutenketsuIndicator = ({slides, ktk}) => {
  const frame = useCurrentFrame();
  if (!ktk) return null;
  // ...
  let activeIdx = -1;
  const entries = Object.entries(ktk);
  for (let i = 0; i < entries.length; i++) {
    const chs = entries[i][1];
    if (chs.includes(currentCh)) activeIdx = i;
  }

if (!ktk) return null; というガードは付いている。しかしこれは「設定が無ければ描かない」ためのガードであって、型のガードではないtrue は真値なので通過する。通過したあと Object.entries(true)[] を返し、ループは0回で終わり、activeIdx-1 のまま。この後の描画は i === activeIdx で点灯を決めているので、4つの丸すべてが「非アクティブ」として描かれる。同じ形の Object.entries(ktk)src/ui.jsx の167行目(週刊ニュース用の帯)にもある。

手前の工程も止めていなかった。台本JSONからタイムラインを生成するPython側(prep.py 260〜264行目)は、こう書いている。

ktk = sc.get("kishoutenketsu") if isinstance(sc, dict) else None
# ...
if ktk:
    timeline["kishoutenketsu"] = ktk

if ktk: は真偽判定であって型判定ではないので、true はそのまま src/timeline.json へ書き出される。JSON→Python→JSXの3工程を、型を見る人が誰もいないまま通り抜けた。

実物:QAフレーム10枚すべてで消灯していた

書き出し済みのフレームを実際に見た結果である。2026年8月14日に、既存のQAフレーム(deliver.py がレンダ後に自動で抜く静止画)から右上のインジケーター領域だけを切り出して確認した。

台本 ビルド日時 確認したフレーム 結果
discovery_loop.json(138スライド・11章・実尺15分05秒) 2026-08-06 21:04 QA 5枚 5枚とも4つの丸すべて消灯
muse_code.json(148スライド・12章・実尺13分46秒) 2026-08-06 09:22 QA 5枚 5枚とも4つの丸すべて消灯
hf_seedance25_yt.json(58スライド・11章・674.3秒) 2026-08-08 01:39 完成mp4から60秒地点・250秒地点 2枚とも4つの丸すべて消灯

比較のために、kishoutenketsu がdict形式で書かれている台本(kimi_k3_open.json・266スライド・17章)のQAフレーム5枚を同じ手順で切り出したところ、こちらは順に 起(青)/承(緑)/転(赤)/転(赤)/結(橙)が点灯していた。同じコード・同じ描画位置で、設定の型だけが違う。

つまり画面上は「インジケーターが出ていない」のではなく「インジケーターは出ているのに、進行を示す色が一度も付かない」。目視レビューで見落としやすいのはこの形である。

対処

1. 検査側に型のHARDチェックを足し、ビルドを止める

当サイトが入れたのは、台本lint(script_lint.py 29〜33行目)の次の5行である。

# kishoutenketsu形式チェック(2026-08-08追加): Main.jsxは {"ki":[0,1],...} のdict形式を期待。
# true(boolean)だと Object.entries(true)=[] でハイライトが一度も点かない(astra_critical/discovery_loop等で実害)
ktk = d.get("kishoutenketsu")
if ktk is not None and not isinstance(ktk, dict):
    hard.append('kishoutenketsu が dict でない(例: {"ki":[0,1],"sho":[2,3],...})。true だとインジケーターが全編点灯しない')

build.sh は先頭に set -euo pipefail があり、31行目で python3 "$DIR/script_lint.py" "$SCRIPT" を呼ぶ。lintがHARD違反で終了コード1を返せば、そこでビルド全体が止まる。2026年8月14日に実際に走らせて確認した終了コードは次のとおり。

台本 kishoutenketsu の型 lintの終了コード
discovery_loop.json bool(true 1
astra_critical_stop.json dict 0

2. 「truthyで通す」箇所を型で通す箇所に変える

if (!ktk) return null;if ktk: は、未設定を弾くためのコードであって、誤設定を弾くためのコードではない。設定が構造を持つ(オブジェクト・配列)なら、真偽ではなく構造で判定する。JSでの最小の直しは次になる。

if (ktk === null || typeof ktk !== "object" || Array.isArray(ktk)) return null;

ただし当サイトはこの直しをJSX側には入れていない。表示側で黙って握りつぶすと「設定を書いたのに出ない」に戻るので、lintで先に止める側を選んだ。どちらを取るかは、その機能が「無ければ無いで良いもの」か「無かったら気づきたいもの」かで変わる。

3. レンダ後に実物のフレームを見る

このバグはコードを読んでも、JSONを読んでも見つからない。出来上がったフレームを見ると一発で分かる。 当サイトは1本あたり5枚前後の静止画をレンダ後に自動で抜く運用にしているが、今回はその5枚を「見た」つもりで見ていなかった。長尺を分割レンダする場合は最初の区間が終わった時点で抜いて確認したほうが、やり直しの単位が小さくなる。

同型の穴:キー名・キー順・出力の取りこぼし

同じ「型や形が変わったのに誰も検査していない」系の穴が、当サイトの同じパイプラインで3つ見つかっている。

(a)キー名の綴り違いで章名が読めなくなる。 prep.py は章名を chapters[k]["title"] で読む(116〜120行目)。"name" で書かれていると v.get("title", "") が空文字を返し、章名だけが消える。これも例外は出ない。対処として script_lint.py の110〜119行目に、chapters の各要素が title キーを持つかを見るHARDチェックを追加してある(name / chTitle / label で書かれている場合は、どのキーで書かれているかを出力に含める)。このチェックのコメントには「2026-07-29追加・章ピルが「第N章」だけになる事故」と書かれているが、そのとき何章分が空になったかの記録は残っていないので、本記事では章数を書かない。 2026年8月14日時点で chapters"name" で書いている台本は4本残っており(9章/10章/14章/17章)、そのうち kimi_k3_open.json にlintをかけるとHARD違反17件・終了コード1になる(同日実行して確認)。

(b)キーの順序が意味を持っている。 インジケーターは Object.entries() が返した順番のi番目を 起・承・転・結 のi番目に対応させている。キー名は一切見ていない。したがって次のようになる(2026年8月14日実行)。

設定 章0のときに点灯する丸
{"ki":[0,1],"sho":[2,3],"ten":[4,5],"ketsu":[6]}
{"ketsu":[6],"ten":[4,5],"sho":[2,3],"ki":[0,1]}
{"起":[0,1],"承":[2,3],"転":[4,5],"結":[6]}

キー名を日本語にしても動くが、並べ替えると黙って別の丸が点く。これは前述のHARDチェックでは検出できない(dictではあるため)。

(c)dictに変えた側が、別の出力で生のまま漏れる。 章情報を「文字列」から {"stage": ..., "title": ...} の辞書に変えたとき、prep.py は両方を扱えるように直された(116〜120行目で isinstance(v, dict) を見て v.get("title", "") を取る)が、納品セットを生成する deliver.py の73行目は chapters.get(str(ch), ...) のままである。結果、生成された概要欄用のチャプター行に辞書がそのまま出ている。

0:00 {'stage': 'ki', 'title': '世界最強のプログラマーが辞めた'}
0:33 {'stage': 'ki', 'title': '8月5日に起きたこと'}

2026年8月14日時点で、この形の行は out/discovery_loop_納品セット.md に11行、out/muse_code_納品セット.md に12行残っている。deliver.py 側は未修正である。片方の消費者だけ直して、もう片方を忘れるのが、この系統のバグの再発の仕方である。

観測条件(2026年8月5日〜8月14日・当サイトのパイプライン1系統)

環境は macOS 15.1.1(ビルド24B91)、Node.js v25.9.0、Remotion 4.0.474、React 18.3.1。台本JSONは同一ディレクトリに置いている。時系列は各ファイルの更新時刻から取った。

日時 出来事
2026-08-05 16:01 aisi_incident.json(447スライド・18章)を "kishoutenketsu": true で保存
2026-08-06 08:53 → 09:22 muse_code.jsontrue で保存 → ビルド完了・QAフレームと納品セット生成
2026-08-06 20:27 → 21:04 discovery_loop.jsontrue で保存 → ビルド完了・QAフレームと納品セット生成
2026-08-08 01:16 → 01:39 hf_seedance25_yt.jsontrue で保存 → 完成mp4(674.3秒)出力
2026-08-08 09:55 astra_critical_stop.json の最終更新(lint追加の13分前)。同ファイルの現在の kishoutenketsu はdict形式
2026-08-08 10:08 script_lint.py にHARDチェックを追加(コメントに「2026-08-08追加」と明記)
2026-08-14 全台本JSONを再点検。"kishoutenketsu" を持つのは台本64本+生成物 src/timeline.json 1件の計65ファイル(grep -rl '"kishoutenketsu"' の実測。台本64本にはバックアップ1本とテスト用1本を含む。この記事は別リポジトリにあるので自己ヒットは無い)。うち true のまま残っているのは discovery_loop.json / muse_code.json / hf_seedance25_yt.json / aisi_incident.json4本

lintを足しても、既に踏んだ分は直らない。 lintのコメントが名指ししている astra_critical_stop.json は、lint追加の13分前(8月8日9時55分)が最終更新で、現在の kishoutenketsu はdict形式である。一方、true のまま残っている4本の最終更新はいずれも8月8日10時08分より前で、lintを足したあと一度も触られていない。lintは以後のビルドを止めるが、既にビルドが終わっている台本のJSONまでは直してくれない。機械の検査を足すことと、既に踏んだ分を直すことは別の作業である。

正直な但し書き

この4本が公開されたかどうかは、この記事では確認していない。 ビルドが完走し、QAフレームと納品セットが生成されたところまでは手元のファイルで確認できる。公開状況を確かめようとYouTube Data APIを叩いたが、2026年8月14日時点で invalid_grant: Bad Request が返り、認証が通らなかった。したがって「公開された」とは書かない。確認できたのは「ビルドと納品セット生成まで到達した台本が4本ある」までである。

Object.entries(true) の実行はNode.js v25.9.0で行った。 Remotionのレンダが実際に走るのはヘッドレスChrome(V8)であり、そちらで同じ式を直接実行して確認したわけではない。ただしどちらもECMAScript仕様に従う実装であり、かつ書き出されたフレームで丸が消灯している(=activeIdx-1 のままだった)という結果は一致している。

lintのHARDチェックは「dictであること」しか見ていない。 章番号が実在するか、4つのステージが揃っているか、キーの順序が起承転結の順かは検査していない。前述のキー順の穴はこのチェックを素通りする。当サイトはまだこの検査を足していない。

deliver.py の辞書漏れは未修正である。 この記事を書いている時点では、生成済みの納品セット2本に痕跡が残っているだけで、コード側の修正は入れていない。

「静かに壊れるか、大声で壊れるか」の境目は今回の型に限った観察である。 {"ki": true} のように内側を壊すと TypeError: chs.includes is not a function で止まることは実行して確認したが、あらゆる誤設定について「外側が静か・内側が大声」と一般化できるかは検証していない。たとえば文字列 "ki" を渡した場合は Object.entries("ki") が2要素を返し、例外も出ず、点灯もしないという第3のパターンになる(同日実行して確認)。

MDNの日本語版ページには機械翻訳の可能性がある。 本記事の引用は英語版の原文を採り、訳は当サイトで付けた。訳の責任は当サイトにある。仕様の引用元 tc39.es/ecma262 は編集中のドラフト(Living Standard)であり、確定版の版番号ではない。取得時の last-modified は、fundamental-objects.html が2026年8月13日22時52分51秒GMT、abstract-operations.html が同日22時52分49秒GMTである(ページごとに別々の値が返る)。

出典

シェア: ポスト はてブ

出典・参照資料

AIニュースの解説を動画でも

YouTubeでは注目ニュースの背景を解説し、Xでは新着記事をお知らせしています。

コメント

まだコメントはありません。最初のコメントを書いてみませんか?

AIについて聞きたいことはありますか?

質問箱で無料で受け付けています。回答は公開され、他の方の参考にもなります。

質問箱を見る →

新しい記事をメールで受け取る

AIの新しい発表を、出典付きで整理して届けます。

関連記事

AI時短ラボ
活用08.14読了31

RemotionのLoopはコンポジション絶対フレームで位相を計算する──カード内の動画クリップが登場した瞬間に終盤から再生される原因と直し方

出典 ─ <Loop> | Remotion公式ドキュ
AI時短ラボ
活用08.14読了26

pkillはレンダプロセスを取り逃す──chrome-headless-shellの孤児36個でWindowServerが73秒無応答になった記録

出典 ─ Errors(セクション: pkill pattern matches the Claude Code process)
AI時短ラボ
活用08.14読了23

ffmpegのエンコード指定を出力ファイル名より後ろに置くと効かない──96000Hzのまま連結して尺が43秒に化けた話

出典 ─ ffmpeg Documentation -
AI時短ラボ
活用08.14読了31

字数でテロップの行数を検査すると通り抜ける──inline-blockのチップと英単語の塊で、85字以内なのに3行になる

出典 ─ display - CSS | MDN
AI時短ラボ
活用08.14読了39

VOICEVOXの/audio_queryのmoras[]から口パクを作る──母音→口形状マップと、実wav尺へのratio補正が「?」で崩れる話

出典 ─ VOICEVOX ENGINE OSS AP
Remotionで長い動画のレンダリングがクラッシュする──分割レンダとffmpeg無劣化連結で完走させる手順の記事画像
活用07.16読了7

Remotionで長い動画のレンダリングがクラッシュする──分割レンダとffmpeg無劣化連結で完走させる手順

出典 ─ npx remotion render |
AIでショート動画を作り続ける工程──固定する5つと、毎回変える3つの記事画像
活用08.14読了16

AIでショート動画を作り続ける工程──固定する5つと、毎回変える3つ

出典 ─ YouTube ショートの作成を始める -
動画生成AIを数十本まとめて回す──CLIとWebの使い分けと、接続が切れた時の実ログの記事画像
活用08.14読了14

動画生成AIを数十本まとめて回す──CLIとWebの使い分けと、接続が切れた時の実ログ

出典 ─ Video generation - Ope