ae81d22775
引擎自 2026-08-01 起已支援條件邊(cypher-executor/src/graph-executor.ts case 'ON_TRUE'/'ON_FALSE'/'ON_BRANCH',VALID_EDGE_TYPES 亦已列入;31 個 cypher-executor 測試全過)。registry/skills/write_intent_workflow.md(單一 真相源)也已在同日更正為教 ON_TRUE/ON_FALSE/ON_BRANCH 是合法邊。 但 cli/scripts/check-harness-generation.mjs 的世代閘還停在舊世代判準: 只要 SKILL.md 出現正面示範的 ON_TRUE 就擋——這道閘本身才是落後的一方, 把已經寫對的教材當錯誤攔下,害乾淨 `npm run build` 必敗。 同源的過時內容還藏在三個手動維護的 harness 原始檔(非腳本產物): CLAUDE.block.md/commands/arcrun.md/hooks/arcrun-guard.sh 都寫著 「引擎沒有條件邊」「沒有 ON_TRUE/ON_FALSE/ON_FAILURE」,一併更正。 真正不存在的邊是 ON_FAILURE(VALID_EDGE_TYPES 只有 ON_FAIL),把 mustNot 判準從 ON_TRUE 換成 ON_FAILURE,並新增 must 規則要求 ON_TRUE 必須出現, 防止教材日後又被改回「條件邊不存在」的舊世代說法。 skills/arcrun-mindset/SKILL.md 是由 registry 於建置期重建的產物 (build-harness-skill.mjs),本次改動只跑 `npm run build:harness` 重建、不手改。 驗證:故意把 SKILL.md 的 ON_FAILURE 改成正面示範,確認閘仍會擋下 (exit 1),還原後 `npm run build` 連跑兩次皆全綠且冪等(SKILL.md md5 不變)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
137 lines
5.9 KiB
JavaScript
137 lines
5.9 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 分型'],
|
||
// 條件邊自 2026-08-01 起引擎已支援(cypher-executor/src/graph-executor.ts
|
||
// case 'ON_TRUE'/'ON_FALSE'/'ON_BRANCH',31 個測試全過)。教材該教會怎麼用,
|
||
// 不是教「不存在」——這條 must 同時防「哪天又被改回舊世代說法」的回歸。
|
||
['ON_TRUE', '條件邊(配 if_control)自 2026-08-01 起引擎已支援,教材須教會用法'],
|
||
],
|
||
mustNot: [
|
||
// ON_FAILURE 才是真的不存在(VALID_EDGE_TYPES 只有 ON_FAIL,見
|
||
// cypher-executor/src/lib/constants.ts)。只准出現在「教它不存在」的脈絡。
|
||
// 2026-08-10 修正:這道閘原本擋的是 ON_TRUE——但 ON_TRUE/ON_FALSE/ON_BRANCH
|
||
// 已是引擎現世代能力,正確教材反而被這道閘擋下,是閘的判準過時了,不是教材寫錯。
|
||
['ON_FAILURE', '引擎沒有這種邊(只有 ON_FAIL);教材不該把它教成可用的邊', /不要寫|不存在|沒有這種|❌|非法/],
|
||
],
|
||
},
|
||
{
|
||
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');
|