結論から書きます。個人の発信をJSON形式の公開スケジュールで一元管理しています。技術記事(Zenn)はスクリプトによる自動公開・公開後の検証・週次監査まで仕組みが揃っている一方、エッセイ(note)は毎回ブラウザから手作業で公開していました。この「手作業だけ」の公開枠で、1本の記事が予定日から29日間、誰にも気づかれないまま滑落していました。
原因は複雑な不具合ではありません。「当日分だけを表示する朝レポート」という、ごく普通の設計そのものにありました。自動化された工程には「動いていない」ことを検知する仕組みが自然に付きますが、手動の工程には何も付きません。29日間の滑落がどう起きたか、追いつき公開でさらに何を見落としたか、そしてLLMを使わない条件分岐だけで作った検知ロジックの中身を、以下に書きます。
気づかれないまま29日が過ぎた公開予定
公開スケジュールは1本のJSONファイルで管理していて、各エントリに媒体・タイトル・予定日を持たせています。毎朝Telegramに届く状況レポートは、このスケジュールから「当日分」だけを読んで表示する作りでした。今日の予定を一目で確認できるようにという、素直な設計です。
7月2日はnoteの公開予定日でした。この日を含む3日間は、リポジトリのコミットがゼロでした。他の作業に時間を取られて、公開作業自体が行われなかったということです。ここまではよくある話です。問題はこのあとに起きました。
予定日を過ぎた瞬間、そのエントリは「当日分だけを表示する」朝レポートから外れ、二度と浮上しなくなりました。レポート側のロジックは「今日の日付と一致するエントリを1件返す」だけなので、日付が一致しなくなったエントリを別の形で拾い上げる経路がどこにもなかったのです。過ぎた予定を「遅延中」として見せる機能が、そもそも存在していませんでした。
7月9日・7月16日の公開枠も、同じパターンで滑落しました。3週続けて同じ穴に落ちたということです。実測のKPIスナップショットを見返すと、17日間にわたって公開数が変わらないまま記録され続けていました。毎朝レポートを眺めていても、それが「今日は予定がない日」なのか「予定が滑落している日」なのか、区別する手がかりが一切ありませんでした。
追いつき公開で、今度は「一番古い1本」を見落とした
7月20日の夜、たまっていることに気づいて追いつき公開を行いました。ここでも見落としが起きました。滞留していた3本のうち、予定日が新しい2本だけを公開し、一番古い1本を見落としたのです。
「今回は後回しにする」という判断の記録は、どこにも残っていませんでした。つまり意図的なスキップではなく、単純な見落としです。作業リストを上から眺めて新しいものから手を付けた結果、リストの下のほうにあった一番古いエントリが視界に入らなかったのだと思います。
この見落としは別の形の実害も生みました。連載として出しているエッセイの、第5話が第4話より先に公開されるという順序の逆転が起きたのです。読者から見れば、話の流れが一本通っていないように見えてしまいます。
見落とされた1本を公開して回収できたのは、8月1日でした。予定日から数えて29日遅れです。同じ時期、自動公開側のZennでは同種の事故は一度も起きていません。理由ははっきりしていて、スクリプトによる自動公開に加えて、公開後の検証と週次監査という二重のチェックが最初から組み込まれているからです。
「当日だけを見る」設計が持つ構造的な穴
ここで起きていたことを一般化すると、こうなります。自動化された工程は、失敗すればエラーログが残り、リトライの仕組みが働き、監査のたびに「予定通り進んでいるか」を再確認されます。仕組みの中に「動いていない」を検知する装置が、最初から織り込まれているのです。
一方、手動の工程には、そうした装置は何も付きません。人が手を動かすことを前提にしているぶん、「今日やる予定だったのに、やらなかった」という状態を誰かが検知しない限り、その状態は放置され続けます。しかも「当日分だけを表示する」タイプのダッシュボードは、輪をかけて厄介です。過ぎたものを見せないという性質のせいで、遅れた瞬間にちょうど視界から消えてしまうからです。
そうは言っても、公開実績のデータと突き合わせれば検知できたのではないか、という見方もあると思います。実際に検討しましたが、採用しませんでした。実績データとスケジュールのタイトルを照合する方式は、表記ゆれを許容するための曖昧照合が必要になります。加えて、実績データを取得するためのセッションが切れているだけのタイミングで動かすと、「公開されていないのに実は公開されている」という誤判定を作りやすいという弱点がありました。判定の材料を増やすほど、判定自体が壊れやすくなるという逆説です。
実URLの有無だけで滑落を判定する
最終的に選んだのは、もっと単純な仕組みです。スケジュールの各エントリに、公開したら実際のURLを記録するpublished_urlというフィールドを追加しました。判定条件は次の3つのANDだけです。媒体が手動公開枠であること、予定日が判定基準日より前であること、実URLが未設定であること。当日分のエントリは対象に含めません。
def is_slipped(entry, basis_date):
if entry["media"] != "manual":
return False
if entry["date"] is None or entry["date"] >= basis_date:
return False
return not entry.get("published_url")
滑落が1本見つかれば「接近」、2本以上見つかれば「要決裁」として扱います。この2段階のステータスは、既存の決裁ゲート群がすでに使っている4段階の判定(平常・接近・要決裁・欠測)にそのまま乗せました。判定はLLMを使わない純粋な条件分岐です。既存のレポート配管にそのまま載るので、新しい通知経路を増やしていません。
この設計にはもう1つ狙いがあります。published_urlのスタンプを忘れると、その記事は翌朝また「公開済みなのに滑落している」として検知結果に出てきます。実際には公開済みなのに検知だけが誤って鳴る、という誤検知の形です。見逃す方向ではなく、余分に鳴る方向に倒しているということです。運用上は、記録し忘れたときに黙って消える設計より、うるさく鳴る設計のほうが結果的に壊れにくいと判断しました。
実装した直後に、すでに分かっている滑落2本が「要決裁」として正しく検出されることを確認しています。少なくとも既知のケースに対しては、狙いどおりに動いていました。
追いつき作業は「最古から」拾う
滑落を検知できるようになっても、それだけでは7月20日に起きた見落としを防げません。検知は「何本たまっているか」までは教えてくれますが、「どの順番で片付けるか」までは決めてくれないからです。
追いつき作業を人の判断に任せると、目についた新しいものから手を付けがちで、リストの奥にある古いものが取り残されやすくなります。これを防ぐ方法は2つあると考えています。ひとつは、追いつき作業は必ず最古のエントリから拾うというルールを明文化すること。もうひとつは、人の目視ではなく機械にすべての滑落エントリを列挙させ、抜けなく処理したことを機械的に確認することです。実際に今回の検知ロジックも、見つかった滑落エントリを日付順に並べて一覧で返す形にしています。目視で拾うのではなく、全件を機械が並べて見せる側に寄せたということです。
まとめ: 検知は「忘れたときに何が起きるか」で設計する
自動化された工程には、失敗を知らせる仕組みが自然に付きます。手動の工程には、それを誰かが意図して足さない限り何も付きません。今回の29日間の滑落は、悪意でも怠慢でもなく、「当日分だけを見せる」という一見無害な設計が、遅れをそのまま視界の外へ押し出していたことが原因でした。
対策として作ったのは、LLMを使わない単純な条件分岐です。予定日を過ぎても実URLが記録されていないエントリを機械的に列挙し、既存の決裁ゲートの仕組みにそのまま乗せました。設計の軸にしたのは、「記録し忘れたときに、検知がどちら側に倒れるか」です。忘れたら黙って消えるのではなく、忘れたら余分に鳴るほうを選んでおくと、運用は壊れにくくなります。チームの仕事でも同じことが起きます。仕組みに乗っている作業は遅れが見える一方、誰かが手でやっている作業は、遅れてもそのことに誰も気づけません。可視化されていない工程こそ、遅延の温床になりやすいということを、あらためて実感しました。
私は現役EMとして、1on1やチーム運営の仕組み化について書いています。1on1管理のNotionテンプレート(無料Lite版)を公開中です → 試してみる。X: @anikuma_tech

コメント