本記事に登場する
HUMAN_INPUTマーカーとは、AI 執筆 skill が記事本文に残す「ここは人間が後で確定値を埋めるべき」を示すプレースホルダー(形式例:<!-- HUMAN_INPUT: 数値を記入 -->)。
「AI 量産ブログを 5 日で全撤退した話」を記事にしようとして、gate 自身に止められた。本文に「HUMAN_INPUT マーカーが 7 か所残っていた」と書いた瞬間、G1(HUMAN_INPUT 残存チェック)が 10 件の fail を報告した。実際の未完了マーカーではなく、失敗事例を説明している文章が引っかかった。plain string match で書いた gate が、失敗を開示しようとする記事を block するという自己矛盾だ。
→ gate の全体構造は owned media 運用エンジン mcluhan の構造設計 を参照してほしい。本記事は G1/G2 の plain match が持つ限界と、markdown 構造を尊重した context-aware 解析への移行設計に特化する。
なぜ失敗開示記事で false positive が出るのか
G1(HUMAN_INPUT 残存)と G2(dead link)が引き起こす自己矛盾のメカニズム
八雲の owned media は「失敗を開示する」ことをコンセプトの核に置いている。gate の設計上の失敗を記事にする、パイプライン改善の過程を記事にする——これは pillar の narrative そのものだ。
ところが、失敗事例の記事には構造的に「失敗の痕跡を説明する文字列」が含まれる。
- 「本文の中に
HUMAN_INPUTマーカーが 7 か所残っていた」 - 「内部リンクは全 76 本が
/blog/...というパスを参照していた」
これらは HUMAN_INPUT マーカーや死リンクそのものではなく、それらについて語っている prose だ。plain string match はこの区別ができない。body.includes('HUMAN_INPUT') は、マーカーが prose に言及されているだけで true を返す。
pillar 記事「AI 量産ブログを 5 日で全撤退した話」の draft で G1 が検出した false positive は 10 件、G2 が検出した false positive は 6 件だった。いずれも、失敗事例の記述が plain match に引っかかった件数だ。
prose の中で失敗を「説明」している文字列と「実際のマーカー」の違い
検出すべきは「プレースホルダーとして未完了のまま残っているマーカー」であって、「マーカーについて語っているテキスト」ではない。この区別を plain string match では表現できない。
正しいアプローチは 2 つある。
- markdown の code context(fenced block / inline code)を前処理で除去してから検索する
- 検出対象のフォーマットを特定の構造(HTML コメント形式・角括弧タグ形式など)に限定する
2 つを組み合わせることで、「本文での説明としての HUMAN_INPUT」と「未完了のプレースホルダーとしての HUMAN_INPUT」を区別できる。
markdown 構造を尊重した前処理
stripCodeContexts ヘルパーの設計 — fenced code block と inline code の除去
前処理ヘルパー stripCodeContexts は fenced code block と inline code を除去する。
function stripCodeContexts(body: string): string {
// fenced code block(```...```)を除去
let result = body.replace(/```[\s\S]*?```/g, '');
// inline code(`...`)を除去
result = result.replace(/`[^`\n]+`/g, '');
return result;
}
この処理後の本文には prose だけが残る。「HUMAN_INPUT マーカーが〜」という説明文も、バッククォートで囲まれた inline code 部分が除去されるため「マーカーが〜」という文になる。plain string match で「HUMAN_INPUT」を探しても hit しない。
「失敗事例を説明する記事の本文で HUMAN_INPUT について語っている」という prose レベルの言及を検出から外す目的を果たせる。
除去後の本文に対して regex マッチを行う設計の流れ
// G1 チェックの改修後フロー
function checkG1(article: Article): GateResult {
const strippedBody = stripCodeContexts(article.body);
const HUMAN_INPUT_PATTERNS = [
/<!--\s*HUMAN_INPUT[\s\S]*?-->/g, // HTML コメント形式
/HUMAN_INPUT\[[A-Z_]+\]/g, // 角括弧タグ形式
/^HUMAN_INPUT:/m, // 行頭コロン形式
];
const matches = HUMAN_INPUT_PATTERNS.flatMap(pattern =>
[...strippedBody.matchAll(pattern)]
);
return matches.length > 0
? { result: 'fail', rule: 'G1', count: matches.length }
: { result: 'pass', rule: 'G1' };
}
stripping 後の prose に対して、3 フォーマットのみを検出対象にする。フォーマット外の言及(「HUMAN_INPUT マーカーとは〜」という散文)は検出から外れる。
ルール別の解析レベル分類
Level 1: plain string match で十分なルール(G3〜G9 の一部)
すべてのルールが context-aware な実装を必要とするわけではない。plain string match が適切なケースも多い。
| ルール | 内容 | 理由 |
|---|---|---|
| G3 | tag SSOT 整合 | frontmatter の tags フィールドはコード文脈での説明が少ない |
| G5 | reviewer 存在確認 | frontmatter の structured field は code context に入らない |
| G7 | scheduledAt 必須 | frontmatter の field 存在確認は prose との混同が起きない |
| G9 | authorSlug 存在 | frontmatter の field は明確に構造化されている |
frontmatter フィールドの検査は plain match で安全だ。問題が起きるのは記事本文(body)をスキャンするルールだ。
Level 2: code context 除去後に regex マッチするルール(G1 / G2)
stripCodeContexts() → 特定フォーマットのみ regex マッチ
本文内の特定の「プレースホルダー」や「パス」を検出するルールは Level 2 が適切だ。code block / inline code 内での説明テキストを誤検出しないために stripping が必要だが、NLP レベルの解析は不要。
| ルール | 内容 | 解析対象 |
|---|---|---|
| G1 | HUMAN_INPUT 残存 | body(stripping 後、3 フォーマット限定) |
| G2 | 死リンク(/blog/ パス) | body(stripping 後、markdown link 形式限定) |
Level 3: proximity 解析が必要なルール(G10 自己言及チェック)
context-aware の最上位に当たるのが G10 だ。会社 / 企業 / firm / company という単語が、Yakumo 自身への言及として使われているかを判定するには、allow-list + pattern + proximity の 3 層が必要になる。
Level 2(stripping)だけでは不十分で、マッチした箇所が「誰を指しているか」まで判定する必要がある。
| ルール | 内容 | 解析方法 |
|---|---|---|
| G10 | 自己言及の brand rule チェック | allow-list → externalReferencePatterns → proximity 解析の 3 層 |
現時点の scripts/blog-gate.ts には G ルール 13 件(G1〜G13)・W ルール 6 件(W1, W2, W3, W4, W5, W7)の合計 19 件が実装されている。
実装例 — G1 の改修
before: body.includes 全体スキャン
// 改修前
function checkG1_before(body: string): boolean {
return body.includes('HUMAN_INPUT');
}
この 1 行が全問題の根本だ。本文のどこかに HUMAN_INPUT という文字列が存在すれば true を返す。Code block 内でも、inline code 内でも、prose の説明中でも区別しない。
after: stripCodeContexts + 3 フォーマット限定 regex
// 改修後
function checkG1_after(body: string): GateResult {
const stripped = stripCodeContexts(body);
const remaining = [
...stripped.matchAll(/<!--\s*HUMAN_INPUT[\s\S]*?-->/g),
...stripped.matchAll(/HUMAN_INPUT\[[A-Z_]+\]/g),
...stripped.matchAll(/^HUMAN_INPUT:/mg),
];
if (remaining.length > 0) {
return {
result: 'fail',
rule: 'G1',
message: `HUMAN_INPUT マーカーが ${remaining.length} 件残存`,
};
}
return { result: 'pass', rule: 'G1' };
}
stripping 後に 3 フォーマットのみを検出対象にすることで、「マーカーについて語る prose」は除外され、「未完了のプレースホルダーとして残っているマーカー」だけが fail になる。
G1 の context-aware 化は commit 5f4289c fix(scripts/blog-gate): make HUMAN_INPUT and /blog/ detection context-aware で完了している。stripping 後に 3 フォーマットへ限定する実装はこの commit が起点になった。
false positive を自動検出するテスト設計
meta-content fixture(失敗事例を説明する記事)を gate のテストに加える
gate のテストに meta-content fixture を追加することが根本的な解決だ。meta-content とは「gate のルールや失敗事例を本文中で説明している記事」だ。
// tests/blog-gate.test.ts
describe('G1 - HUMAN_INPUT 残存チェック', () => {
it('実際のマーカーは fail', () => {
const body = '<!-- HUMAN_INPUT: 数値を記入してください -->';
expect(checkG1(body).result).toBe('fail');
});
it('prose での説明は pass(meta-content 対応)', () => {
const body = '本文に `HUMAN_INPUT` マーカーが 7 か所残っていた。';
expect(checkG1(body).result).toBe('pass');
});
it('code block 内での説明は pass', () => {
const body = '```\n<!-- HUMAN_INPUT: ... -->\n```';
expect(checkG1(body).result).toBe('pass');
});
});
「gate 自身の失敗を記事にする」という構造を事前にテストに組み込んでおくと、次回 meta-content を執筆したときに gate が自己矛盾を起こさないことを保証できる。
ただし現時点で tests/blog-gate.test.ts のような専用テストファイルは未作成だ。本記事で示した meta-content fixture の設計は方針として記録しているが、テスト化は今後の作業として残っている。
まとめ — 読者が持ち帰る原則
pre-publish gate を markdown 対応にする設計の核は 2 点に集約できる。
1. ルールに適切な解析レベルを割り当てる
| レベル | 適用先 | 手法 |
|---|---|---|
| L1 | frontmatter フィールドの検査 | plain match |
| L2 | body 内のプレースホルダー検出 | stripCodeContexts + フォーマット限定 regex |
| L3 | body 内の文脈依存チェック | allow-list + pattern + proximity |
2. meta-content テストを gate のテストスイートに加える
失敗開示記事が gate に弾かれる自己矛盾は、「meta-content fixture で gate が正しく動くか」のテストがなかったことで生まれた。テストにこの fixture を加えれば、次回の同種 pillar 執筆で同じ詰まりは起きない。
gate の設計思想と false positive / false negative のトレードオフについては Pre-publish gate 設計 — false positive と false negative のバランス を参照してほしい。G10 の brand rule 自己言及検出の設計は Brand rule の機械チェック化 — 自己言及 vs 他社言及の文脈判別パターン で扱っている。