#!/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. * - --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 改用 (? 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.');