ppxans-harness 2.4.0 → 3.2.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.
Files changed (186) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +218 -265
  3. package/bin/ppx-channels.js +2 -2
  4. package/bin/ppx-serve.js +5 -5
  5. package/bin/ppx-setup.js +124 -0
  6. package/bin/ppx-web.js +140 -0
  7. package/bin/ppx.js +2 -2
  8. package/config/identity.md +6 -6
  9. package/config/ishiki.md +16 -16
  10. package/config/ppx.json +15 -151
  11. package/config/ppx.json.example +143 -0
  12. package/package.json +17 -10
  13. package/skills/.usage.json +6 -0
  14. package/skills/agent-professional-training/SKILL.md +94 -0
  15. package/skills/brainstorm/SKILL.md +24 -0
  16. package/skills/cupid-lover-comms/SKILL.md +37 -0
  17. package/skills/debug/SKILL.md +26 -0
  18. package/skills/plan/SKILL.md +25 -0
  19. package/skills/ponytail/SKILL.md +25 -0
  20. package/skills/ppx-memory/SKILL.md +91 -0
  21. package/skills/ppx-memory/scripts/cli.js +192 -0
  22. package/skills/ppx-memory/scripts/experience.js +133 -0
  23. package/skills/ppx-memory/scripts/fact-store.js +842 -0
  24. package/skills/ppx-memory/scripts/l0.js +52 -0
  25. package/skills/ppx-memory/scripts/l2.js +146 -0
  26. package/skills/ppx-memory/scripts/l3.js +112 -0
  27. package/skills/ppx-memory/scripts/memory-ticker.js +238 -0
  28. package/skills/ppx-memory/scripts/pii.js +42 -0
  29. package/skills/ppx-memory/scripts/schema.js +80 -0
  30. package/skills/ppx-memory/scripts/session.js +398 -0
  31. package/skills/ppx-memory/scripts/similarity.js +43 -0
  32. package/skills/ppx-memory/scripts/store.js +116 -0
  33. package/skills/ppx-memory/scripts/wal.js +38 -0
  34. package/skills/ppx-selfheal/SKILL.md +24 -0
  35. package/skills/ppx-selfheal/scripts/cli.js +80 -0
  36. package/skills/ppx-selfheal/scripts/healer.js +184 -0
  37. package/skills/ppx-selfheal/scripts/logger.js +17 -0
  38. package/skills/ppx-selfheal/scripts/store.js +116 -0
  39. package/skills/prompt-depth-kit/SKILL.md +28 -0
  40. package/skills/session-naming/SKILL.md +36 -0
  41. package/skills/verify/SKILL.md +25 -0
  42. package/src/agent/index.js +1340 -717
  43. package/src/agent/prompts.js +47 -3
  44. package/src/aml-server.js +197 -151
  45. package/src/ans/eviction.js +123 -143
  46. package/src/ans/guard.js +159 -120
  47. package/src/ans/lifecycle.js +96 -93
  48. package/src/ans/proactive.js +112 -129
  49. package/src/ans/reward.js +95 -111
  50. package/src/ans/values.js +15 -15
  51. package/src/audit/audit-chain.js +43 -7
  52. package/src/audit/verifier.js +157 -120
  53. package/src/bus/circuit-breaker.js +9 -1
  54. package/src/bus/runtime-bus.js +107 -93
  55. package/src/channels/base.js +57 -34
  56. package/src/channels/feishu.js +118 -126
  57. package/src/channels/http.js +1090 -592
  58. package/src/channels/index.js +111 -110
  59. package/src/channels/log.js +29 -29
  60. package/src/channels/wechat-crypto.js +73 -74
  61. package/src/channels/wechat.js +191 -197
  62. package/src/channels/workspace.js +94 -0
  63. package/src/channels-cli.js +126 -124
  64. package/src/cli.js +129 -120
  65. package/src/commands/index.js +142 -0
  66. package/src/config/channels.js +137 -170
  67. package/src/config/index.js +277 -224
  68. package/src/config/placeholder.js +31 -0
  69. package/src/config/providers.js +148 -188
  70. package/src/config/settings.js +156 -182
  71. package/src/core/policy.js +69 -18
  72. package/src/core/trace.js +8 -6
  73. package/src/edit/editblock.js +266 -0
  74. package/src/edit/snapshot.js +67 -0
  75. package/src/evidence/index.js +153 -0
  76. package/src/evolve/playbook.js +9 -10
  77. package/src/hooks/index.js +113 -0
  78. package/src/llm/client.js +188 -446
  79. package/src/llm/dsml.js +74 -74
  80. package/src/llm/embedder.js +41 -35
  81. package/src/llm/fence.js +52 -105
  82. package/src/llm/index.js +4 -4
  83. package/src/llm/local-embedder.js +94 -0
  84. package/src/llm/presets.js +113 -0
  85. package/src/llm/pricing.js +93 -0
  86. package/src/llm/retry.js +73 -73
  87. package/src/llm/router.js +92 -97
  88. package/src/mcp/admin.js +326 -0
  89. package/src/mcp/client.js +487 -375
  90. package/src/mcp/http.js +203 -0
  91. package/src/mcp/index.js +116 -116
  92. package/src/mcp/server.js +392 -0
  93. package/src/mcp/tasks.js +133 -0
  94. package/src/memory/asset-hub.js +6 -11
  95. package/src/memory/canvas.js +2 -5
  96. package/src/memory/compaction.js +28 -28
  97. package/src/memory/experience.js +133 -122
  98. package/src/memory/fact-store.js +914 -698
  99. package/src/memory/failure-episode.js +20 -11
  100. package/src/memory/fork.js +17 -8
  101. package/src/memory/index.js +8 -6
  102. package/src/memory/l0.js +53 -52
  103. package/src/memory/l2.js +145 -130
  104. package/src/memory/l3.js +111 -111
  105. package/src/memory/legion-board.js +71 -0
  106. package/src/memory/memory-ticker.js +239 -240
  107. package/src/memory/session.js +398 -397
  108. package/src/memory/sqlite-store.js +581 -0
  109. package/src/mode/blackboard.js +49 -49
  110. package/src/mode/graph.js +42 -41
  111. package/src/mode/index.js +64 -64
  112. package/src/mode/legion.js +54 -51
  113. package/src/mode/plan-exec.js +50 -50
  114. package/src/mode/router.js +28 -40
  115. package/src/orchestrator/agent-worker.js +69 -69
  116. package/src/orchestrator/dag.js +90 -83
  117. package/src/orchestrator/experts.js +76 -0
  118. package/src/orchestrator/index.js +1 -1
  119. package/src/orchestrator/legion.js +179 -187
  120. package/src/orchestrator/supervisor.js +6 -8
  121. package/src/permissions/index.js +378 -0
  122. package/src/persona/index.js +28 -29
  123. package/src/plugin/builtin.js +313 -212
  124. package/src/plugin/context.js +80 -79
  125. package/src/plugin/index.js +62 -62
  126. package/src/plugin/v3.js +73 -0
  127. package/src/protocol/index.js +148 -0
  128. package/src/repomap/index.js +309 -0
  129. package/src/review/index.js +393 -0
  130. package/src/seam/registry.js +3 -0
  131. package/src/seam/shell.js +55 -55
  132. package/src/security/injection.js +79 -0
  133. package/src/selfheal/evolve.js +67 -67
  134. package/src/selfheal/healer.js +184 -167
  135. package/src/selfheal/run.js +9 -9
  136. package/src/server.js +63 -60
  137. package/src/services/diagnose.js +180 -0
  138. package/src/services/learning-service.js +9 -0
  139. package/src/services/memory-health.js +34 -6
  140. package/src/services/memory-service.js +42 -9
  141. package/src/services/triage.js +138 -0
  142. package/src/session/parts.js +76 -0
  143. package/src/session/projection.js +73 -0
  144. package/src/session/rollout.js +54 -0
  145. package/src/session/turn.js +137 -0
  146. package/src/skills/lint.js +72 -0
  147. package/src/skills/loader.js +231 -150
  148. package/src/skills/search.js +58 -0
  149. package/src/skills/verify.js +95 -100
  150. package/src/tools/advanced.js +388 -352
  151. package/src/tools/builtin.js +384 -297
  152. package/src/tools/catalog.js +283 -159
  153. package/src/tools/command-guard.js +112 -112
  154. package/src/tools/custom.js +47 -47
  155. package/src/tools/delegate.js +383 -297
  156. package/src/tools/document.js +254 -253
  157. package/src/tools/git.js +151 -0
  158. package/src/tools/governance.js +47 -20
  159. package/src/tools/index.js +16 -11
  160. package/src/tools/methods.js +178 -178
  161. package/src/tools/ocr.js +59 -59
  162. package/src/tools/sandbox-worker.js +40 -0
  163. package/src/tools/sandbox.js +92 -0
  164. package/src/tools/seam.js +162 -125
  165. package/src/tools/selfmod.js +196 -176
  166. package/src/tools/v3.js +225 -0
  167. package/src/tools/vad.js +176 -0
  168. package/src/tools/voice.js +238 -0
  169. package/src/utils/async.js +14 -0
  170. package/src/utils/config-file.js +53 -0
  171. package/src/utils/crashguard.js +88 -0
  172. package/src/utils/http.js +53 -0
  173. package/src/utils/id.js +8 -0
  174. package/src/utils/json-state.js +33 -0
  175. package/src/utils/logger.js +17 -17
  176. package/src/utils/ndjson.js +25 -0
  177. package/src/utils/pii.js +42 -42
  178. package/src/utils/rate-limit.js +50 -0
  179. package/src/utils/schema.js +80 -0
  180. package/src/utils/similarity.js +43 -0
  181. package/src/utils/store.js +170 -108
  182. package/src/utils/text.js +15 -15
  183. package/src/utils/trace.js +153 -153
  184. package/src/utils/wal.js +39 -0
  185. package/src/utils/winutf8.js +16 -15
  186. package/src/wiki/index.js +170 -0
@@ -0,0 +1,58 @@
1
+ // src/skills/search.js - 技能检索与高置信匹配 (蓝皮书 2026: 发现机制是技能生态的瓶颈)
2
+ // 1) scoreSkills: name 命中加权 3 > description 命中 1, 按分数降序, 无交叠不返回
3
+ // 2) matchSkill: 高置信阈值 —— 单 bigram 噪音不触发 (需 >= 2 次命中), 并列同分不押注 (返回 null)
4
+ // tokenize: 英文/数字取整词, CJK 取 2-gram (与 memory/fact-store 的粗召回口径一致)
5
+ export function tokenize(text) {
6
+ const s = String(text || "").toLowerCase();
7
+ const out = new Set();
8
+ const re = /[a-z0-9_]+|[\u4e00-\u9fff]+/g;
9
+ for (const m of s.matchAll(re)) {
10
+ const t = m[0];
11
+ if (/^[a-z0-9_]+$/.test(t)) {
12
+ out.add(t);
13
+ continue;
14
+ }
15
+ if (t.length === 1) {
16
+ out.add(t);
17
+ continue;
18
+ }
19
+ for (let i = 0; i + 1 < t.length; i++) out.add(t.slice(i, i + 2));
20
+ }
21
+ return out;
22
+ }
23
+
24
+ function overlap(a, b) {
25
+ let n = 0;
26
+ for (const x of a) if (b.has(x)) n++;
27
+ return n;
28
+ }
29
+
30
+ // 对全部技能打分并降序返回 (score > 0 才有条目)
31
+ export function scoreSkills(loader, query, { nameWeight = 3, descWeight = 1 } = {}) {
32
+ const q = tokenize(query);
33
+ if (!q.size) return [];
34
+ const skills = typeof loader?.list === "function" ? loader.list() : [];
35
+ const ranked = [];
36
+ for (const s of skills) {
37
+ const nHits = overlap(q, tokenize(s.name || s.id));
38
+ const dHits = overlap(q, tokenize(s.description || ""));
39
+ const score = nHits * nameWeight + dHits * descWeight;
40
+ if (score <= 0) continue;
41
+ ranked.push({ ...s, score, hits: nHits + dHits, nameHits: nHits, descHits: dHits });
42
+ }
43
+ // 稳定排序: 分数降序 → 原始顺序 (list 已按 id 排序)
44
+ return ranked
45
+ .map((r, i) => ({ r, i }))
46
+ .sort((a, b) => b.r.score - a.r.score || a.i - b.i)
47
+ .map((x) => x.r);
48
+ }
49
+
50
+ // 高置信单技能匹配: 命中不足以形成信号时返回 null, 并列歧义也不押注
51
+ export function matchSkill(loader, query, { minHits = 2 } = {}) {
52
+ const ranked = scoreSkills(loader, query);
53
+ if (!ranked.length) return null;
54
+ const top = ranked[0];
55
+ if (top.hits < minHits) return null;
56
+ if (ranked[1] && ranked[1].score === top.score) return null; // 并列 → 不押注
57
+ return top;
58
+ }
@@ -1,100 +1,95 @@
1
- // src/skills/verify.js - 技能入库前确定性验证闸门
2
- // 背景 (DeepSeek-Harness 自进化四拼图之「可靠验证」):
3
- // refineSkill 由 LLM 提炼技能后直接 create_skill 落盘, 无独立验收环节 —
4
- // 「完成是自报的, 没有独立验收者」。这会让半成品/幻觉技能污染 skills/。
5
- // 本模块提供确定性验收 (不依赖 LLM, 静态可测, 缺一即拒):
6
- // - 结构验收: SKILL.md 必须含 ## 流程 + ## 验证
7
- // - 落地接地: 内容必须真实引用提炼它的高频工具, 且该工具确有 ≥minFreq 条近期成功轨迹
8
- // - held-out 回归: (P0② Self-Harness) 若提供 heldOutTraces, 要求接地工具在未见过的子集也有背书, 防过拟合
9
- // 用法: verifySkill({ name, content, hotTools, okTraces, minFreq, heldOutTraces })
10
- // -> { ok: boolean, reason?: string, matchedTool?, traceCount?, heldOutCount? }
11
-
12
- // 结构验收: 验收必需段落 (create_skill 契约为 流程/反合理化/验证; 至少 流程+验证 缺一不可)
13
- export function requiredSections(content) {
14
- const c = String(content || "");
15
- const need = ["## 流程", "## 验证"];
16
- const missing = need.filter((s) => !c.includes(s));
17
- return {
18
- ok: missing.length === 0,
19
- missing,
20
- hasFlow: c.includes("## 流程"),
21
- hasVerify: c.includes("## 验证"),
22
- };
23
- }
24
-
25
- // 落地接地: 技能内容是否真实基于提炼它的高频工具 (反幻觉)
26
- export function groundedInTools(content, hotTools = []) {
27
- const c = String(content || "");
28
- const names = (Array.isArray(hotTools) ? hotTools : []).filter(Boolean);
29
- if (names.length === 0) return { ok: false, reason: "无高频工具可核对" };
30
- const hit = names.find((t) => c.toLowerCase().includes(String(t).toLowerCase()));
31
- return { ok: !!hit, matchedTool: hit || null, hotTools: names.slice(0, 5) };
32
- }
33
-
34
- // 轨迹接地: 该工具确有 ≥minFreq 条近期成功轨迹
35
- export function traceBacked(tool, okTraces = [], minFreq = 2) {
36
- const arr = Array.isArray(okTraces) ? okTraces : [];
37
- if (!tool) return { ok: false, reason: "无工具名" };
38
- const n = arr.filter((t) => t && String(t.tool) === tool).length;
39
- return { ok: n >= minFreq, count: n, minFreq, tool };
40
- }
41
-
42
- // held-out 回归闸门 (Self-Harness: 候选改动必须在 held-out 子集也不退化才合并)
43
- // 用途: refineSkill 提炼的技能若只在训练轨迹接地、在未见过的 held-out 轨迹里不接地 = 过拟合 → 拒
44
- export function verifyHeldOut({ tool, heldOutTraces = [], minFreq = 2 } = {}) {
45
- const need = Math.max(1, Math.floor(minFreq / 2));
46
- if (!tool) return { ok: false, reason: "无工具名", need };
47
- const n = (Array.isArray(heldOutTraces) ? heldOutTraces : []).filter((t) => t && String(t.tool) === tool).length;
48
- return { ok: n >= need, count: n, need, tool };
49
- }
50
-
51
- // 总闸门
52
- export function verifySkill({ name, content, hotTools, okTraces, minFreq = 2, heldOutTraces } = {}) {
53
- // 1. 结构验收
54
- const s = requiredSections(content);
55
- if (!s.ok) {
56
- return { ok: false, reason: `技能缺必需段落: ${s.missing.join(", ")} (SKILL.md 契约要求 ## 流程 + ## 验证)` };
57
- }
58
- // 内容太短 (只够段落标题没实质步骤) 也拒
59
- const body = String(content || "").replace(/##\s*\S+/g, "").trim();
60
- if (body.length < 20) {
61
- return { ok: false, reason: "技能内容太短, 缺实质步骤/检查点" };
62
- }
63
- // 2. 落地接地: 内容引用高频工具
64
- const g = groundedInTools(content, hotTools);
65
- if (!g.ok) {
66
- return { ok: false, reason: `技能内容未引用提炼它的高频工具 (${g.hotTools.join(", ")})` };
67
- }
68
- // 3. 轨迹接地: 该工具有足够近期成功轨迹背书
69
- const tb = traceBacked(g.matchedTool, okTraces, minFreq);
70
- if (!tb.ok) {
71
- return { ok: false, reason: `工具 ${g.matchedTool} 成功轨迹不足 (${tb.count}/${tb.minFreq})` };
72
- }
73
- // 4. held-out 回归: 若提供, 要求接地工具在 held-out 子集也有 ≥ceil(minFreq/2) 条成功轨迹背书 (防过拟合训练集)
74
- if (heldOutTraces && Array.isArray(heldOutTraces)) {
75
- const ho = verifyHeldOut({ tool: g.matchedTool, heldOutTraces, minFreq });
76
- if (!ho.ok) {
77
- return { ok: false, reason: `工具 ${g.matchedTool} held-out 回归轨迹不足 (${ho.count}/${ho.need}), 疑似过拟合训练集` };
78
- }
79
- }
80
- return { ok: true, matchedTool: g.matchedTool, traceCount: tb.count, heldOutCount: (heldOutTraces && Array.isArray(heldOutTraces)) ? traceBacked(g.matchedTool, heldOutTraces, 1).count : null };
81
- }
82
-
83
- // 技能升级专用验收 (用中自进化): 不重新做 grounding (创建时已接地验证),
84
- // 重点防止退化: 缺段落 / 正文缩水 / 变空。返回 { ok, reason, changed }
85
- export function verifyUpgradeSkill({ content, prevContent }) {
86
- const c = String(content || "").replace(/^---[\s\S]*?---\s*/, ""); // 剥 frontmatter (skills.read 返回 ---meta--- 全文)
87
- const p = String(prevContent || "").replace(/^---[\s\S]*?---\s*/, "");
88
- // 1. 结构验收 (必须仍含 流程+验证)
89
- const s = requiredSections(c);
90
- if (!s.ok) return { ok: false, reason: "升级版缺必需段落: " + s.missing.join(", ") };
91
- // 2. 正文不缩水 (去段落标题后比旧版短 = 退化)
92
- const bodyNow = c.replace(/##\s*\S+/g, "").trim();
93
- const bodyPrev = p.replace(/##\s*\S+/g, "").trim();
94
- if (bodyNow.length < 20) return { ok: false, reason: "升级版内容太短" };
95
- if (bodyPrev.length && bodyNow.length < bodyPrev.length * 0.6) {
96
- return { ok: false, reason: "升级版正文缩水 (" + bodyNow.length + "<" + Math.ceil(bodyPrev.length * 0.6) + "), 疑似退化" };
97
- }
98
- const changed = bodyNow !== bodyPrev;
99
- return { ok: true, changed };
100
- }
1
+ // src/skills/verify.js - 技能入库/升级验证闸门 (零 LLM, 确定性)
2
+ // 设计依据: self-evolution "reliable verification" —— gate before persist.
3
+ // 入库闸门 (verifySkill): 结构(## 流程 + ## 验证) + 接地(正文引用高频工具) + 轨迹背书(该工具有足够成功调用)
4
+ // 升级闸门 (verifyUpgradeSkill): 结构不缩水 + 正文不缩水(防退化) + 有实质变化
5
+ // 目标: 幻觉技能/退化技能一律拦在落盘之前, 保证 skills/ 目录只进"被真实轨迹背书"的方法。
6
+ import { parseSections } from "./loader.js";
7
+
8
+ // 技能正文必须包含的章节 (与 learning-service 的 "## Process + ## Verify" 语义一致)
9
+ export const requiredSections = ["流程", "验证"];
10
+
11
+ // 正文长度下限 (低于视为"只写了标题没写实质")
12
+ export const MIN_CONTENT_LEN = 20;
13
+
14
+ // 升级版相对旧版的最小长度比 (低于即判"缩水退步")
15
+ export const MIN_UPGRADE_RATIO = 0.8;
16
+
17
+ // 正文是否真实引用了高频工具 (按整词匹配, 避免子串误判)。命中返回工具名, 否则 null。
18
+ export function groundedInTools(content, hotTools = []) {
19
+ const text = String(content || "");
20
+ for (const t of hotTools) {
21
+ const name = String(t || "").trim();
22
+ if (!name) continue;
23
+ const esc = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
24
+ if (new RegExp(`(^|[^a-zA-Z0-9_])${esc}([^a-zA-Z0-9_]|$)`).test(text)) return name;
25
+ }
26
+ return null;
27
+ }
28
+
29
+ // 章节齐全性检查 → 缺失章节数组
30
+ export function missingSections(content) {
31
+ const secs = parseSections(String(content || ""));
32
+ return requiredSections.filter((s) => !secs[s] || !String(secs[s]).trim());
33
+ }
34
+
35
+ // 入库闸门: 结构 + 接地 + 轨迹背书
36
+ // opts: { name, content, hotTools, okTraces, minFreq, heldOutTraces }
37
+ export function verifySkill({ name = "", content = "", hotTools = [], okTraces = [], minFreq = 2, heldOutTraces = null } = {}) {
38
+ const body = String(content || "").trim();
39
+ if (!body) return { ok: false, reason: "内容为空" };
40
+
41
+ // 1) 结构: 必需章节齐全
42
+ const missing = missingSections(body);
43
+ if (missing.length) return { ok: false, reason: "缺少必要章节: " + missing.join(" / ") };
44
+
45
+ // 2) 实质: 长度下限 (必须在接地检查之前 —— 空壳正文应报"太短"而非"未引用")
46
+ if (body.length < MIN_CONTENT_LEN) return { ok: false, reason: `内容太短 (< ${MIN_CONTENT_LEN} 字), 疑似空壳` };
47
+
48
+ // 3) 接地: 必须引用至少一个高频工具
49
+ const matchedTool = groundedInTools(body, hotTools);
50
+ if (!matchedTool) {
51
+ const list = (hotTools || []).filter(Boolean).slice(0, 6).join(", ") || "(无)";
52
+ return { ok: false, reason: `内容未引用任何高频工具, 疑似幻觉技能. 应引用的工具: ${list}` };
53
+ }
54
+
55
+ // 4) 轨迹背书: 该高频工具要有足够成功调用 (minFreq 为频次门槛)
56
+ const traceCount = (okTraces || []).filter((t) => t && t.tool === matchedTool).length;
57
+ if (traceCount < minFreq) {
58
+ return { ok: false, reason: `高频工具 ${matchedTool} 的成功轨迹不足 (${traceCount}/${minFreq})`, matchedTool, traceCount };
59
+ }
60
+
61
+ // 5) held-out 回归 (可选): 样本够多时切出的未见子集也要有背书, 防过拟合
62
+ if (Array.isArray(heldOutTraces) && heldOutTraces.length) {
63
+ const heldCount = heldOutTraces.filter((t) => t && t.tool === matchedTool).length;
64
+ if (heldCount < 1) {
65
+ return { ok: false, reason: `held-out 子集中无 ${matchedTool} 的背书 (疑似过拟合)`, matchedTool, traceCount };
66
+ }
67
+ }
68
+
69
+ return { ok: true, reason: null, matchedTool, traceCount, name };
70
+ }
71
+
72
+ // 升级闸门: 防退化 (结构不缩水 + 正文不缩水) + 必须真的有变化
73
+ // opts: { content, prevContent }
74
+ export function verifyUpgradeSkill({ content = "", prevContent = "" } = {}) {
75
+ const next = String(content || "").trim();
76
+ const prev = String(prevContent || "");
77
+ if (!next) return { ok: false, reason: "升级结果为空" };
78
+
79
+ // 1) 结构: 不得丢掉必需章节 (防"改着改着把验证段删了")
80
+ const missing = missingSections(next);
81
+ if (missing.length) return { ok: false, reason: "升级版缺少必要章节: " + missing.join(" / ") };
82
+
83
+ // 2) 实质长度
84
+ if (next.length < MIN_CONTENT_LEN) return { ok: false, reason: `升级版内容太短 (< ${MIN_CONTENT_LEN} 字)` };
85
+
86
+ // 3) 缩水检测: 相对旧版大幅变短即判退化
87
+ if (prev.trim() && next.length < prev.trim().length * MIN_UPGRADE_RATIO) {
88
+ return { ok: false, reason: `升级版正文缩水 (${next.length} < ${prev.trim().length} × ${MIN_UPGRADE_RATIO})` };
89
+ }
90
+
91
+ const changed = next !== prev.trim();
92
+ if (!changed) return { ok: false, reason: "升级版与旧版完全相同, 无实质变化", changed: false };
93
+
94
+ return { ok: true, reason: null, changed: true };
95
+ }