ANTIGRAVITY LABEN
記事一覧/Editor View
Editor View/2026-09-01初級

YAML frontmatter がエラーを出さずに値を書き換える条件を確かめました

Antigravity 2.11.0 は frontmatter を整形カードとして描画します。カードにならない1本を追ううちに、パースが通るのに値が変わるパターンが5つ見つかりました。実測した分岐条件と、40行の検査スクリプトをまとめます。

frontmatterYAMLMarkdownAntigravity 2.112

Antigravity 2.11.0 に上げてから、Markdown ファイルを開いたときの見た目が変わりました。冒頭の --- で囲まれた frontmatter が、生のテキストではなく、キーと値が並んだ整形済みのカードとして描かれます。

しばらくは「読みやすくなった」以上の感想を持っていませんでした。ところがある日、いつもどおりファイルを開いたら、1本だけカードにならずに生のテキストのまま表示されていたのです。

原因はタイトルの中のコロンでした。ただ、そこで終わらせずに周辺を調べたところ、エラーを出さないまま値が変わってしまうパターンのほうが多いことがわかりました。落ちてくれる壊れ方は、カードにならないので目で気づけます。困るのはカードが普通に描かれているのに、中身が自分の書いたものと違っている場合です。

そこで分岐条件を一つずつ手を動かして確かめ、公開前に走らせる検査を用意しました。以下はその記録です。

コロンがあると壊れる、ではありませんでした

最初に思い込んでいたのは「値の中にコロンを書いたら壊れる」でした。これは正確ではありません。

Python の PyYAML 6.0.3 で、同じ1行をパターンだけ変えて読ませてみました。

書いた行結果
title: Antigravity 2.11.0: 何が変わったかScannerError(落ちる)
title: Antigravity 2.11.0:何が変わったか文字列として通る
note: 実行は 12:30 から文字列として通る
title: Antigravity 2.11.0:何が変わったか(全角コロン)文字列として通る
title: "Antigravity 2.11.0: 何が変わったか"文字列として通る

分岐しているのはコロンの有無ではなく、コロンの直後に半角空白があるかどうかでした。12:30 が無事なのも、全角コロンが無事なのも、同じ理由です。YAML はコロンそのものではなく「コロン+半角空白」または「行末のコロン」をキーと値の区切りとして扱います。

ここまでなら、落ちてくれる分だけ親切な仕様です。問題は次からでした。

エラーを出さずに値が変わるパターンが5つありました

同じやり方で範囲を広げて確かめたところ、パースは成功するのに、読み取った値が書いたものと違うケースが並びました。

書いた行読み取られた値何が起きたか
title: 見出し #1 の話"見出し"空白+# 以降がコメント扱いで切り捨てられる
title: #1 の話None行全体がコメント扱いになり値が消える
draft: yesTrue(真偽値)文字列のつもりが真偽値になる
draft: noFalse(真偽値)同上
code: 012383(整数)先頭ゼロが8進数として解釈される

最後の1行は、実際に見たとき二度見しました。0123 と書いたつもりの値が 83 になっています。8進数の 0123 は10進で 83 ですから、YAML としては筋の通った解釈です。しかし商品コードや型番を frontmatter に置いている場合、これは黙って別の値になったのと同じです。

yesno も同じ性質です。言語コードや短い回答を値に持たせていると、文字列を期待した先に真偽値が届きます。

そして、私が最初に踏んだのはこの表にない6つ目でした。

リスト項目の中のコロンだけは、型そのものが変わります

highlightstags のように、リストを持つキーがあります。その項目の中でコロンを使うと、こうなります。

highlights:
  - 判定できる: 切り分け方がわかる

このブロックは、エラーを出さずに次のように読まれます。

{'highlights': [{'判定できる': '切り分け方がわかる'}]}

リストの中身は文字列ではなく、キーが1つだけの辞書になっています。項目をダブルクォートで囲めば、期待どおり文字列として読まれます。

highlights:
  - "判定できる: 切り分け方がわかる"

厄介なのは、この差がファイルを眺めている限りほとんど見えないことです。インデントも記号も正しく、YAML としても正当です。ビルドも通ります。

では、その値がテンプレートまで届くとどうなるか。Node.js v22 で、辞書になってしまった項目を文字列として扱ってみました。

const item = { "判定できる": "切り分け方がわかる" };
 
console.log(`${item}`);                       // [object Object]
console.log([item, "正しい項目"].join(" / ")); // [object Object] / 正しい項目

文字列連結の経路なら [object Object] がそのままページに出ます。React のように、オブジェクトを子要素として渡された時点で例外を投げる仕組みであれば、レンダリング側で落ちます。書いた場所と壊れる場所が離れているので、原因にたどり着くまでが遠いというのが、この壊れ方のいちばん面倒なところです。

なお、リスト項目に - a: b: c のようにコロンを2つ書いた場合はパース時に落ちます。1つのときだけ黙る、という覚え方でだいたい足ります。

落ちる側の壊れ方も控えておくと切り分けが速くなります

黙る側ばかり書きましたが、落ちる側も把握しておくとカードが描かれなかったときの当たりがつけやすくなります。同じ環境で確かめた結果です。

書いた行結果
title: 見出し:(行末がコロン)ScannerError
title: のあとにタブ文字ScannerError
owner: @doliceScannerError(@ は予約文字)
title: *強調 の話ScannerError(* はエイリアス記法)
title: [検証] 手順ParserError(フロー配列として解釈される)
title: {検証} 手順ParserError(フロー辞書として解釈される)

一方で title: -3 の補正 は文字列として素直に通ります。先頭のハイフンは、直後に空白がなければリスト項目とは見なされないためです。

こうして並べると、避けるべき文字は @ * [ { # と、タブ、そして「コロン+半角空白」に絞られます。私はこの7つを覚えるより、値に記号が混ざるなら最初からダブルクォートで囲むほうを習慣にしました。囲んでいれば上の表のほとんどは無害になります。

公開前に走らせている40行の検査

黙る壊れ方は目視では拾えないので、機械に読ませます。frontmatter を全数パースして、落ちるものと型が変わったものを分けて報告するだけのスクリプトです。

#!/usr/bin/env python3
"""Markdown の frontmatter を全数パースし、落ちるものと黙って型が変わるものを分けて報告する。"""
import sys, glob, yaml
 
# キー名 -> 期待する Python 型
EXPECTED = {
    "title": str,
    "description": str,
    "tags": list,
    "highlights": list,
}
# 中身まで str であることを求めるリスト型キー
STR_LIST_KEYS = {"tags", "highlights"}
 
def split_frontmatter(text):
    if not text.startswith("---"):
        return None
    end = text.find("\n---", 3)
    if end == -1:
        return None
    return text[3:end]
 
def check(path):
    problems = []
    block = split_frontmatter(open(path, encoding="utf-8").read())
    if block is None:
        return [("NO_FRONTMATTER", "先頭に --- で囲まれたブロックがありません")]
    try:
        data = yaml.safe_load(block)
    except yaml.YAMLError as e:
        line = getattr(getattr(e, "problem_mark", None), "line", None)
        where = f"{line + 2} 行目付近" if line is not None else "位置不明"
        return [("PARSE_ERROR", f"{type(e).__name__}: {where}")]
    if not isinstance(data, dict):
        return [("NOT_A_MAP", f"最上位が {type(data).__name__} です")]
    for key, want in EXPECTED.items():
        if key not in data:
            continue
        got = data[key]
        if not isinstance(got, want):
            problems.append((
                "TYPE_DRIFT",
                f"{key}{want.__name__} を期待しましたが {type(got).__name__} でした",
            ))
            continue
        if key in STR_LIST_KEYS:
            for i, item in enumerate(got):
                if not isinstance(item, str):
                    problems.append((
                        "SILENT_MAP",
                        f"{key}[{i}] が {type(item).__name__} です。"
                        f"未クォートのコロンが原因です -> {item}",
                    ))
    return problems
 
def main(paths):
    bad = 0
    for path in sorted(paths):
        problems = check(path)
        if not problems:
            print(f"OK          {path}")
            continue
        bad += 1
        for kind, detail in problems:
            print(f"{kind:<12}{path}: {detail}")
    print(f"\n検査 {len(paths)} 件 / 問題あり {bad} 件")
    return 1 if bad else 0
 
if __name__ == "__main__":
    targets = sys.argv[1:] or glob.glob("content/**/*.md", recursive=True)
    sys.exit(main(targets))

意図的に壊した5ファイルに対して走らせた結果です。

PARSE_ERROR content/at.md: ScannerError: 3 行目付近
PARSE_ERROR content/hard.md: ScannerError: 3 行目付近
OK          content/ok.md
SILENT_MAP  content/silent.md: highlights[0] が dict です。未クォートのコロンが原因です -> {'判定できる': '切り分け方がわかる'}
PARSE_ERROR content/tab.md: ScannerError: 3 行目付近
 
検査 5 件 / 問題あり 4 件

要点は3つあります。

第一に、EXPECTED期待する型を書き下すこと。パースが通ったかどうかだけを見る検査では、highlights が辞書のリストになっていても素通りします。型を宣言してはじめて SILENT_MAP が拾えます。

第二に、リスト型のキーは中身まで見ること。isinstance(got, list) は辞書のリストでも真になります。STR_LIST_KEYS のループがないと、いちばん困る壊れ方だけがすり抜けます。

第三に、問題があれば終了コード 1 を返すこと。公開手順の中でこのスクリプトを挟んでおけば、黙って壊れた値がそのまま外に出ることはなくなります。個人開発でいくつかの Markdown ベースのサイトを回しておりますが、この検査を入れる前は、公開後に読者から見える形になってようやく気づくことがありました。手元で1秒かからずに終わる検査で防げるなら、そちらのほうが良いと考えています。

エージェントにファイルを編集させる場面が増えている今は、なおさら効きます。非 ASCII を含むファイルの編集についても日本語を含むファイルをエージェントに編集させる前に、バイト列で確かめている3つの検査で別の角度から書いています。ルールファイルや agent 定義の frontmatter を厳格に検証したい場合はagent.md の綴り誤りが権限を広げていたのほうが近い話です。

次の一歩

まず、いま手元にある Markdown を1本開いて、Antigravity のエディタで frontmatter が整形カードとして描かれているかを見てみてください。カードにならない1本があれば、そこには落ちる側の壊れ方が入っています。

カードが全部きれいに描かれていても安心はできません。そのときは上のスクリプトを保存して、コンテンツのディレクトリに向けて一度走らせてみてください。黙っていた分は、そこで初めて声を上げます。

お読みいただきありがとうございました。

シェア

お読みいただきありがとうございます

Antigravity Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥2,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

Editor View2026-08-29
Generative UI で描いたグラフは、外部参照を残したまま渡すと欠けます
Antigravity 2.11.0 の Generative UI が描く HTML は、多くの場合 CDN 参照付きで生成されます。外部参照を数える20行のチェッカーと、一枚に畳む手順を実測値つきで紹介します。
Editor View2026-08-27
Antigravity 2.10.0 の埋め込みターミナルで、エージェントの変更を検証するループを組む
2.10.0 でサイドバーから直接ターミナルを開けるようになりました。エージェントの変更を通す前の検証を3つのコマンドに固定し、変更ファイルだけを対象にする手順を、実測値とスクリプト付きでまとめます。
Editor View2026-08-27
日本語を含むファイルをエージェントに編集させる前に、バイト列で確かめている3つの検査
エージェントの編集で日本語が1文字だけ入れ替わっても、git の差分は普通の1行変更として通ります。不正な UTF-8・置換文字・正規化ゆれの3つを、1パスの検査にまとめるまでの運用記録です。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →