pre-publish gate に brand rule チェックを組み込もうとした時点で、八雲のサイト内には他組織の正式名や「自称 AI 開発会社」のような他社カテゴリ言及が既に存在していた。flat な禁則語リストで「会社」「企業」「firm」「company」を一律に禁止すると、brand rule の例外条項(他社への言及は問題ない)に該当する表現まで全てブロックされてしまう。gate スクリプトには「Yakumo 自身への言及」と「他社への言及」を区別する機構が必要だった。
exception を gate スクリプト側にハードコードする修正は避けた。新しい他組織名が記事に登場するたびに gate を修正するのは持続できない運用だからだ。解決策は exception の SSOT 化——brand.ts に allow-list として構造化して持ち、gate スクリプトは参照するだけにする設計だ。
→ gate の全体設計は owned media 運用エンジン mcluhan の構造設計 を参照してほしい。本記事は brand check の exception 設計(SSOT 化)に特化する。
allow-list と pattern-list の 2 層設計
Layer 1: externalOrgAllowlist — 既知の組織名(完全一致)
最初の層は既知の他組織の正式名を完全一致で照合するリストだ。
記事に他組織の正式名が登場する場面は具体的だ。「前職の事業会社で AI 推進を担当していた」という経歴記述や、「市場にいる他組織との比較」という文脈——いずれも Yakumo 自身への言及ではない。そこに含まれる組織名を allow-list に登録することで、該当する文字列を pass にできる。
完全一致の判定は単純だ。body.includes(orgName) が allow-list 内の文字列で true なら、その周辺の「会社」マッチは false positive として除外する。
Layer 2: externalReferencePatterns — 他社カテゴリ言及パターン(正規表現)
第 2 層は正規表現で「他社への言及パターン」を構造化する。allow-list が個別の組織名を管理するのに対して、pattern-list は「他社カテゴリを指している文脈」を汎用的に捉える。
典型的なパターンは次の 3 種類だ。
- 他社カテゴリ対比: 「他の AI 開発会社との違いは〜」「Web 制作会社とは異なり〜」
- 他社批判: 「自称 AI 開発会社の多くは〜」「OpenAI を叩くだけの会社は〜」
- 過去経歴: 「共同創業した会社での経験を〜」「以前所属していた会社の慣習が〜」
これら 3 パターンはいずれも自己言及ではなく、brand rule の対象外だ。正規表現で構造化することで、個々の文字列を allow-list に登録せずに pass にできる。
2 層の使い分け基準
| 層 | 対象 | 管理方法 |
|---|---|---|
| Layer 1 (externalOrgAllowlist) | 既知の具体的な組織名(「株式会社〜」「〜Inc」など) | 完全一致の文字列リスト |
| Layer 2 (externalReferencePatterns) | 汎用的な他社言及パターン(「他の〜会社」「自称〜」など) | 正規表現 |
基準: 「この言及が今後も繰り返し登場するか」で判断する。特定の組織名は Layer 1 へ、「他社カテゴリ比較」のような汎用表現は Layer 2 へ。迷ったら Layer 2 の正規表現で始めて、誤検出が増えたら Layer 1 に個別登録する方向で調整する。
brand.ts への SSOT 化
externalOrgAllowlist の型定義と初期値
src/config/brand.ts に 2 つのフィールドを追加した。
// src/config/brand.ts(追加分)
externalOrgAllowlist: [
'〈他組織の正式名 A〉',
'〈Other Org A〉',
] as const,
型は readonly string[] 相当。as const により TypeScript が literal union 型に推論するため、typo があればコンパイル時に検出できる。
現時点の externalOrgAllowlist には 2 件の組織名が登録されている。
externalReferencePatterns の正規表現定義
// src/config/brand.ts(追加分)
externalReferencePatterns: [
/他の[^。]{0,30}?(会社|企業)/g,
/自称[^。]{0,30}?(会社)/g,
/(他|別の)[^。]{0,30}?(事業|開発)[^。]{0,30}?会社/g,
/(共同|過去に)?創業(した|していた)?会社/g,
/(Web|web)\s*制作会社/g,
/other\s+(AI\s+)?(development\s+)?(firms|companies)/gi,
/self-described\s+["「]?AI\s+(companies|firms)["」]?/gi,
] as const,
各パターンの設計は「文をまたいだ誤検出を防ぐ」ことを優先している。[^。]{0,30} は句点(。)を含まない最大 30 文字を意味する。これにより、直前の文に自己言及があって「会社」と書いた場合(自己言及 NG)が、次の文の「他の〜」パターンにまぎれ込んで pass にならない。
現時点の externalReferencePatterns には 7 件のパターンが定義されている。
既存 forbiddenWords との責務分担
brand.ts には forbiddenWords(「画期的」「革命的」「DX」など)が既に定義されていた。これらは「どの文脈でも使ってはいけない」絶対禁止語だ。
一方、今回追加した externalOrgAllowlist と externalReferencePatterns は「文脈によっては使ってよい語」を扱う。責務は明確に異なる。
| フィールド | 責務 | 判定ロジック |
|---|---|---|
forbiddenWords | 絶対禁止語(context 不問) | plain match で fail |
externalOrgAllowlist | 他組織の正式名(false positive 防止) | 完全一致で pass に除外 |
externalReferencePatterns | 他社言及の汎用パターン(false positive 防止) | 正規表現でマッチしたら pass に除外 |
この責務分担を brand.ts 内で 1 ファイルに整理することが SSOT 化の本質だ。gate スクリプトは brand.ts を import して各フィールドを参照するだけで、exception ロジックを自分で持たなくてよい。
gate スクリプトからの参照設計
brand.ts を import して allow-list と照合するパターン
gate スクリプトの G10 実装は brand.ts を参照する薄いラッパーだ。
// scripts/blog-gate.ts
import { brand } from '../src/config/brand';
function checkG10(body: string): GateResult {
// Step 1: allow-list 内文字列を含む部分を除外
let processedBody = body;
for (const orgName of brand.externalOrgAllowlist) {
// orgName を含む sentence を一時的にプレースホルダーに置換
processedBody = processedBody.replace(
new RegExp(`[^。]*${escapeRegex(orgName)}[^。]*`, 'g'),
'__ALLOWED__'
);
}
// Step 2: externalReferencePatterns にマッチする部分を除外
for (const pattern of brand.externalReferencePatterns) {
processedBody = processedBody.replace(pattern, '__ALLOWED__');
}
// Step 3: 残ったマッチに proximity 解析を適用
const companyWords = /会社|企業|firm|company|corporation/gi;
const selfIdentifiers = ['Yakumo', '八雲', '私たち'];
const PROXIMITY = 50;
let match: RegExpExecArray | null;
const failures: string[] = [];
while ((match = companyWords.exec(processedBody)) !== null) {
if (match[0] === '__ALLOWED__') continue;
const window = processedBody.slice(
Math.max(0, match.index - PROXIMITY),
Math.min(processedBody.length, match.index + PROXIMITY)
);
if (selfIdentifiers.some(id => window.includes(id))) {
failures.push(`"${match[0]}" at position ${match.index}`);
}
}
return failures.length > 0
? { result: 'fail', rule: 'G10', message: failures.join(', ') }
: { result: 'pass', rule: 'G10' };
}
マッチの優先順位(allow-list → pattern → proximity → fail)
判定フローを整理すると次のようになる。
- externalOrgAllowlist で除外 → 既知の他組織名を含む文は pass
- externalReferencePatterns で除外 → 他社言及パターンに一致する部分は pass
- proximity 解析で判定 → 残ったマッチが Yakumo / 八雲 / 私たちと近接していれば fail
- それ以外は warning → 文脈が不明(他組織の可能性)。人間確認に委ねる
この順序が重要だ。allow-list と pattern を先に処理することで、proximity 解析が false positive をつかまえるリスクを下げる。
allow-list 照合を担う Gate ルールは G10(scripts/blog-gate.ts 内の checkG10_CompanySelfReference)として実装されている。リポジトリ上の初出は 2026-05-18 の commit d1e3ae7(feat(brand/gate): G10 catches Yakumo self-reference as 会社 / firm)で、同じ commit で src/config/brand.ts に externalOrgAllowlist / externalReferencePatterns フィールドが追加されている。
allow-list の更新フロー
新しい他組織名を記事で使うときの 1 ファイル更新手順
allow-list を SSOT 化した最大のメリットは「記事で新しい他組織名を使うときの作業が 1 ファイルの更新で完結する」ことだ。
補足しておくと、この allow-list は「false positive が出てから後追いで作った」ものではない。G10 を実装する時点で、Yakumo のサイト内には他組織の正式名と「他社カテゴリへの言及」(例: 「自称 AI 開発会社」のような対比表現)が既に存在しており、flat な禁則語実装ではこれらをすべてブロックしてしまう。そのため、G10 はリリース時点から allow-list と reference pattern を同梱した状態で導入した。新規の他組織名を後から追加する場合も、同じ allow-list を 1 ファイル更新するだけで済む。
手順は 3 ステップだ。
src/config/brand.tsのexternalOrgAllowlistに組織名の文字列を追加するnpm run blog-gateを実行して false positive が解消されたことを確認する- PR で変更をレビューしてもらい、merge する
gate スクリプトに手を入れる必要はない。brand.ts を変更するだけで gate の挙動が変わる。
PR レビューで allow-list の妥当性を確認するチェックリスト
allow-list への追加は慎重に判断する必要がある。「何でも allow にすると brand rule が形骸化する」リスクがあるからだ。PR レビューでは以下の点を確認する。
- 追加する組織名は Yakumo 以外の実在する他組織 の正式名か
-
自社内で〜や私たちの〜という表現と近接して使われていないか(自己言及の偽装でないか) - BRAND.md の例外条項(「他社・他組織への言及は問題ない」)に当てはまるか
- 追加理由(どの記事で使うか、どの文脈か)が PR の説明に書かれているか
4 つをすべて満たす場合のみ allow-list への追加を承認する。
まとめ — SSOT 化で gate メンテコストを下げる
gate スクリプトを薄いラッパーに保つ設計方針の整理
allow-list と pattern-list を brand.ts に集約した後の gate スクリプトは「brand.ts を参照して判定ロジックを実行するだけの薄いラッパー」になる。
| 作業 | SSOT 化前 | SSOT 化後 |
|---|---|---|
| 新しい他組織名を記事で使う | gate スクリプトを修正 | brand.ts の 1 行追加のみ |
| 他社言及パターンを追加する | gate スクリプトを修正 | brand.ts の 1 正規表現追加のみ |
| 「なぜ pass になるのか」を確認する | gate スクリプトのコードを読む | brand.ts のフィールドを読む |
「exception はどこにあるか」という問いに対して、常に「brand.ts にある」と答えられる状態が SSOT 化の価値だ。gate スクリプトがビジネスルールを持たず、brand.ts が唯一の真実の情報源になることで、引き継ぎ・レビュー・変更管理の全コストが下がる。
実運用での更新頻度はかなり低い。G10 と同時に登録した allow-list / pattern set が現時点でもそのまま機能しており、SSOT 化以降に組織名を追加した実績は 0 件だ。allow-list は「追加頻度が高いほど価値が出る仕組み」ではなく、「初期登録で既知の他社言及を網羅し、以降の追加は例外として丁寧に審査する仕組み」として運用するのが実態に近い。追加が必要になった瞬間こそ、PR レビューで前述のチェックリストを通すべきタイミングになる。
技術的な実装の詳細(proximity 解析・stripCodeContexts の設計)は Brand rule の機械チェック化 — 自己言及 vs 他社言及の文脈判別パターン を参照してほしい。false positive と false negative のバランス設計については Pre-publish gate 設計 — false positive と false negative のバランス で扱っている。