lume-dsh-plugin 0.7.3 → 0.8.0
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/CHANGELOG.md +460 -0
- package/README.md +245 -275
- package/lib/client.js +242 -181
- package/lib/core/card.js +11 -9
- package/lib/core/citations.js +235 -0
- package/lib/core/coverage.js +149 -0
- package/lib/core/dialogue-mining.js +49 -11
- package/lib/core/knowledge.js +165 -0
- package/lib/core/leak-detector.js +1 -3
- package/lib/core/ledger.js +129 -18
- package/lib/core/manifest.js +3 -1
- package/lib/core/memory-id.js +105 -0
- package/lib/core/metrics.js +270 -0
- package/lib/core/persona-limits.js +25 -0
- package/lib/core/scope.js +124 -0
- package/lib/core/signals.js +285 -5
- package/lib/core/task-memory.js +143 -0
- package/lib/core/text.js +45 -3
- package/lib/host/backfill.js +218 -0
- package/lib/host/bootstrap.js +130 -0
- package/lib/host/boundary.js +1 -3
- package/lib/host/clauses.js +180 -0
- package/lib/host/config.js +7 -0
- package/lib/host/diag.js +44 -7
- package/lib/host/distill-prompt.js +365 -0
- package/lib/host/distill.js +22 -348
- package/lib/host/extraction.js +11 -3
- package/lib/host/host-context.js +1 -0
- package/lib/host/host-events.js +100 -0
- package/lib/host/identity.js +12 -29
- package/lib/host/inbound.js +133 -0
- package/lib/host/injection.js +2 -6
- package/lib/host/llm-aux.js +130 -0
- package/lib/host/llm-route.js +3 -0
- package/lib/host/methods.js +182 -11
- package/lib/host/metrics-log.js +169 -0
- package/lib/host/notices.js +62 -0
- package/lib/host/project-access.js +210 -0
- package/lib/host/project.js +153 -2
- package/lib/host/prompt-blocks.js +113 -0
- package/lib/host/protocol.js +144 -11
- package/lib/host/reflection.js +28 -5
- package/lib/host/registry.js +0 -4
- package/lib/host/requirements-scan.js +108 -0
- package/lib/host/rpc-bridge.js +23 -4
- package/lib/host/rpc.js +1 -1
- package/lib/host/sections.js +48 -0
- package/lib/host/session-deps.js +27 -0
- package/lib/host/session-events.js +371 -0
- package/lib/host/session-runtime.js +18 -5
- package/lib/host/thinking.js +10 -1
- package/lib/host/tools.js +367 -0
- package/lib/host/triggers.js +30 -6
- package/lib/host/turn-boundary.js +116 -0
- package/lib/host/wiring.js +280 -0
- package/lib/host/workspace-map.js +81 -0
- package/lib/index.js +334 -836
- package/package.json +13 -4
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/** 每次会话自动沉淀的条数上限(用户规范 / 助手结论 / 工具结果共用)。 */
|
|
2
|
+
export const AUTO_FACT_CAP = 6;
|
|
3
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
// 事件处理里的机制各自成函数:session-events 的 case 主体只保留「调用顺序」。
|
|
5
|
+
// 现场(2026-09-24 评审):user/message 一个 case 塞了六件事、assistant/message 同样,
|
|
6
|
+
// 读的人看不出每块的边界与前置条件;抽出来之后每块有名字、有前置说明、可单独测。
|
|
7
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
/** 压缩检查点:宿主的摘要消息不能当成用户当前的话(返回 true 表示已处理、该 break)。 */
|
|
9
|
+
export function handleCompactionCheckpoint(sid, st, deps, data) {
|
|
10
|
+
if (!deps.isCompactionCheckpoint(data))
|
|
11
|
+
return false;
|
|
12
|
+
if (!st.compaction || st.compaction.turnIndex !== st.turnIndex) {
|
|
13
|
+
st.compaction = { turnIndex: st.turnIndex, shadowedItems: 0, tokens: 0 };
|
|
14
|
+
}
|
|
15
|
+
deps.appendLumeLog(`[${sid}] 检测到上下文压缩检查点(第 ${st.turnIndex} 轮)`);
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
/** 从运行时快照里学工作目录(这台宿主只有快照带 cwd),并预取本目录的会话记忆。 */
|
|
19
|
+
export function learnWorkspaceFromSnapshot(sid, st, deps, data) {
|
|
20
|
+
if (!st.cwd) {
|
|
21
|
+
const snapshotWorkspace = deps.workspaceFromSnapshotText(deps.messageText(data));
|
|
22
|
+
if (snapshotWorkspace) {
|
|
23
|
+
st.cwd = snapshotWorkspace;
|
|
24
|
+
deps.ctx.logger?.warn?.(`lume: [${sid}] 工作目录已解析(来源:运行时快照)→ ${snapshotWorkspace}`);
|
|
25
|
+
if (deps.projectMemoryOn)
|
|
26
|
+
deps.flushPendingFacts(sid, data);
|
|
27
|
+
deps.rememberWorkspace(sid, snapshotWorkspace);
|
|
28
|
+
// 工作目录已知 → 预取本目录的会话记忆(提示块是同步装配的,先落到缓存):
|
|
29
|
+
// 新会话开局要靠它接上「上一个会话干了什么」——上下文撑满时那边已经聊不动了。
|
|
30
|
+
if (st.taskMemories === null) {
|
|
31
|
+
st.taskMemories = [];
|
|
32
|
+
void deps.taskMemoriesOf(sid, 4).then((list) => {
|
|
33
|
+
st.taskMemories = list;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** 需求锚点:插件自己逐字锚定用户原话(不依赖模型调工具)。 */
|
|
40
|
+
export function anchorRequirement(sid, st, deps, text) {
|
|
41
|
+
if (deps.projectMemoryOn && (deps.TASK_SIGNAL_RE.test(text) || deps.DESIGN_SIGNAL_RE.test(text))) {
|
|
42
|
+
deps.projectTask(sid, "需求锚点落账", (store) => store.appendRequirement(sid, { text: text.trim().slice(0, 800), at: Date.now() }));
|
|
43
|
+
st.requirementFresh = true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** 用户的规范陈述(必须/一律/唯一约定…)是最权威的跨会话知识,逐条进暂存。 */
|
|
47
|
+
export function collectUserRuleFacts(sid, st, deps, text, data) {
|
|
48
|
+
if (deps.projectMemoryOn && st.agent.autoFacts < AUTO_FACT_CAP) {
|
|
49
|
+
for (const candidate of deps.extractKnowledgeCandidates(text, { source: "user" })) {
|
|
50
|
+
if (st.agent.autoFacts >= AUTO_FACT_CAP)
|
|
51
|
+
break;
|
|
52
|
+
const fact = deps.normalizeProjectFact({ kind: candidate.kind, text: candidate.text }, Date.now(), {
|
|
53
|
+
taskTitle: st.sessionTitle,
|
|
54
|
+
requirementHints: deps.requirementHintsOf(st.cwd),
|
|
55
|
+
});
|
|
56
|
+
if (!fact || deps.looksSensitive(fact.text))
|
|
57
|
+
continue;
|
|
58
|
+
st.pendingFacts.push(fact);
|
|
59
|
+
if (st.pendingFacts.length > 8)
|
|
60
|
+
st.pendingFacts.shift();
|
|
61
|
+
st.agent.autoFacts++;
|
|
62
|
+
deps.ctx.logger?.warn?.(`lume: [${sid}] 自动沉淀候选(用户规范·${fact.kind}):${fact.text.slice(0, 60)}`);
|
|
63
|
+
}
|
|
64
|
+
deps.flushPendingFacts(sid, data);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** 即时对齐纠偏:用户明确纠正 / 重复提同一请求,都提示先复核上一轮理解。 */
|
|
68
|
+
export function applyAlignNotice(st, deps, explicitCorrection, repeatedRequest) {
|
|
69
|
+
deps.forceNotice(st, "align", explicitCorrection
|
|
70
|
+
? deps.buildAlignmentCorrection("user-correction")
|
|
71
|
+
: repeatedRequest
|
|
72
|
+
? deps.buildAlignmentCorrection("repeated-request")
|
|
73
|
+
: null);
|
|
74
|
+
}
|
|
75
|
+
/** 助手可见回答里的项目约定/结论也是沉淀来源(判据与工具来源同源)。 */
|
|
76
|
+
export function collectAssistantFacts(sid, st, deps, text, data) {
|
|
77
|
+
if (deps.projectMemoryOn && text.length > 40 && st.agent.autoFacts < AUTO_FACT_CAP) {
|
|
78
|
+
for (const candidate of deps.extractKnowledgeCandidates(text, { source: "assistant", userText: st.userText ?? "" })) {
|
|
79
|
+
if (st.agent.autoFacts >= AUTO_FACT_CAP)
|
|
80
|
+
break;
|
|
81
|
+
const fact = deps.normalizeProjectFact({ kind: candidate.kind, text: candidate.text }, Date.now(), {
|
|
82
|
+
taskTitle: st.sessionTitle,
|
|
83
|
+
});
|
|
84
|
+
if (!fact)
|
|
85
|
+
continue;
|
|
86
|
+
st.pendingFacts.push(fact);
|
|
87
|
+
if (st.pendingFacts.length > 8)
|
|
88
|
+
st.pendingFacts.shift();
|
|
89
|
+
st.agent.autoFacts++;
|
|
90
|
+
deps.ctx.logger?.warn?.(`lume: [${sid}] 自动沉淀候选(助手结论·${fact.kind}):${fact.text.slice(0, 60)}`);
|
|
91
|
+
}
|
|
92
|
+
deps.flushPendingFacts(sid, data);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** 需求漂移(词法级、零成本):只在模型把「需求没提的变更」说成自己要做时才顶一句。 */
|
|
96
|
+
export function checkRequirementDrift(sid, st, deps, text) {
|
|
97
|
+
const requirementText = [
|
|
98
|
+
deps
|
|
99
|
+
.requirementsOf(sid)
|
|
100
|
+
.map((item) => item.text)
|
|
101
|
+
.join("\n"),
|
|
102
|
+
st.recentUserQueries.join("\n"),
|
|
103
|
+
st.userText,
|
|
104
|
+
].join("\n");
|
|
105
|
+
const driftWords = requirementText && deps.noticeOpen(st, "drift") ? deps.unrequestedChangeWords(requirementText, text, st.driftWordsReported) : [];
|
|
106
|
+
if (deps.setNotice(st, "drift", deps.buildDriftDirective(driftWords)))
|
|
107
|
+
st.driftWordsReported.push(...driftWords);
|
|
108
|
+
}
|
|
109
|
+
/** 上下文压力预警:接近上限时先把会话记忆落盘,再劝换窗口。 */
|
|
110
|
+
export function checkContextPressure(sid, st, deps, data) {
|
|
111
|
+
const usage = data?.usage;
|
|
112
|
+
const usedTokens = Number(usage?.totalTokens ?? Number(usage?.inputTokens ?? 0) + Number(usage?.cacheReadTokens ?? 0));
|
|
113
|
+
if (usedTokens > 0 && st.contextWindow > 0) {
|
|
114
|
+
const pressure = deps.contextPressure(usedTokens, st.contextWindow);
|
|
115
|
+
const level = pressure.level === "critical" ? "critical" : "warn";
|
|
116
|
+
if (pressure.level !== "ok" && deps.noticeOpen(st, "pressure")) {
|
|
117
|
+
void (async () => {
|
|
118
|
+
const saved = await deps.saveSessionMemory(sid);
|
|
119
|
+
deps.forceNotice(st, "pressure", deps.buildContextPressureDirective(level, pressure.ratio, saved));
|
|
120
|
+
})();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** 证据类核对:引用没读过的行、没核实的否定断言、抛回给用户的问题清单。 */
|
|
125
|
+
export function checkEvidenceNotices(st, deps, text) {
|
|
126
|
+
const citations = deps.noticeOpen(st, "citation") ? deps.unsupportedCitations(st.agent.evidence, text) : [];
|
|
127
|
+
deps.setNotice(st, "citation", deps.buildCitationDirective(citations, (key) => deps.formatWindows(st.agent.evidence, key)));
|
|
128
|
+
// 断言-证据对齐:没核实过的否定断言(「X 没映射」)同样要能顶回去
|
|
129
|
+
const claims = deps.noticeOpen(st, "claim") ? deps.unsupportedClaims(st.agent.evidence, st.agent.seenSymbols, text) : [];
|
|
130
|
+
deps.setNotice(st, "claim", deps.buildClaimDirective(claims));
|
|
131
|
+
// 提问核对:把"你抛了几个问题"摆出来(现场:4 条"待你定"里 3 条是自己造的疑问)
|
|
132
|
+
deps.setNotice(st, "question", deps.noticeOpen(st, "question") ? deps.buildQuestionAuditDirective(deps.auditOpenQuestions(text)) : null);
|
|
133
|
+
}
|
package/lib/host/injection.js
CHANGED
|
@@ -81,13 +81,9 @@ export function buildPersonaRuntimeSection(input) {
|
|
|
81
81
|
// 1. 习得的风格约定(覆盖语义:与基础盘冲突时以此为准)
|
|
82
82
|
const styles = input.styleRules;
|
|
83
83
|
if (styles.length > 0) {
|
|
84
|
-
const chosen = config.strategy === "full"
|
|
85
|
-
? styles.slice(-config.styleInject)
|
|
86
|
-
: topKByRelevance(styles, (r) => r.rule, query, config.styleInject);
|
|
84
|
+
const chosen = config.strategy === "full" ? styles.slice(-config.styleInject) : topKByRelevance(styles, (r) => r.rule, query, config.styleInject);
|
|
87
85
|
if (chosen.length > 0) {
|
|
88
|
-
parts.push(`【习得的风格约定】以下是你在对话中学到的最新要求,与基础风格冲突时以此为准:\n${chosen
|
|
89
|
-
.map((r) => `- ${r.rule}`)
|
|
90
|
-
.join("\n")}`);
|
|
86
|
+
parts.push(`【习得的风格约定】以下是你在对话中学到的最新要求,与基础风格冲突时以此为准:\n${chosen.map((r) => `- ${r.rule}`).join("\n")}`);
|
|
91
87
|
}
|
|
92
88
|
}
|
|
93
89
|
// 2. 记忆:core 恒注入 + 其余按相关度 top-k
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { BlockAssembler, ReasoningEffortId, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
2
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
export function createAuxLlm(deps) {
|
|
4
|
+
/** 小模型单次调用(提取/蒸馏等辅助功能用);路由由调用方解析后传入,不可用时返回 null。signal 中止时抛错。
|
|
5
|
+
* 组装时保留全部块(text + reasoning),蒸馏解析需要完整的模型输出——
|
|
6
|
+
* 推理型模型可能把 JSON 拆在 reasoning 块尾部,只取 text 会拿到半成品。
|
|
7
|
+
* 蒸馏类调用传完整控制参数:reasoningEffort=low(复述风模型常吃 4000+ token 复述指令,低推理显著缩短)、
|
|
8
|
+
* temperature=0(稳定)。模型不支持低推理时会抛 UNSUPPORTED_REASONING_EFFORT,捕获降级重试(去掉 effort 重发)。 */
|
|
9
|
+
async function callLlm(route, system, userText, maxTokens, signal) {
|
|
10
|
+
if (!route)
|
|
11
|
+
return null;
|
|
12
|
+
const llm = deps.ctx.get("llm");
|
|
13
|
+
if (!llm)
|
|
14
|
+
return null;
|
|
15
|
+
try {
|
|
16
|
+
const messages = [
|
|
17
|
+
createUserMessage({
|
|
18
|
+
content: [{ type: "text", text: userText }],
|
|
19
|
+
source: { kind: "plugin", plugin: "lume" },
|
|
20
|
+
}),
|
|
21
|
+
];
|
|
22
|
+
const assembler = new BlockAssembler();
|
|
23
|
+
try {
|
|
24
|
+
for await (const chunk of llm.stream({
|
|
25
|
+
provider: route.provider,
|
|
26
|
+
model: route.model,
|
|
27
|
+
messages,
|
|
28
|
+
system,
|
|
29
|
+
maxTokens,
|
|
30
|
+
reasoningEffort: ReasoningEffortId("low"),
|
|
31
|
+
temperature: 0,
|
|
32
|
+
...(signal ? { signal } : {}),
|
|
33
|
+
})) {
|
|
34
|
+
assembler.push(chunk);
|
|
35
|
+
}
|
|
36
|
+
// 错误经流内 finish chunk 传输(不 throw)——检查 finish.kind === "error"
|
|
37
|
+
if (assembler.finish.kind === "error") {
|
|
38
|
+
const code = assembler.finish.failure?.code;
|
|
39
|
+
if (code !== "UNSUPPORTED_REASONING_EFFORT")
|
|
40
|
+
throw new Error(String(assembler.finish.failure?.message ?? "unnamed stream error"));
|
|
41
|
+
// 不支持 effort:降级无 effort 重发
|
|
42
|
+
const assembler2 = new BlockAssembler();
|
|
43
|
+
for await (const chunk of llm.stream({
|
|
44
|
+
provider: route.provider,
|
|
45
|
+
model: route.model,
|
|
46
|
+
messages,
|
|
47
|
+
system,
|
|
48
|
+
maxTokens,
|
|
49
|
+
temperature: 0,
|
|
50
|
+
...(signal ? { signal } : {}),
|
|
51
|
+
})) {
|
|
52
|
+
assembler2.push(chunk);
|
|
53
|
+
}
|
|
54
|
+
if (assembler2.finish.kind === "error") {
|
|
55
|
+
throw new Error(String(assembler2.finish.failure?.message ?? "unnamed stream error"));
|
|
56
|
+
}
|
|
57
|
+
return assembler2
|
|
58
|
+
.blocks()
|
|
59
|
+
.map((block) => {
|
|
60
|
+
const text = block?.text;
|
|
61
|
+
return typeof text === "string" ? text : "";
|
|
62
|
+
})
|
|
63
|
+
.filter((text) => text.length > 0)
|
|
64
|
+
.join(" ")
|
|
65
|
+
.trim();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
// throw 形态的错误:非 UNSUPPORTED 直接抛;是则降级重试
|
|
70
|
+
if (error?.code !== "UNSUPPORTED_REASONING_EFFORT")
|
|
71
|
+
throw error;
|
|
72
|
+
const assembler2 = new BlockAssembler();
|
|
73
|
+
for await (const chunk of llm.stream({
|
|
74
|
+
provider: route.provider,
|
|
75
|
+
model: route.model,
|
|
76
|
+
messages,
|
|
77
|
+
system,
|
|
78
|
+
maxTokens,
|
|
79
|
+
temperature: 0,
|
|
80
|
+
...(signal ? { signal } : {}),
|
|
81
|
+
})) {
|
|
82
|
+
assembler2.push(chunk);
|
|
83
|
+
}
|
|
84
|
+
if (assembler2.finish.kind === "error") {
|
|
85
|
+
throw new Error(String(assembler2.finish.failure?.message ?? "unnamed stream error"));
|
|
86
|
+
}
|
|
87
|
+
return assembler2
|
|
88
|
+
.blocks()
|
|
89
|
+
.map((block) => {
|
|
90
|
+
const text = block?.text;
|
|
91
|
+
return typeof text === "string" ? text : "";
|
|
92
|
+
})
|
|
93
|
+
.filter((text) => text.length > 0)
|
|
94
|
+
.join(" ")
|
|
95
|
+
.trim();
|
|
96
|
+
}
|
|
97
|
+
const allBlocks = assembler
|
|
98
|
+
.blocks()
|
|
99
|
+
.map((block) => {
|
|
100
|
+
const text = block?.text;
|
|
101
|
+
return typeof text === "string" ? text : "";
|
|
102
|
+
})
|
|
103
|
+
.filter((text) => text.length > 0);
|
|
104
|
+
// 诊断探针:完整输出落盘(含 max-tokens 截断标记;追加,一次失败可看全程)
|
|
105
|
+
try {
|
|
106
|
+
const existing = readFileSync(deps.llmDumpPath, "utf8");
|
|
107
|
+
const dumps = existing ? JSON.parse(existing) : [];
|
|
108
|
+
dumps.push({
|
|
109
|
+
ts: Date.now(),
|
|
110
|
+
route: `${route.provider}/${route.model}`,
|
|
111
|
+
maxTokens,
|
|
112
|
+
finish: assembler.finish,
|
|
113
|
+
blocks: allBlocks.map((t) => t.slice(0, 6000)),
|
|
114
|
+
});
|
|
115
|
+
writeFileSync(deps.llmDumpPath, JSON.stringify(dumps, null, 2), "utf8");
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
/* 诊断失败不阻断 */
|
|
119
|
+
}
|
|
120
|
+
return allBlocks.join(" ").trim();
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (signal?.aborted)
|
|
124
|
+
throw error; // 用户取消:向上抛,任务状态走 cancelled
|
|
125
|
+
deps.ctx.logger?.warn?.("lume: 小模型调用失败,本轮跳过", error);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return { callLlm };
|
|
130
|
+
}
|
package/lib/host/methods.js
CHANGED
|
@@ -9,16 +9,17 @@
|
|
|
9
9
|
* 成本模型:这些都注入到尾部快照,因此只在**内容变化时**付费(实测 58 步只产生 9 条快照),
|
|
10
10
|
* 每轮多几百字是可接受的;真正要避免的是把它们塞进 system 段(那会作废整段前缀)。
|
|
11
11
|
*/
|
|
12
|
+
import { MAX_OPEN_QUESTIONS } from "../core/signals.js";
|
|
12
13
|
/** 任务型请求且尚无契约时注入:把「先量化」变成一次具体的产出。 */
|
|
13
14
|
export function buildContractMethodDirective() {
|
|
14
15
|
return [
|
|
15
16
|
"〔先量化后动手〕本轮是任务型请求。开工前先写任务契约(lume_contract):",
|
|
16
17
|
"- 目标:一句话,可观察的结果(不是「优化一下」这种动词)",
|
|
17
18
|
"- 范围:精确到路径 / 模块 / 章节 / 表",
|
|
18
|
-
"-
|
|
19
|
+
"- 数量:必填,先估一个数,探索后回填实际值(实在无法估时传 0 并说明理由)——交付时要用实际数量对账",
|
|
19
20
|
"- 完成判据:可执行、可核对(命令 / 回读 / 对照),不是「改完」",
|
|
20
21
|
"- 非目标:明确不动什么,防止范围蔓延",
|
|
21
|
-
"-
|
|
22
|
+
"- 待确认:默认 0 条。只列确实阻塞、且代码与文档都答不了的(≤2 个,写清为什么答不了);其余按默认假设前进并写明假设",
|
|
22
23
|
].join("\n");
|
|
23
24
|
}
|
|
24
25
|
/** 文档任务轮注入:文档的失败模式是静默内容丢失,所以方法围绕「结构 + 最小编辑 + 回读」。 */
|
|
@@ -29,6 +30,7 @@ export function buildDocumentMethodDirective() {
|
|
|
29
30
|
"2. 最小编辑:只改目标区域,保留原有格式、编号、交叉引用与样式——不要整份重写。",
|
|
30
31
|
"3. 术语与称谓全文一致:改一个术语前先全文检索它的全部出现位置,否则会留下半新半旧。",
|
|
31
32
|
"4. 交付前回读改动区域,列出「改了什么 / 没动什么 / 未核对什么」;没有回读证据不要说已改好。",
|
|
33
|
+
"不要写「本文档不含 X」「不在本次范围」这类此地无银的声明:需求外的内容直接删;确实要标来源,最多一行。",
|
|
32
34
|
].join("\n");
|
|
33
35
|
}
|
|
34
36
|
/** 执行轮注入:改动之前的影响面清单,治「边写边想」。 */
|
|
@@ -36,6 +38,7 @@ export function buildImpactDirective() {
|
|
|
36
38
|
return [
|
|
37
39
|
"〔改动影响面〕动手前列出:要改的符号 → 谁调用它、它实现或被实现于谁、配置或 SQL 映射、前端/模板引用;并标出「不打算改但需一并确认」的位置。",
|
|
38
40
|
"每处改动写明验证方式;同一文件的相关改动一次做完,不要反复回来改同一个文件。",
|
|
41
|
+
"每改完一处就用 lume_change 记一条(target=文件/符号、change=改了什么、verify=怎么验、status=done),验完推进到 verified;台账积着未验证项时会有提醒。",
|
|
39
42
|
].join("\n");
|
|
40
43
|
}
|
|
41
44
|
/** 环境里有结构分析/符号工具时的一句提示:用符号级定位替代通篇 read。 */
|
|
@@ -49,16 +52,184 @@ export function buildStructureHint(toolName) {
|
|
|
49
52
|
* 预算存在的意义是防止"载具越积越多,把注意力挤没"——实测尾部快照约 1.8-2k 字符时
|
|
50
53
|
* 命中率与合规都健康,这里给到 4200 字符仍有充足余量。
|
|
51
54
|
*/
|
|
52
|
-
|
|
55
|
+
/**
|
|
56
|
+
* 设计 pass(0.7.5):现场实测这类型任务(新增字段/接口/页面)的轨迹是「澄清需求 → 找代码 → 直接动手」,
|
|
57
|
+
* 中间**没有设计决策**:不写数据落在哪、不定义接口契约、不指名既有范式、没有取舍记录。
|
|
58
|
+
* 触发条件是「已摸到代码 + 功能型任务 + 还没有设计记录」,所以它不是泛泛的方法论,而是此刻缺的那一步。
|
|
59
|
+
*/
|
|
60
|
+
/**
|
|
61
|
+
* 需求解读规则(0.7.5):现场实测模型的两大坏习惯——
|
|
62
|
+
* ① 拿需求已写死的选择去「重新设计」(需求说新增字段,它论证复用 create_id/modify_id);
|
|
63
|
+
* ② 脑补需求没提的变更(需求说新增选项,它推论删除/割接)。
|
|
64
|
+
* 另外它在该看代码的时候反而抛问题(用户原话:「你只要分析清楚字段的值从哪来就行,怎么还有这么多疑问?」)。
|
|
65
|
+
*/
|
|
66
|
+
export function buildRequirementMethodDirective(taskMethods = true) {
|
|
67
|
+
// 问答 / 查找 / 讨论轮只保留**边界规则**:不把执行轮的方法塞进"直接回答、别调工具"的轮次。
|
|
68
|
+
// 现场事故(2026-09-23):同一轮里同时出现「当前模式:问答,不要调用工具」与「本轮是任务型
|
|
69
|
+
// 请求,开工前先写任务契约」——模型只能赌哪条为准,结果两条都没执行(13 轮 0 次契约调用)。
|
|
70
|
+
if (!taskMethods) {
|
|
71
|
+
return [
|
|
72
|
+
"〔需求解读〕用户刚给了或改了需求。硬规则:",
|
|
73
|
+
"- 不得引入需求没提的变更类型(删除 / 替换 / 割接 / 回滚 / 重构)。想到这类动作时先自问:这是需求要求的,还是我脑补的?",
|
|
74
|
+
].join("\n");
|
|
75
|
+
}
|
|
76
|
+
return [
|
|
77
|
+
"〔需求解读〕用户刚给了或改了需求。下面是硬规则:",
|
|
78
|
+
"- 需求写死的选择照做:说「新增字段」就新增,不要改成复用既有字段;说「新增选项」就不要引入删除/替换。只有需求没说的地方(怎么存、怎么查、要不要冗余)才由你做设计。",
|
|
79
|
+
"- 不得引入需求没提的变更类型(删除 / 替换 / 割接 / 回滚 / 重构)。想到这类动作时先自问:这是需求要求的,还是我脑补的?",
|
|
80
|
+
"- 提问的默认值是 0:先读代码,能定的自己定下并写明依据与默认假设(「我按 X 做,除非你反对」);只有确实只能由用户提供的信息才问,最多 2 条,且写清你查到哪一步、为什么代码答不了。已核实过的事实不要再挂「待确认」——那等于把工作退回给用户。",
|
|
81
|
+
"- 提问的前提必须已核实:把决定权交回用户之前,先确认这个决定的前提是你**从这条路径的代码**里读到的。未核实的推断不能包装成「要你定」的选项——那会让用户在假前提上做决定。",
|
|
82
|
+
"- 提问纪律:一次 grep/read 能确认的,先自己确认,不要拿它当问题;能定的直接定并写明默认假设。抛回用户的「待确认」不超过 2 条。",
|
|
83
|
+
].join("\n");
|
|
84
|
+
}
|
|
85
|
+
/** 需求漂移提示:模型输出里出现需求原话没有的变更类型词时顶一句。 */
|
|
86
|
+
export function buildDriftDirective(words) {
|
|
87
|
+
if (words.length === 0)
|
|
88
|
+
return null;
|
|
89
|
+
return `〔需求漂移〕你的输出里出现了需求原话里没有的变更类型:${words.join("、")}。先自问:这是需求要求的,还是你补出来的?若需求确实没要求、但技术上必须这么动,就说明它为什么必须、并请用户确认;若是自己补的,收回它,只按需求做。`;
|
|
90
|
+
}
|
|
91
|
+
export function buildDesignMethodDirective() {
|
|
92
|
+
return [
|
|
93
|
+
"〔设计三问〕这条需求要动数据或接口。动手前先做一次设计 pass(写进 lume_design,之后每步以它为准):",
|
|
94
|
+
"0. 先划边界:需求已明确的选择(字段是新增还是复用、选项是新增还是替换)一律照做,不得重新讨论;下面三问只在需求没写的维度上做。",
|
|
95
|
+
"1. 数据落在哪:需求给了口径就照它(说「新增字段」就新增)。需求没说时再定:计算即得还是落库冗余;类型 / 是否可空 / 默认值;旧数据为 NULL 时列表与导出怎么表现;要不要迁移与回滚?",
|
|
96
|
+
"2. 接口长什么样:请求与响应字段、筛选与分页契约、调用方兼容、空值与错误语义——分页/导出这类看似「前端组件」的改动往往是接口变更。",
|
|
97
|
+
"3. 照哪个既有范式:在本仓库指名一个同类实现(文件:行),并写明与它不一致的地方为什么必须不一致。",
|
|
98
|
+
"另外三行也要写:放弃的方案与理由(至少一条,没写就是没做取舍)/回归面(会经过哪些既有路径:其它 tab、导出、导入、报表、外部同步)/分期(能独立上线的切片顺序)。",
|
|
99
|
+
"写完把关键结论贴给用户确认,再开始改代码。",
|
|
100
|
+
].join("\n");
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* 引用核对(0.7.5):模型引用了自己这次没打开过的代码行。
|
|
104
|
+
*
|
|
105
|
+
* 现场事故(2026-09-23 turn 18):它用「单条新增分支」的注释(159-160)去断定
|
|
106
|
+
* 「导入路径不会改 status」,据此让用户拍一个假选择;用户反问后它自己承认说错,
|
|
107
|
+
* 而导入路径的 `setStatus` 在 534 行。加"要核实"的散文没用——它以为自己核实过了。
|
|
108
|
+
* 所以这里只**复述事实**:这行你这次没看,看过的是这些范围。
|
|
109
|
+
*/
|
|
110
|
+
export function buildCitationDirective(items, windowsOf) {
|
|
111
|
+
if (items.length === 0)
|
|
112
|
+
return null;
|
|
113
|
+
const lines = items
|
|
114
|
+
.slice(0, 3)
|
|
115
|
+
.map((item) => `- 你引用了 ${item.file}:${item.line},本会话读到过这个文件的范围是:${windowsOf(item.key) || "(没有)"}`);
|
|
116
|
+
return [
|
|
117
|
+
"〔引用核对〕下面这些引用是**这次没打开过**的代码行:",
|
|
118
|
+
...lines,
|
|
119
|
+
"先把这几处打开确认,再把它当依据;如果依据其实在另一处(另一条路径/另一个方法),改引那一条。不要把别处的行为当成这里的行为。",
|
|
120
|
+
].join("\n");
|
|
121
|
+
}
|
|
122
|
+
/** 提问核对:把"你抛了几个问题"摆出来,并把核实责任推回模型(不是训话,是复述事实 + 给出口)。 */
|
|
123
|
+
export function buildQuestionAuditDirective(audit) {
|
|
124
|
+
if (audit.count === 0)
|
|
125
|
+
return null;
|
|
126
|
+
if (audit.count <= MAX_OPEN_QUESTIONS && audit.unsupported.length === 0)
|
|
127
|
+
return null;
|
|
128
|
+
const lines = [
|
|
129
|
+
audit.unsupported.length > 0
|
|
130
|
+
? `〔提问核对〕你这轮把 ${audit.count} 条「待用户确认」摆出来了,其中这几条**没有行号证据**:`
|
|
131
|
+
: `〔提问核对〕你这轮把 ${audit.count} 条「待用户确认」摆出来了(规则:真正阻塞的 ≤${MAX_OPEN_QUESTIONS} 条):`,
|
|
132
|
+
];
|
|
133
|
+
for (const item of audit.unsupported)
|
|
134
|
+
lines.push(`- ${item.slice(0, 100)}`);
|
|
135
|
+
lines.push("- 你这次读过的代码能不能定下?能定就定,写明依据与默认假设(「我按 X 做,除非你反对」)——已核实的事实再挂「待确认」等于把工作退回用户;");
|
|
136
|
+
lines.push("- 只有确实只能由用户提供的信息(环境 / 账号 / 业务取舍)才留,且写清你查到哪一步、为什么代码答不了。");
|
|
137
|
+
return lines.join("\n");
|
|
138
|
+
}
|
|
139
|
+
/** 需求覆盖核对:把需求**原文条目**与交付物里提到它的句子并列,替代模型的自证式「N 条全有落点」。 */
|
|
140
|
+
export function buildRequirementCoverageDirective(rows, opts = {}) {
|
|
141
|
+
if (rows.length === 0)
|
|
142
|
+
return null;
|
|
143
|
+
const lines = [`〔需求覆盖核对〕需求原文共 ${rows.length} 条(**按你的原文切分,不是你的总结**)——逐条给落点,别只写「N 条全有落点」:`];
|
|
144
|
+
for (const row of rows.slice(0, 10)) {
|
|
145
|
+
lines.push(`${row.label} 原文:${row.text.slice(0, 110)}`);
|
|
146
|
+
if (row.mentions.length === 0)
|
|
147
|
+
lines.push(" 交付物里**没找到**提到这条的句子——先确认是漏了、还是有意剔除(有意就写明归属)。");
|
|
148
|
+
else
|
|
149
|
+
for (const mention of row.mentions)
|
|
150
|
+
lines.push(` 交付物里提到它的句子(命中片段「${mention.gram}」):${mention.text}`);
|
|
151
|
+
}
|
|
152
|
+
if (rows.length > 10)
|
|
153
|
+
lines.push(`(其余 ${rows.length - 10} 条同理,逐条自查)`);
|
|
154
|
+
if (opts.figures)
|
|
155
|
+
lines.push("⚠ 需求里有「如图/图一/附件」这类图形引用,交付物里没提到图:交互细节在图里,确认你的落点是否覆盖了图上描述的行为。");
|
|
156
|
+
for (const ref of opts.danglingRefs ?? [])
|
|
157
|
+
lines.push(`⚠ 你引用了 ${ref} 节,但交付物里没有这一节(落点写错或章节被删)。`);
|
|
158
|
+
lines.push("逐条比对时只判一件事:**交付物这句写的是不是需求这条要的意思**(不是「有没有提到」)。有出入就写清差异与依据。");
|
|
159
|
+
return lines.join("\n");
|
|
160
|
+
}
|
|
161
|
+
/** 载具缺口:动了代码却没有任何契约/设计——交付时如实说出来(触发器发了它也没动,只能靠事实对账)。 */
|
|
162
|
+
export function buildCarrierGapNotice(input) {
|
|
163
|
+
if (input.mutations <= 0)
|
|
164
|
+
return null;
|
|
165
|
+
if (input.hasContract && input.hasDesign)
|
|
166
|
+
return null;
|
|
167
|
+
const missing = [!input.hasContract ? "任务契约 0 条" : "", !input.hasDesign ? "设计 pass 0 条" : ""].filter(Boolean).join("、");
|
|
168
|
+
return [
|
|
169
|
+
`〔载具缺口〕这次会话已经有 ${input.mutations} 处改动,但${missing}——交付前按这两条自查:`,
|
|
170
|
+
"- 做完的判据是什么(可核对的那种,不是「改完了」)?",
|
|
171
|
+
"- 关键取舍写下来了没有:数据落在哪 / 接口长什么样 / 为什么不那样做?",
|
|
172
|
+
"补一句也算(lume_contract / lume_design)。交付只有动作清单,用户就得自己替你核。",
|
|
173
|
+
].join("\n");
|
|
174
|
+
}
|
|
175
|
+
/** 交付对账:把「还没验证的具体条目」摆出来,而不是泛泛提醒「要有验证证据」。 */
|
|
176
|
+
export function buildUnverifiedDeliveryNotice(items) {
|
|
177
|
+
const pending = items.filter((item) => item.status === "done" || item.status === "planned");
|
|
178
|
+
if (pending.length === 0)
|
|
179
|
+
return null;
|
|
180
|
+
const lines = pending.slice(0, 8).map((item) => {
|
|
181
|
+
const mark = item.status === "done" ? "已改未验" : "计划中";
|
|
182
|
+
return `- [${mark}] ${item.target} — ${item.change}${item.verify ? `(打算验:${item.verify})` : ""}`;
|
|
183
|
+
});
|
|
184
|
+
return [
|
|
185
|
+
`〔交付对账〕台账里还有 ${pending.length} 项没有验证证据:`,
|
|
186
|
+
...lines,
|
|
187
|
+
"交付文案里逐项写清「已验证 / 未验证」,并说明怎么验的(命令 + 结果)。没验的就说没验,不要把动作完成说成判据达成。",
|
|
188
|
+
].join("\n");
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* 〔断言核对〕把「没核实过的否定断言」摆回给模型:符号本会话从没出现过 / 断言没给行号。
|
|
192
|
+
* 与引用核对的分工:引用核对管「你引用的行你打开过吗」,这条管「你没给行号就说它不存在」。
|
|
193
|
+
*/
|
|
194
|
+
export function buildClaimDirective(claims) {
|
|
195
|
+
if (!claims.length)
|
|
196
|
+
return null;
|
|
197
|
+
const lines = claims.map((claim, i) => {
|
|
198
|
+
const why = claim.reason === "unseen"
|
|
199
|
+
? `(符号 \`${claim.symbol}\` 在本会话的工具结果里**一次都没出现过**)`
|
|
200
|
+
: `(\`${claim.symbol}\` 见过,但这条断言没给行号)`;
|
|
201
|
+
return `${i + 1}. 「${claim.sentence}」${why}`;
|
|
202
|
+
});
|
|
203
|
+
return [
|
|
204
|
+
"〔断言核对〕你写了否定性断言,但本会话没有可核实的依据:",
|
|
205
|
+
...lines,
|
|
206
|
+
'否定断言和引用一样要给出处:读到那一行再下结论("我查过 X,它没有 Y" 而不是 "X 没有 Y")。',
|
|
207
|
+
].join("\n");
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* 装配并**记账**:与 composeBlocks 同一套逻辑,只是把「丢了几块」也报出来。
|
|
211
|
+
*
|
|
212
|
+
* 为什么要记账(2026-09-24 复盘):一轮最多能塞 20+ 个块,唯一的减法只有超预算时
|
|
213
|
+
* 从尾部丢 droppable——丢没丢、丢了几块,此前**没有任何地方知道**。没有这个计数,
|
|
214
|
+
* 「条款该不该做预算」就只能靠感觉。
|
|
215
|
+
*/
|
|
216
|
+
export function composeBlocksDetailed(blocks, budgetChars = 4200) {
|
|
53
217
|
const present = blocks.filter((block) => Boolean(block.text));
|
|
54
218
|
let out = present.map((block) => block.text).join("\n\n");
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
219
|
+
let dropped = 0;
|
|
220
|
+
if (out.length > budgetChars) {
|
|
221
|
+
for (let i = present.length - 1; i >= 0 && out.length > budgetChars; i--) {
|
|
222
|
+
if (!present[i].droppable)
|
|
223
|
+
continue;
|
|
224
|
+
present.splice(i, 1);
|
|
225
|
+
dropped++;
|
|
226
|
+
out = present.map((block) => block.text).join("\n\n");
|
|
227
|
+
}
|
|
228
|
+
if (out.length > budgetChars)
|
|
229
|
+
out = out.slice(0, budgetChars);
|
|
62
230
|
}
|
|
63
|
-
return out.length
|
|
231
|
+
return { text: out, kept: present.length, dropped, chars: out.length, budget: budgetChars };
|
|
232
|
+
}
|
|
233
|
+
export function composeBlocks(blocks, budgetChars = 4200) {
|
|
234
|
+
return composeBlocksDetailed(blocks, budgetChars).text;
|
|
64
235
|
}
|