#!/usr/bin/env node /** * check-harness-generation.mjs — 世代閘:harness 內容脫節就讓 build/publish 失敗 * * 【為什麼要這道閘】 * 2026-07-31 實錄:`acr install-harness` 的管道一直是好的,但它鋪出去的**內容停在上一代**—— * harness skill grep「意圖」「>>」= 0 命中,只講世界觀。管道綠燈、交付物過時, * 沒有任何機械檢查會抱怨 ⇒ 世代脫節可以無聲存在好幾個月。 * * 這道閘檢查四件交付物的「現世代指紋」。缺指紋 = exit 1,擋掉 build 與 npm publish。 * 指紋要挑「上一代絕不會有、現世代一定有」的字串,不是隨便的關鍵字。 */ import { readFileSync, existsSync, statSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; import { execFileSync } from 'node:child_process'; const here = dirname(fileURLToPath(import.meta.url)); const harness = join(here, '..', 'harness'); const repoRoot = join(here, '..', '..'); /** @type {{file: string, must: [string, string][], mustNot?: [string,string][]}[]} */ const CHECKS = [ { file: 'skills/arcrun-mindset/SKILL.md', must: [ ['>>', '意圖語法(`A >> 邊 >> B`)——步驟 1 的核心教材'], ['ON_SUCCESS', '合法邊之一'], ['對每個', 'FOREACH 邊(十題裡有四題要用)'], ['input', '第一個節點固定是 input'], ['not_found', '現世代查詢狀態(舊版寫 missing/假 found)'], ['腹語術', '缺件不准改寫成 code 的紅線'], ['recipe', '零件 vs recipe 分型'], ], mustNot: [ // ON_TRUE 只准出現在「教它不存在」的脈絡(否定詞/實測證據)。 // 若哪天它出現在範本裡(正面示範),就是教材寫錯,該擋。 ['ON_TRUE', '引擎沒有條件邊,教材不該把它當可用的邊', /不要寫|不存在|沒有條件|❌|非法|grep|= 0/], ], }, { file: 'CLAUDE.block.md', must: [ ['>>', '意圖語法要在 CLAUDE.md 就先亮相'], ['not_found', '缺件兩條路的觸發點'], ], }, { file: 'commands/arcrun.md', must: [ ['>>', '/arcrun 的第一步就該是寫意圖'], ['acr search', '現世代的跨類搜尋指令'], ], }, { file: 'hooks/arcrun-guard.sh', must: [ ['arcrun-mindset', 'hook 被擋下時要把 AI 導向 skill,而不是叫它去翻 repo 文件'], ['>>', 'hook 的正路提示要提到意圖語法'], ], }, ]; let fail = 0; const say = (s) => console.log(s); say('\n 世代閘:檢查 harness 交付物是否為現世代內容\n'); for (const c of CHECKS) { const p = join(harness, c.file); if (!existsSync(p)) { say(` ❌ ${c.file} — 檔案不存在`); fail++; continue; } const text = readFileSync(p, 'utf8'); const missing = c.must.filter(([needle]) => !text.includes(needle)); const badNot = (c.mustNot ?? []).filter(([needle, , allowIfNear]) => { if (!text.includes(needle)) return false; if (!allowIfNear) return true; // 允許「在教『不要用』的脈絡裡」出現:看該字串所在行是否有豁免詞 return !text .split('\n') .filter((l) => l.includes(needle)) .every((l) => allowIfNear.test(l)); }); if (missing.length === 0 && badNot.length === 0) { say(` ✓ ${c.file}`); } else { fail++; say(` ❌ ${c.file}`); for (const [needle, why] of missing) say(` 缺指紋「${needle}」— ${why}`); for (const [needle, why] of badNot) say(` 不該出現「${needle}」— ${why}`); } } // harness skill 必須是由 registry 重建的最新版(防「改了 registry 忘了重跑 build」) const skillPath = join(harness, 'skills', 'arcrun-mindset', 'SKILL.md'); const registrySkill = join(repoRoot, 'registry', 'skills', 'write_intent_workflow.md'); if (existsSync(skillPath) && existsSync(registrySkill)) { try { execFileSync(process.execPath, [join(here, 'build-harness-skill.mjs')], { stdio: 'pipe' }); const rebuilt = readFileSync(skillPath, 'utf8'); const before = statSync(skillPath); // 重建後內容即為期望值 void before; // 重建是冪等的:若重建後與 git 中的版本不同,git diff 會在 CI 顯示; // 這裡直接比對「重建結果是否含 registry 當前的關鍵段落」 const reg = readFileSync(registrySkill, 'utf8'); const marker = reg.includes('## 7. 常犯的錯') ? '## 7. 常犯的錯' : null; if (marker && !rebuilt.includes(marker)) { say(` ❌ harness skill 與 registry 漂移:registry 有「${marker}」但重建產物沒有`); fail++; } else { say(' ✓ harness skill 與 registry/skills/write_intent_workflow.md 同步'); } } catch (e) { say(` ❌ 無法由 registry 重建 harness skill:${e.message}`); fail++; } } say(''); if (fail) { say(` 🔴 世代閘擋下(${fail} 項)。harness 交付的內容落後於現世代。`); say(' 修法:改 registry/skills/write_intent_workflow.md(單一真相源)或對應的'); say(' cli/harness/ 檔案,然後跑 `npm run build:harness` 重建,再跑本檢查。\n'); process.exit(1); } say(' ✅ 世代閘通過:四件交付物都帶現世代指紋\n');