bookworm-smart-assistant/scripts/patches/patch-l1d-bookworm-implicit-meta-trigger.js
Bookworm Admin b7a8e29d21 release: v6.7.0 - OTA E2E test release
- 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)
2026-04-27 17:59:44 +08:00

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.');