Engineering content-gen 13 min read

Pre-publish gate の高度化 — 単純 grep から proximity 解析へ

plain string match の gate が false positive を出す原因と対策。markdown の code context を除去する stripCodeContexts と proximity 解析で自己矛盾を解消する実装設計。

公開 2026-05-23 森本拓見

本記事に登場する 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 つある。

  1. markdown の code context(fenced block / inline code)を前処理で除去してから検索する
  2. 検出対象のフォーマットを特定の構造(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 が適切なケースも多い。

ルール内容理由
G3tag SSOT 整合frontmatter の tags フィールドはコード文脈での説明が少ない
G5reviewer 存在確認frontmatter の structured field は code context に入らない
G7scheduledAt 必須frontmatter の field 存在確認は prose との混同が起きない
G9authorSlug 存在frontmatter の field は明確に構造化されている

frontmatter フィールドの検査は plain match で安全だ。問題が起きるのは記事本文(body)をスキャンするルールだ。

Level 2: code context 除去後に regex マッチするルール(G1 / G2)

stripCodeContexts() → 特定フォーマットのみ regex マッチ

本文内の特定の「プレースホルダー」や「パス」を検出するルールは Level 2 が適切だ。code block / inline code 内での説明テキストを誤検出しないために stripping が必要だが、NLP レベルの解析は不要。

ルール内容解析対象
G1HUMAN_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. ルールに適切な解析レベルを割り当てる

レベル適用先手法
L1frontmatter フィールドの検査plain match
L2body 内のプレースホルダー検出stripCodeContexts + フォーマット限定 regex
L3body 内の文脈依存チェック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 他社言及の文脈判別パターン で扱っている。

SHARE X でシェア B! はてブ