- VERSION file as authoritative version source - export.mjs reads VERSION with package.json fallback - bw-ota.ps1 DryRun mode for safe testing - auto-setup.ps1 bumped to v3.2.0 (Phase 8 OTA)
203 lines
8.2 KiB
JavaScript
203 lines
8.2 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Patch L1d: Bookworm 隐式元词触发覆盖 (2026-04-25)
|
|
*
|
|
* 上下文 (承接 L1 / L1b / L1c):
|
|
* L1 实测短词查询 ("系统自检" / "路由分析" / "钩子管线审查") L1
|
|
* 虚拟 agent 注入未触发. 经断点排查, _loadAgentNamesCached 工作正常,
|
|
* R85 ("梳理 skill 矩阵" 双分支模式) 注入 self-auditor 成功为铁证;
|
|
* 失败查询的共同特征: trigger 强制要求字面量 `bookworm|booworm`,
|
|
* 而用户在已设定 Bookworm 上下文后省略品牌词, 规则不命中,
|
|
* _candidateAgents 集合为空, 不注入虚拟项, 业务 skill 上位.
|
|
*
|
|
* 根因 (假设 B 验证通过):
|
|
* R84 trigger:
|
|
* "(?:bookworm|booworm).{0,20}(?:路由|消歧|钩子|...)|
|
|
* (?:路由|消歧|钩子).{0,10}(?:bookworm|booworm)"
|
|
* 两支均要求 `bookworm` 字面共现, 短词查询全失配.
|
|
* R86 同样问题. R85 已采用双分支 (with/without bookworm) 模式,
|
|
* 是已被验证的治本范式.
|
|
*
|
|
* 修复 (镜像 R85 范式):
|
|
* 1. 给 R84 追加无 bookworm 短词分支 — 仅匹配 Bookworm 内部独有的复合元词
|
|
* (路由分析 / 路由消歧 / 钩子管线 / 管线审查 / 路由引擎 / 意图分类器 /
|
|
* 权重学习器 / 消歧规则), 必须用边界字符或行首/尾包裹防误触.
|
|
* 2. 给 R86 追加 "系统自检" 强语义短词分支 (system-self-check 是
|
|
* Bookworm self-auditor 独占语义, 业务项目自检会带"项目/这个/帮我审"
|
|
* 上下文, 由 R27/project-audit-expert 处理).
|
|
* 3. 反回归保护: R86 新分支前置 negative lookahead, 排除业务前缀
|
|
* "项目|这个|帮我审|代码|应用|系统级 (业务领域词)".
|
|
*
|
|
* 安全性 / 反回归:
|
|
* - "帮我审一下这个项目" 不含 "系统自检" 字面, 不命中 R86 新分支.
|
|
* project-audit-expert 路径 (R27 + 业务 BM25) 完全保留.
|
|
* - "vue 路由分析" / "next.js 路由" / "react router 分析" 因 trigger
|
|
* 未列入这些词, 路径不变 (vue-expert / nextjs-developer 仍胜出).
|
|
* 新分支只匹配 "路由分析" 短查询且无业务词前缀.
|
|
* - R85 已 PASS 用例 ("梳理 skill 矩阵" / "bookworm hook 检查") 不动.
|
|
* - L1c 5 case 测试矩阵 (project-audit-expert 业务保护) 不影响.
|
|
*
|
|
* Fail-close:
|
|
* - JSON.parse / JSON.stringify 异常 → 中止写入 + stderr 警告.
|
|
* - sentinel (rules._meta.l1d_applied) 防重入.
|
|
* - 备份原文件至 patches/bak/ + .bak.<ts>
|
|
* - --apply 之前 dry-run 校验新 trigger regex 可被 RegExp 编译.
|
|
*
|
|
* 用法:
|
|
* node patches/patch-l1d-bookworm-implicit-meta-trigger.js # dry-run
|
|
* node patches/patch-l1d-bookworm-implicit-meta-trigger.js --apply # 写入
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
|
|
const TARGET = path.join(__dirname, '..', 'disambiguation-rules.json');
|
|
const BAK_DIR = path.join(__dirname, 'bak');
|
|
const SENTINEL_KEY = 'l1d_implicit_meta_applied_v2';
|
|
const LEGACY_SENTINEL = 'l1d_implicit_meta_applied'; // v1 sentinel, allow upgrade
|
|
const APPLY = process.argv.includes('--apply');
|
|
|
|
function fail(msg, code = 2) {
|
|
process.stderr.write('[L1d] ' + msg + '\n');
|
|
process.exit(code);
|
|
}
|
|
function info(msg) {
|
|
process.stdout.write('[L1d] ' + msg + '\n');
|
|
}
|
|
|
|
if (!fs.existsSync(TARGET)) fail('target missing: ' + TARGET);
|
|
|
|
let raw;
|
|
try {
|
|
raw = fs.readFileSync(TARGET, 'utf8');
|
|
} catch (e) {
|
|
fail('read failed: ' + e.message);
|
|
}
|
|
|
|
let json;
|
|
try {
|
|
json = JSON.parse(raw);
|
|
} catch (e) {
|
|
fail('parse failed: ' + e.message);
|
|
}
|
|
|
|
if (!json || !json._meta || !Array.isArray(json.rules)) {
|
|
fail('schema mismatch: missing _meta or rules[]');
|
|
}
|
|
|
|
if (json._meta[SENTINEL_KEY]) {
|
|
info('sentinel ' + SENTINEL_KEY + ' present, already patched. no-op.');
|
|
process.exit(0);
|
|
}
|
|
if (json._meta[LEGACY_SENTINEL]) {
|
|
info('legacy v1 sentinel detected, upgrading to v2 (CJK boundary fix).');
|
|
}
|
|
|
|
// 新 trigger 设计 v2 (镜像 R85 双分支, negative lookbehind 反业务回归)
|
|
// v1 用边界字符 (?:^|[^...]) 包裹, 但 CJK 流式短语 (钩子管线审查) 内部无分隔
|
|
// 导致命中失败. v2 改用 (?<!business-prefix) negative lookbehind, 短语内部
|
|
// 任意位置都能命中, 业务前缀 (vue/react/项目/这个) 仍被精准排除.
|
|
const R84_NEW_TRIGGER =
|
|
"(?:bookworm|booworm).{0,20}(?:路由|消歧|钩子|hook|管线|注入器|分类器|引擎|遥测|盲点|融合权重|意图分类|消歧规则|路由引擎|权重学习器|状态文件|state\\s*file|追踪|trace|telemetry)" +
|
|
"|(?:路由|消歧|钩子).{0,10}(?:bookworm|booworm)" +
|
|
"|(?<!vue.?|vue\\s|next\\.?js?\\s|react\\s|angular\\s|nuxt\\s|项目|应用|这个)(?:路由分析|路由消歧|钩子管线|管线审查|路由引擎|意图分类器|权重学习器|消歧规则|消歧引擎|融合权重)";
|
|
|
|
// R86: "系统自检" 独占短词分支, 排除业务上下文前缀
|
|
// 业务前缀: 项目|这个|应用|代码|帮我审|帮我检查 → 走 R27/project-audit-expert
|
|
const R86_NEW_TRIGGER =
|
|
"(?:bookworm|booworm).{0,15}(?:全量梳理|工作流梳理|系统梳理|文件梳理|模块梳理|架构梳理|hook\\s*梳理|技术梳理)" +
|
|
"|(?<!项目|这个|应用|代码|帮我审|帮我检查)系统自检";
|
|
|
|
// 校验新 trigger 可编译
|
|
try {
|
|
new RegExp(R84_NEW_TRIGGER, 'i');
|
|
new RegExp(R86_NEW_TRIGGER, 'i');
|
|
} catch (e) {
|
|
fail('new trigger regex invalid: ' + e.message);
|
|
}
|
|
|
|
// 定位 R84 / R86
|
|
const r84 = json.rules.find(r => r.id === 'R84');
|
|
const r86 = json.rules.find(r => r.id === 'R86');
|
|
if (!r84) fail('R84 not found in rules[]');
|
|
if (!r86) fail('R86 not found in rules[]');
|
|
|
|
const r84OldTrigger = r84.trigger;
|
|
const r86OldTrigger = r86.trigger;
|
|
|
|
// v2 升级: 即使 v1 已扩展, 也强制覆盖 (修 CJK 边界 bug)
|
|
if (r84OldTrigger.includes('意图分类器')) {
|
|
info('R84 has legacy meta-words, overwriting with v2 (lookbehind).');
|
|
}
|
|
if (r86OldTrigger.includes('系统自检')) {
|
|
info('R86 has legacy 系统自检 branch, overwriting with v2 (lookbehind).');
|
|
}
|
|
|
|
// 应用变更
|
|
r84.trigger = R84_NEW_TRIGGER;
|
|
r86.trigger = R86_NEW_TRIGGER;
|
|
|
|
// 更新 _meta
|
|
json._meta[SENTINEL_KEY] = true;
|
|
json._meta.l1d_patched_at = new Date().toISOString();
|
|
json._meta.description = (json._meta.description || '') +
|
|
' | L1d (2026-04-25): R84/R86 追加无 bookworm 短词分支 (路由分析/钩子管线/系统自检 等)';
|
|
if (Array.isArray(json._meta.changelog)) {
|
|
json._meta.changelog.push(
|
|
'L1d: R84 追加 (路由分析|路由消歧|钩子管线|管线审查|路由引擎|意图分类器|权重学习器|消歧规则|消歧引擎|融合权重) 无 bookworm 短词分支',
|
|
'L1d: R86 追加 系统自检 无 bookworm 短词分支 (反回归: 边界字符前缀)'
|
|
);
|
|
}
|
|
|
|
const newRaw = JSON.stringify(json, null, 2);
|
|
|
|
// 校验新 JSON 可解析
|
|
try {
|
|
JSON.parse(newRaw);
|
|
} catch (e) {
|
|
fail('serialized JSON re-parse failed: ' + e.message);
|
|
}
|
|
|
|
const oldHash = crypto.createHash('sha256').update(raw).digest('hex').slice(0, 12);
|
|
const newHash = crypto.createHash('sha256').update(newRaw).digest('hex').slice(0, 12);
|
|
|
|
info('target : ' + TARGET);
|
|
info('R84 old : ' + r84OldTrigger.length + ' chars');
|
|
info('R84 new : ' + R84_NEW_TRIGGER.length + ' chars (+' + (R84_NEW_TRIGGER.length - r84OldTrigger.length) + ')');
|
|
info('R86 old : ' + r86OldTrigger.length + ' chars');
|
|
info('R86 new : ' + R86_NEW_TRIGGER.length + ' chars (+' + (R86_NEW_TRIGGER.length - r86OldTrigger.length) + ')');
|
|
info('old sha : ' + oldHash);
|
|
info('new sha : ' + newHash);
|
|
|
|
if (!APPLY) {
|
|
info('dry-run OK. re-run with --apply to write.');
|
|
process.exit(0);
|
|
}
|
|
|
|
// 备份
|
|
if (!fs.existsSync(BAK_DIR)) {
|
|
try { fs.mkdirSync(BAK_DIR, { recursive: true }); } catch (e) { fail('mkdir bak failed: ' + e.message); }
|
|
}
|
|
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
const bakPath = path.join(BAK_DIR, 'disambiguation-rules.json.bak.' + ts);
|
|
try {
|
|
fs.writeFileSync(bakPath, raw, 'utf8');
|
|
} catch (e) {
|
|
fail('backup write failed: ' + e.message);
|
|
}
|
|
info('backup : ' + bakPath);
|
|
|
|
// 原子写入: tmp → rename
|
|
const tmpPath = TARGET + '.tmp.' + process.pid;
|
|
try {
|
|
fs.writeFileSync(tmpPath, newRaw, 'utf8');
|
|
fs.renameSync(tmpPath, TARGET);
|
|
} catch (e) {
|
|
try { fs.unlinkSync(tmpPath); } catch (_) {}
|
|
fail('write failed: ' + e.message);
|
|
}
|
|
info('applied OK.');
|