@bolloon/bolloon-agent 0.1.33 → 0.1.35

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 (80) hide show
  1. package/.auto-evolve-calls +1 -0
  2. package/.last-auto-evolve-baseline +1 -0
  3. package/Bolloon.md +103 -0
  4. package/README.md +7 -2
  5. package/dist/agents/pi-sdk.js +264 -12
  6. package/dist/bollharness-integration/index.js +8 -1
  7. package/dist/bootstrap/bootstrap.js +114 -0
  8. package/dist/bootstrap/context-collector.js +296 -0
  9. package/dist/bootstrap/lifecycle-hooks.js +109 -0
  10. package/dist/bootstrap/project-context.js +151 -0
  11. package/dist/heartbeat/Watchdog.js +9 -1
  12. package/dist/index.js +11 -0
  13. package/dist/llm/pi-ai.js +31 -21
  14. package/dist/network/p2p-direct.js +59 -2
  15. package/dist/pi-ecosystem/index.js +9 -6
  16. package/dist/pi-ecosystem-judgment/adaptive-scan.js +231 -0
  17. package/dist/pi-ecosystem-judgment/causal-judge.js +449 -0
  18. package/dist/pi-ecosystem-judgment/decision.js +5 -2
  19. package/dist/pi-ecosystem-judgment/detect-hook.js +168 -0
  20. package/dist/pi-ecosystem-judgment/distill-prompt.js +226 -0
  21. package/dist/pi-ecosystem-judgment/evolve-judgment.js +170 -0
  22. package/dist/pi-ecosystem-judgment/human-value-pipeline.js +21 -0
  23. package/dist/pi-ecosystem-judgment/human-value-store.js +283 -22
  24. package/dist/pi-ecosystem-judgment/injection-gate.js +166 -0
  25. package/dist/pi-ecosystem-judgment/monitor-gate.js +188 -0
  26. package/dist/security/builtin-guards.js +124 -0
  27. package/dist/security/context-router-tool.js +106 -0
  28. package/dist/security/react-harness.js +143 -0
  29. package/dist/security/tool-gate.js +235 -0
  30. package/dist/social/heartbeat.js +19 -2
  31. package/dist/utils/auto-evolve-policy.js +117 -0
  32. package/dist/utils/clamp.js +7 -0
  33. package/dist/utils/double.js +6 -0
  34. package/dist/web/api-config.html +3 -3
  35. package/dist/web/client.js +1328 -351
  36. package/dist/web/index.html +34 -31
  37. package/dist/web/server.js +1128 -58
  38. package/dist/web/style.css +370 -0
  39. package/lefthook.yml +29 -0
  40. package/package.json +4 -2
  41. package/scripts/auto-evolve-loop.ts +376 -0
  42. package/scripts/auto-evolve-oneshot.sh +155 -0
  43. package/scripts/auto-evolve-snapshot.sh +136 -0
  44. package/scripts/detect-schema-changes.sh +48 -0
  45. package/scripts/diff-reviewer.ts +159 -0
  46. package/scripts/weekly-report.ts +364 -0
  47. package/src/agents/pi-sdk.ts +293 -15
  48. package/src/bollharness-integration/index.ts +8 -32
  49. package/src/bootstrap/bootstrap.ts +132 -0
  50. package/src/bootstrap/context-collector.ts +342 -0
  51. package/src/bootstrap/lifecycle-hooks.ts +176 -0
  52. package/src/bootstrap/project-context.ts +163 -0
  53. package/src/heartbeat/Watchdog.ts +9 -1
  54. package/src/index.ts +11 -0
  55. package/src/llm/pi-ai.ts +33 -22
  56. package/src/network/p2p-direct.ts +59 -3
  57. package/src/security/builtin-guards.ts +162 -0
  58. package/src/security/context-router-tool.ts +122 -0
  59. package/src/security/react-harness.ts +177 -0
  60. package/src/security/tool-gate.ts +294 -0
  61. package/src/social/ant-colony/index.js +19 -0
  62. package/src/social/heartbeat.ts +18 -2
  63. package/src/utils/auto-evolve-policy.ts +138 -0
  64. package/src/utils/clamp.ts +5 -0
  65. package/src/web/api-config.html +3 -3
  66. package/src/web/client.js +1328 -351
  67. package/src/web/index.html +34 -31
  68. package/src/web/server.ts +1179 -53
  69. package/src/web/style.css +370 -0
  70. package/staging/auto-evolve/clean-001/.review-verdict +9 -0
  71. package/staging/auto-evolve/clean-001/clean-001.patch +14 -0
  72. package/staging/auto-evolve/e2e-001/.patch-id +1 -0
  73. package/staging/auto-evolve/e2e-001/.review-verdict +12 -0
  74. package/staging/auto-evolve/e2e-001/e2e-001.patch +11 -0
  75. package/staging/auto-evolve/test-bad/.review-verdict +12 -0
  76. package/staging/auto-evolve/test-bad/test-bad.patch +11 -0
  77. package/src/social/ant-colony/AdaptiveHeartbeat.ts +0 -131
  78. package/src/social/ant-colony/PheromoneEngine.ts +0 -302
  79. package/src/social/ant-colony/index.ts +0 -18
  80. package/src/social/ant-colony/types.ts +0 -94
@@ -0,0 +1,48 @@
1
+ #!/bin/bash
2
+ # detect-schema-changes.sh — 阶段 C 护栏 6
3
+ #
4
+ # 检查 LLM 改的文件里有没有动 schema (interface / type / enum 声明)
5
+ # 有则强制要求 reviewer 双签 (在 .auto-evolve-review-required 文件)
6
+ #
7
+ # 用法 (在 staging 准备好 patch 后, apply 前):
8
+ # bash scripts/detect-schema-changes.sh <patch-id>
9
+
10
+ set -euo pipefail
11
+
12
+ REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
13
+ cd "$REPO_ROOT"
14
+
15
+ patch_id="${1:-}"
16
+ if [ -z "$patch_id" ]; then
17
+ echo "用法: $0 <patch-id>"
18
+ exit 1
19
+ fi
20
+
21
+ staging_dir="staging/auto-evolve/$patch_id"
22
+ if [ ! -d "$staging_dir" ]; then
23
+ echo "❌ staging 不存在: $staging_dir"
24
+ exit 1
25
+ fi
26
+
27
+ flag="$staging_dir/.schema-changed"
28
+ rm -f "$flag"
29
+
30
+ found=0
31
+ for patch in "$staging_dir"/*.patch; do
32
+ [ -e "$patch" ] || continue
33
+ # 检查 patch 里有没有新增/修改 interface、type、enum 声明
34
+ if grep -E '^\+.*(interface |type [A-Z][a-zA-Z0-9_]* =|enum [A-Z][a-zA-Z0-9_]*)' "$patch" > /dev/null 2>&1; then
35
+ found=1
36
+ echo "⚠️ schema 改动检测到: $patch"
37
+ grep -E '^\+.*(interface |type [A-Z][a-zA-Z0-9_]* =|enum [A-Z][a-zA-Z0-9_]*)' "$patch" | head -3
38
+ fi
39
+ done
40
+
41
+ if [ $found -eq 1 ]; then
42
+ touch "$flag"
43
+ echo ""
44
+ echo "🚨 schema 改动标记: $flag"
45
+ echo " 护栏 6 触发: apply 时会强制要求双签 reviewer"
46
+ else
47
+ echo "✅ 无 schema 改动, 走单签"
48
+ fi
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * diff-reviewer.ts — 阶段 C 护栏 4
4
+ *
5
+ * 2nd LLM call 审第 1 个的 diff:
6
+ * - 默认用 Haiku (便宜, ~$0.001/diff)
7
+ * - schema 改动升级 Sonnet (护栏 6 双签)
8
+ *
9
+ * 输入: patch 文件 (git format-patch 风格)
10
+ * 输出: .review-verdict 写到 staging/<patch-id>/, 含 PASS/FAIL + 原因
11
+ *
12
+ * 用法:
13
+ * tsx scripts/diff-reviewer.ts <patch-id> [--model sonnet]
14
+ */
15
+
16
+ import * as fs from 'fs/promises';
17
+ import * as path from 'path';
18
+ // Anthropic SDK 在 review() 里 lazy import — 无 API key 或没装 SDK 都不应炸
19
+
20
+ const STAGING_DIR = 'staging/auto-evolve';
21
+ const ENV_KEY_MODEL = 'AUTO_EVOLVE_REVIEW_MODEL';
22
+
23
+ const REVIEW_PROMPT = `你是一个严格的代码审查员. 审查以下 git diff (LLM 自动生成的源码改动).
24
+
25
+ 只回答 JSON, 字段:
26
+ {
27
+ "verdict": "PASS" | "FAIL",
28
+ "concerns": ["concern 1", "concern 2", ...],
29
+ "suggestions": ["suggestion 1", ...]
30
+ }
31
+
32
+ PASS 条件 (全部满足才 PASS):
33
+ 1. 改动能解决它声称要解决的问题
34
+ 2. 没引入明显 bug (空指针, 内存泄漏, 死循环, race condition)
35
+ 3. 没引入安全漏洞 (injection, XSS, 越权)
36
+ 4. 边界条件考虑 (空数组, undefined, 极值, 并发)
37
+ 5. 改动局限在它声称的范围, 不夹带私货
38
+ 6. 跟现有代码风格一致
39
+
40
+ FAIL 条件 (任一即 FAIL):
41
+ - 满足上面任一 PASS 条件的反例
42
+ - 引入了新依赖 (必须在 concerns 里说)
43
+ - 删除了测试 / 注释掉失败用例
44
+ - 用了 any / unknown / @ts-ignore 偷懒
45
+
46
+ DIFF:
47
+ {{DIFF}}
48
+ `;
49
+
50
+ function degradedGrepReview(patchContent: string): { verdict: string; concerns: string[]; suggestions: string[]; raw: string } {
51
+ console.warn('[diff-reviewer] ⚠️ 降级 grep 检查 (无 API key 或无 SDK)');
52
+ const concerns: string[] = [];
53
+ if (/\+.*\bany\b/.test(patchContent)) concerns.push('使用了 any');
54
+ if (/\+.*\b@ts-ignore\b/.test(patchContent)) concerns.push('使用了 @ts-ignore');
55
+ if (/\+.*\bconsole\.log\b/.test(patchContent)) concerns.push('留了 console.log');
56
+ if (/\-.*test\(/.test(patchContent)) concerns.push('删除了测试');
57
+ return {
58
+ verdict: concerns.length > 0 ? 'FAIL' : 'PASS',
59
+ concerns,
60
+ suggestions: [],
61
+ raw: '(degraded grep mode)',
62
+ };
63
+ }
64
+
65
+ async function review(patchContent: string, model: string): Promise<{ verdict: string; concerns: string[]; suggestions: string[]; raw: string }> {
66
+ // 无 API key → 直接降级, 不 import SDK (避免 no-ANTHROPIC 环境下也炸)
67
+ if (!process.env.ANTHROPIC_API_KEY) {
68
+ return degradedGrepReview(patchContent);
69
+ }
70
+
71
+ // 有 API key → 尝试 lazy import SDK
72
+ let Anthropic: any;
73
+ try {
74
+ ({ Anthropic } = await import('@anthropic-ai/sdk'));
75
+ } catch (err) {
76
+ console.warn('[diff-reviewer] ⚠️ @anthropic-ai/sdk 未装, 降级 grep');
77
+ return degradedGrepReview(patchContent);
78
+ }
79
+
80
+ const client = new Anthropic();
81
+ const prompt = REVIEW_PROMPT.replace('{{DIFF}}', patchContent.slice(0, 20000)); // 限长
82
+ const resp = await client.messages.create({
83
+ model,
84
+ max_tokens: 1024,
85
+ messages: [{ role: 'user', content: prompt }],
86
+ });
87
+ const text = resp.content[0].type === 'text' ? resp.content[0].text : '';
88
+ // 解析 JSON
89
+ const m = /\{[\s\S]*\}/.exec(text);
90
+ if (!m) {
91
+ return { verdict: 'FAIL', concerns: ['LLM 没返回 JSON'], suggestions: [], raw: text };
92
+ }
93
+ try {
94
+ const parsed = JSON.parse(m[0]);
95
+ return {
96
+ verdict: parsed.verdict === 'PASS' ? 'PASS' : 'FAIL',
97
+ concerns: parsed.concerns || [],
98
+ suggestions: parsed.suggestions || [],
99
+ raw: text,
100
+ };
101
+ } catch {
102
+ return { verdict: 'FAIL', concerns: ['JSON 解析失败'], suggestions: [], raw: text };
103
+ }
104
+ }
105
+
106
+ async function main() {
107
+ const args = process.argv.slice(2);
108
+ const patchId = args[0];
109
+ if (!patchId) {
110
+ console.error('用法: tsx diff-reviewer.ts <patch-id> [--model sonnet]');
111
+ process.exit(1);
112
+ }
113
+ const forceModel = args.includes('--model') ? args[args.indexOf('--model') + 1] : null;
114
+
115
+ const stagingDir = path.join(STAGING_DIR, patchId);
116
+ const schemaFlag = path.join(stagingDir, '.schema-changed');
117
+ const isSchemaChange = await fs.access(schemaFlag).then(() => true).catch(() => false);
118
+
119
+ const defaultModel = isSchemaChange ? 'claude-sonnet-4-6' : 'claude-haiku-4-5';
120
+ const model = forceModel || process.env[ENV_KEY_MODEL] || defaultModel;
121
+
122
+ console.log(`[diff-reviewer] patch=${patchId} schema=${isSchemaChange} model=${model}`);
123
+
124
+ // 合并所有 patch
125
+ const files = await fs.readdir(stagingDir);
126
+ const patches = files.filter((f) => f.endsWith('.patch'));
127
+ if (patches.length === 0) {
128
+ console.error('❌ staging 里没有 .patch 文件');
129
+ process.exit(1);
130
+ }
131
+
132
+ let combined = '';
133
+ for (const p of patches) {
134
+ combined += `\n=== ${p} ===\n` + (await fs.readFile(path.join(stagingDir, p), 'utf-8'));
135
+ }
136
+
137
+ const r = await review(combined, model);
138
+
139
+ const verdict = {
140
+ patchId,
141
+ model,
142
+ schemaChange: isSchemaChange,
143
+ verdict: r.verdict,
144
+ concerns: r.concerns,
145
+ suggestions: r.suggestions,
146
+ reviewedAt: new Date().toISOString(),
147
+ };
148
+ await fs.writeFile(path.join(stagingDir, '.review-verdict'), JSON.stringify(verdict, null, 2));
149
+
150
+ console.log(`[diff-reviewer] verdict=${r.verdict} concerns=${r.concerns.length}`);
151
+ if (r.verdict === 'FAIL') {
152
+ for (const c of r.concerns) console.log(` - ${c}`);
153
+ process.exit(2);
154
+ } else {
155
+ for (const s of r.suggestions) console.log(` 💡 ${s}`);
156
+ }
157
+ }
158
+
159
+ main().catch((e) => { console.error(e); process.exit(1); });
@@ -0,0 +1,364 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * weekly-report.ts — Bolloon 每周表现报告 (阶段 B)
4
+ *
5
+ * 输入:
6
+ * ~/.bolloon/human-values/evolution.jsonl (接受/拒绝/回滚事件)
7
+ * ~/.bolloon/human-values/usage.jsonl (judgment 使用记录)
8
+ * ~/.bolloon/human-values/counterfactual-audit.jsonl (反事实审计)
9
+ * ~/.bolloon/human-values/judgments.json (当前 judgment 库)
10
+ * ~/.bolloon/self-improve-audit.log (改源码尝试的审计, 即使被拒)
11
+ *
12
+ * 输出:
13
+ * ~/.bolloon/reports/2026-W24.md (markdown 报告)
14
+ *
15
+ * 设计原则:
16
+ * - 纯本地计算 + 纯函数式分析 (不调 LLM, 避免幻觉)
17
+ * - 周范围默认 ISO 周(周一为起点), 可 --week 2026-W24 指定
18
+ * - 不写 judgments.json, 不动 persona, 仅追加 reports/*.md
19
+ * - 失败静默 + 退出码 != 0 让 cron 知道坏了
20
+ *
21
+ * 用法:
22
+ * tsx scripts/weekly-report.ts # 生成上周
23
+ * tsx scripts/weekly-report.ts --week 2026-W24 # 指定周
24
+ * tsx scripts/weekly-report.ts --week 2026-W24 --dry-run
25
+ */
26
+
27
+ import * as fs from 'fs/promises';
28
+ import * as os from 'os';
29
+ import * as path from 'path';
30
+
31
+ const HOME = () => os.homedir() || process.env.HOME || '/tmp';
32
+ const ROOT = () => HOME() + '/.bolloon';
33
+ const REPORTS_DIR = () => ROOT() + '/reports';
34
+
35
+ const FILES = {
36
+ evolution: () => ROOT() + '/human-values/evolution.jsonl',
37
+ usage: () => ROOT() + '/human-values/usage.jsonl',
38
+ counterfactual: () => ROOT() + '/human-values/counterfactual-audit.jsonl',
39
+ judgments: () => ROOT() + '/human-values/judgments.json',
40
+ selfImproveAudit: () => ROOT() + '/self-improve-audit.log',
41
+ };
42
+
43
+ interface EvolutionEntry {
44
+ ts: string;
45
+ action: 'accept' | 'reject' | 'revert';
46
+ suggestion: { kind: string; judgmentId: string; action: string };
47
+ appliedPatch?: Record<string, unknown>;
48
+ }
49
+ interface UsageEntry {
50
+ ts: string;
51
+ channelId: string | null;
52
+ userInputPreview: string;
53
+ usedIds: string[];
54
+ }
55
+ interface CounterfactualEntry {
56
+ ts: string;
57
+ trigger: { userInput: string; aiReply: string; violatedPrinciples: unknown[] };
58
+ verdict: string;
59
+ recomendaciones?: string[];
60
+ }
61
+
62
+ async function readJsonl<T>(p: string): Promise<T[]> {
63
+ try {
64
+ const content = await fs.readFile(p, 'utf-8');
65
+ return content
66
+ .trim()
67
+ .split('\n')
68
+ .filter(Boolean)
69
+ .map((l) => {
70
+ try {
71
+ return JSON.parse(l) as T;
72
+ } catch {
73
+ return null;
74
+ }
75
+ })
76
+ .filter((e): e is T => Boolean(e));
77
+ } catch {
78
+ return [];
79
+ }
80
+ }
81
+
82
+ async function readJson<T>(p: string): Promise<T | null> {
83
+ try {
84
+ return JSON.parse(await fs.readFile(p, 'utf-8')) as T;
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
89
+
90
+ function isoWeekString(d: Date): string {
91
+ // ISO week: 1 = 包含 1 月 4 日的那周
92
+ const target = new Date(d.valueOf());
93
+ const dayNr = (d.getUTCDay() + 6) % 7; // 周一=0
94
+ target.setUTCDate(target.getUTCDate() - dayNr + 3);
95
+ const firstThursday = new Date(Date.UTC(target.getUTCFullYear(), 0, 4));
96
+ const week =
97
+ 1 +
98
+ Math.round(
99
+ ((target.valueOf() - firstThursday.valueOf()) / 86400000 - 3 + ((firstThursday.getUTCDay() + 6) % 7)) / 7
100
+ );
101
+ return `${target.getUTCFullYear()}-W${String(week).padStart(2, '0')}`;
102
+ }
103
+
104
+ function weekRange(iso: string): { start: Date; end: Date } {
105
+ const m = /^(\d{4})-W(\d{1,2})$/.exec(iso);
106
+ if (!m) throw new Error(`bad ISO week: ${iso}`);
107
+ const year = Number(m[1]);
108
+ const week = Number(m[2]);
109
+ // ISO 周 1 是包含 1 月 4 日的那周
110
+ const jan4 = new Date(Date.UTC(year, 0, 4));
111
+ const jan4Day = (jan4.getUTCDay() + 6) % 7; // 周一=0
112
+ const week1Mon = new Date(jan4);
113
+ week1Mon.setUTCDate(jan4.getUTCDate() - jan4Day);
114
+ const start = new Date(week1Mon);
115
+ start.setUTCDate(week1Mon.getUTCDate() + (week - 1) * 7);
116
+ const end = new Date(start);
117
+ end.setUTCDate(start.getUTCDate() + 7);
118
+ return { start, end };
119
+ }
120
+
121
+ function inWeek(ts: string, start: Date, end: Date): boolean {
122
+ const t = new Date(ts).getTime();
123
+ return t >= start.getTime() && t < end.getTime();
124
+ }
125
+
126
+ function pct(n: number, d: number): string {
127
+ if (d === 0) return 'n/a';
128
+ return `${((n / d) * 100).toFixed(1)}%`;
129
+ }
130
+
131
+ function topN<T>(arr: T[], key: (x: T) => string, n: number): Array<[string, number]> {
132
+ const m = new Map<string, number>();
133
+ for (const x of arr) {
134
+ const k = key(x);
135
+ m.set(k, (m.get(k) || 0) + 1);
136
+ }
137
+ return Array.from(m.entries())
138
+ .sort((a, b) => b[1] - a[1])
139
+ .slice(0, n);
140
+ }
141
+
142
+ interface Report {
143
+ week: string;
144
+ range: { start: string; end: string };
145
+ generatedAt: string;
146
+ summary: {
147
+ totalUsage: number;
148
+ uniqueChannels: number;
149
+ uniqueJudgments: number;
150
+ evolutionEvents: number;
151
+ acceptRate: number;
152
+ rejected: number;
153
+ reverted: number;
154
+ counterfactualScans: number;
155
+ };
156
+ topJudgments: Array<[string, number]>;
157
+ topKinds: Array<[string, number]>;
158
+ policyAudit: {
159
+ selfImproveAttempts: number;
160
+ blockedByPolicy: number;
161
+ note: string;
162
+ };
163
+ openQuestions: string[];
164
+ }
165
+
166
+ async function buildReport(weekIso: string): Promise<Report> {
167
+ const { start, end } = weekRange(weekIso);
168
+
169
+ const [evolution, usage, counterfactual, judgments, audit] = await Promise.all([
170
+ readJsonl<EvolutionEntry>(FILES.evolution()),
171
+ readJsonl<UsageEntry>(FILES.usage()),
172
+ readJsonl<CounterfactualEntry>(FILES.counterfactual()),
173
+ readJson<unknown[]>(FILES.judgments()),
174
+ (async () => {
175
+ try {
176
+ const txt = await fs.readFile(FILES.selfImproveAudit(), 'utf-8');
177
+ return txt.split('\n').filter(Boolean);
178
+ } catch {
179
+ return [];
180
+ }
181
+ })(),
182
+ ]);
183
+
184
+ const evoInWeek = evolution.filter((e) => inWeek(e.ts, start, end));
185
+ const useInWeek = usage.filter((u) => inWeek(u.ts, start, end));
186
+ const cfInWeek = counterfactual.filter((c) => inWeek(c.ts, start, end));
187
+ const auditInWeek = audit.filter((line) => {
188
+ // audit 是日志行, 格式不一定, 简单按日期前缀过滤
189
+ const d = line.match(/^(\d{4}-\d{2}-\d{2})/);
190
+ if (!d) return false;
191
+ const ts = new Date(d[1] + 'T00:00:00Z').getTime();
192
+ return ts >= start.getTime() && ts < end.getTime();
193
+ });
194
+
195
+ const totalUsage = useInWeek.length;
196
+ const channels = new Set(useInWeek.map((u) => u.channelId || 'null'));
197
+ const ids = new Set<string>();
198
+ for (const u of useInWeek) for (const id of u.usedIds) ids.add(id);
199
+
200
+ const accepts = evoInWeek.filter((e) => e.action === 'accept').length;
201
+ const rejects = evoInWeek.filter((e) => e.action === 'reject').length;
202
+ const reverts = evoInWeek.filter((e) => e.action === 'revert').length;
203
+ const acceptRate = accepts + rejects === 0 ? 0 : accepts / (accepts + rejects);
204
+
205
+ // 自改审计 (路径白/黑名单拦截)
206
+ const selfImproveAttempts = auditInWeek.filter((l) => /attempt|尝试/i.test(l)).length;
207
+ const blockedByPolicy = auditInWeek.filter((l) => /block|deny|拒绝|denylist/i.test(l)).length;
208
+
209
+ return {
210
+ week: weekIso,
211
+ range: { start: start.toISOString().slice(0, 10), end: end.toISOString().slice(0, 10) },
212
+ generatedAt: new Date().toISOString(),
213
+ summary: {
214
+ totalUsage,
215
+ uniqueChannels: channels.size,
216
+ uniqueJudgments: ids.size,
217
+ evolutionEvents: evoInWeek.length,
218
+ acceptRate,
219
+ rejected: rejects,
220
+ reverted: reverts,
221
+ counterfactualScans: cfInWeek.length,
222
+ },
223
+ topJudgments: topN(useInWeek.flatMap((u) => u.usedIds.map((id) => ({ id }))), (x) => x.id, 10),
224
+ topKinds: topN(evoInWeek, (e) => e.suggestion.kind, 5),
225
+ policyAudit: {
226
+ selfImproveAttempts,
227
+ blockedByPolicy,
228
+ note: selfImproveAttempts === 0 ? '本周无源码自改尝试(护栏未触发)' : '见 self-improve-audit.log',
229
+ },
230
+ openQuestions: openQuestions(evoInWeek, cfInWeek, useInWeek),
231
+ };
232
+ }
233
+
234
+ function openQuestions(evo: EvolutionEntry[], cf: CounterfactualEntry[], use: UsageEntry[]): string[] {
235
+ const out: string[] = [];
236
+ if (evo.length === 0 && use.length > 0) {
237
+ out.push('本周有使用但无自适应建议 → 可能 judgment 库太稳定, 或扫描器未触发');
238
+ }
239
+ if (use.length === 0) {
240
+ out.push('本周无 judgment 使用记录 → 检查 usage.jsonl 是否在写, 或渠道是否活跃');
241
+ }
242
+ if (cf.length > 0) {
243
+ const conflictCount = cf.filter((c) => /冲突|conflict|不合理/i.test(c.verdict)).length;
244
+ if (conflictCount > 0) {
245
+ out.push(`反事实审计发现 ${conflictCount} 条潜在冲突 → 看 counterfactual-audit.jsonl`);
246
+ }
247
+ }
248
+ const reverts = evo.filter((e) => e.action === 'revert').length;
249
+ if (reverts >= 2) {
250
+ out.push(`本周回滚 ${reverts} 次 → 类 B 建议可能过激, 考虑收紧阈值`);
251
+ }
252
+ return out;
253
+ }
254
+
255
+ function toMarkdown(r: Report, totalJudgments: number): string {
256
+ const lines: string[] = [];
257
+ lines.push(`# 📊 Bolloon 周报 — ${r.week}`);
258
+ lines.push('');
259
+ lines.push(`> 范围: ${r.range.start} → ${r.range.end} · 生成于 ${r.generatedAt}`);
260
+ lines.push('');
261
+ lines.push('## 核心数字');
262
+ lines.push('');
263
+ lines.push('| 指标 | 本周 |');
264
+ lines.push('|------|------|');
265
+ lines.push(`| judgment 调用次数 | ${r.summary.totalUsage} |`);
266
+ lines.push(`| 触达渠道数 | ${r.summary.uniqueChannels} |`);
267
+ lines.push(`| 用到的不同 judgment 数 | ${r.summary.uniqueJudgments} |`);
268
+ lines.push(`| 自适应事件数 | ${r.summary.evolutionEvents} |`);
269
+ lines.push(`| 接受率 | ${pct(r.summary.acceptRate * 100, 100)} (${Math.round(r.summary.acceptRate * 100)}%) |`);
270
+ lines.push(`| 拒绝数 | ${r.summary.rejected} |`);
271
+ lines.push(`| 回滚数 | ${r.summary.reverted} |`);
272
+ lines.push(`| 反事实审计次数 | ${r.summary.counterfactualScans} |`);
273
+ lines.push(`| 当前 judgment 库总条数 | ${totalJudgments} |`);
274
+ lines.push('');
275
+ lines.push('## 最常被引用的 judgment');
276
+ lines.push('');
277
+ if (r.topJudgments.length === 0) {
278
+ lines.push('_(本周无引用)_');
279
+ } else {
280
+ for (const [id, n] of r.topJudgments) {
281
+ lines.push(`- \`${id}\` × ${n}`);
282
+ }
283
+ }
284
+ lines.push('');
285
+ lines.push('## 自适应建议类型分布');
286
+ lines.push('');
287
+ if (r.topKinds.length === 0) {
288
+ lines.push('_(本周无自适应事件)_');
289
+ } else {
290
+ for (const [kind, n] of r.topKinds) {
291
+ lines.push(`- **${kind}** × ${n}`);
292
+ }
293
+ }
294
+ lines.push('');
295
+ lines.push('## 护栏审计');
296
+ lines.push('');
297
+ lines.push(`- 源码自改尝试: **${r.policyAudit.selfImproveAttempts}**`);
298
+ lines.push(`- 被策略拦截: **${r.policyAudit.blockedByPolicy}**`);
299
+ lines.push(`- ${r.policyAudit.note}`);
300
+ lines.push('');
301
+ lines.push('## 关注事项');
302
+ lines.push('');
303
+ if (r.openQuestions.length === 0) {
304
+ lines.push('_(本周一切正常, 无特别关注)_');
305
+ } else {
306
+ for (const q of r.openQuestions) {
307
+ lines.push(`- ${q}`);
308
+ }
309
+ }
310
+ lines.push('');
311
+ lines.push('---');
312
+ lines.push('');
313
+ lines.push('> 本报告由 `scripts/weekly-report.ts` 纯本地生成, 不调 LLM. 数据源见 ~/.bolloon/human-values/');
314
+ lines.push('');
315
+ return lines.join('\n');
316
+ }
317
+
318
+ function parseArgs(argv: string[]): { week?: string; dryRun: boolean } {
319
+ const out: { week?: string; dryRun: boolean } = { dryRun: false };
320
+ for (let i = 0; i < argv.length; i++) {
321
+ if (argv[i] === '--week' && argv[i + 1]) {
322
+ out.week = argv[++i];
323
+ } else if (argv[i] === '--dry-run') {
324
+ out.dryRun = true;
325
+ }
326
+ }
327
+ return out;
328
+ }
329
+
330
+ async function main() {
331
+ const args = parseArgs(process.argv.slice(2));
332
+ // 默认: 上周(更合理 — 周末才回顾)
333
+ const now = new Date();
334
+ const lastWeek = new Date(now);
335
+ lastWeek.setUTCDate(now.getUTCDate() - 7);
336
+ const week = args.week || isoWeekString(lastWeek);
337
+ const { start, end } = weekRange(week);
338
+
339
+ console.log(`[weekly-report] 生成 ${week} (${start.toISOString().slice(0, 10)} → ${end.toISOString().slice(0, 10)})`);
340
+
341
+ const r = await buildReport(week);
342
+ const judgments = (await readJson<unknown[]>(FILES.judgments())) || [];
343
+ const md = toMarkdown(r, judgments.length);
344
+
345
+ const outDir = REPORTS_DIR();
346
+ const outPath = path.join(outDir, `${week}.md`);
347
+
348
+ if (args.dryRun) {
349
+ console.log(`[weekly-report] DRY-RUN, 不会写盘. 输出预览:`);
350
+ console.log('---');
351
+ console.log(md);
352
+ console.log('---');
353
+ return;
354
+ }
355
+
356
+ await fs.mkdir(outDir, { recursive: true });
357
+ await fs.writeFile(outPath, md, 'utf-8');
358
+ console.log(`[weekly-report] ✅ 写入 ${outPath} (${md.length} bytes)`);
359
+ }
360
+
361
+ main().catch((err) => {
362
+ console.error('[weekly-report] ❌ 失败:', err);
363
+ process.exit(1);
364
+ });