46afea83c2
管道本來就是好的(install-harness 功能完整、冪等),**過時的是內容**:
harness skill(4066B)grep「意圖」「>>」= 0 命中,只講世界觀/別寫 Python
⇒ 新裝的封測者拿不到步驟 1 的核心教材(`>>` 意圖語法)。
■ 單一真相源:harness skill 改為建置期由 registry 複製
build-harness-skill.mjs=head + registry/skills/write_intent_workflow.md 正文 + tail。
選「建置期複製」的理由:npm files 只收 harness/,registry 不進套件;
symlink 在 npm pack 與 Windows 不可靠。產物 commit 進 repo(npm 裝的是產物、不跑 build)。
head/tail 是 harness 專屬(CLI 語境入口/acr 指令表/暴露同意/誠實鐵律),
install-harness 的 copyTree 跳過 .head/.tail,不鋪進使用者專案。
■ 其餘三件逐份對照現世代事實後更新(過時的直接刪,不留死代碼)
- CLAUDE.block.md:補 >> 意圖語法、not_found 兩條路、零件 vs recipe 分型、
腹語術紅線、金鑰只拿名字
- commands/arcrun.md:步驟改成「先寫意圖 → 丟去查 → 再寫 YAML」,補 acr search/validate
- hooks/arcrun-guard.sh:**正路提示改為指向 arcrun-mindset Skill +意圖語法**
(呼應「hook 沒提 skill 反而把 AI 導向 repo 文件」的教訓);
新增 code 節點腹語術提醒,settings.fragment 補 Write|Edit|MultiEdit matcher
■ 世代閘(防再度脫節)
check-harness-generation.mjs 檢查四件交付物的現世代指紋,缺指紋 exit 1,
掛進 npm run build(prepublishOnly 因此也擋)。
反向驗證:把 skill/CLAUDE.block 換回上一代 → 兩者都被擋下並逐條點名缺哪個指紋。
■ 驗收(考生 haiku/受測物=環境)
乾淨臨時目錄 acr install-harness → 四件鋪好;重跑冪等(全檔 md5 不變、
CLAUDE.md 66 行不變、hooks 條目 2 不變、arcrun 區塊仍 1 個)。
haiku 只讀該目錄的 CLAUDE.md+SKILL.md(明令禁讀 ~/.claude、禁上網;
兩份教材 md5 與大小均不同,可證非考本機那支)答十題
→ grade-step1.sh **10 / 10 通過**(判分器同時反向驗證仍會抓
ON_TRUE/ON_FAILURE/第一節點非 input)。
npm test 18/18、tsc 綠。
SDD:workflow-discovery/tasks.md 3.11
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
131 lines
5.2 KiB
JavaScript
131 lines
5.2 KiB
JavaScript
#!/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');
|