ppxans-harness 2.6.0 → 3.2.1

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 (189) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +195 -195
  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 +6 -124
  11. package/config/ppx.json.example +143 -0
  12. package/package.json +16 -6
  13. package/skills/.usage.json +6 -0
  14. package/skills/agent-professional-training/SKILL.md +94 -0
  15. package/skills/agent-professional-training/trajectories/2026-09-11-dpo-pairs.jsonl +2 -0
  16. package/skills/agent-professional-training/trajectories/2026-09-11-round2-trajectory.json +15 -0
  17. package/skills/agent-professional-training/trajectories/2026-09-13-first-run-trajectory.json +17 -0
  18. package/skills/brainstorm/SKILL.md +24 -0
  19. package/skills/cupid-lover-comms/SKILL.md +37 -0
  20. package/skills/debug/SKILL.md +26 -0
  21. package/skills/plan/SKILL.md +25 -0
  22. package/skills/ponytail/SKILL.md +25 -0
  23. package/skills/ppx-memory/SKILL.md +91 -0
  24. package/skills/ppx-memory/scripts/cli.js +192 -0
  25. package/skills/ppx-memory/scripts/experience.js +133 -0
  26. package/skills/ppx-memory/scripts/fact-store.js +842 -0
  27. package/skills/ppx-memory/scripts/l0.js +52 -0
  28. package/skills/ppx-memory/scripts/l2.js +146 -0
  29. package/skills/ppx-memory/scripts/l3.js +112 -0
  30. package/skills/ppx-memory/scripts/memory-ticker.js +238 -0
  31. package/skills/ppx-memory/scripts/pii.js +42 -0
  32. package/skills/ppx-memory/scripts/schema.js +80 -0
  33. package/skills/ppx-memory/scripts/session.js +398 -0
  34. package/skills/ppx-memory/scripts/similarity.js +43 -0
  35. package/skills/ppx-memory/scripts/store.js +116 -0
  36. package/skills/ppx-memory/scripts/wal.js +38 -0
  37. package/skills/ppx-selfheal/SKILL.md +24 -0
  38. package/skills/ppx-selfheal/scripts/cli.js +80 -0
  39. package/skills/ppx-selfheal/scripts/healer.js +184 -0
  40. package/skills/ppx-selfheal/scripts/logger.js +17 -0
  41. package/skills/ppx-selfheal/scripts/store.js +116 -0
  42. package/skills/prompt-depth-kit/SKILL.md +28 -0
  43. package/skills/session-naming/SKILL.md +36 -0
  44. package/skills/verify/SKILL.md +25 -0
  45. package/src/agent/index.js +1352 -717
  46. package/src/agent/prompts.js +46 -2
  47. package/src/aml-server.js +197 -151
  48. package/src/ans/eviction.js +123 -143
  49. package/src/ans/guard.js +159 -120
  50. package/src/ans/lifecycle.js +96 -93
  51. package/src/ans/proactive.js +112 -129
  52. package/src/ans/reward.js +95 -111
  53. package/src/ans/values.js +15 -15
  54. package/src/audit/audit-chain.js +43 -7
  55. package/src/audit/verifier.js +157 -120
  56. package/src/bus/circuit-breaker.js +9 -1
  57. package/src/bus/runtime-bus.js +107 -93
  58. package/src/channels/base.js +57 -34
  59. package/src/channels/feishu.js +118 -126
  60. package/src/channels/http.js +1090 -651
  61. package/src/channels/index.js +111 -110
  62. package/src/channels/log.js +29 -29
  63. package/src/channels/wechat-crypto.js +73 -74
  64. package/src/channels/wechat.js +191 -197
  65. package/src/channels/workspace.js +94 -0
  66. package/src/channels-cli.js +126 -124
  67. package/src/cli.js +129 -120
  68. package/src/commands/index.js +142 -0
  69. package/src/config/channels.js +137 -170
  70. package/src/config/index.js +277 -224
  71. package/src/config/placeholder.js +31 -0
  72. package/src/config/providers.js +148 -182
  73. package/src/config/settings.js +156 -182
  74. package/src/core/policy.js +69 -18
  75. package/src/core/trace.js +8 -6
  76. package/src/edit/editblock.js +266 -0
  77. package/src/edit/snapshot.js +67 -0
  78. package/src/evidence/index.js +153 -0
  79. package/src/evolve/playbook.js +9 -10
  80. package/src/hooks/index.js +113 -0
  81. package/src/llm/client.js +4 -3
  82. package/src/llm/dsml.js +74 -74
  83. package/src/llm/embedder.js +41 -35
  84. package/src/llm/fence.js +52 -52
  85. package/src/llm/index.js +4 -4
  86. package/src/llm/local-embedder.js +94 -0
  87. package/src/llm/presets.js +113 -0
  88. package/src/llm/pricing.js +93 -0
  89. package/src/llm/retry.js +73 -73
  90. package/src/llm/router.js +92 -89
  91. package/src/mcp/admin.js +44 -10
  92. package/src/mcp/client.js +487 -477
  93. package/src/mcp/http.js +23 -37
  94. package/src/mcp/index.js +116 -116
  95. package/src/mcp/server.js +23 -18
  96. package/src/mcp/tasks.js +9 -14
  97. package/src/memory/asset-hub.js +6 -11
  98. package/src/memory/canvas.js +2 -5
  99. package/src/memory/compaction.js +28 -28
  100. package/src/memory/experience.js +133 -122
  101. package/src/memory/fact-store.js +914 -698
  102. package/src/memory/failure-episode.js +20 -11
  103. package/src/memory/fork.js +17 -8
  104. package/src/memory/index.js +8 -6
  105. package/src/memory/l0.js +53 -52
  106. package/src/memory/l2.js +145 -130
  107. package/src/memory/l3.js +111 -111
  108. package/src/memory/legion-board.js +71 -0
  109. package/src/memory/memory-ticker.js +239 -240
  110. package/src/memory/session.js +398 -397
  111. package/src/memory/sqlite-store.js +581 -0
  112. package/src/mode/blackboard.js +49 -49
  113. package/src/mode/graph.js +42 -41
  114. package/src/mode/index.js +64 -64
  115. package/src/mode/legion.js +54 -51
  116. package/src/mode/plan-exec.js +50 -50
  117. package/src/mode/router.js +28 -40
  118. package/src/orchestrator/agent-worker.js +69 -69
  119. package/src/orchestrator/dag.js +90 -83
  120. package/src/orchestrator/experts.js +76 -0
  121. package/src/orchestrator/index.js +1 -1
  122. package/src/orchestrator/legion.js +179 -187
  123. package/src/orchestrator/supervisor.js +6 -8
  124. package/src/permissions/index.js +378 -0
  125. package/src/persona/index.js +28 -29
  126. package/src/plugin/builtin.js +313 -215
  127. package/src/plugin/context.js +80 -79
  128. package/src/plugin/index.js +62 -62
  129. package/src/plugin/v3.js +73 -0
  130. package/src/protocol/index.js +148 -0
  131. package/src/repomap/index.js +309 -0
  132. package/src/review/index.js +393 -0
  133. package/src/seam/registry.js +3 -0
  134. package/src/seam/shell.js +55 -55
  135. package/src/security/injection.js +79 -0
  136. package/src/selfheal/evolve.js +67 -67
  137. package/src/selfheal/healer.js +184 -167
  138. package/src/selfheal/run.js +9 -9
  139. package/src/server.js +63 -60
  140. package/src/services/diagnose.js +180 -0
  141. package/src/services/learning-service.js +9 -0
  142. package/src/services/memory-health.js +34 -6
  143. package/src/services/memory-service.js +42 -9
  144. package/src/services/triage.js +138 -0
  145. package/src/session/parts.js +76 -0
  146. package/src/session/projection.js +73 -0
  147. package/src/session/rollout.js +54 -0
  148. package/src/session/turn.js +137 -0
  149. package/src/skills/lint.js +72 -0
  150. package/src/skills/loader.js +231 -150
  151. package/src/skills/search.js +58 -0
  152. package/src/skills/verify.js +95 -100
  153. package/src/tools/advanced.js +388 -352
  154. package/src/tools/builtin.js +384 -297
  155. package/src/tools/catalog.js +283 -159
  156. package/src/tools/command-guard.js +112 -112
  157. package/src/tools/custom.js +47 -47
  158. package/src/tools/delegate.js +383 -297
  159. package/src/tools/document.js +254 -253
  160. package/src/tools/git.js +151 -0
  161. package/src/tools/governance.js +47 -20
  162. package/src/tools/index.js +16 -11
  163. package/src/tools/methods.js +178 -178
  164. package/src/tools/ocr.js +59 -59
  165. package/src/tools/sandbox-worker.js +40 -0
  166. package/src/tools/sandbox.js +92 -0
  167. package/src/tools/seam.js +162 -125
  168. package/src/tools/selfmod.js +196 -176
  169. package/src/tools/v3.js +225 -0
  170. package/src/tools/vad.js +176 -0
  171. package/src/tools/voice.js +238 -0
  172. package/src/utils/async.js +14 -0
  173. package/src/utils/config-file.js +53 -0
  174. package/src/utils/crashguard.js +88 -0
  175. package/src/utils/http.js +53 -0
  176. package/src/utils/id.js +8 -0
  177. package/src/utils/json-state.js +33 -0
  178. package/src/utils/logger.js +17 -17
  179. package/src/utils/ndjson.js +25 -0
  180. package/src/utils/pii.js +42 -42
  181. package/src/utils/rate-limit.js +50 -0
  182. package/src/utils/schema.js +80 -0
  183. package/src/utils/similarity.js +43 -0
  184. package/src/utils/store.js +170 -108
  185. package/src/utils/text.js +15 -15
  186. package/src/utils/trace.js +153 -153
  187. package/src/utils/wal.js +39 -0
  188. package/src/utils/winutf8.js +16 -15
  189. package/src/wiki/index.js +170 -0
package/src/utils/text.js CHANGED
@@ -1,16 +1,16 @@
1
- // src/utils/text.js - 零依赖文本工具 (token 估算等, 供 agent/llm 等共用)
2
- // 单一实现, 避免 agent 与 llm 各自维护一份 token 估算导致口径漂移。
3
-
4
- // 估算 token 数: 中文字符约 1 字 ≈ 0.6 token, 1 token ≈ 4 字符; 统一按字符数 /1.6 估。
5
- export function estimateTokens(s) {
6
- return Math.ceil(String(s || "").length / 1.6);
7
- }
8
-
9
- // 按 token 预算截断字符串: 保留头部信息 (供 persona/角色设定等需保留 key 信息的场景),
10
- // 超预算时在尾部标注截断来源与总量。与 estimateTokens 同口径 (字符/1.6)。
11
- export function truncateByTokens(s, budget) {
12
- const str = String(s || "");
13
- const maxLen = Math.floor(budget * 1.6);
14
- if (str.length <= maxLen) return str;
15
- return str.slice(0, maxLen) + `\n...[persona 已按预算截断, 共 ${str.length} 字符]...`;
1
+ // src/utils/text.js - 零依赖文本工具 (token 估算等, 供 agent/llm 等共用)
2
+ // 单一实现, 避免 agent 与 llm 各自维护一份 token 估算导致口径漂移。
3
+
4
+ // 估算 token 数: 中文字符约 1 字 ≈ 0.6 token, 1 token ≈ 4 字符; 统一按字符数 /1.6 估。
5
+ export function estimateTokens(s) {
6
+ return Math.ceil(String(s || "").length / 1.6);
7
+ }
8
+
9
+ // 按 token 预算截断字符串: 保留头部信息 (供 persona/角色设定等需保留 key 信息的场景),
10
+ // 超预算时在尾部标注截断来源与总量。与 estimateTokens 同口径 (字符/1.6)。
11
+ export function truncateByTokens(s, budget) {
12
+ const str = String(s || "");
13
+ const maxLen = Math.floor(budget * 1.6);
14
+ if (str.length <= maxLen) return str;
15
+ return str.slice(0, maxLen) + `\n...[persona 已按预算截断, 共 ${str.length} 字符]...`;
16
16
  }
@@ -1,153 +1,153 @@
1
- // src/utils/trace.js - 结构化可观测轨迹 (Harness 第四层)
2
- // 记录每次工具调用的完整轨迹: 工具/参数/结果摘要/耗时/成败
3
- // 写 JSONL 到 data/logs/traces/YYYY-MM-DD.jsonl, 供事后复盘"哪一步坏了"
4
- // v1.0.8: args/result 落盘前 PII 脱敏 (凭证不写日志); read(day) 支持按指定日期读取
5
- // P0 (2026-09-15): 事件源不变量 (model-visible = logged) ——
6
- // turn/step 边界事件落同一 JSONL + verifyReplay() 重放校验 (配对完整性/顺序性)。
7
- // 对齐 dsh "会话日志是模型可见内容的唯一事实源" 思想, 用皮皮虾自有结构增量实现。
8
- import fs from "node:fs";
9
- import path from "node:path";
10
- import { ensureDir, logicalDay } from "./store.js";
11
- import { scrubPII } from "./pii.js";
12
-
13
- const MAX_TRACE_BODY = 2000; // 单条结果保留上限, 防爆文件
14
-
15
- // 事件类型常量 (turn/step 边界)
16
- export const EVT = {
17
- TURN_START: "turn/start",
18
- TURN_END: "turn/end",
19
- STEP_START: "step/start",
20
- STEP_END: "step/end",
21
- TOOL: "tool/call",
22
- };
23
-
24
- export class Traces {
25
- constructor(dataDir) {
26
- this.dir = path.join(dataDir, "logs", "traces");
27
- ensureDir(this.dir);
28
- this.sessionId = "s_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
29
- this.count = 0;
30
- }
31
-
32
- _file(day = logicalDay()) { return path.join(this.dir, `${day}.jsonl`); }
33
-
34
- // 记录一次工具调用轨迹
35
- record({ tool, args, result, ok, durationMs, error }) {
36
- const entry = this._entry({ evt: EVT.TOOL, tool, args, result, ok, durationMs, error });
37
- fs.appendFileSync(this._file(), JSON.stringify(entry) + "\n", "utf8");
38
- return entry;
39
- }
40
-
41
- // ---- P0: turn/step 边界事件 (事件源事实) ----
42
- recordTurnStart({ sessionId, reason } = {}) {
43
- return this._append({ evt: EVT.TURN_START, tsIso: new Date().toISOString(), reason: reason || "user", sessionId: sessionId || this.sessionId });
44
- }
45
- recordTurnEnd({ ok = true, rounds = 0, reason } = {}) {
46
- return this._append({ evt: EVT.TURN_END, ok: !!ok, rounds, reason: reason || null });
47
- }
48
- recordStepStart({ round = 0, context } = {}) {
49
- // context 快照 = 该 step 模型看到的上下文投影摘要 (验证 model-visible = logged)
50
- const safe = context ? scrubPII(JSON.stringify(context)).cleaned : null;
51
- return this._append({ evt: EVT.STEP_START, round, context: safe ? this._truncate(safe) : null });
52
- }
53
- recordStepEnd({ round = 0, ok = true, tool = null, error } = {}) {
54
- return this._append({ evt: EVT.STEP_END, round, ok: !!ok, tool: tool || null, error: error || null });
55
- }
56
-
57
- _append(extra) {
58
- this.count += 1;
59
- const entry = { ...this._base(), ...extra };
60
- fs.appendFileSync(this._file(), JSON.stringify(entry) + "\n", "utf8");
61
- return entry;
62
- }
63
-
64
- _base() {
65
- return { ts: new Date().toISOString(), sessionId: this.sessionId, seq: this.count };
66
- }
67
-
68
- _entry({ evt, tool, args, result, ok, durationMs, error }) {
69
- const safeArgs = scrubPII(JSON.stringify(args ?? {})).cleaned;
70
- const safeResult = scrubPII(String(result ?? "")).cleaned;
71
- return {
72
- ...this._base(),
73
- evt: evt || EVT.TOOL,
74
- tool,
75
- args: this._truncate(safeArgs),
76
- result: this._truncate(safeResult),
77
- ok: !!ok,
78
- durationMs: Math.round(durationMs || 0),
79
- error: error || null,
80
- };
81
- }
82
-
83
- _truncate(s) {
84
- const str = String(s ?? "");
85
- return str.length > MAX_TRACE_BODY ? str.slice(0, MAX_TRACE_BODY) + "…" : str;
86
- }
87
-
88
- // ---- P0: 事件源不变量断言 (model-visible = logged) ----
89
- // 重放校验: 1) 每行可解析 2) turn/step 配对完整 (start 必有 end, 嵌套合法) 3) step round 递增无重复
90
- // 返回 { ok, total, errors: [] }。任何不变量破坏 → ok=false + 具体错误行, 供自愈/测试定位。
91
- verifyReplay(day = logicalDay()) {
92
- const lines = this.readRaw(day);
93
- const errors = [];
94
- const turnStack = [];
95
- const stepRounds = new Set();
96
- let total = 0;
97
- for (let i = 0; i < lines.length; i++) {
98
- let e;
99
- try { e = JSON.parse(lines[i]); } catch (err) { errors.push({ line: i + 1, kind: "parse", detail: err.message }); continue; }
100
- total++;
101
- if (!e || !e.evt) { errors.push({ line: i + 1, kind: "no-evt" }); continue; }
102
- if (e.evt === EVT.TURN_START) turnStack.push(i);
103
- else if (e.evt === EVT.TURN_END) {
104
- if (!turnStack.length) errors.push({ line: i + 1, kind: "turn-end-orphan" });
105
- else turnStack.pop();
106
- } else if (e.evt === EVT.STEP_START) {
107
- const r = Number(e.round);
108
- if (stepRounds.has(r)) errors.push({ line: i + 1, kind: "step-round-duplicate", detail: `round=${r}` });
109
- stepRounds.add(r);
110
- }
111
- }
112
- if (turnStack.length) errors.push({ kind: "turn-unclosed", detail: `剩余 ${turnStack.length} 个未闭合 turn` });
113
- return { ok: errors.length === 0, total, errors };
114
- }
115
-
116
- // 读原始行 (verifyReplay 用, 不跳过坏行)
117
- readRaw(day = logicalDay()) {
118
- const file = this._file(day);
119
- if (!fs.existsSync(file)) return [];
120
- return fs.readFileSync(file, "utf8").split("\n").filter(Boolean);
121
- }
122
-
123
- // 读取某天轨迹 (最近 N 条); day 缺省 = 今天
124
- read(day, limit = 100) {
125
- const file = this._file(day);
126
- if (!fs.existsSync(file)) return [];
127
- return fs.readFileSync(file, "utf8").split("\n").filter(Boolean).slice(-limit)
128
- .map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
129
- }
130
-
131
- // 统计: 失败率/平均耗时/慢工具
132
- stats(day) {
133
- const all = this.read(day, 10000);
134
- if (!all.length) return { count: 0, failed: 0, failRate: "0%", slowTools: [] };
135
- const failed = all.filter((t) => !t.ok);
136
- const byTool = {};
137
- for (const t of all) {
138
- (byTool[t.tool] = byTool[t.tool] || { calls: 0, fails: 0, totalMs: 0 });
139
- byTool[t.tool].calls++;
140
- if (!t.ok) byTool[t.tool].fails++;
141
- byTool[t.tool].totalMs += t.durationMs;
142
- }
143
- const slow = Object.entries(byTool)
144
- .map(([tool, v]) => ({ tool, ...v, avgMs: Math.round(v.totalMs / v.calls) }))
145
- .sort((a, b) => b.avgMs - a.avgMs).slice(0, 5);
146
- return {
147
- count: all.length,
148
- failed: failed.length,
149
- failRate: all.length ? (failed.length / all.length * 100).toFixed(1) + "%" : "0%",
150
- slowTools: slow,
151
- };
152
- }
153
- }
1
+ // src/utils/trace.js - 结构化可观测轨迹 (Harness 第四层)
2
+ // 记录每次工具调用的完整轨迹: 工具/参数/结果摘要/耗时/成败
3
+ // 写 JSONL 到 data/logs/traces/YYYY-MM-DD.jsonl, 供事后复盘"哪一步坏了"
4
+ // v1.0.8: args/result 落盘前 PII 脱敏 (凭证不写日志); read(day) 支持按指定日期读取
5
+ // P0 (2026-09-15): 事件源不变量 (model-visible = logged) ——
6
+ // turn/step 边界事件落同一 JSONL + verifyReplay() 重放校验 (配对完整性/顺序性)。
7
+ // 对齐 dsh "会话日志是模型可见内容的唯一事实源" 思想, 用皮皮虾自有结构增量实现。
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import { logsDir, logicalDay } from "./store.js";
11
+ import { shortId } from "./id.js";
12
+ import { scrubPII } from "./pii.js";
13
+
14
+ const MAX_TRACE_BODY = 2000; // 单条结果保留上限, 防爆文件
15
+
16
+ // 事件类型常量 (turn/step 边界)
17
+ export const EVT = {
18
+ TURN_START: "turn/start",
19
+ TURN_END: "turn/end",
20
+ STEP_START: "step/start",
21
+ STEP_END: "step/end",
22
+ TOOL: "tool/call",
23
+ };
24
+
25
+ export class Traces {
26
+ constructor(dataDir) {
27
+ this.dir = logsDir(dataDir); // 与 core/trace.js 同一目录 (logs/traces)
28
+ this.sessionId = shortId("s_", 6);
29
+ this.count = 0;
30
+ }
31
+
32
+ _file(day = logicalDay()) { return path.join(this.dir, `${day}.jsonl`); }
33
+
34
+ // 记录一次工具调用轨迹
35
+ record({ tool, args, result, ok, durationMs, error }) {
36
+ const entry = this._entry({ evt: EVT.TOOL, tool, args, result, ok, durationMs, error });
37
+ fs.appendFileSync(this._file(), JSON.stringify(entry) + "\n", "utf8");
38
+ return entry;
39
+ }
40
+
41
+ // ---- P0: turn/step 边界事件 (事件源事实) ----
42
+ recordTurnStart({ sessionId, reason } = {}) {
43
+ return this._append({ evt: EVT.TURN_START, tsIso: new Date().toISOString(), reason: reason || "user", sessionId: sessionId || this.sessionId });
44
+ }
45
+ recordTurnEnd({ ok = true, rounds = 0, reason } = {}) {
46
+ return this._append({ evt: EVT.TURN_END, ok: !!ok, rounds, reason: reason || null });
47
+ }
48
+ recordStepStart({ round = 0, context } = {}) {
49
+ // context 快照 = 该 step 模型看到的上下文投影摘要 (验证 model-visible = logged)
50
+ const safe = context ? scrubPII(JSON.stringify(context)).cleaned : null;
51
+ return this._append({ evt: EVT.STEP_START, round, context: safe ? this._truncate(safe) : null });
52
+ }
53
+ recordStepEnd({ round = 0, ok = true, tool = null, error } = {}) {
54
+ return this._append({ evt: EVT.STEP_END, round, ok: !!ok, tool: tool || null, error: error || null });
55
+ }
56
+
57
+ _append(extra) {
58
+ this.count += 1;
59
+ const entry = { ...this._base(), ...extra };
60
+ fs.appendFileSync(this._file(), JSON.stringify(entry) + "\n", "utf8");
61
+ return entry;
62
+ }
63
+
64
+ _base() {
65
+ return { ts: new Date().toISOString(), sessionId: this.sessionId, seq: this.count };
66
+ }
67
+
68
+ _entry({ evt, tool, args, result, ok, durationMs, error }) {
69
+ const safeArgs = scrubPII(JSON.stringify(args ?? {})).cleaned;
70
+ const safeResult = scrubPII(String(result ?? "")).cleaned;
71
+ return {
72
+ ...this._base(),
73
+ evt: evt || EVT.TOOL,
74
+ tool,
75
+ args: this._truncate(safeArgs),
76
+ result: this._truncate(safeResult),
77
+ ok: !!ok,
78
+ durationMs: Math.round(durationMs || 0),
79
+ error: error || null,
80
+ };
81
+ }
82
+
83
+ _truncate(s) {
84
+ const str = String(s ?? "");
85
+ return str.length > MAX_TRACE_BODY ? str.slice(0, MAX_TRACE_BODY) + "…" : str;
86
+ }
87
+
88
+ // ---- P0: 事件源不变量断言 (model-visible = logged) ----
89
+ // 重放校验: 1) 每行可解析 2) turn/step 配对完整 (start 必有 end, 嵌套合法) 3) step round 递增无重复
90
+ // 返回 { ok, total, errors: [] }。任何不变量破坏 → ok=false + 具体错误行, 供自愈/测试定位。
91
+ verifyReplay(day = logicalDay()) {
92
+ const lines = this.readRaw(day);
93
+ const errors = [];
94
+ const turnStack = [];
95
+ const stepRounds = new Set();
96
+ let total = 0;
97
+ for (let i = 0; i < lines.length; i++) {
98
+ let e;
99
+ try { e = JSON.parse(lines[i]); } catch (err) { errors.push({ line: i + 1, kind: "parse", detail: err.message }); continue; }
100
+ total++;
101
+ if (!e || !e.evt) { errors.push({ line: i + 1, kind: "no-evt" }); continue; }
102
+ if (e.evt === EVT.TURN_START) turnStack.push(i);
103
+ else if (e.evt === EVT.TURN_END) {
104
+ if (!turnStack.length) errors.push({ line: i + 1, kind: "turn-end-orphan" });
105
+ else turnStack.pop();
106
+ } else if (e.evt === EVT.STEP_START) {
107
+ const r = Number(e.round);
108
+ if (stepRounds.has(r)) errors.push({ line: i + 1, kind: "step-round-duplicate", detail: `round=${r}` });
109
+ stepRounds.add(r);
110
+ }
111
+ }
112
+ if (turnStack.length) errors.push({ kind: "turn-unclosed", detail: `剩余 ${turnStack.length} 个未闭合 turn` });
113
+ return { ok: errors.length === 0, total, errors };
114
+ }
115
+
116
+ // 读原始行 (verifyReplay 用, 不跳过坏行)
117
+ readRaw(day = logicalDay()) {
118
+ const file = this._file(day);
119
+ if (!fs.existsSync(file)) return [];
120
+ return fs.readFileSync(file, "utf8").split("\n").filter(Boolean);
121
+ }
122
+
123
+ // 读取某天轨迹 (最近 N 条); day 缺省 = 今天
124
+ read(day, limit = 100) {
125
+ const file = this._file(day);
126
+ if (!fs.existsSync(file)) return [];
127
+ return fs.readFileSync(file, "utf8").split("\n").filter(Boolean).slice(-limit)
128
+ .map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
129
+ }
130
+
131
+ // 统计: 失败率/平均耗时/慢工具
132
+ stats(day) {
133
+ const all = this.read(day, 10000);
134
+ if (!all.length) return { count: 0, failed: 0, failRate: "0%", slowTools: [] };
135
+ const failed = all.filter((t) => !t.ok);
136
+ const byTool = {};
137
+ for (const t of all) {
138
+ (byTool[t.tool] = byTool[t.tool] || { calls: 0, fails: 0, totalMs: 0 });
139
+ byTool[t.tool].calls++;
140
+ if (!t.ok) byTool[t.tool].fails++;
141
+ byTool[t.tool].totalMs += t.durationMs;
142
+ }
143
+ const slow = Object.entries(byTool)
144
+ .map(([tool, v]) => ({ tool, ...v, avgMs: Math.round(v.totalMs / v.calls) }))
145
+ .sort((a, b) => b.avgMs - a.avgMs).slice(0, 5);
146
+ return {
147
+ count: all.length,
148
+ failed: failed.length,
149
+ failRate: all.length ? (failed.length / all.length * 100).toFixed(1) + "%" : "0%",
150
+ slowTools: slow,
151
+ };
152
+ }
153
+ }
@@ -0,0 +1,39 @@
1
+ // src/utils/wal.js - 追加式变更日志 (Write-Ahead Log) 轻量实现
2
+ // 用途: 记忆数据的高频增量落盘。每次变更 append 一行事件到 <file>.wal (单行 JSON, 原子追加),
3
+ // 达到阈值或显式 flush 时全量 compact 到主文件 + 清空 WAL。
4
+ // 崩溃安全: 主文件 = 最后一次 flush 快照, WAL = 快照后的增量; 启动时重放 WAL 恢复。
5
+ // 重放幂等: 事件按 id upsert/remove, 重复应用无害 (flush 后崩溃最多重放已落盘变更)。
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ import { ensureDir } from "./store.js";
9
+ import { debug } from "../utils/logger.js";
10
+
11
+ export function walFileOf(file) {
12
+ return file + ".wal";
13
+ }
14
+
15
+ // 追加一条事件 (JSON 行)。appendFileSync 单行追加在正常写入下原子; 失败抛错由调用方降级 (内存态仍正确, 下次 flush 补齐)。
16
+ export function appendWal(walFile, evt) {
17
+ ensureDir(path.dirname(walFile));
18
+ fs.appendFileSync(walFile, JSON.stringify(evt) + "\n", "utf8");
19
+ }
20
+
21
+ // 读 WAL: 逐行解析; 尾部半行 (崩溃中断写入) 静默丢弃; 返回事件数组
22
+ export function readWal(walFile) {
23
+ if (!fs.existsSync(walFile)) return [];
24
+ let raw;
25
+ try { raw = fs.readFileSync(walFile, "utf8"); } catch { return []; }
26
+ if (!raw) return [];
27
+ const events = [];
28
+ for (const line of raw.split("\n")) {
29
+ const t = line.trim();
30
+ if (!t) continue;
31
+ try { events.push(JSON.parse(t)); } catch { break; } // 半行丢弃, 后面的已不可信
32
+ }
33
+ return events;
34
+ }
35
+
36
+ // 清空 WAL (compact 成功后调用; 与 append 同一锁内执行, 防丢事件)
37
+ export function truncateWal(walFile) {
38
+ try { fs.rmSync(walFile, { force: true }); } catch (e) { debug(`[utils/wal] 已忽略异常: ${e && e.message ? e.message : e}`); }
39
+ }
@@ -1,15 +1,16 @@
1
- // src/utils/winutf8.js - Windows 控制台 UTF-8 引导:强制 chcp 65001 + stdout/stderr 用 utf8
2
- // 根治 PowerShell/GBK 控制台把 Node UTF-8 输出解成乱码(鐨尰铏?/鑳藉姏... 那种)
3
- import { execSync } from "node:child_process";
4
-
5
- export function ensureUTF8Console() {
6
- if (process.platform !== "win32") return;
7
- try {
8
- // 改当前控制台活动代码页为 UTF-8(仅当是真实终端时;stdout 非 TTY 时跳过,避免写日志管道报错)
9
- if (process.stdout?.isTTY) {
10
- execSync("chcp 65001", { stdio: "ignore", windowsHide: true });
11
- }
12
- } catch { /* 非交互/无控制台,忽略 */ }
13
- try { process.stdout.setDefaultEncoding?.("utf8"); } catch {}
14
- try { process.stderr.setDefaultEncoding?.("utf8"); } catch {}
15
- }
1
+ // src/utils/winutf8.js - Windows 控制台 UTF-8 引导:强制 chcp 65001 + stdout/stderr 用 utf8
2
+ // 根治 PowerShell/GBK 控制台把 Node UTF-8 输出解成乱码(鐨尰铏?/鑳藉姏... 那种)
3
+ import { execSync } from "node:child_process";
4
+ import { debug } from "../utils/logger.js";
5
+
6
+ export function ensureUTF8Console() {
7
+ if (process.platform !== "win32") return;
8
+ try {
9
+ // 改当前控制台活动代码页为 UTF-8(仅当是真实终端时;stdout 非 TTY 时跳过,避免写日志管道报错)
10
+ if (process.stdout?.isTTY) {
11
+ execSync("chcp 65001", { stdio: "ignore", windowsHide: true });
12
+ }
13
+ } catch { /* 非交互/无控制台,忽略 */ }
14
+ try { process.stdout.setDefaultEncoding?.("utf8"); } catch (e) { debug(`[utils/winutf8] 已忽略异常: ${e && e.message ? e.message : e}`); }
15
+ try { process.stderr.setDefaultEncoding?.("utf8"); } catch (e) { debug(`[utils/winutf8] 已忽略异常: ${e && e.message ? e.message : e}`); }
16
+ }
@@ -0,0 +1,170 @@
1
+ // src/wiki/index.js - 代码库 Wiki 生成器 (2026-10-02 吸收 ZCode Wiki 机制)
2
+ // 对齐 ZCode repo-wiki 三要素:
3
+ // 1. 每个结论绑定源码位置 (file:line)
4
+ // 2. Mermaid 架构/依赖图
5
+ // 3. 敏感文件排除 (文件名含 token/secret/credential/password 等一律不读不引)
6
+ // 零依赖, 复用 repomap 的扫描与符号索引 (defRecords 已含 file/line/sig)。
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { scanRepo, extractSymbols, DEFAULT_IGNORE } from "../repomap/index.js";
10
+ import { debug } from "../utils/logger.js";
11
+
12
+ // 敏感文件名模式 (ZCode Wiki 同款语义): 命中即从 wiki 中排除, 不读不引
13
+ export const SENSITIVE_PATTERNS = [
14
+ /token/i, /secret/i, /credential/i, /password/i, /passwd/i,
15
+ /api[-_]?key/i, /\.pem$/i, /\.key$/i, /private[-_]?key/i,
16
+ ];
17
+
18
+ export function isSensitiveFile(rel) {
19
+ return SENSITIVE_PATTERNS.some((p) => p.test(path.basename(rel)));
20
+ }
21
+
22
+ // 解析一个 js/ts 文件的内部 import 依赖 (相对路径 → 仓库内文件)
23
+ function internalImports(content, rel, root) {
24
+ const out = [];
25
+ const re = /(?:from\s+|require\(\s*|import\s*\(\s*)["']([^"']+)["']/g;
26
+ let m;
27
+ while ((m = re.exec(content))) {
28
+ const spec = m[1];
29
+ if (!spec.startsWith(".")) continue; // 只看相对依赖 (内部模块图)
30
+ try {
31
+ let abs = path.resolve(path.dirname(path.join(root, rel)), spec);
32
+ // 补扩展名 (ESM 无扩展名约定)
33
+ const tries = ["", ".js", ".mjs", ".cjs", ".ts", "/index.js", "/index.ts"];
34
+ const hit = tries.map((t) => abs + t).find((p) => {
35
+ try { return fs.statSync(p).isFile(); } catch { return false; }
36
+ });
37
+ if (!hit) continue;
38
+ const relTo = path.relative(root, hit).split(path.sep).join("/");
39
+ if (relTo && relTo !== rel && !relTo.startsWith("..")) out.push(relTo);
40
+ } catch { /* 解析失败跳过 */ }
41
+ }
42
+ return [...new Set(out)];
43
+ }
44
+
45
+ // 生成 wiki 文本
46
+ export function generateWiki(root, opts = {}) {
47
+ const { topPerFile = 5, maxNodes = 40 } = opts;
48
+ const scan = scanRepo(root, { ignore: [...DEFAULT_IGNORE, "test", "tests", "__tests__"] });
49
+
50
+ // 敏感文件过滤
51
+ const files = scan.files.filter((f) => !isSensitiveFile(f.rel));
52
+ const skippedSensitive = scan.files.length - files.length;
53
+
54
+ // 文件 → 定义列表 (来自 defRecords: name → [{file,line,sig,kind}])
55
+ const defsByFile = new Map(); // rel -> [{name, line, sig, kind}]
56
+ for (const [name, recs] of scan.defRecords) {
57
+ for (const r of recs) {
58
+ if (!defsByFile.has(r.file)) defsByFile.set(r.file, []);
59
+ defsByFile.get(r.file).push({ name, line: r.line, sig: r.sig, kind: r.kind });
60
+ }
61
+ }
62
+
63
+ // 内部 import 边 (只读非敏感的 js/ts 文件)
64
+ const edges = new Set();
65
+ const nodeSet = new Set();
66
+ for (const f of files) {
67
+ if (!/\.(js|mjs|cjs|ts)$/.test(f.ext)) continue;
68
+ let content;
69
+ try { content = fs.readFileSync(path.join(root, f.rel), "utf8"); } catch { continue; }
70
+ for (const to of internalImports(content, f.rel, root)) {
71
+ if (isSensitiveFile(to)) continue; // 敏感文件不进图
72
+ edges.add(`${f.rel} -> ${to}`);
73
+ nodeSet.add(f.rel);
74
+ nodeSet.add(to);
75
+ }
76
+ }
77
+
78
+ // 渲染
79
+ const L = [];
80
+ const short = (p) => p.replace(/^src\//, "s/").replace(/\.(js|mjs|cjs|ts)$/, "").replace(/[^A-Za-z0-9_]/g, "_");
81
+ L.push(`# ${path.basename(path.resolve(root))} · 代码库 Wiki`);
82
+ L.push("");
83
+ L.push(`> 由皮皮虾自动生成 (ZCode repo-wiki 机制对齐)。每个结论绑定源码位置 (file:line); 敏感文件 (token/secret/credential/password 等) 已排除 ${skippedSensitive} 个。`);
84
+ L.push("");
85
+ L.push(`**规模**: 文件 ${files.length} · 定义 ${[...defsByFile.values()].reduce((s, d) => s + d.length, 0)} · 内部依赖边 ${edges.size}`);
86
+ L.push("");
87
+
88
+ // 按目录分组, 每文件列 top 定义 (按引用数排序: 用 refCounts)
89
+ const byDir = new Map();
90
+ for (const f of files) {
91
+ const dir = f.dir;
92
+ if (!byDir.has(dir)) byDir.set(dir, []);
93
+ byDir.get(dir).push(f);
94
+ }
95
+ for (const dir of [...byDir.keys()].sort()) {
96
+ L.push(`## ${dir === "." ? "(根目录)" : dir}`);
97
+ L.push("");
98
+ for (const f of byDir.get(dir).sort((a, b) => a.rel.localeCompare(b.rel))) {
99
+ const defs = (defsByFile.get(f.rel) || [])
100
+ // 噪音过滤 (ZCode wiki 语义: 文档骨架而非全文键值): md 只留一二级标题, json 不进正文
101
+ .filter((d) => {
102
+ if (d.kind === ".json") return false;
103
+ if (d.kind === ".md" && !/^#{1,2} /.test(d.sig || "")) return false;
104
+ return true;
105
+ })
106
+ // 定义类型分级: function/class 是骨架, 裸 const 赋值是噪音 → 降级
107
+ .map((d) => ({ ...d, tier: /^(export\s+)?(async\s+)?(function|class)\b/.test(d.sig || "") ? 0
108
+ : /^(export\s+)?(const|let)\s+\w+\s*=\s*(async\s*)?(\(|function|[A-Za-z])/.test(d.sig || "") ? 1 : 2 }))
109
+ .sort((a, b) => (a.tier - b.tier) || ((scan.refCounts.get(b.name) || 0) - (scan.refCounts.get(a.name) || 0)))
110
+ .slice(0, topPerFile);
111
+ if (!defs.length) continue;
112
+ L.push(`### \`${f.rel}\``);
113
+ L.push("");
114
+ for (const d of defs) {
115
+ L.push(`- \`${d.sig || d.name}\` — ${f.rel}:${d.line}`);
116
+ }
117
+ L.push("");
118
+ }
119
+ }
120
+
121
+ // mermaid 依赖图 (节点数限幅)
122
+ if (nodeSet.size) {
123
+ const nodes = [...nodeSet].slice(0, maxNodes);
124
+ const nset = new Set(nodes);
125
+ L.push(`## 模块依赖图 (top ${nodes.length})`);
126
+ L.push("");
127
+ L.push("```mermaid");
128
+ L.push("graph LR");
129
+ for (const e of edges) {
130
+ const [from, to] = e.split(" -> ");
131
+ if (nset.has(from) && nset.has(to)) {
132
+ L.push(` ${short(from)} --> ${short(to)}`);
133
+ }
134
+ }
135
+ L.push("```");
136
+ L.push("");
137
+ }
138
+
139
+ return {
140
+ text: L.join("\n"),
141
+ stats: {
142
+ files: files.length,
143
+ defs: [...defsByFile.values()].reduce((s, d) => s + d.length, 0),
144
+ edges: edges.size,
145
+ },
146
+ sensitiveSkipped: skippedSensitive,
147
+ // ZCode 语义: 代码变化后 wiki 自动标记陈旧 — stale = 源码里最新的 mtime 晚于 wiki 文件
148
+ stale: null, // 由调用方填充 (需要 wiki 文件路径对比)
149
+ };
150
+ }
151
+
152
+ // 陈旧检测: wikiOutPath 不存在 = 从未生成 (stale); 任一源码文件 mtime 更新 = 陈旧
153
+ // 返回 { stale: boolean, reason }
154
+ export function checkStaleness(wikiOutPath, root) {
155
+ try {
156
+ const wikiMtime = fs.statSync(wikiOutPath).mtimeMs;
157
+ const scan = scanRepo(root, {});
158
+ let newest = 0;
159
+ for (const f of scan.files) {
160
+ try {
161
+ const m = fs.statSync(path.join(root, f.rel)).mtimeMs;
162
+ if (m > newest) newest = m;
163
+ } catch (e) { debug(`[wiki/index] 已忽略异常: ${e && e.message ? e.message : e}`); }
164
+ }
165
+ if (newest > wikiMtime) return { stale: true, reason: "源码在 wiki 生成后有变更" };
166
+ return { stale: false, reason: "wiki 与源码同步" };
167
+ } catch {
168
+ return { stale: true, reason: "wiki 尚未生成" };
169
+ }
170
+ }