◉ANTIGRAVITY LABEN
記事一覧/Agents & Manager
◈ Agents & Manager/2026-10-14上級

位置で読む API 応答を変えるとき、出荷済みの旧版を起動時に落とさないための門番を置きました

壁紙アプリのように、サーバーの一覧応答を「何番目の項目か」で読むクライアントは、出荷済みの旧版が長く残ります。項目の削除や並び替えで旧版を起動時に落とさないよう、末尾追加だけを通す検査と、出荷済みの版ディレクトリを凍結する台帳の作り方を、動かした出力つきで書き残します。

API設計後方互換個人開発104モバイルアプリ3CI10Antigravity380

✦ プレミアム記事

ストアに出したアプリは、私の手元の都合では引っ込められません。新しい版を出した日にも、古い版は誰かの端末で動き続けます。そして古い版は、出荷したときのサーバーの応答の形を、そのまま覚えております。

個人開発で運用している壁紙アプリは、サーバーが返す一覧を「何番目の項目が何か」で読む作りです。1番目が ID、2番目がタイトル、3番目がカテゴリ、というように、項目の名前ではなく位置で受け取っております。この形は軽くて速い反面、サーバー側で項目を一つ消したり、並びを入れ替えたりした瞬間に、出荷済みの旧版が別の項目を読み始めます。私の運用では、その結果として旧版が起動時にクラッシュする、というところまで進みます。

だから決まりは一つに絞りました。位置で読まれる応答には、項目を末尾にしか足しません。 削除も並び替えもしません。ただ、決まりは書いただけでは守れません。エージェントに配信ツリーの整理を任せた日や、疲れた夜の手作業では、うっかり破ります。そこで、決まりを機械に守らせることにしました。

ここでは、その門番を二つ、動かした出力つきでお見せします。一つは応答の形を比べる検査、もう一つは出荷済みの版ディレクトリを凍結する台帳です。

先に、何を守るのかを一枚の表にしました

最初にやったのは、変えてよいものと変えてはいけないものの仕分けです。「位置で読まれる」と一口に言っても、壊れ方は変更の種類ごとに違います。

サーバー側の変更旧版への影響扱い
行(壁紙)を末尾に追加旧版は新しい行を普通に読むだけ自由に行う
各行の末尾に項目を追加旧版は知らない位置を読まない許可(追加は末尾のみ)
途中の項目を削除後ろの項目が一つ前にずれる禁止
項目の並び替え別の項目を読む。型が同じなら気づきにくい禁止
出荷済みの行を削除・並べ替えお気に入りや通知の参照先が消える禁止
閲覧数のように、もともと値が動く項目の更新位置は変わらない許可(変えてよい位置を明示)

この表の三行目と四行目が、事故の中心です。行を足すのは安全で、項目を足すのも末尾なら安全です。危ないのは、すでに出荷した位置の意味を動かす変更だけです。検査はこの一点を見張ればよい、と整理できました。

一つ目の門番: 出荷済みの応答と、上げる応答を比べます

発想は単純で、出荷した時点の応答を「基準」として一つ取っておき、これから上げる応答と突き合わせます。見るのは四つです。

  1. 一番短い行の列数が減っていないか
  2. 既存の位置の型が変わっていないか
  3. 先頭の列を主キーとみなし、出荷済みの行が消えたり並びが入れ替わったりしていないか
  4. 出荷済みの行の値が、変えてよいと明示した位置を除いて一致しているか
#!/usr/bin/env python3
"""出荷済み版の応答(baseline)と、これから上げる応答(candidate)を比べ、
「位置で読む」クライアントを壊す変更だけを落とす。
使い方: append_only_check.py BASELINE.json CANDIDATE.json [--mutable 3,5]
--mutable: 出荷済みの行でも値が変わってよい列の位置(例: 閲覧数)。それ以外は値まで一致を求める
終了コード: 0=安全 / 1=壊す変更あり / 2=入力不正
"""
import json, sys
from collections import Counter
 
def kind(v):
    # 値でなく「型」を見る。壁紙が増えて値が変わるのは正常だから
    if isinstance(v, bool): return "bool"
    if isinstance(v, int): return "int"
    if isinstance(v, float): return "float"
    if isinstance(v, str): return "str"
    if v is None: return "null"
    return type(v).__name__
 
def column_kinds(rows):
    """列ごとの型の集合(null は「未設定」なので型の判定から外す)"""
    cols = {}
    for r in rows:
        for i, v in enumerate(r):
            if v is not None:
                cols.setdefault(i, set()).add(kind(v))
    return cols
 
def main(base_path, cand_path, mutable=frozenset()):
    try:
        base = json.load(open(base_path, encoding="utf-8"))
        cand = json.load(open(cand_path, encoding="utf-8"))
        assert all(isinstance(r, list) for r in base + cand)
    except Exception as e:
        print(f"入力不正: {e}"); return 2
 
    problems = []
    # 1) 行ごとの列数: 出荷済みの行より短くしない
    base_w = Counter(len(r) for r in base)
    cand_w = Counter(len(r) for r in cand)
    min_base, min_cand = min(base_w), min(cand_w)
    if min_cand < min_base:
        problems.append(f"最短の行が {min_base} 列 → {min_cand} 列に短くなっています")
 
    # 2) 既存の列の型が変わっていないか(位置 i の意味が入れ替わっていないか)
    bk, ck = column_kinds(base), column_kinds(cand)
    for i in sorted(bk):
        if i not in ck:
            problems.append(f"列 {i} が候補から消えています(型 {sorted(bk[i])})")
        elif not ck[i] <= bk[i]:
            problems.append(f"列 {i} の型が {sorted(bk[i])} → {sorted(ck[i])} に変わっています")
 
    # 3) 先頭列を主キーとみなし、出荷済みの行が消えていないか・並びが入れ替わっていないか
    base_ids = [r[0] for r in base]
    cand_ids = [r[0] for r in cand]
    if cand_ids[:len(base_ids)] != base_ids:
        gone = [x for x in base_ids if x not in set(cand_ids)]
        problems.append(f"出荷済みの行の先頭一致が崩れています(消えた行 {len(gone)} 件)"
                        if gone else "出荷済みの行の並びが入れ替わっています")
 
    # 4) 同じ型の列どうしの入れ替えは型だけでは見えない。出荷済みの行は値まで一致を見る
    by_id = {r[0]: r for r in cand}
    diffs = Counter()
    for r in base:
        c = by_id.get(r[0])
        if c is None: continue
        for i, v in enumerate(r):
            if i in mutable: continue
            if i >= len(c) or c[i] != v: diffs[i] += 1
    for i, n in sorted(diffs.items()):
        problems.append(f"列 {i}: 出荷済みの行 {n} 件で値が変わっています(入れ替えの疑い)")
 
    print(f"baseline: {len(base)} 行 / 列数 {dict(sorted(base_w.items()))}")
    print(f"candidate: {len(cand)} 行 / 列数 {dict(sorted(cand_w.items()))}")
    if problems:
        for p in problems: print("NG  " + p)
        return 1
    added_cols = max(cand_w) - max(base_w)
    print(f"OK  末尾追加のみ(追加行 {len(cand)-len(base)} 件・追加列 {max(added_cols,0)} 本)")
    return 0
 
if __name__ == "__main__":
    args = [a for a in sys.argv[1:]]
    mutable = frozenset()
    if "--mutable" in args:
        k = args.index("--mutable")
        mutable = frozenset(int(x) for x in args[k + 1].split(",") if x)
        del args[k:k + 2]
    if len(args) != 2:
        print(__doc__); sys.exit(2)
    sys.exit(main(args[0], args[1], mutable))

実際に動かして確かめた出力を、ここに残します。基準は 3 行 × 4 列の小さな応答です。実運用の応答の形そのものではなく、検査の挙動を確かめるための見本として用意しました。

--- ok(行を1件足し、各行の末尾に1列足した)
baseline: 3 行 / 列数 {4: 3}
candidate: 4 行 / 列数 {5: 4}
OK  末尾追加のみ(追加行 1 件・追加列 1 本)
exit=0
 
--- drop(3列目を丸ごと消した)
baseline: 3 行 / 列数 {4: 3}
candidate: 3 行 / 列数 {3: 3}
NG  最短の行が 4 列 → 3 列に短くなっています
NG  列 2 の型が ['str'] → ['int'] に変わっています
NG  列 3 が候補から消えています(型 ['int'])
NG  列 2: 出荷済みの行 3 件で値が変わっています(入れ替えの疑い)
exit=1
 
--- rowgone(出荷済みの2行目を消した)
NG  出荷済みの行の先頭一致が崩れています(消えた行 1 件)
exit=1
✦

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

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
✦サーバーの応答を直す前に、出荷済みの旧版が「何番目の項目」を読んでいるかを棚卸しして、変えてよい箇所と末尾にしか足せない箇所を線引きできるようになります
✦型の比較だけでは同じ型の列の入れ替えを見逃す理由が分かり、値の一致まで見る検査をアップロード前に自動で走らせる形を自分のスクリプトに移せるようになります
✦出荷済みの版ディレクトリをハッシュで凍結しておくことで、エージェントに配信ツリーを任せた日にも旧版を壊す変更をアップロード前に止められ、公開後に旧版の不具合報告を追いかける時間を省けるようになります
Stripe による安全な決済 · いつでもキャンセル可能
✦

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

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

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

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

関連記事

◈ Agents & Manager2026-10-14
エージェントの PR は人の PR より待たされると思っていましたが、公開リポジトリの 246 件を数えると逆でした
dotnet/runtime の 2026 年 7〜8 月の PR 246 件で、エージェントが開いた PR の「最初の人の反応」までの時間を人の PR と比べました。計測スクリプトと、個人開発向けの滞留チェックまで載せています。
◈ Agents & Manager2026-07-29
agent.md の綴り誤りが権限を広げていた — frontmatter を厳格に検証する lint を書く
agent.md の frontmatter は綴りを間違えてもエラーになりません。既定値へ静かに落ちた結果、意図と逆の権限が適用されていた実例と、権限キーだけを厳しく検証する lint の実装・実測を記録しました。
◈ Agents & Manager2026-04-27
Antigravity を個人開発の「夜のもう1人」として迎え入れる運用モデル
Antigravity のエージェントを、個人開発者にとっての「夜のもう1人」として運用するための設計指針。タスクの渡し方・境界線の引き方・朝の3点確認をスクリプトにする方法まで、実際に運用して整えてきた手順を書き残します。