はじめに
前回の記事 で、
Playwright + Jenkins + LLMトリアージ + Evalite を1本のパイプラインに通した記録を書いた。
そのときの宿題が3つ残っていた。
- Evalite の
fn_detection_rateが5回連続で ぴったり 0.00 だった。追検証でgpt-5.4に替えると同じ入力で 1.00 まで動いた。これが偶然か、再現するのか - 5 run 通して AI が一貫して
test_issueと返してきた FP-4 を、Ground Truth 側のラベル誤りとして直す必要があった - CSV を1回ごとに眺めるのに飽きてきた。1回目と2回目を並べて比較できる状態にしたかった
この記事は、その3つを片付けた記録になる。
やったことは大きく3点に集約できる。
- AIトリアージのデフォルトモデルを
gpt-4.1-miniからgpt-5.4に切り替え、5 run × 2グループでメタフラグ検知の安定性を確かめた - Ground Truth を「設計時に決めたラベル」と「観測ベースで後から付けたラベル」に分離し、FP-4 のラベルを実エラーログに合わせて書き直した
-
build-artifacts/配下の CSV/JSON 群から静的HTML/CSS/JSだけのダッシュボードを組み、Jenkins 横断ジョブで成果物として配布できる形にした
ソース一式は GitHub に置いてある: ui-test-analytics
シリーズ2本目にあたる。1本目を読まなくても辿れるように書いているが、罠の設計や3系統のテストスイート(
1a-whitebox/1b-blackbox/1c-naive)の前提は前回の記事にある。
検証観点
確かめたかったのはこの5点。
- モデルを切り替えるだけで、
fn_detection_rateは本当に動くのか。1回の追検証で出た 0.00 → 1.00 が、複数 run で再現するのか -
accuracy(verdict 一致率)は犠牲になっていないか。メタフラグを取りに行ってラベル分類を壊していないか - 「設計時に決めた Ground Truth」と「パイプラインを回す中で見つけた観測ラベル」を、メトリクスの汚染なしに分離できるか
- フレームワークを足さずに、CSV/JSON 群を素材に静的ダッシュボードを組めるか
- Jenkins の成果物として
build-artifacts/dashboard/を ZIP 配布できる状態にできるか
「AIトリアージの精度が大幅に上がりました」とは言いたくなかった。
動いた指標と動かなかった指標を、データに沿って素直に出すのが目的になる。
パイプライン全体像(前回記事からの差分)
1a-playwright-whitebox
↓
1b-playwright-blackbox
↓
1c-playwright-naive
↓
2-parse-and-triage ← AIトリアージ(モデルを gpt-4.1-mini → gpt-5.4 に変更)
↓
3-evalite ← Ground Truth(designed + observed)と突き合わせ
↓
4-aggregate ← 単一runのダッシュボード用JSON
↓
5-dashboard-aggregate ← 横断集約(当記事で追加)
最後の 5-dashboard-aggregate だけが今回の追加分で、/var/jenkins_results/runs/<raw-run-id>/results/ を横断して build-artifacts/1st〜13th/ に正規化コピーし、summary.json と summary-data.js を作る。
ダッシュボード本体(dashboard/index.html / styles.css / charts.js)は、このジョブが build-artifacts/dashboard/ に同梱する。ZIP を解凍して index.html を開けば、追加のサーバー起動なしでチャートが描画される。
1. モデルを切り替える
前回記事の追検証で見えた仮説は単純だった。
is_false_negative=true を立てるかどうかは、プロンプトの問題ではなくモデル能力の問題ではないか。
プロンプトを動かしてしまうと、何で動いたかが切り分けられなくなる。なのでまずモデルだけ替えて 5 run 回した。
差分はほぼ1行。
// pipeline/triage/index.ts より抜粋
function modelName(): string {
return process.env.OPENAI_MODEL?.trim() || 'gpt-5.4'; // 前回記事では 'gpt-4.1-mini'
}
プロンプト本体(triageInstructions())はこの時点では前回記事のまま据え置く。
プロンプトの微修正は後段(B-3)で別 run として入れた差分なので、そちらに寄せて書く。
2. Ground Truth を「設計」と「観測」に分ける
前回記事で FP-4(レート制限テスト)が 5 run 連続で AI と Ground Truth が食い違い、実エラーログを開いたら AIの判定の方が正しかった ことが分かった。
expect(locator).toBeVisible() が 'h1, .product-card' を strict mode 違反で弾いていただけで、レート制限の挙動はそもそも踏んでいない。
これを直すには、tests/ground-truth/ground-truth-labels.json の FP-4 を実態に合わせて書き直すしかない。
ただ書き直すだけだと、設計時の意図と観測ベースの修正が混ざってしまうので、ファイル分離も同時に入れた。
ファイル分離
tests/ground-truth/ を次の役割で分けた。
-
ground-truth-labels.json: テスト設計時に意図的に仕込んだ罠のラベル(label_source: designed)。Evalite では既定でメトリクスに含まれる -
observed-labels.json: パイプラインを回す中で観測した失敗パターンのラベル(label_source: observed)。status: provisionalとconfidence_required: 3を付け、確信が固まるまではメトリクスから除外する
観測ラベルの構造
observed-labels.json は、設計時に想定していなかった失敗(blackboxスイートで散発的に出るチェックアウトのタイムアウトなど)を、設計ラベルに混ぜずに残しておくための置き場になる。
今のところ1件だけ入っている。
{
"test_id": "OBS-FP-1",
"source": "observed",
"status": "provisional",
"first_seen_run": "pipeline-2",
"confidence_required": 3,
"trap_type": "observed_false_positive",
"expected_verdict": "flaky",
"root_cause": "黒箱チェックアウトフローで Proceed to Checkout 待機中にタイムアウトした。1回のCI実行だけでは、UIタイミングの不安定さと環境遅延を切り分けられない。",
"is_app_bug": false,
"notes": "同じ挙動が少なくとも3runで観測されるまでは provisional のままにする。"
}
provisional から confirmed に昇格できる条件は、同じテストケース・同じ失敗モード・同じ推定原因が confidence_required 回数以上の独立した run で再現し、テストログまたはトレースでアプリ本体の既知バグではないことを確認できた場合とする。
昇格時は status: confirmed に変更し、notes に確認した run と判断理由を残す。
ここを文章化しておかないと「なんとなく追加した暫定ラベルがいつの間にかメトリクスに入っている」状態になる。
Evalite 側の処理
evals/triage-data.ts で、provisional ラベルを既定で除外する分岐を入れた。
// evals/triage-data.ts より抜粋
function findGroundTruthLabel(
testCase: string,
labelIndex: LabelIndex,
includeProvisionalLabels: boolean,
): {
label?: GroundTruthLabel;
reason: EvalUnmatchedCase['reason'];
provisionalLabel?: GroundTruthLabel;
} {
const normalizedTestCase = normalize(testCase);
const confirmedLabel = labelIndex.confirmedByKey.get(normalizedTestCase);
if (confirmedLabel) {
return { label: confirmedLabel, reason: 'no_label' };
}
const provisionalLabel = labelIndex.provisionalByKey.get(normalizedTestCase);
if (provisionalLabel && includeProvisionalLabels) {
return { label: provisionalLabel, reason: 'provisional_label', provisionalLabel };
}
if (provisionalLabel) {
return { reason: 'provisional_label', provisionalLabel };
}
return { reason: 'no_label' };
}
includeProvisionalLabels は環境変数 EVAL_INCLUDE_PROVISIONAL_LABELS=1 か CLI フラグ --include-provisional-labels で立てる。
通常の CI 実行では立てない(provisional を含めると、確信の弱いラベルがメトリクスを動かしてしまう)。
暫定ラベルを含めて確認したい場合だけ明示的に立てて回す。
FP-4 の Ground Truth 修正
ファイル分離が済んだら、FP-4 を実エラーログに合わせて書き直す。
{
"test_id": "FP-4",
"test_name": "短時間に大量リクエストを送ると429が返る",
- "expected_verdict": "environment_issue",
- "root_cause": "全APIエンドポイントに対して10秒間に20リクエストの制限",
- "is_app_bug": false,
+ "expected_verdict": "test_issue",
+ "root_cause": "Locator strict mode違反: 'h1, .product-card' が複数要素に解決される",
+ "is_app_bug": false,
+ "notes": "設計時は環境問題を想定していたが、実エラーログ観測でテスト実装側の問題と判明(前回記事 の追検証で確定)"
}
修正後に前回記事の5 run を再 eval すると、FP-4 が verdict_match=true で評価されるようになり、accuracy も実態に近づいた。
これは「AIが間違いを起こしたか」を測る場が、同時に「ラベル設計の見落としを浮かび上がらせる」場でもあることの実例になっている。
3. ダッシュボードを軽く保つ
前回記事で蓄積した results/runs/pipeline-1〜5 と、今回追加した pipeline-6〜13 の CSV/JSON 群を1画面で見られるようにした。
方針は3つだけ。
- フレームワーク不使用。React も Vue も入れない
- グラフライブラリは Chart.js の CDN を1つだけ参照
- データソースは
summary.json1ファイルに集約。ダッシュボード側で CSV を再パースしない
データの集約形
pipeline/artifacts/summarize-runs.ts が build-artifacts/<n>/ 配下の CSV/JSON を読んで、1本の summary.json を生成する。
スキーマは run の配列で、ダッシュボードが必要とする集計をすべてここに乗せる。
// pipeline/artifacts/summarize-runs.ts より抜粋
interface RunSummary {
artifact_dir: string;
run_id: string;
counts: { /* 各 CSV の行数 */ };
test_status: Record<string, number>; // passed / failed / error
test_error_type: Record<string, number>; // locator / assertion / server / timeout / unknown
diff_status: Record<string, number>; // still_passing / new_failure / unresolved / resolved
ai_verdict: Record<string, number>; // real_bug / flaky / test_issue / environment_issue
false_negative_by_trap: Record<string, number>;
suite_breakdown: SuiteSummary[]; // 1a / 1b / 1c
eval_latest: CsvRow | null; // accuracy / fp_detection_rate / fn_detection_rate
eval_details: Record<string, unknown>[]; // details_json をパースしたもの
triage_rows: CsvRow[];
false_negative_candidate_rows: CsvRow[];
top_still_passing: CsvRow[];
}
ここに eval_details(details_json をパース済みの形)を含めるかどうかは設計判断だった。
含めない選択肢もあったが、ダッシュボード側で毎回 JSON.parse(eval_results.details_json) するのは無駄な処理になるので、集約スクリプト側で一度パースして展開しておくほうを選んだ。
ダッシュボードは summary.json をそのまま読み、可視化のための変形だけをやればよくなる。
CORS 回避のための summary-data.js
build-artifacts/dashboard/index.html を ZIP 解凍して直接ブラウザで開いたとき、fetch('./summary.json') は file:// スキームの CORS 制限で大抵失敗する。
これを避けるために、pipeline/artifacts/write-dashboard-data.ts が summary.json を summary-data.js にラップする。
// pipeline/artifacts/write-dashboard-data.ts より抜粋
const content = [
'window.__UI_TEST_ANALYTICS_SUMMARY__ = ',
JSON.stringify(summary, null, 2),
';\n',
].join('');
await writeFile(outputPath, content, 'utf8');
ダッシュボード側は、グローバルがあればそれを使い、なければ fetch にフォールバックする。
// dashboard/charts.js より抜粋
async function loadSummary() {
if (window.__UI_TEST_ANALYTICS_SUMMARY__) {
return window.__UI_TEST_ANALYTICS_SUMMARY__;
}
const errors = [];
for (const url of SUMMARY_URLS) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`${url}: ${response.status} ${response.statusText}`);
}
return response.json();
} catch (error) {
errors.push(error instanceof Error ? error.message : String(error));
}
}
throw new Error(errors.join(' / '));
}
これでローカル開発時は dashboard/ から fetch で build-artifacts/summary.json を読み、Jenkins 成果物の ZIP を解凍したケースでは summary-data.js のグローバル経由で読む、という二重化ができる。
追加のサーバー起動を要求しない。
グループ集計(前回記事 vs 当記事 vs B-3)
charts.js で run を3つのグループに分けて集計する。
// dashboard/charts.js より抜粋
const GROUPS = [
{ id: 'article1', label: '前回記事', runIds: ['pipeline-1', 'pipeline-2', 'pipeline-3', 'pipeline-4', 'pipeline-5'] },
{ id: 'article2', label: '当記事', runIds: ['pipeline-6', 'pipeline-7', 'pipeline-8', 'pipeline-9', 'pipeline-10'] },
{ id: 'prompt', label: 'B-3', runIds: ['pipeline-11', 'pipeline-12', 'pipeline-13'] },
];
function computeGroupMetrics(runs) {
const details = runs.flatMap((run) => run.eval_details ?? []);
if (!details.length) {
return { accuracy: 0, fpDetection: 0, fnDetection: 0 };
}
const fpExpected = details.filter((row) => row.expected_false_positive === true);
const fnExpected = details.filter((row) => row.expected_false_negative === true);
return {
accuracy: ratio(details.filter((row) => row.verdict_match === true).length, details.length),
fpDetection: ratio(fpExpected.filter((row) => row.is_false_positive === true).length, fpExpected.length),
fnDetection: ratio(fnExpected.filter((row) => row.is_false_negative === true).length, fnExpected.length),
};
}
加重平均にしているのは、各 run の total_cases がバラついている(4〜6件)ため、単純な算術平均だとサンプル数の差を無視することになるからだ。
expected が立っているケース数を分母にすることで、fp_detection_rate と fn_detection_rate がそれぞれの母集団に対する検知率として読める。
4. 5 run × 2グループの結果
ここから本題。前回記事(gpt-4.1-mini)と当記事(gpt-5.4)の 5 run × 2 を加重平均で並べる。
| 指標 | 前回記事 (gpt-4.1-mini) | 当記事 (gpt-5.4) | 動き |
|---|---|---|---|
| accuracy(verdict 一致) | 0.720 | 0.680 | -0.040 |
| 失敗ログ内 FP シグナル検知率 | 0.706 | 0.789 | +0.083 |
| 失敗ログ内 FN シグナル検知率 | 0.000 | 0.833 | +0.833 |
前回記事で5 run 連続で 0.00 だった fn_detection_rate が、モデルを切り替えるだけで 0.833 まで動いた。
追検証で見えた仮説は再現した、と言える。
accuracy が動かなかったことを誠実に出す
accuracy は 0.720 → 0.680 で 0.04 ポイント微減した。
これは記事の主張を弱めるのでむしろ目立たせたい。
- メタを取りに行って verdict を犠牲にしたわけではない
- 25 ケースに対する 1〜2件分のブレで、LLM ノイズの範囲に収まる
- 前回記事のように accuracy を上げる目的の調整はしていない(プロンプトはほぼ据え置き、モデルだけ替えた)
ダッシュボード上でもこの誠実さを保つために、accuracy の比較カードに「ほぼ横ばい、LLMノイズ範囲」と注記を出している。
// dashboard/charts.js より抜粋
comparisonCard({
label: '分類一致率',
value: deltaText(before.accuracy, after.accuracy),
tone: 'accuracy',
cells: [
...metricCells(before.accuracy, after.accuracy),
{ label: 'B-3', value: formatPercent(prompt.accuracy) },
],
note: 'ほぼ横ばい、LLMノイズ範囲',
}),
改善されたケースの中身
前回記事で立たなかったメタフラグが、当記事でどのテストケースに対して立つようになったか。
ダッシュボードの「失敗ログ内メタフラグ改善ケース」テーブルで、before/after のヒット率が上がったケースだけを抽出している。
| 種別 | test_id | テストケース | 1回目(5run) | 2回目(5run) |
|---|---|---|---|---|
| FN | FN-3 | カートクーポンで合計が割引される | 0/1 | 1/1 |
| FN | FN-4 | 注文履歴のページネーション境界 | 0/3 | 3/3 |
| FN | FN-5 | カート数量+ボタン連打の競合 | 0/2 | 1/2 |
| FP | FP-2 | 注文ステータスの非決定的タイミング | 0/4 | 4/4 |
| FP | FP-3 | A/Bテストでフォームレイアウト変動 | 0/1 | 1/1 |
FN-3 / FN-4 / FN-5 は「テストが本当のバグを引いている」ケースで、ここに is_false_negative=true が立つと「AIに任せて良い領域」が広がる。
FP-2 / FP-3 は「flaky と判定したテストに、ちゃんと is_false_positive=true も立てる」ケース。前回記事の gpt-4.1-mini は分類ラベル(flaky)は当てるのにメタフラグだけ立てない、という慎重さがあった。
モデルを上げると、その慎重さが緩む。
accuracy 微減の中身
verdict 一致率が落ちたのはどのケースか、をダッシュボード上で確認する。
「誤判定の前後差分」テーブルで、改善 / 後退 / 両方で外れ の3区分を色分けして見せている。
// dashboard/charts.js より抜粋
function compareVerdictBuckets(beforeRuns, afterRuns) {
// ...
for (const [key, beforeCase] of before.entries()) {
const afterCase = after.get(key);
if (!afterCase) continue;
const beforeRate = hitRate(beforeCase, 'correct');
const afterRate = hitRate(afterCase, 'correct');
if (beforeCase.incorrect > 0 && afterRate > beforeRate) {
rows.push(verdictBucketRow('improved', beforeCase, afterCase, 'improved-row'));
} else if (afterRate < beforeRate) {
rows.push(verdictBucketRow('regressed', beforeCase, afterCase, 'regressed-row'));
} else if (beforeCase.incorrect > 0 && afterCase.incorrect > 0) {
rows.push(verdictBucketRow('still missed', beforeCase, afterCase, 'missed-row'));
}
}
// ...
}
開けてみると、verdict が「両方で外れ」で残っているのは FN-5(数量+連打の競合)と FN-1(税計算の丸め)の2系統だった。
これらは AI に渡る失敗ログ自体に「数量連打」や「丸め誤差」を直接示す痕跡が薄く、エラーが timeout や assertion に化けて入ってくる。
モデルを上げてもメタフラグ(is_false_negative=true)は立つが、verdict 自体は environment_issue や flaky に流れがち。
ここに B-3 のプロンプト微修正がはまる余地があった。
5. プロンプトを後から詰める(B-3)
モデル切り替えで fn_detection_rate=0.833 まで動いたあと、残りのメタフラグ取りこぼしを詰めるためにプロンプトを動かした。
ただし重要なのは順序で、モデルの効果を見てからプロンプトを動かす。
逆にすると、何故動いたか切り分けられなくなる。
修正したのは triageInstructions() の2行だけ。is_false_positive と is_false_negative の説明に、「concurrency / quantity」「surfaced as timeout or selector-looking failures」などの手がかり語を1〜2語ずつ足した。
プロンプト全面書き直しも few-shot 追加も temperature 変更もしていない。
// pipeline/triage/index.ts より抜粋(前回記事からの差分は強調部分)
'is_false_positive should be true when a current failure is probably not an app bug, ' +
'including status/setup timeouts, selector brittleness, rollback, or other flaky behavior.',
'is_false_negative should be true when evidence suggests a real app bug that weaker tests could miss, ' +
'especially edge-case validation, calculation, ordering, pagination, coupon, quantity, or concurrency defects ' +
'even when surfaced as timeout or selector-looking failures.',
3 run(pipeline-11〜13)回した結果が次のとおり。
| 指標 | 前回記事 (gpt-4.1-mini) | 当記事 (gpt-5.4) | B-3 (gpt-5.4 + プロンプト微修正) |
|---|---|---|---|
| accuracy | 0.720 | 0.680 | 0.923 |
| 失敗ログ内 FP シグナル検知率 | 0.706 | 0.789 | 1.000 |
| 失敗ログ内 FN シグナル検知率 | 0.000 | 0.833 | 1.000 |
3 run しか回していないので「上限を測った」とまでは言わない。
ただ少なくともこの3 run では、accuracy も検知率もすべて当記事より上がった。
プロンプトの効きは「メタフラグが立つように具体例を1語足す」程度で十分で、ここを書き換えすぎるとプロンプトが膨らんで他の指標を壊しに行く、という前回記事からの予感は外れていなかった。
注記: B-3 の FP / FN シグナル検知率の 1.000 は、3 run 合計で fp_expected 10件・fn_expected 3件しか分母がない。そのまま「100%」と読むより「外し1件があれば 0.90 / 0.67 に揺れる狭い母集団」と読むほうが安全になる。B-3 を「決め技」として語るより、「モデルを動かしたあとに、残った取りこぼしを安く詰める手段」として位置づけた。
6. PASS の森に紛れた偽陰性は、構造で受け止め続ける
ここまではすべて AIに渡された FAIL の中 での話。
PASS してしまっている偽陰性は、どんなにモデルを上げても AI の入力に出てこない。
前回記事で導入した false-negative-candidates.csv(Ground Truth の偽陰性ラベルに alias 一致した PASS テスト)は、当記事でも変えていない。
ダッシュボードでも別カードに分けて、「AI判定ではなく、別名/仕様で拾う」と注記している。
pipeline-10(当記事の最終 run)でも、1c-naive 由来で5件が候補として残っている。
モデルを上げても、これらは AI の射程外で動く。
run_id,test_suite,test_case,status,matched_test_id,expected_verdict,detection_source
pipeline-10,cart.spec.ts,カートページ › カートの合計金額が表示される,passed,FN-1,real_bug,ground_truth_alias_passed_test
pipeline-10,cart.spec.ts,カートページ › クーポンを適用すると割引メッセージが表示される,passed,FN-3,real_bug,ground_truth_alias_passed_test
pipeline-10,checkout.spec.ts,チェックアウトページ › 正常な情報で注文を確定できる,passed,FN-6,real_bug,ground_truth_alias_passed_test
pipeline-10,menu-and-detail.spec.ts,メニューページ › 価格で昇順ソートできる,passed,FN-2,real_bug,ground_truth_alias_passed_test
pipeline-10,menu-and-detail.spec.ts,メニューページ › ソート後に商品カードをクリックして詳細に遷移できる,passed,FN-2,real_bug,ground_truth_alias_passed_test
切り分けるとこうなる。
- Evalite の
fn_detection_rate→ 失敗ログ内のメタ判定。モデル能力で動く。今回 gpt-5.4 に替えて 0.000 → 0.833、プロンプト微修正で 1.000 まで動いた -
false-negative-candidates.csvの構造的検出 → PASSの森に紛れた偽陰性。モデルを替えても動かない。alias 一致と仕様照合で先に拾うしかない
前回記事で「全部構造の話」と片付けて、前回記事の追検証で「半分は実はモデルの話だった」と訂正したところを、当記事では数字つきで再確認できた。
分かったこと
1. メタフラグはプロンプトより先にモデルで動く
前回記事で gpt-4.1-mini の fn_detection_rate=0 を見たとき、最初の解釈は「PASSの偽陰性が AI に渡らないから 0」だった。
その解釈は半分しか正しくなく、もう半分は「失敗ログ内の偽陰性ケースに対して is_false_negative=true を立てるかどうか」というモデル能力の話だった。
当記事の 5 run × 2グループで、その仮説が再現した。
プロンプトを動かす前に、まずモデルを動かしてみる、という順番は意外と効く。
2. accuracy は微減しても問題ない場合がある
当記事の accuracy 0.720 → 0.680 を「精度が下がった」と書いてしまうと嘘になる。
verdict 一致率は 25 ケース中の 1〜2件で動く粒度の指標で、ここを 0.04 動かすために調整を入れたわけではない。
メタフラグ系を 0.83 ポイント動かすために、その範囲のノイズが乗ったというのが正確な読みになる。
数字を残すときに「動いた指標」「動かなかった指標」「LLMノイズ範囲で動いた指標」を分けて出す習慣が、ダッシュボードに比較ビューを置いてから固まった。
3. Ground Truth は観測で動く対象
前回記事で FP-4 が 5 run 連続で AI 側と食い違ったとき、最初は「AIが外している」と読んでいた。
実エラーログを開いたら、外していたのは Ground Truth の方だった。
ground-truth-labels.json と observed-labels.json を分離したのはこの体験から来ている。
設計時の仮説と観測ベースの修正を同じファイルに混ぜ込むと、後から見たときに「これは罠の意図か、実ログから直したのか」が読み取れなくなる。
label_source と status を持たせて、provisional の暫定ラベルは確信が固まるまで EVAL_INCLUDE_PROVISIONAL_LABELS=1 を立てた検証 run の中だけで動かす、という運用に切り出した。
4. ダッシュボードは集約スキーマで決まる
summary.json の runs[].eval_details に details_json のパース済みデータを展開した時点で、ダッシュボードは「ただのレンダリング層」になった。
JS 側で CSV をパースする処理は1行も書かずに済んでいる。
逆にここでスキーマ設計をサボると、ダッシュボードに「特定の run だけの集計用ロジック」が滲み出してくる。
集約スクリプトを「1 run の構造」と「複数 run の比較に必要な構造」の両方を出すように書いておくと、charts.js 側は GROUPS の定義を変えるだけで比較対象のグルーピングを動かせる。
5. プロンプト調整は最後の詰めに置く
B-3 で 1.000 / 1.000 / 0.923 まで動いたが、ここに到達するためにプロンプトを大きく書き換える必要はなかった。
1〜2語の手がかりを足すだけ。
プロンプトを膨らませると逆に他の指標を壊しに行くという仮説は、3 run しか回していないので断言はできないが、少なくともこの範囲では当たっていた。
「分類精度を求めるならプロンプト、メタ判定を求めるならモデル」という整理は、記事3以降の「AIに任せる範囲」「構造で受け止める範囲」の線引きに繋がる。
次にやること
ここまでで、AIトリアージのメタフラグ検知は、この実験範囲では実用に近い手応えが出た(B-3 で fp/fn 検知率 1.000、accuracy 0.923。ただし 3 run / 13 ケース)。
ただこれは「失敗ログの中で AI が判定する」範囲だけの話で、PASS してしまっている偽陰性は依然として false-negative-candidates.csv の alias 一致でしか拾えていない。
そこを次の記事で扱う。
方針は、テスト結果(PASS/FAIL)ではなく、テストコードと仕様書を入力にして、各仕様項目に対してテストが何をassertしているかを静的に照合する というもの。
入力: app-spec.md(仕様書)+ テストコード(.spec.ts)
分析: 各仕様項目に対して、テストが何をassertしているかを照合
出力: gap-analysis.csv(assertされていない仕様項目 = 偽陰性リスク)
例として、FN-3(クーポン)の場合:
- 仕様: 「クーポン適用で合計金額が割引される」
-
1c-naiveのテスト: 「割引メッセージが表示される」だけを検証 - ギャップ: 「合計金額の変化」が assert されていない
- → 偽陰性リスク: 高
これが動くと、Ground Truth を手作業で作る運用がスケールしない問題(当記事で実感した)を解決する方向にも繋がる。
仕様とアサーションのギャップから Ground Truth を半自動で下書きできれば、人間のレビューだけで済む。
その先には、ギャップを埋める補強テストの自動生成(リテスト層)と、Ground Truth MCP化が乗る。
当記事は「動かない指標を動かす」回だったが、記事3は「AIの射程外を構造で広げる」回になる予定。
リポジトリ
- ui-test-analytics: https://github.com/AnarchyAtelier/UnderControl/tree/main/ai-in-the-loop-pipeline/ui-test-analytics
- ダッシュボード本体(
dashboard/)、横断集約スクリプト(pipeline/artifacts/)、Jenkins ジョブ(jenkins/jobs/5-dashboard-aggregate.Jenkinsfile) - 当記事用の確定スナップショット(
build-artifacts/1st〜13th/)、summary.json、run-map.json
- ダッシュボード本体(
- Ground Truth ラベル運用: tests/ground-truth/README.md
- 仕様書(前回記事から継続): docs/app-spec.md




