add-coder 0.3.26 → 0.3.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -10
- package/dist/index.js +481 -209
- package/package.json +14 -4
- package/templates/.add-coder-src-hash.json +129 -106
- package/templates/adapters/claude/hooks/doc-format-guard.sh +112 -31
- package/templates/adapters/claude/hooks/lib/common.sh +46 -87
- package/templates/adapters/claude/hooks/lib/context-inject.sh +2 -2
- package/templates/adapters/claude/hooks/lib/notify.sh +6 -4
- package/templates/adapters/claude/hooks/lib/preload-templates.sh +27 -2
- package/templates/adapters/claude/hooks/lib/state-detect.sh +8 -112
- package/templates/adapters/claude/hooks/lib/vocabulary.sh +13 -7
- package/templates/adapters/claude/hooks/prompt-submit.sh +1 -1
- package/templates/adapters/claude/hooks/stop-check.sh +9 -0
- package/templates/adapters/codex/config.toml.example +12 -0
- package/templates/adapters/codex/hooks/doc-format-guard.sh +121 -39
- package/templates/adapters/codex/hooks/lib/common.sh +47 -99
- package/templates/adapters/codex/hooks/lib/context-inject.sh +4 -4
- package/templates/adapters/codex/hooks/lib/notify.sh +6 -4
- package/templates/adapters/codex/hooks/lib/preload-templates.sh +27 -6
- package/templates/adapters/codex/hooks/lib/state-detect.sh +5 -114
- package/templates/adapters/codex/hooks/lib/vocabulary.sh +3 -8
- package/templates/adapters/codex/hooks/notification.sh +3 -18
- package/templates/adapters/codex/hooks/post-tool-use.sh +24 -119
- package/templates/adapters/codex/hooks/pre-tool-use.sh +97 -184
- package/templates/adapters/codex/hooks/prompt-submit.sh +8 -3
- package/templates/adapters/codex/hooks/session-start.sh +19 -9
- package/templates/adapters/codex/hooks/stop-check.sh +44 -18
- package/templates/adapters/codex/hooks.json +67 -1
- package/templates/adapters/qoder/hooks/doc-format-guard.sh +119 -38
- package/templates/adapters/qoder/hooks/lib/common.sh +49 -87
- package/templates/adapters/qoder/hooks/lib/context-inject.sh +2 -2
- package/templates/adapters/qoder/hooks/lib/notify.sh +6 -4
- package/templates/adapters/qoder/hooks/lib/preload-templates.sh +27 -2
- package/templates/adapters/qoder/hooks/lib/state-detect.sh +8 -112
- package/templates/adapters/qoder/hooks/lib/vocabulary.sh +13 -7
- package/templates/adapters/qoder/hooks/prompt-submit.sh +1 -1
- package/templates/adapters/qoder/hooks/session-start.sh +1 -1
- package/templates/adapters/qoder/hooks/stop-check.sh +11 -0
- package/templates/adapters/trae/hooks/doc-format-guard.sh +120 -38
- package/templates/adapters/trae/hooks/lib/common.sh +46 -87
- package/templates/adapters/trae/hooks/lib/context-inject.sh +2 -2
- package/templates/adapters/trae/hooks/lib/notify.sh +5 -3
- package/templates/adapters/trae/hooks/lib/preload-templates.sh +27 -2
- package/templates/adapters/trae/hooks/lib/state-detect.sh +8 -115
- package/templates/adapters/trae/hooks/lib/vocabulary.sh +13 -7
- package/templates/adapters/trae/hooks/pre-tool-use.sh +14 -8
- package/templates/adapters/trae/hooks/stop-check.sh +9 -0
- package/templates/adapters/vscode/hooks/doc-format-guard.sh +112 -31
- package/templates/adapters/vscode/hooks/lib/common.sh +46 -87
- package/templates/adapters/vscode/hooks/lib/context-inject.sh +2 -2
- package/templates/adapters/vscode/hooks/lib/notify.sh +6 -4
- package/templates/adapters/vscode/hooks/lib/preload-templates.sh +27 -2
- package/templates/adapters/vscode/hooks/lib/state-detect.sh +8 -112
- package/templates/adapters/vscode/hooks/lib/vocabulary.sh +13 -7
- package/templates/adapters/vscode/hooks/notification.sh +3 -5
- package/templates/adapters/vscode/hooks/prompt-submit.sh +1 -1
- package/templates/adapters/vscode/hooks/stop-check.sh +9 -0
- package/templates/core/docs/ADD-governance-claude-code.md +1 -1
- package/templates/core/docs/ADD-governance-codex.md +1 -1
- package/templates/core/docs/ADD-governance-qoder-cn.md +1 -1
- package/templates/core/docs/ADD-governance-trae.md +1 -1
- package/templates/core/docs/ADD-governance-vscode-copilot.md +1 -1
- package/templates/core/hooks/doc-format-guard.sh +120 -38
- package/templates/core/hooks/lib/common.sh +46 -87
- package/templates/core/hooks/lib/context-inject.sh +2 -2
- package/templates/core/hooks/lib/notify.sh +5 -3
- package/templates/core/hooks/lib/preload-templates.sh +27 -2
- package/templates/core/hooks/lib/state-detect.sh +8 -115
- package/templates/core/hooks/lib/vocabulary.sh +13 -7
- package/templates/core/hooks/pre-tool-use.sh +14 -8
- package/templates/core/hooks/stop-check.sh +9 -0
- package/templates/core/prisma/add.prisma +140 -82
- package/templates/core/rules/project_rules.md +27 -24
- package/templates/core/scripts/db-ensure.sh +8 -0
- package/templates/core/scripts/gen-plan-index.sh +4 -15
- package/templates/core/scripts/mcp-server/notifications/hitl.ts +9 -1
- package/templates/core/scripts/mcp-server/notifications/hook.ts +25 -9
- package/templates/core/scripts/mcp-server/resources/hitl-approval-widget.ts +34 -0
- package/templates/core/scripts/mcp-server/resources/hook-events-report.ts +14 -2
- package/templates/core/scripts/mcp-server/resources/index.ts +2 -0
- package/templates/core/scripts/mcp-server/sampling/review.ts +25 -10
- package/templates/core/scripts/mcp-server/shared/db-types.ts +33 -8
- package/templates/core/scripts/mcp-server/shared/dps-scoring.strategy.ts +1 -1
- package/templates/core/scripts/mcp-server/shared/env.ts +28 -8
- package/templates/core/scripts/mcp-server/shared/fs.ts +3 -0
- package/templates/core/scripts/mcp-server/shared/hitl-interaction.strategy.ts +2 -3
- package/templates/core/scripts/mcp-server/shared/hitl-lifecycle-mutation.ts +129 -0
- package/templates/core/scripts/mcp-server/shared/hitl-proposal-mutation.ts +98 -0
- package/templates/core/scripts/mcp-server/shared/hitl-ui.ts +3 -0
- package/templates/core/scripts/mcp-server/shared/plan-lifecycle-events.ts +62 -0
- package/templates/core/scripts/mcp-server/shared/plan-lifecycle-mutation.ts +78 -0
- package/templates/core/scripts/mcp-server/shared/plan-lifecycle-subscriber.ts +145 -0
- package/templates/core/scripts/mcp-server/shared/plan-lifecycle.ts +153 -0
- package/templates/core/scripts/mcp-server/shared/plan-round-events.ts +56 -0
- package/templates/core/scripts/mcp-server/shared/plan-round-mutation.ts +143 -0
- package/templates/core/scripts/mcp-server/shared/plan-round-store.ts +47 -0
- package/templates/core/scripts/mcp-server/shared/plan-round-subscriber.ts +99 -0
- package/templates/core/scripts/mcp-server/shared/plan-status-store.ts +37 -0
- package/templates/core/scripts/mcp-server/shared/plan-tracking-mutation.ts +71 -0
- package/templates/core/scripts/mcp-server/shared/prisma.ts +12 -6
- package/templates/core/scripts/mcp-server/shared/project-root-strategy.ts +1 -2
- package/templates/core/scripts/mcp-server/shared/quant-recheck.strategy.ts +16 -0
- package/templates/core/scripts/mcp-server/shared/redact.ts +28 -0
- package/templates/core/scripts/mcp-server/shared/response.ts +3 -2
- package/templates/core/scripts/mcp-server/shared/runtime-context.ts +77 -0
- package/templates/core/scripts/mcp-server/shared/schema-topology.ts +159 -0
- package/templates/core/scripts/mcp-server/tools/audit.ts +49 -7
- package/templates/core/scripts/mcp-server/tools/context.ts +117 -121
- package/templates/core/scripts/mcp-server/tools/contract.ts +34 -9
- package/templates/core/scripts/mcp-server/tools/gateway/check_add_route_status.ts +50 -3
- package/templates/core/scripts/mcp-server/tools/gateway/check_doc_similarity.ts +218 -0
- package/templates/core/scripts/mcp-server/tools/gateway/check_dps.ts +22 -7
- package/templates/core/scripts/mcp-server/tools/gateway/check_rahs.ts +75 -4
- package/templates/core/scripts/mcp-server/tools/gateway/check_spec_sync.ts +19 -6
- package/templates/core/scripts/mcp-server/tools/gateway/index.ts +2 -0
- package/templates/core/scripts/mcp-server/tools/hitl.ts +160 -75
- package/templates/core/scripts/mcp-server/tools/hook-event-report.ts +9 -2
- package/templates/core/scripts/mcp-server/tools/index.ts +69 -3
- package/templates/core/scripts/mcp-server/tools/plan.ts +140 -15
- package/templates/core/scripts/mcp-server/tools/review.ts +44 -15
- package/templates/core/scripts/mcp-server.ts +60 -1
- package/templates/core/scripts/plan-status-bridge.ts +26 -0
- package/templates/core/skills/add-paradigm/SKILL.md +2 -2
- package/templates/core/templates/add-route-template.schema.json +24 -56
- package/templates/core/templates/collab-contract-template.md +1 -1
- package/templates/core/templates/handoff-multi-round-template.schema.json +48 -0
- package/templates/core/templates/handoff-single-round-template.schema.json +92 -0
- package/templates/core/templates/handoff-single-round.schema.json +1 -1
- package/templates/core/templates/hitl-approval-widget.html +222 -37
- package/templates/core/templates/spec-template.schema.json +9 -46
- package/templates/core/templates/standard-plan-template.schema.json +1 -1
- package/templates/core/templates/tasks-template.schema.json +13 -54
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @Description : 量化复检层 — 形似义异判定(锚点加权 Jaccard + TF-IDF cosine + RRF + EWMA ±kσ)
|
|
3
|
+
* @File : templates/core/scripts/mcp-server/tools/gateway/check_doc_similarity.ts
|
|
4
|
+
* @契约 : 消费 v3 Spec §6 jsonl(anchor_hit/struct_score/override)与 quant-events.jsonl(自记录)
|
|
5
|
+
* @回流 : Specs Review P0-1(参照物章节全文)/P0-2(候选集)/P1-5(量纲解耦)/P1-6(校准样本)/P1-7(decide 事件)
|
|
6
|
+
*/
|
|
7
|
+
import * as z from "zod/v4";
|
|
8
|
+
import type { ToolRegistrar } from "../registrar.js";
|
|
9
|
+
import { join, basename } from "path";
|
|
10
|
+
import { appendFileSync, mkdirSync, existsSync, readFileSync } from "fs";
|
|
11
|
+
import { readFileSafe, readdirRecursive, PROJECT_ROOT, MAGIC_DIR } from "../../shared/fs.js";
|
|
12
|
+
import { textResponse, errorResponse } from "../../shared/response.js";
|
|
13
|
+
import { QUANT_RECHECK_CONFIG as CFG } from "../../shared/quant-recheck.strategy.js";
|
|
14
|
+
import { tokenize, jaccard, tfVector } from "./helpers.js";
|
|
15
|
+
import { cosineSimilarity } from "vector-cosine-similarity";
|
|
16
|
+
|
|
17
|
+
// ── 锚点加权 Jaccard(锚点 token ×ANCHOR_WEIGHT,普通 token ×1)──
|
|
18
|
+
export function weightedJaccard(a: Set<string>, b: Set<string>, anchors: Set<string>): number {
|
|
19
|
+
const w = (t: string) => (anchors.has(t) ? CFG.ANCHOR_WEIGHT : 1);
|
|
20
|
+
let num = 0, den = 0;
|
|
21
|
+
const all = new Set([...a, ...b]);
|
|
22
|
+
for (const t of all) {
|
|
23
|
+
const wa = a.has(t) ? w(t) : 0;
|
|
24
|
+
const wb = b.has(t) ? w(t) : 0;
|
|
25
|
+
num += Math.min(wa, wb);
|
|
26
|
+
den += Math.max(wa, wb);
|
|
27
|
+
}
|
|
28
|
+
return den > 0 ? num / den : 1;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── RRF 排名融合(Spec §2 语义:目标文档双路排名和,非候选均值)──
|
|
32
|
+
export function rrfScore(candidates: { j: number; t: number }[]): number {
|
|
33
|
+
// 双路排名:分数降序得 rank(并列同 rank)
|
|
34
|
+
const rankBy = (get: (c: { j: number; t: number }) => number) => {
|
|
35
|
+
const sorted = [...candidates].sort((x, y) => get(y) - get(x));
|
|
36
|
+
const ranks = new Map<{ j: number; t: number }, number>();
|
|
37
|
+
sorted.forEach((c, i) => { if (!ranks.has(c)) ranks.set(c, i + 1); });
|
|
38
|
+
return ranks;
|
|
39
|
+
};
|
|
40
|
+
const rj = rankBy((c) => c.j);
|
|
41
|
+
const rt = rankBy((c) => c.t);
|
|
42
|
+
// 目标文档 = 双路综合最优候选(j+t 最高)
|
|
43
|
+
const best = candidates.reduce((a, b) => (a.j + a.t >= b.j + b.t ? a : b));
|
|
44
|
+
// RRF(文档) = 1/(K+rank_j) + 1/(K+rank_t)
|
|
45
|
+
return 1 / (CFG.RRF_K + (rj.get(best) ?? 1)) + 1 / (CFG.RRF_K + (rt.get(best) ?? 1));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── EWMA ±kσ 阈值(历史复合分序列;冷启动用保守基线)──
|
|
49
|
+
export function ewmaThreshold(history: number[]): { mu: number; sigma: number; lower: number } {
|
|
50
|
+
if (history.length < CFG.EWMA_COLD_START) {
|
|
51
|
+
return { mu: CFG.EWMA_BASELINE, sigma: 0, lower: CFG.EWMA_BASELINE };
|
|
52
|
+
}
|
|
53
|
+
let mu = history[0], varEst = 0;
|
|
54
|
+
for (let i = 1; i < history.length; i++) {
|
|
55
|
+
const diff = history[i] - mu;
|
|
56
|
+
mu = CFG.EWMA_LAMBDA * history[i] + (1 - CFG.EWMA_LAMBDA) * mu;
|
|
57
|
+
varEst = CFG.EWMA_LAMBDA * diff * diff + (1 - CFG.EWMA_LAMBDA) * varEst;
|
|
58
|
+
}
|
|
59
|
+
const sigma = Math.sqrt(varEst);
|
|
60
|
+
return { mu, sigma, lower: mu - CFG.EWMA_K * sigma };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── 模板匹配(R3:目录扫描 + 特征序 + 存在性校验,与守卫注册表同序,不硬编码死模板)──
|
|
64
|
+
const TEMPLATE_PATTERNS: [RegExp, string][] = [
|
|
65
|
+
[/add-route.*heavy|heavy.*add-route/, "add-route-template-heavyweight.md"],
|
|
66
|
+
[/add-route/, "add-route-template.md"],
|
|
67
|
+
[/hitl/, "hitl-template.md"],
|
|
68
|
+
[/handoff/, "handoff-multi-round-template.md"],
|
|
69
|
+
[/checklist/, "checklist-template.md"],
|
|
70
|
+
[/fix-verif/, "fix-verification-template.md"],
|
|
71
|
+
[/report.*runtime|runtime.*report/, "runtime-report-template.md"],
|
|
72
|
+
[/report/, "report-template.md"],
|
|
73
|
+
[/tasks/, "tasks-template.md"],
|
|
74
|
+
[/spec/, "spec-template.md"],
|
|
75
|
+
[/prd.*increment|increment.*prd/, "prd-incremental-template.md"],
|
|
76
|
+
[/prd/, "prd-standard-template.md"],
|
|
77
|
+
[/simple.*plan|plan.*simple/, "simple-plan-template.md"],
|
|
78
|
+
[/plan/, "standard-plan-template.md"],
|
|
79
|
+
[/review.*runtime|runtime.*review/, "review-runtime-template.md"],
|
|
80
|
+
[/review.*implement|implement.*review/, "review-implementation-template.md"],
|
|
81
|
+
[/review/, "review-template.md"],
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
async function matchTemplate(templatesDir: string, docPath: string): Promise<string> {
|
|
85
|
+
// 目录扫描可用模板清单(模板增删自动感知,不硬编码)
|
|
86
|
+
const all = (await readdirRecursive(templatesDir)) as string[];
|
|
87
|
+
const tmpls = new Set(all.filter((f) => f.endsWith("-template.md")));
|
|
88
|
+
// 只匹配文件名(basename),避免路径目录词污染(如 reports/ 误命中 report 模板——R4)
|
|
89
|
+
const base = basename(docPath || "");
|
|
90
|
+
const n = base.toLowerCase();
|
|
91
|
+
for (const [re, tmpl] of TEMPLATE_PATTERNS) {
|
|
92
|
+
if (re.test(n) && tmpls.has(tmpl)) return tmpl;
|
|
93
|
+
}
|
|
94
|
+
return "standard-plan-template.md"; // 缺省回退 Plan 模板
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── 判定记录(P1-5:quant-events.jsonl 追加,与 hook-events 量纲解耦)──
|
|
98
|
+
function appendQuantEvent(rec: Record<string, unknown>): void {
|
|
99
|
+
try {
|
|
100
|
+
const dir = join(PROJECT_ROOT, MAGIC_DIR, "reports");
|
|
101
|
+
mkdirSync(dir, { recursive: true });
|
|
102
|
+
appendFileSync(join(dir, "quant-events.jsonl"), `${JSON.stringify(rec)}\n`, "utf-8");
|
|
103
|
+
} catch { /* 旁路:判定不因落盘失败中断 */ }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── 建议清单(P1-7/Task 2.2:status + event 追加式流转,人类 decide 事件为校准样本)──
|
|
107
|
+
function appendSuggestion(rec: Record<string, unknown>): void {
|
|
108
|
+
try {
|
|
109
|
+
const dir = join(PROJECT_ROOT, MAGIC_DIR, "reports");
|
|
110
|
+
mkdirSync(dir, { recursive: true });
|
|
111
|
+
appendFileSync(join(dir, "quant-suggestions.jsonl"), `${JSON.stringify(rec)}\n`, "utf-8");
|
|
112
|
+
} catch { /* 旁路 */ }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function registerCheckDocSimilarity(server: ToolRegistrar) {
|
|
116
|
+
server.registerTool(
|
|
117
|
+
"check_doc_similarity",
|
|
118
|
+
{
|
|
119
|
+
description:
|
|
120
|
+
`量化复检层:「形似义异」语义判定。锚点加权 Jaccard + TF-IDF cosine 双路相似度,RRF 融合,EWMA ±kσ 自适应阈值。非关键路径调用(Step 3.5/收敛判断时)。`,
|
|
121
|
+
inputSchema: z.object({
|
|
122
|
+
planKeyword: z.string().describe("Plan 文件的关键词"),
|
|
123
|
+
docPath: z.string().optional().describe("目标文档相对路径(缺省取当前活跃 Plan)"),
|
|
124
|
+
}),
|
|
125
|
+
},
|
|
126
|
+
async (args: Record<string, unknown>, _ctx: unknown) => {
|
|
127
|
+
try {
|
|
128
|
+
const pp = args.planKeyword as string;
|
|
129
|
+
if (!pp) return errorResponse("planKeyword 参数不能为空");
|
|
130
|
+
|
|
131
|
+
// ── 候选集(P0-2):模板全部 sections 章节全文(P0-1 参照物粒度)──
|
|
132
|
+
const templatesDir = join(PROJECT_ROOT, MAGIC_DIR, "templates");
|
|
133
|
+
const plansDir = join(PROJECT_ROOT, MAGIC_DIR, "plans");
|
|
134
|
+
const reportsDir = join(PROJECT_ROOT, MAGIC_DIR, "reports");
|
|
135
|
+
|
|
136
|
+
// 定位 Plan(递归扫日期子目录;排除 .hitl.md 提案)
|
|
137
|
+
const planFiles = (await readdirRecursive(plansDir)) as string[];
|
|
138
|
+
const planFile = planFiles.find((f) => f.toLowerCase().includes(pp.toLowerCase()) && f.includes("-plan-v") && !f.endsWith(".hitl.md"));
|
|
139
|
+
if (!planFile) return errorResponse(`未找到匹配的 Plan(关键词: ${pp})`);
|
|
140
|
+
const planPath = join(plansDir, planFile);
|
|
141
|
+
const planContent = await readFileSafe(planPath) || "";
|
|
142
|
+
const anchorMatch = planContent.match(/anchor\s*[::]\s*"?([A-Za-z_]+)/);
|
|
143
|
+
const anchor = anchorMatch ? anchorMatch[1] : "plan_track";
|
|
144
|
+
|
|
145
|
+
// 读模板文件全文(R3:按文档类型匹配模板,refText = 模板章节全文——P0-1)
|
|
146
|
+
const tmplName = await matchTemplate(templatesDir, String(args.docPath ?? ""));
|
|
147
|
+
const tmplPath = join(templatesDir, tmplName);
|
|
148
|
+
const tmplContent = await readFileSafe(tmplPath) || "";
|
|
149
|
+
const refTokens = tokenize(tmplContent);
|
|
150
|
+
const anchorTokens = new Set([...tokenize(anchor)]);
|
|
151
|
+
|
|
152
|
+
// 候选集:模板 sections(按 ## 切分)+ 历史文档抽样
|
|
153
|
+
const sections = tmplContent.split(/^##\s+/m).filter((s) => s.trim().length > 0);
|
|
154
|
+
const candidates = sections.map((sec) => tokenize(sec)).slice(0, 20);
|
|
155
|
+
const docContent = args.docPath
|
|
156
|
+
? (await readFileSafe(join(PROJECT_ROOT, String(args.docPath))) || "")
|
|
157
|
+
: planContent;
|
|
158
|
+
const docTokens = tokenize(docContent);
|
|
159
|
+
|
|
160
|
+
// 双路打分(候选集内)
|
|
161
|
+
const scored = candidates.map((ct) => {
|
|
162
|
+
const j = weightedJaccard(docTokens, ct, anchorTokens);
|
|
163
|
+
const globalTokens = candidates.length >= CFG.IDF_MIN_DOCS ? candidates : [ct];
|
|
164
|
+
const t = cosineSimilarity(tfVector(docTokens, globalTokens), tfVector(ct, globalTokens));
|
|
165
|
+
return { j, t };
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// RRF 融合(候选不足退保守路径)
|
|
169
|
+
const rrf = scored.length >= CFG.RRF_MIN_CANDIDATES ? rrfScore(scored) : Math.min(...scored.map((s) => s.j));
|
|
170
|
+
|
|
171
|
+
// EWMA 阈值(P1-5:序列仅量化层复合分)
|
|
172
|
+
let history: number[] = [];
|
|
173
|
+
try {
|
|
174
|
+
const evFile = join(reportsDir, "quant-events.jsonl");
|
|
175
|
+
if (existsSync(evFile)) {
|
|
176
|
+
history = readFileSync(evFile, "utf-8").trim().split("\n")
|
|
177
|
+
.map((l) => { try { return (JSON.parse(l) as { rrf?: number }).rrf ?? NaN; } catch { return NaN; } })
|
|
178
|
+
.filter((v) => Number.isFinite(v)) as number[];
|
|
179
|
+
}
|
|
180
|
+
} catch { /* 冷启动 */ }
|
|
181
|
+
const th = ewmaThreshold(history);
|
|
182
|
+
// 复合判定:RRF 相对排名 ≥ 自适应阈值 且 双路综合绝对匹配达标(双闸门)
|
|
183
|
+
// 绝对闸门用 max(j, t):真实文档与模板全文 token 重叠天然低(骨架 vs 实例),Jaccard 单路会误杀
|
|
184
|
+
const bestMatch = Math.max(...scored.map((s) => Math.max(s.j, s.t)));
|
|
185
|
+
const absOk = bestMatch >= CFG.MIN_ABS_SIMILARITY;
|
|
186
|
+
const verdict = rrf >= th.lower && absOk ? "PASS" : "WARN_BLOCKED";
|
|
187
|
+
|
|
188
|
+
// 判定记录(P1-5/6/7:decide 事件由建议清单人类裁决写入)
|
|
189
|
+
appendQuantEvent({ ts: new Date().toISOString(), planKeyword: pp, jaccard: scored[0]?.j ?? 0, tfidf: scored[0]?.t ?? 0, rrf, ewmaThreshold: th.lower, verdict });
|
|
190
|
+
|
|
191
|
+
const suggestion = verdict === "WARN_BLOCKED"
|
|
192
|
+
? { schemaHint: `${tmplName} anchor=${anchor}`, evidence: `j=${scored[0]?.j.toFixed(3)} t=${scored[0]?.t.toFixed(3)} rrf=${rrf.toFixed(4)}`, suggestion: "review schema 声明松紧(guard-rules.toml)" }
|
|
193
|
+
: null;
|
|
194
|
+
|
|
195
|
+
// 建议落盘(P1-7:status=open + event=create;人类 decide 后追加 event=decide + status=closed)
|
|
196
|
+
if (suggestion) {
|
|
197
|
+
appendSuggestion({
|
|
198
|
+
ts: new Date().toISOString(), planKeyword: pp, docPath: args.docPath ?? planFile,
|
|
199
|
+
...suggestion, status: "open", event: "create",
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return textResponse(
|
|
204
|
+
`=== check_doc_similarity(量化复检)===\n` +
|
|
205
|
+
`Plan: ${planFile}\n` +
|
|
206
|
+
`Jaccard: ${(scored[0]?.j ?? 0).toFixed(3)}(锚点加权 ×${CFG.ANCHOR_WEIGHT})\n` +
|
|
207
|
+
`TF-IDF: ${(scored[0]?.t ?? 0).toFixed(3)}(候选集 ${scored.length},IDF 语料锚 ${candidates.length >= CFG.IDF_MIN_DOCS ? "启用" : "退化" })\n` +
|
|
208
|
+
`RRF: ${rrf.toFixed(4)}(K=${CFG.RRF_K})\n` +
|
|
209
|
+
`EWMA 阈值: ${th.lower.toFixed(4)}(历史 ${history.length} 条${history.length < CFG.EWMA_COLD_START ? ",冷启动基线" : ",自适应"})\n` +
|
|
210
|
+
`判定: ${verdict}\n` +
|
|
211
|
+
(suggestion ? `建议: ${JSON.stringify(suggestion)}\n` : "")
|
|
212
|
+
);
|
|
213
|
+
} catch (e) {
|
|
214
|
+
return errorResponse(`check_doc_similarity 异常: ${e instanceof Error ? e.message : String(e)}`);
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
);
|
|
218
|
+
}
|
|
@@ -28,8 +28,10 @@ import {
|
|
|
28
28
|
fftWeights,
|
|
29
29
|
getEmbeddings,
|
|
30
30
|
} from "./helpers.js";
|
|
31
|
+
import { getRuntimeContext } from "../../shared/env.js";
|
|
31
32
|
|
|
32
33
|
export function registerCheckDps(server: ToolRegistrar) {
|
|
34
|
+
const runtimeContext = getRuntimeContext();
|
|
33
35
|
server.registerTool(
|
|
34
36
|
"check_dps",
|
|
35
37
|
{
|
|
@@ -57,12 +59,18 @@ export function registerCheckDps(server: ToolRegistrar) {
|
|
|
57
59
|
const apf = (await readdirRecursive(plansDir)).filter(
|
|
58
60
|
(f) => f.endsWith(".md") && !f.includes(".hitl"),
|
|
59
61
|
);
|
|
60
|
-
|
|
62
|
+
// 多版本(-plan-vN)共存时取版本号最高者:活跃 Plan 优先,避免评分旧版(2026-08-12 修复)
|
|
63
|
+
const matched = apf.filter(
|
|
61
64
|
(f) =>
|
|
62
65
|
f.toLowerCase().includes(pp.toLowerCase()) && f.includes("-plan-v"),
|
|
63
66
|
);
|
|
64
|
-
if (!
|
|
67
|
+
if (!matched.length)
|
|
65
68
|
return errorResponse(`未找到匹配的 Plan 文件(关键词: ${pp})`);
|
|
69
|
+
const pm = matched.sort((a, b) => {
|
|
70
|
+
const va = parseInt(a.match(/-plan-v(\d+)/)?.[1] ?? "0", 10);
|
|
71
|
+
const vb = parseInt(b.match(/-plan-v(\d+)/)?.[1] ?? "0", 10);
|
|
72
|
+
return vb - va;
|
|
73
|
+
})[0];
|
|
66
74
|
const planPath = join(plansDir, pm);
|
|
67
75
|
const pc = await readFileSafe(planPath);
|
|
68
76
|
if (!pc) return errorResponse(`无法读取 Plan 文件: ${pm}`);
|
|
@@ -106,8 +114,7 @@ export function registerCheckDps(server: ToolRegistrar) {
|
|
|
106
114
|
// 子维度 A: 映射结构 (40%) — 映射表是否存在 + 覆盖行数
|
|
107
115
|
// 子维度 B: 延续性 (60%) — Plan 决策文本 vs Spec §N 内容的 embedding 余弦相似度
|
|
108
116
|
const planTerms = tokenize(pc),
|
|
109
|
-
specTermsRaw = tokenize(sc)
|
|
110
|
-
reviewTerms = tokenize(rc);
|
|
117
|
+
specTermsRaw = tokenize(sc);
|
|
111
118
|
const planMapping = pc.match(/###\s+\d+\.\d+\s+Plan.*?Spec.*?实施映射/);
|
|
112
119
|
let mappingTotal = 0,
|
|
113
120
|
mappingMatched = 0;
|
|
@@ -456,12 +463,16 @@ export function registerCheckDps(server: ToolRegistrar) {
|
|
|
456
463
|
|
|
457
464
|
// FFT 自适应权重
|
|
458
465
|
const fourScores = [semScore, entropyScore, cpmScore, structFinal];
|
|
459
|
-
|
|
466
|
+
const histMatrix: number[][] = [];
|
|
460
467
|
try {
|
|
461
468
|
const history = (await (
|
|
462
469
|
prisma.planRecord as Record<string, (...a: unknown[]) => unknown>
|
|
463
470
|
).findMany({
|
|
464
|
-
where: {
|
|
471
|
+
where: {
|
|
472
|
+
projectKey: runtimeContext.projectKey,
|
|
473
|
+
adapterKey: runtimeContext.adapterKey,
|
|
474
|
+
dpsComposite: { not: null },
|
|
475
|
+
},
|
|
465
476
|
orderBy: { updatedAt: "desc" },
|
|
466
477
|
take: CFG.FFT_HISTORY_LIMIT,
|
|
467
478
|
})) as {
|
|
@@ -534,7 +545,11 @@ export function registerCheckDps(server: ToolRegistrar) {
|
|
|
534
545
|
(...a: unknown[]) => unknown
|
|
535
546
|
>;
|
|
536
547
|
await planRec.updateMany({
|
|
537
|
-
where: {
|
|
548
|
+
where: {
|
|
549
|
+
projectKey: runtimeContext.projectKey,
|
|
550
|
+
adapterKey: runtimeContext.adapterKey,
|
|
551
|
+
planName: { contains: pp },
|
|
552
|
+
},
|
|
538
553
|
data: {
|
|
539
554
|
dpsSemScore: semScore,
|
|
540
555
|
dpsEntropyScore: entropyScore,
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import * as z from "zod/v4";
|
|
10
10
|
import type { ToolRegistrar } from "../registrar.js";
|
|
11
11
|
import { existsSync } from "fs";
|
|
12
|
-
import { join } from "path";
|
|
12
|
+
import { join, basename } from "path";
|
|
13
13
|
import { textResponse, errorResponse } from "../../shared/response.js";
|
|
14
14
|
import {
|
|
15
15
|
readFileSafe,
|
|
@@ -19,8 +19,10 @@ import {
|
|
|
19
19
|
} from "../../shared/fs.js";
|
|
20
20
|
import { prisma } from "../../shared/prisma.js";
|
|
21
21
|
import { runCommand } from "../../shared/run-command.js";
|
|
22
|
+
import { getRuntimeContext } from "../../shared/env.js";
|
|
22
23
|
|
|
23
24
|
export function registerCheckRahs(server: ToolRegistrar) {
|
|
25
|
+
const runtimeContext = getRuntimeContext();
|
|
24
26
|
server.registerTool(
|
|
25
27
|
"check_rahs",
|
|
26
28
|
{
|
|
@@ -42,7 +44,9 @@ export function registerCheckRahs(server: ToolRegistrar) {
|
|
|
42
44
|
);
|
|
43
45
|
const pm = apf.find(
|
|
44
46
|
(f) =>
|
|
45
|
-
f.toLowerCase().includes(pp.toLowerCase()) &&
|
|
47
|
+
f.toLowerCase().includes(pp.toLowerCase()) &&
|
|
48
|
+
f.includes("-plan-v") &&
|
|
49
|
+
!f.includes(".hitl"),
|
|
46
50
|
);
|
|
47
51
|
if (!pm)
|
|
48
52
|
return errorResponse(`未找到匹配的 Plan 文件(关键词: ${pp})`);
|
|
@@ -64,12 +68,14 @@ export function registerCheckRahs(server: ToolRegistrar) {
|
|
|
64
68
|
0,
|
|
65
69
|
100 - tsc.stderr.split("\n").filter(Boolean).length * 5,
|
|
66
70
|
);
|
|
67
|
-
} catch {}
|
|
71
|
+
} catch { /* type check is best-effort */ }
|
|
68
72
|
try {
|
|
69
73
|
const logs = (await (
|
|
70
74
|
prisma.devOperation as Record<string, (...a: unknown[]) => unknown>
|
|
71
75
|
).findMany({
|
|
72
76
|
where: {
|
|
77
|
+
projectKey: runtimeContext.projectKey,
|
|
78
|
+
producerAdapterKey: runtimeContext.adapterKey,
|
|
73
79
|
OR: [
|
|
74
80
|
{ planKeyword: { contains: pp, mode: "insensitive" } },
|
|
75
81
|
{ targetId: { contains: pp, mode: "insensitive" } },
|
|
@@ -79,7 +85,72 @@ export function registerCheckRahs(server: ToolRegistrar) {
|
|
|
79
85
|
take: 20,
|
|
80
86
|
})) as Array<{ id: string }>;
|
|
81
87
|
auditScore = Math.min(100, logs.length * 10);
|
|
82
|
-
} catch {}
|
|
88
|
+
} catch { /* audit score remains conservative */ }
|
|
89
|
+
// ── 范围保真度 / Spec 合规 / 阶段对称性:从静态基线改为动态计算(修复:
|
|
90
|
+
// 三个维度原为固定 80,任何 Plan 的 RAHS 上限 88 永远无法通过)──
|
|
91
|
+
try {
|
|
92
|
+
const arFile = apf.find(
|
|
93
|
+
(f) =>
|
|
94
|
+
f.toLowerCase().includes(pp.toLowerCase()) &&
|
|
95
|
+
f.toLowerCase().includes("add-route"),
|
|
96
|
+
);
|
|
97
|
+
if (arFile) {
|
|
98
|
+
const arContent =
|
|
99
|
+
(await readFileSafe(join(plansDir, arFile))) || "";
|
|
100
|
+
const checks = arContent.match(/^- \[[ x]\]/gm) || [];
|
|
101
|
+
const done = checks.filter((c) => c.includes("[x]")).length;
|
|
102
|
+
// 阶段对称性:add-route Step 产出项勾选率(Handoff/Report Closure 等
|
|
103
|
+
// 条件项未勾选时按比例折算,不阻塞)
|
|
104
|
+
symScore = checks.length > 0
|
|
105
|
+
? Math.max(70, Math.round((done / checks.length) * 100))
|
|
106
|
+
: 70;
|
|
107
|
+
}
|
|
108
|
+
} catch { /* add-route progress unavailable */ }
|
|
109
|
+
try {
|
|
110
|
+
// Spec 合规:checklist [T] 项勾选率([R] 运行时项不计入)
|
|
111
|
+
const specDir = join(PROJECT_ROOT, MAGIC_DIR, "specs");
|
|
112
|
+
const sn = basename(pm, ".md").replace(/-plan-v\d+$/i, "");
|
|
113
|
+
const clPath = join(specDir, sn, "checklist.md");
|
|
114
|
+
if (existsSync(clPath)) {
|
|
115
|
+
const cl = (await readFileSafe(clPath)) || "";
|
|
116
|
+
const tItems = cl.match(/^- \[[ x]\] \[T\]/gm) || [];
|
|
117
|
+
const tDone = tItems.filter((c) => c.includes("[x]")).length;
|
|
118
|
+
specScore = tItems.length > 0
|
|
119
|
+
? Math.max(70, Math.round((tDone / tItems.length) * 100))
|
|
120
|
+
: 70;
|
|
121
|
+
}
|
|
122
|
+
} catch { /* checklist progress unavailable */ }
|
|
123
|
+
try {
|
|
124
|
+
// 范围保真度:git diff 变更文件与 add-route 附录清单匹配率
|
|
125
|
+
const arFile = apf.find(
|
|
126
|
+
(f) =>
|
|
127
|
+
f.toLowerCase().includes(pp.toLowerCase()) &&
|
|
128
|
+
f.toLowerCase().includes("add-route"),
|
|
129
|
+
);
|
|
130
|
+
if (arFile) {
|
|
131
|
+
const arContent =
|
|
132
|
+
(await readFileSafe(join(plansDir, arFile))) || "";
|
|
133
|
+
const appendix = (
|
|
134
|
+
arContent.match(/`[^`]+\.(ts|js|sh|md|tsx|json|yml|yaml)`/g) ||
|
|
135
|
+
[]
|
|
136
|
+
).map((f: string) => f.replace(/`/g, "").toLowerCase());
|
|
137
|
+
const diff = runCommand("git", ["diff", "--name-only"], {
|
|
138
|
+
cwd: PROJECT_ROOT,
|
|
139
|
+
timeout: 5000,
|
|
140
|
+
});
|
|
141
|
+
const diffFiles = diff.stdout
|
|
142
|
+
.trim()
|
|
143
|
+
.split("\n")
|
|
144
|
+
.filter(Boolean)
|
|
145
|
+
.map((f: string) => f.toLowerCase());
|
|
146
|
+
if (appendix.length > 0 && diffFiles.length > 0) {
|
|
147
|
+
const matched = diffFiles.filter((f) =>
|
|
148
|
+
appendix.some((a) => f === a || f.endsWith(`/${a}`)),
|
|
149
|
+
).length;
|
|
150
|
+
scopeScore = Math.max(70, Math.round((matched / diffFiles.length) * 100));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} catch { /* git scope evidence unavailable */ }
|
|
83
154
|
const parts = [
|
|
84
155
|
"=== RAHS:Runtime Architecture Health Score ===",
|
|
85
156
|
`Plan 关键词: "${pp}"`,
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import * as z from "zod/v4";
|
|
11
11
|
import type { ToolRegistrar } from "../registrar.js";
|
|
12
|
+
import { MAGIC_PREFIXES } from "../../shared/fs.js";
|
|
12
13
|
import { existsSync } from "fs";
|
|
13
14
|
import { join, basename } from "path";
|
|
14
15
|
import { textResponse, errorResponse } from "../../shared/response.js";
|
|
@@ -43,10 +44,17 @@ export function registerCheckSpecSync(server: ToolRegistrar) {
|
|
|
43
44
|
f.endsWith(".md"),
|
|
44
45
|
);
|
|
45
46
|
const kw = args.planKeyword as string;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
// 多版本(-plan-vN)共存时取版本号最高者:活跃 Plan 优先,避免评分旧版(2026-08-12 修复,同 check_dps R9)
|
|
48
|
+
let planMatch: string | undefined = planFiles
|
|
49
|
+
.filter(
|
|
50
|
+
(f) =>
|
|
51
|
+
f.toLowerCase().includes(kw.toLowerCase()) && f.includes("-plan-v"),
|
|
52
|
+
)
|
|
53
|
+
.sort((a, b) => {
|
|
54
|
+
const va = parseInt(a.match(/-plan-v(\d+)/)?.[1] ?? "0", 10);
|
|
55
|
+
const vb = parseInt(b.match(/-plan-v(\d+)/)?.[1] ?? "0", 10);
|
|
56
|
+
return vb - va;
|
|
57
|
+
})[0];
|
|
50
58
|
if (!planMatch)
|
|
51
59
|
planMatch = planFiles.find((f) =>
|
|
52
60
|
f.toLowerCase().includes(kw.toLowerCase()),
|
|
@@ -69,7 +77,7 @@ export function registerCheckSpecSync(server: ToolRegistrar) {
|
|
|
69
77
|
lines.push(`add-route: ${arFile}`);
|
|
70
78
|
const arContent = (await readFileSafe(join(plansDir, arFile))) || "";
|
|
71
79
|
// 提取 add-route 附录文件清单
|
|
72
|
-
const appendixFiles = (arContent.match(/`[^`]+\.(ts|js|sh|md|tsx|json)`/g) || [])
|
|
80
|
+
const appendixFiles = (arContent.match(/`[^`]+\.(ts|js|sh|md|tsx|json|yml|yaml)`/g) || [])
|
|
73
81
|
.map((f: string) => f.replace(/`/g, ""));
|
|
74
82
|
lines.push(`附录文件: ${appendixFiles.length} 个`);
|
|
75
83
|
|
|
@@ -89,8 +97,13 @@ export function registerCheckSpecSync(server: ToolRegistrar) {
|
|
|
89
97
|
// 交叉比对
|
|
90
98
|
if (appendixFiles.length > 0 && diffFiles.length > 0) {
|
|
91
99
|
const appendixSet = new Set(appendixFiles.map((f: string) => f.toLowerCase()));
|
|
100
|
+
// 豁免 sync 自动生成产物(mirror 副本 + 备份)——非本 Plan 实施文件(2026-08-12 修复)
|
|
101
|
+
const isSyncGenerated = (lf: string) =>
|
|
102
|
+
MAGIC_PREFIXES.some((p) => lf.startsWith(`${p}/`));
|
|
92
103
|
const unmatched = diffFiles.filter(
|
|
93
|
-
(f: string) =>
|
|
104
|
+
(f: string) =>
|
|
105
|
+
!isSyncGenerated(f.toLowerCase()) &&
|
|
106
|
+
!appendixSet.has(f.toLowerCase()),
|
|
94
107
|
);
|
|
95
108
|
if (unmatched.length > 0) {
|
|
96
109
|
lines.push(`⚠️ ${unmatched.length} 个文件在 git diff 中但不在 add-route 附录中:`);
|
|
@@ -6,6 +6,7 @@ import { registerCheckSpecSync } from "./check_spec_sync.js";
|
|
|
6
6
|
import { registerCheckAddRouteCompleteness } from "./check_add_route_completeness.js";
|
|
7
7
|
import { registerCheckDps } from "./check_dps.js";
|
|
8
8
|
import { registerCheckRahs } from "./check_rahs.js";
|
|
9
|
+
import { registerCheckDocSimilarity } from "./check_doc_similarity.js";
|
|
9
10
|
|
|
10
11
|
export function registerGatewayTools(server: ToolRegistrar) {
|
|
11
12
|
registerCheckAddRouteStatus(server);
|
|
@@ -13,4 +14,5 @@ export function registerGatewayTools(server: ToolRegistrar) {
|
|
|
13
14
|
registerCheckAddRouteCompleteness(server);
|
|
14
15
|
registerCheckDps(server);
|
|
15
16
|
registerCheckRahs(server);
|
|
17
|
+
registerCheckDocSimilarity(server);
|
|
16
18
|
}
|