本記事に登場する
HUMAN_INPUTマーカーとは、AI 執筆 skill が記事本文に残す「ここは人間が後で確定値を埋めるべき」を示すプレースホルダー(形式例:<!-- HUMAN_INPUT: 数値を記入 -->)。
pre-publish gate を自前で組む senior engineer / コンテンツ運用設計者向け。4 状態機械・Gate G1-G13・SSOT・retro 累積による self-improving pipeline の Yakumo 実装を全開示する。owned media 運用の経営判断・品質管理投資・透明性戦略といった business 観点は、経営判断側の記事 「AI 量産時代の owned media 運用 — mcluhan の経営判断と品質管理投資」 を参照してほしい。本記事では engine の構造そのものを扱う。
mcluhan は八雲が運用する汎用 owned media 運用エンジンだ。記事の状態管理(執筆中 / 監査待ち / 公開予約 / 公開済)、公開前の機械チェック 13 項目、公開時刻の自動 drip 化、運用中の学びを記事素材として累積する仕組み — owned media を回す手前で必要になる仕掛けを全部引き受ける。
なぜこれを作ったか。2026 年 5 月、八雲は AI 支援で書いた 48 本の記事を一斉公開して 5 日で全撤退する経験をした(詳細は 「AI 量産ブログを 5 日で全撤退した話」、本文では「Phase 0」と呼ぶ)。本文に残った HUMAN_INPUT マーカー、/blog/ への死リンク、表記揺れだらけのタグ、品質審査に全部落ちる featured 選定 — 全部が事後 audit で発見できた「機械が止められたはずのもの」だった。
そこで作ったのが mcluhan だ。命名は Marshall McLuhan の「The medium is the message」から取っている。表面の記事ではなく、それを運用する engine こそがメディアの message を伝える、という設計意図だ。
そして読者が今読んでいるこの記事自身が、mcluhan の dogfooding 出力の 1 つだ。後述する 4 状態機械と 13 項目の Gate を実際に通過して公開されている。このドキュメントはその構造の全貌を開示する。
なぜ engine を自前で作るのか — 事業との接点
tech の設計判断に入る前に、なぜ SaaS を使わず自前で書いたかを一言で整理する。
八雲が目指したのは記事を書くツールではなく、SSOT + context-aware Gate + reviewer scope の機械検証といった owned media 全体の構造規律を持つ AI エージェントシステムだった。その運用 layer を提供する SaaS は見つからなかった。加えて、Astro / Vercel / Claude Code と AI 支援を組み合わせると、Phase 1 の SSOT + Gate + scheduler + retro 累積は数日で組み上がり、構築コストが SaaS の制約をねじ曲げるコストより安かった。運用要件の複雑さが高く、構築コストが低かった — これが自前で書いた理由だ。
事業面の詳細(受託転用・外販計画・dogfooding の意義)は business spoke 「AI コンテンツ品質ゲートの ROI」 に委ねる。ここからは engine の構造そのものを見ていく。
なぜ owned media に専用エンジンが必要だったか
Phase 0 の失敗から学んだことは、failure detection(失敗の検出)と failure prevention(次回の防止)は別物だということだ。
監査を走らせると、HUMAN_INPUT の本文残存・死リンク・タグ表記揺れ・featured 選定ミス・1 日に 37 本公開した frontmatter — すべてが機械可読な情報として見えた。grep 1 行で見つかる程度の単純なミスが、publish ボタンの手前で誰にも止められなかった。
次回も同じ失敗を防ぐにはどうするか。素直に考えると、最初に出てくる答えは「人間が頑張れば止められる」だった。これは間違った答えだ。人間が頑張らないと止まらない pipeline は、いずれ必ず破綻する。疲れた日、急ぐ日、頭が切れない日が確実にやってくる。
正しい答えは「機械が機械的に検出できる失敗は、機械が止める」だった。
engine の責任範囲
mcluhan が引き受けるのは以下の領域:
- 公開前検査(HUMAN_INPUT 残存・死リンク・タグ揺れ・著者整合・時刻分散)
- 状態管理(執筆中 / 監査待ち / 公開予約 / 公開済)
- 公開スケジューラ(指定時刻に build を trigger)
- 透明性表示(AI-assisted フッター)
- 改善ループ(運用中の学びを retro として累積)
人間が判断する領域は以下のまま:
- voice / 文体・トーン
- 一次情報の質と独自性
- 公開すべきか否かの最終判断
- editorial vision(何を書くか、何を書かないか)
「機械化できるもの」と「人間にしかできないもの」の境界線を意識して設計した。
4 状態機械 — 執筆と公開を decouple する
mcluhan の中核は frontmatter で表現する 4 状態の machine。
[執筆中] → [監査待ち] → [公開予約] → [公開済]
draft:true draft:true draft:false draft:false
reviewed:f reviewed:f reviewed:true reviewed:true
scheduled:- scheduled:- scheduled:future scheduled:past
状態は記事の frontmatter で直接表現する。専用の DB やキューは持たない。記事のメタデータがそのまま状態を表す。
---
title: "..."
publishedAt: 2026-05-19
draft: true # 状態フラグ
reviewed: false # 状態フラグ
reviewedBy: "" # 監査者の slug
scheduledAt: null # 公開予定時刻
authorSlug: takumi-morimoto
aiAssisted: true
---
build フィルタで「公開済」だけが magazine に出る:
// src/lib/magazine.ts
export async function getAllPosts(): Promise<MagazineEntry[]> {
const now = new Date();
const posts = await getCollection('magazine', ({ data }) => {
if (data.draft) return false; // 執筆中・監査待ち
if (!data.reviewed) return false; // 監査未完了
if (data.scheduledAt && data.scheduledAt > now) return false; // 公開予定が未来
return true;
});
return posts.sort(/* ... */);
}
この設計の利点は 執筆と公開のリズムを decouple できること。
土日に AI 支援で記事を batch 執筆 → drafts に蓄積。平日に編集者が daily に review → reviewed:true に切り替え + scheduledAt を未来日時に割り当て。Vercel cron が daily に build を trigger → scheduledAt が来た記事だけが新たに公開される。
書く時間と公開する時間を分けた瞬間、量産モードと品質モードを両立できる。
Gate — 公開前検査の 13+7 ルール
scripts/blog-gate.ts が src/content/magazine/**/*.md を走査し、必須チェック G1-G13 と警告チェック W1-W7 をかける。npm run prebuild で毎ビルド前に走るので、失敗を含む記事は build から物理的に出せない。
必須チェック(fail → build 停止)
| # | チェック | 失敗条件 |
|---|---|---|
| G1 | HUMAN_INPUT marker | <!-- HUMAN_INPUT --> / HUMAN_INPUT[TAG] / 行頭コロン形式の残存 |
| G2 | 死リンク | markdown link 形式の ]\(/blog/...\) 残存 |
| G3 | tag SSOT 整合 | frontmatter tags に tagCatalog 外の値 |
| G4 | track-category 一致 | frontmatter category と物理パスの不整合 |
| G5 | reviewer 存在 | draft: false 時 reviewedBy が members.ts に存在しない |
| G6 | reviewer scope | reviewedBy の reviewScope が記事 track をカバーしていない |
| G7 | scheduledAt 必須 | draft: false 時 scheduledAt 未設定 |
| G8 | time-slot 整合 | scheduledAt の時刻が publishPolicy.timeSlots 外 |
| G9 | authorSlug 存在 | authorSlug 未設定 / members.ts に存在しない |
| G10 | self-reference 検出 | Yakumo 自身を 会社 / 企業 / firm / company と表記 |
| G11 | pillar brief 整合 | pillar 記事の brief に cluster / audience.primary / seo セクションが揃っているか |
| G12 | 英語版存在 | 英語版(magazine-en)が存在するか(WARN のみ、build は止めない) |
| G13 | locale prefix | Magazine 内部リンクが locale prefix(/magazine/ / /en/magazine/)を持っているか |
警告チェック(build は止めない)
| # | チェック | 警告条件 |
|---|---|---|
| W1 | rate-limit (daily) | scheduledAt が同一日に 6 本超 |
| W2 | rate-limit (weekly) | 同週に 50 本超 |
| W3 | rate-limit (monthly) | 同月に 200 本超 |
| W4 | description 長 | 60 字未満 or 130 字超 |
| W5 | title 長 | 18 字未満 or 60 字超 |
| W6 | pillar inbound link | spoke 記事から所属 pillar への内部リンクなし(実装予定) |
| W7 | series 整合 | series 指定が seriesCatalog に存在しない |
context-aware — 機械的検出の精度
最初は単純 grep だった。だがこの記事自身のように 失敗事例を本文で説明する記事を書こうとすると、説明文に含まれる HUMAN_INPUT や /blog/... まで誤検出する。失敗開示型 owned media を運用するには、Gate がメタな記述にも対応する必要があった。
解決策は markdown の code context を pre-stripping すること。
function stripCodeContexts(body: string): string {
// fenced code block (```...```) を除去
let result = body.replace(/```[\s\S]*?```/g, '');
// inline code (`...`) を除去
result = result.replace(/`[^`\n]+`/g, '');
return result;
}
stripping 後の本文に対して、検出は特定フォーマットに限定する:
- HTML コメント:
<!--\s*HUMAN_INPUT - 角括弧タグ:
HUMAN_INPUT\[[A-Z_]+\] - 行頭コロン:
^HUMAN_INPUT: - markdown link 形式:
\]\(/blog/[^)]+\)
これで「HUMAN_INPUT を本文中で説明している inline code」は除外され、「HTML コメントとして残った実 marker」だけが fail する。
G10 — 文脈判定の応用
八雲は未法人化のため 「八雲は ... 会社」 「Yakumo is a firm」 のような自己言及を禁止している。だが 「株式会社マーケットエンタープライズ」(他社の正式名)や 「自称 AI 開発会社」(他社カテゴリ批判)は OK。同じ「会社」という単語でも、文脈で OK/NG が分かれる。
G10 は allow-list と pattern-list で判定する:
// src/config/brand.ts
externalOrgAllowlist: [
'株式会社マーケットエンタープライズ',
'Market Enterprise',
],
externalReferencePatterns: [
/他の[^。]{0,30}?(会社|企業)/g,
/自称[^。]{0,30}?(会社)/g,
/other\s+(AI\s+)?(development\s+)?(firms|companies)/gi,
// ...
],
検出フロー:
- 本文から
会社/企業/firm/companyを抽出 - 各マッチが allowlist 内文字列に含まれていれば除外
- external pattern にマッチする部分文字列内なら除外
- 残ったマッチについて前後 50 文字以内に Yakumo / 八雲 / 私たち があれば fail
- それ以外は warning(要人間確認)
context-aware を「stripping 系」と「allow-list + pattern 系」の 2 アプローチで実装した。今後同じパターンを他のルール(「弊社」「当社」「内輪用語」など)にも適用可能。
SSOT 群 — 表記揺れと禁則の機械可読化
frontmatter で参照する値はすべて SSOT に集約してある。
| ファイル | 責務 |
|---|---|
src/config/tags.ts | TagKey 一覧(執筆当時 約 25 種、現在は src/config/tags.ts を参照)。frontmatter tags は SSOT 外の値を許さない |
src/config/pillars.ts | pillar 記事の slug 宣言。spoke は所属 pillar への inbound link を W6 で警告される |
src/config/series.ts | MagazineSeriesId の SSOT。frontmatter series は SSOT 外を許さない |
src/config/members.ts | 執筆者・レビュアー定義。canReview / reviewScope で G5/G6 を検証 |
src/config/publish-policy.ts | timeSlots / rateLimits / aiAssistedFooterTemplate / editorNoteCadence |
Phase 0 で発見した失敗の 1 つは 77 種類のユニークタグ、うち 55 が孤立タグだった。agents / agent-design / エージェント設計 のような表記揺れが Topical authority を破壊していた。SSOT 化と Gate G3 の組み合わせで、新規記事が表記揺れを混入させる経路を物理的に閉じた。
frontmatter からの参照は単純な文字列キー:
tags: [content-ops, quality-control, blog-audit]
series: mcluhan-engine
authorSlug: takumi-morimoto
reviewedBy: takumi-morimoto
content-ops という文字列が tagCatalog['content-ops'] に存在しなければ Gate が fail する。mcluhan-engine が seriesCatalog に存在しなければ frontmatter validation で zod が剥がす。
SSOT を変更すれば全記事の表記が変わる。SSOT を変更しなければ全記事が同じ表記で揃う。表記揺れは構造的に発生しない。
Vercel Cron スケジューラ — 自動 drip 公開
scheduledAt が未来日時の記事は build に含まれない。だが Vercel Cron が定期的に rebuild すれば、scheduledAt が現在時刻を過ぎた記事は次の build から自動的に公開対象になる。
// vercel.json
{
"crons": [
{
"path": "/api/cron/rebuild-magazine",
"schedule": "0 0,9 * * *"
}
]
}
cron 起動時刻は UTC 00 / 02 / 05 / 07 / 09 — JST 09:00 / 11:00 / 14:00 / 16:00 / 18:00 の 5 slot 構成。spoke 量産フェーズ用に 2 slot から拡張済(詳細は src/config/publish-policy.ts を SSOT として参照)。publishPolicy.timeSlots と完全に一致する。
// src/config/publish-policy.ts(※ 最新値は publish-policy.ts を参照)
timeSlots: ['09:00', '11:00', '14:00', '16:00', '18:00'] as const,
scheduledAt: 2026-05-20T11:00:00+09:00 の記事は、UTC 02:00 より後の最初の cron(UTC 09:00 = JST 18:00)で rebuild されて初めて公開される。publishedAt(display 用)と scheduledAt(machine 用)を分けることで、display では「2026-05-20 公開」、Google の discovery date は実際の build trigger 時刻、両方を整合させる。
/api/cron/rebuild-magazine の中身は Vercel Deploy Hook を fetch する単純な proxy:
// src/pages/api/cron/rebuild-magazine.ts
export async function GET() {
const hookUrl = process.env.VERCEL_REBUILD_HOOK_URL;
if (!hookUrl) return new Response('OK (no hook)', { status: 200 });
await fetch(hookUrl, { method: 'POST' });
return new Response('rebuilt', { status: 200 });
}
scheduledAt をいくら未来日時に設定しても、その時刻が来るまでは記事は公開されない。逆に scheduledAt が過去日時なら、次の cron 起動で必ず公開される。「いつ公開するか」を frontmatter の 1 フィールドだけで完全制御できる設計。
AI-assisted フッター — 透明性の設計
全記事の最下部に AI 支援の事実と reviewer 名義を表示する。
# publishPolicy.aiAssistedFooterTemplate
ja: 'この記事は AI 支援でドラフトされ、{reviewer} が {reviewedAt} にレビューしました。'
en: 'This article was drafted with AI assistance and reviewed by {reviewer} on {reviewedAt}.'
src/components/magazine/article/ArticleFooterMeta.astro が frontmatter の aiAssisted / reviewedBy / reviewedAt を読み取り、members.ts から reviewer 名を引いて locale ごとに文字列展開する。
なぜ表示するかというと、AI 量産時代の Google ranking signal は「AI 生成を隠していない」「accountability が明示されている」ことを評価する方向に動いている。隠す方が deceptive 判定されるリスクが高い。
AI で書いた事実を消そうとする owned media は多い。mcluhan の設計は逆方向 — AI 支援を publicly committed な事実として表示し、reviewer 名義で accountability を渡す。これが Google Scaled Content Abuse 判定の境界線(unique value × volume × oversight)の oversight 部分を満たす設計。
retro 累積 — pipeline 自身が改善対象
mcluhan の運用中に発見した issue / fix / learning は blog-ops/retros/{YYYY-MM-DD}-{slug}.md に記録する。blog-retro skill が format に従って書き出す。
---
date: 2026-05-18
short_slug: gate-false-positive
title: "Gate の grep が失敗開示記事に false positive を出した"
trigger: "..."
related_pillar: "2026-05-magazine-reset-timeline"
spoke_potential:
- "Pre-publish gate の設計 — false positive と false negative のバランス"
- "失敗事例を記事化するときの Gate 対応"
status: captured
---
## 経緯
## 根本原因
## 修正
## 学び
## 派生 spoke 候補
執筆当時(2026-05-18)時点で 3 件の retro が累積していた(現時点で 9 件: blog-ops/retros/ を参照):
2026-05-18-gate-false-positive: G1/G2 の plain grep が記事中のHUMAN_INPUT説明に false positive を出した → context-aware stripping で解決2026-05-18-naming-rule-english-parity: ja の「会社」ルールを定義したが en の company/firm に並走漏れ → BRAND.md のルール定義を locale 並走で記述する必要2026-05-18-context-aware-company-gate: 「会社/企業」ルールを機械チェック化する際の文脈判別仕様 → G10 として実装
現在、blog-ops/config/pipelines/ 配下に 5 本の pipeline が定義されている(audit / new-article-pillar / new-article-spoke / rewrite-tech / rewrite-case)。
各 retro は 将来の spoke 記事の素材になる。「pre-publish gate の設計」「multi-locale brand rule の SSOT 化」「context-aware 静的解析」など、抽象化した spoke を書ける材料が retro として既に揃っている。
つまり mcluhan は 使うたびに次の記事を生む engine でもある。失敗の検出 → 仕組み化 → 記事化 → さらなる検出 のループが、pipeline 自身を改善対象として組み込んでいる。engine が自分自身の記録を素材として消費し続ける構造だ。
命名 — なぜ mcluhan か
Marshall McLuhan は 1960 年代のメディア論者。代表的なフレーズが「the medium is the message」— メディアそのものが伝えるメッセージは、表面のコンテンツではなく、それを運ぶ媒体の構造にこそ宿る、という主張だ。
owned media に当てはめると、訪問者が読み取るのは記事の文字列だけではない。「この組織はどう記事を運用しているか」「どう品質を担保しているか」「失敗をどう開示するか」— pipeline の設計自体が、編集の姿勢を伝える。
だから engine の名前として mcluhan を選んだ。記事は流れていく。engine の設計は残る。
ちなみに src/lib/bateson/ も同じ流儀で命名している。Gregory Bateson(人類学者・サイバネティクス論者)から取った。Yakumo の src/lib/ 配下は「人間と情報のシステム論」の思想家・批評家の人名空間として一貫させている。bateson が booking engine(サイトでの sales agent)、mcluhan が owned media engine(サイトでの marketing agent)— 訪問者が体験する 2 つの面が、それぞれ思想家の名前を持つ独立した engine として動いている。
汎用化への道 — 他組織でも動くか
mcluhan は現在、八雲コーポレートサイトで dogfooding 中。将来は他組織が自分たちの owned media に install して動かせる切り出し版として独立予定だ。bateson と同じ設計原則を継承する:
- corporate-site に依存しない(逆向き依存のみ)
- すべての公開 API は
tenantIdを引数として持つ - Adapter パターンで外部システムを差し替え可能(storage / ai / publish / analytics)
- テンプレ・ブランド要素は注入(pillar 定義・著者・カテゴリは tenant config)
- UI は headless 寄り(agent からも CLI からも UI からも同じ core を呼ぶ)
切り出し後の構造は以下を想定:
src/lib/mcluhan/
├── core/
│ ├── types.ts # Article / Author / Tag / Series / Pillar / Reviewer 型
│ ├── content.ts # createContentCore(adapters) — adapter DI
│ ├── editor-policies.ts # track ごとの policy 検証
│ └── gate.ts # pre-publish gate logic
├── adapters/
│ ├── storage/ # GitHub / Notion / filesystem
│ ├── ai/ # Claude / OpenAI
│ ├── publish/ # Vercel / Cloudflare
│ └── analytics/ # Search Console / GA4
└── ui/
現在の Phase 1 では src/config/*.ts と scripts/blog-gate.ts が core の役割を担う。汎用化時にこれらを src/lib/mcluhan/core/ に移植する設計。Phase 4 でエージェント化、Phase 5 で切り出し版として独立配布に進む予定だ。
まとめ — engine が分けるもの
AI 量産時代の owned media は「engine をどう作るか」で差がつく。記事を AI が書ける時代に、差別化要因として残るのは:
- 機械的に検出できる失敗を機械が止める仕組み(Gate)
- 状態管理と公開タイミングを decouple する設計(4 状態機械)
- 表記揺れと禁則を SSOT で物理的に閉じる構造(tags / pillars / series / members)
- 公開を自動 drip 化する scheduler(Vercel cron + scheduledAt)
- AI 利用の透明性を担保する footer(accountability)
- pipeline 自身を改善対象として組み込む retro 累積(self-referential ループ)
人間が時間を使う領域は voice 設計と編集判断と一次情報の生成に限定される。それ以外は engine が引き受ける。
そしてこの記事自身が、mcluhan の最初の本番出力の 1 つとして、5/21 18:00 JST の slot に予約され、Vercel cron が build を trigger し、Gate G1-G13 を通過して、reviewer takumi-morimoto が承認して、今読者が読んでいる。
記事は流れていく。engine の設計は残り、次の記事を生み続ける。