pi-skill-evolve 0.0.0-stage → 0.1.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 (157) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +81 -2
  3. package/extensions/skill-evolve/fixtures/selftest/delete-ban/after.md +10 -0
  4. package/extensions/skill-evolve/fixtures/selftest/delete-ban/change.diff +8 -0
  5. package/extensions/skill-evolve/fixtures/selftest/delete-ban/fixture.json +6 -0
  6. package/extensions/skill-evolve/fixtures/selftest/rg-to-find/after.md +9 -0
  7. package/extensions/skill-evolve/fixtures/selftest/rg-to-find/change.diff +8 -0
  8. package/extensions/skill-evolve/fixtures/selftest/rg-to-find/fixture.json +6 -0
  9. package/extensions/skill-evolve/fixtures/selftest/weak-description/after.md +8 -0
  10. package/extensions/skill-evolve/fixtures/selftest/weak-description/change.diff +8 -0
  11. package/extensions/skill-evolve/fixtures/selftest/weak-description/fixture.json +6 -0
  12. package/extensions/skill-evolve/index.ts +300 -0
  13. package/extensions/skill-evolve/lib/apply-gates.ts +204 -0
  14. package/extensions/skill-evolve/lib/apply-targets.ts +90 -0
  15. package/extensions/skill-evolve/lib/apply.ts +141 -0
  16. package/extensions/skill-evolve/lib/attribution.ts +138 -0
  17. package/extensions/skill-evolve/lib/audit.ts +221 -0
  18. package/extensions/skill-evolve/lib/baseline.ts +176 -0
  19. package/extensions/skill-evolve/lib/child-env.ts +10 -0
  20. package/extensions/skill-evolve/lib/classify-action.ts +161 -0
  21. package/extensions/skill-evolve/lib/command.ts +100 -0
  22. package/extensions/skill-evolve/lib/config-command.ts +38 -0
  23. package/extensions/skill-evolve/lib/config.ts +194 -0
  24. package/extensions/skill-evolve/lib/detect.ts +62 -0
  25. package/extensions/skill-evolve/lib/diff-apply.ts +90 -0
  26. package/extensions/skill-evolve/lib/diff-gen.ts +78 -0
  27. package/extensions/skill-evolve/lib/dispatch.ts +300 -0
  28. package/extensions/skill-evolve/lib/evolution-apply.ts +265 -0
  29. package/extensions/skill-evolve/lib/evolution-model.ts +72 -0
  30. package/extensions/skill-evolve/lib/evolution-observe.ts +82 -0
  31. package/extensions/skill-evolve/lib/evolution-views.ts +89 -0
  32. package/extensions/skill-evolve/lib/evolution.ts +289 -0
  33. package/extensions/skill-evolve/lib/flags.ts +44 -0
  34. package/extensions/skill-evolve/lib/gate0.ts +78 -0
  35. package/extensions/skill-evolve/lib/hard-rules.ts +167 -0
  36. package/extensions/skill-evolve/lib/lock.ts +215 -0
  37. package/extensions/skill-evolve/lib/logfile.ts +52 -0
  38. package/extensions/skill-evolve/lib/mechanical-report.ts +95 -0
  39. package/extensions/skill-evolve/lib/mechanical.ts +136 -0
  40. package/extensions/skill-evolve/lib/metrics.ts +172 -0
  41. package/extensions/skill-evolve/lib/model-config.ts +95 -0
  42. package/extensions/skill-evolve/lib/notify.ts +132 -0
  43. package/extensions/skill-evolve/lib/panel.ts +174 -0
  44. package/extensions/skill-evolve/lib/pending.ts +34 -0
  45. package/extensions/skill-evolve/lib/proc.ts +65 -0
  46. package/extensions/skill-evolve/lib/prompt.ts +80 -0
  47. package/extensions/skill-evolve/lib/proposal.ts +227 -0
  48. package/extensions/skill-evolve/lib/prune-plan.ts +62 -0
  49. package/extensions/skill-evolve/lib/queue.ts +136 -0
  50. package/extensions/skill-evolve/lib/replay-fixture.ts +80 -0
  51. package/extensions/skill-evolve/lib/replay.ts +246 -0
  52. package/extensions/skill-evolve/lib/report.ts +181 -0
  53. package/extensions/skill-evolve/lib/review-post.ts +101 -0
  54. package/extensions/skill-evolve/lib/review-prompt.ts +82 -0
  55. package/extensions/skill-evolve/lib/review-record.ts +63 -0
  56. package/extensions/skill-evolve/lib/review-run.ts +131 -0
  57. package/extensions/skill-evolve/lib/review-spawn.ts +83 -0
  58. package/extensions/skill-evolve/lib/review-supervisor.ts +267 -0
  59. package/extensions/skill-evolve/lib/round.ts +116 -0
  60. package/extensions/skill-evolve/lib/run-watch.ts +34 -0
  61. package/extensions/skill-evolve/lib/runs.ts +208 -0
  62. package/extensions/skill-evolve/lib/selftest.ts +201 -0
  63. package/extensions/skill-evolve/lib/session-index.ts +145 -0
  64. package/extensions/skill-evolve/lib/shutdown-dispatch.ts +177 -0
  65. package/extensions/skill-evolve/lib/single-flight.ts +111 -0
  66. package/extensions/skill-evolve/lib/spawn-plan.ts +103 -0
  67. package/extensions/skill-evolve/lib/stale.ts +217 -0
  68. package/extensions/skill-evolve/lib/telemetry.ts +205 -0
  69. package/extensions/skill-evolve/lib/ui-gate.ts +34 -0
  70. package/extensions/skill-evolve/lib/web/actions.ts +299 -0
  71. package/extensions/skill-evolve/lib/web/activity.ts +57 -0
  72. package/extensions/skill-evolve/lib/web/applied.ts +64 -0
  73. package/extensions/skill-evolve/lib/web/approvals.ts +58 -0
  74. package/extensions/skill-evolve/lib/web/audit-log.ts +115 -0
  75. package/extensions/skill-evolve/lib/web/autostart.ts +177 -0
  76. package/extensions/skill-evolve/lib/web/data.ts +298 -0
  77. package/extensions/skill-evolve/lib/web/diff-lines.ts +141 -0
  78. package/extensions/skill-evolve/lib/web/filter.ts +203 -0
  79. package/extensions/skill-evolve/lib/web/http.ts +68 -0
  80. package/extensions/skill-evolve/lib/web/json-read.ts +33 -0
  81. package/extensions/skill-evolve/lib/web/panel-pid.ts +99 -0
  82. package/extensions/skill-evolve/lib/web/render-controls.ts +156 -0
  83. package/extensions/skill-evolve/lib/web/render-rows.ts +252 -0
  84. package/extensions/skill-evolve/lib/web/render-run.ts +188 -0
  85. package/extensions/skill-evolve/lib/web/render-skills.ts +164 -0
  86. package/extensions/skill-evolve/lib/web/render.ts +136 -0
  87. package/extensions/skill-evolve/lib/web/reports.ts +160 -0
  88. package/extensions/skill-evolve/lib/web/run-detail.ts +234 -0
  89. package/extensions/skill-evolve/lib/web/run-path.ts +21 -0
  90. package/extensions/skill-evolve/lib/web/server.ts +268 -0
  91. package/extensions/skill-evolve/lib/web/shell.ts +87 -0
  92. package/extensions/skill-evolve/lib/web/skills.ts +202 -0
  93. package/extensions/skill-evolve/lib/web/style.ts +107 -0
  94. package/extensions/skill-evolve/lib/web/text.ts +52 -0
  95. package/extensions/skill-evolve/lib/web/trash.ts +36 -0
  96. package/extensions/skill-evolve/lib/weekly.ts +285 -0
  97. package/extensions/skill-evolve/prompts//345/244/215/347/233/230.md +172 -0
  98. package/extensions/skill-evolve/scripts/replay.ts +93 -0
  99. package/extensions/skill-evolve/test/apply.test.ts +278 -0
  100. package/extensions/skill-evolve/test/approve.test.ts +248 -0
  101. package/extensions/skill-evolve/test/attribution.test.ts +133 -0
  102. package/extensions/skill-evolve/test/audit.test.ts +151 -0
  103. package/extensions/skill-evolve/test/baseline.test.ts +132 -0
  104. package/extensions/skill-evolve/test/classify-action.test.ts +133 -0
  105. package/extensions/skill-evolve/test/command.test.ts +267 -0
  106. package/extensions/skill-evolve/test/config-command.test.ts +73 -0
  107. package/extensions/skill-evolve/test/config.test.ts +154 -0
  108. package/extensions/skill-evolve/test/detect.test.ts +71 -0
  109. package/extensions/skill-evolve/test/diff-apply.test.ts +46 -0
  110. package/extensions/skill-evolve/test/dispatch-busy.test.ts +223 -0
  111. package/extensions/skill-evolve/test/dispatch-supervisor.test.ts +88 -0
  112. package/extensions/skill-evolve/test/dispatch.test.ts +306 -0
  113. package/extensions/skill-evolve/test/evolution.test.ts +378 -0
  114. package/extensions/skill-evolve/test/fake-review-spawn.ts +60 -0
  115. package/extensions/skill-evolve/test/hard-rules.test.ts +116 -0
  116. package/extensions/skill-evolve/test/lock.test.ts +138 -0
  117. package/extensions/skill-evolve/test/logfile.test.ts +85 -0
  118. package/extensions/skill-evolve/test/mechanical-wiring.test.ts +217 -0
  119. package/extensions/skill-evolve/test/metrics.test.ts +129 -0
  120. package/extensions/skill-evolve/test/model-config.test.ts +84 -0
  121. package/extensions/skill-evolve/test/notify.test.ts +133 -0
  122. package/extensions/skill-evolve/test/observe.test.ts +100 -0
  123. package/extensions/skill-evolve/test/panel-pid.test.ts +84 -0
  124. package/extensions/skill-evolve/test/panel.test.ts +115 -0
  125. package/extensions/skill-evolve/test/proposal.test.ts +159 -0
  126. package/extensions/skill-evolve/test/queue.test.ts +110 -0
  127. package/extensions/skill-evolve/test/reject.test.ts +134 -0
  128. package/extensions/skill-evolve/test/replay.test.ts +111 -0
  129. package/extensions/skill-evolve/test/report.test.ts +66 -0
  130. package/extensions/skill-evolve/test/review-post.test.ts +92 -0
  131. package/extensions/skill-evolve/test/review-prompt.test.ts +105 -0
  132. package/extensions/skill-evolve/test/review-spawn.test.ts +81 -0
  133. package/extensions/skill-evolve/test/review-supervisor.test.ts +243 -0
  134. package/extensions/skill-evolve/test/selftest.test.ts +113 -0
  135. package/extensions/skill-evolve/test/session-index.test.ts +128 -0
  136. package/extensions/skill-evolve/test/single-flight.test.ts +122 -0
  137. package/extensions/skill-evolve/test/stale.test.ts +272 -0
  138. package/extensions/skill-evolve/test/telemetry.test.ts +145 -0
  139. package/extensions/skill-evolve/test/ui-gate.test.ts +77 -0
  140. package/extensions/skill-evolve/test/web-actions.test.ts +262 -0
  141. package/extensions/skill-evolve/test/web-applied.test.ts +100 -0
  142. package/extensions/skill-evolve/test/web-audit.test.ts +87 -0
  143. package/extensions/skill-evolve/test/web-autostart.test.ts +268 -0
  144. package/extensions/skill-evolve/test/web-data.test.ts +420 -0
  145. package/extensions/skill-evolve/test/web-diff-lines.test.ts +155 -0
  146. package/extensions/skill-evolve/test/web-filter.test.ts +212 -0
  147. package/extensions/skill-evolve/test/web-panel-pid.test.ts +97 -0
  148. package/extensions/skill-evolve/test/web-render-run.test.ts +132 -0
  149. package/extensions/skill-evolve/test/web-render-skills.test.ts +134 -0
  150. package/extensions/skill-evolve/test/web-render.test.ts +300 -0
  151. package/extensions/skill-evolve/test/web-run-detail.test.ts +256 -0
  152. package/extensions/skill-evolve/test/web-server-views.test.ts +113 -0
  153. package/extensions/skill-evolve/test/web-server.test.ts +465 -0
  154. package/extensions/skill-evolve/test/web-shell.test.ts +67 -0
  155. package/extensions/skill-evolve/test/web-skills.test.ts +239 -0
  156. package/extensions/skill-evolve/test/weekly.test.ts +123 -0
  157. package/package.json +34 -4
@@ -0,0 +1,239 @@
1
+ /**
2
+ * 技能管理页的数据聚合:曝光按「轮」算、加载按事件算、三个状态标签不合并、窗口只认 7/30/90。
3
+ * 全部喂临时目录(技能目录 + jsonl 遥测 + queue + baseline),不读真仓库。
4
+ */
5
+ import assert from "node:assert/strict";
6
+ import * as fs from "node:fs";
7
+ import * as os from "node:os";
8
+ import * as path from "node:path";
9
+ import test from "node:test";
10
+ import { skillTags } from "../lib/web/render-skills.ts";
11
+ import { DEFAULT_SKILL_DAYS, parseDays, readSkillDetail, readSkills } from "../lib/web/skills.ts";
12
+ import { shardFile } from "../lib/telemetry.ts";
13
+
14
+ const DAY = 24 * 60 * 60 * 1000;
15
+ /** 固定时钟:readEvents 按本地日期倒推分片,所以本地日期往前推才和写盘时的 shardFile 对齐 */
16
+ const NOW = new Date(2026, 10, 8, 12).getTime();
17
+
18
+ interface Fixture {
19
+ cwd: string;
20
+ evolutionDir: string;
21
+ }
22
+
23
+ function workspace(): Fixture {
24
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "skill-evolve-webskills-"));
25
+ const evolutionDir = path.join(cwd, ".pi", "evolution");
26
+ fs.mkdirSync(path.join(evolutionDir, "runs"), { recursive: true });
27
+ return { cwd, evolutionDir };
28
+ }
29
+
30
+ /** 建一个技能目录(`.pi/skills/<名>/SKILL.md`),可选带 `.cursor/skills/<名>/SKILL.md` */
31
+ function makeSkill(fixture: Fixture, name: string, options: { cursorStub?: boolean } = {}): void {
32
+ const skillDir = path.join(fixture.cwd, ".pi", "skills", name);
33
+ fs.mkdirSync(skillDir, { recursive: true });
34
+ fs.writeFileSync(path.join(skillDir, "SKILL.md"), `---\nname: ${name}\n---\n# ${name}\n`, "utf8");
35
+ if (options.cursorStub) {
36
+ const stub = path.join(fixture.cwd, ".cursor", "skills", name);
37
+ fs.mkdirSync(stub, { recursive: true });
38
+ fs.writeFileSync(path.join(stub, "SKILL.md"), "路由 stub\n", "utf8");
39
+ }
40
+ }
41
+
42
+ /** 往某一天的遥测分片追事件(与 telemetry.shardFile 同一口径,测试自己拼文件名就会对不上) */
43
+ function appendEvents(fixture: Fixture, ts: number, events: Record<string, unknown>[]): void {
44
+ const file = shardFile(path.join(fixture.evolutionDir, "telemetry"), ts);
45
+ fs.mkdirSync(path.dirname(file), { recursive: true });
46
+ const text = events.map((event) => JSON.stringify({ ts, iso: new Date(ts).toISOString(), ...event })).join("\n");
47
+ fs.appendFileSync(file, `${text}\n`, "utf8");
48
+ }
49
+
50
+ function writeRun(fixture: Fixture, runId: string, record: Record<string, unknown>): void {
51
+ const runDir = path.join(fixture.evolutionDir, "runs", runId);
52
+ fs.mkdirSync(runDir, { recursive: true });
53
+ fs.writeFileSync(path.join(runDir, "run.json"), JSON.stringify({ runId, ...record }), "utf8");
54
+ }
55
+
56
+ test("曝光按轮算:同一轮里的重复技能只算一次,disabled 不计;加载按事件算,ok:false 不计", () => {
57
+ const fixture = workspace();
58
+ makeSkill(fixture, "demo", { cursorStub: true });
59
+ makeSkill(fixture, "other", { cursorStub: true });
60
+ // 第 1 轮:demo 出现两次(同一轮去重)+ other 一次 + 一个 disabled 的
61
+ appendEvents(fixture, NOW - 2 * DAY, [
62
+ {
63
+ kind: "advertised",
64
+ sessionId: "s1",
65
+ skills: [
66
+ { name: "demo", disabled: false },
67
+ { name: "demo", disabled: false },
68
+ { name: "other", disabled: false },
69
+ { name: "retired", disabled: true },
70
+ ],
71
+ },
72
+ { kind: "loaded", sessionId: "s1", skill: "demo", ok: true, how: "read" },
73
+ { kind: "loaded", sessionId: "s1", skill: "demo", ok: false, how: "read" },
74
+ ]);
75
+ // 第 2 轮:只有 demo
76
+ appendEvents(fixture, NOW - 1 * DAY, [
77
+ { kind: "advertised", sessionId: "s2", skills: [{ name: "demo", disabled: false }] },
78
+ { kind: "loaded", sessionId: "s2", skill: "other", how: "read" },
79
+ ]);
80
+
81
+ const view = readSkills(fixture.evolutionDir, { now: NOW, telemetryOn: true });
82
+ const demo = view.rows.find((row) => row.name === "demo")!;
83
+ assert.equal(demo.exposed, 2, "两轮里出现过 demo,同轮重复不重复计数");
84
+ assert.equal(demo.loaded, 1, "ok:false 的读取不算加载");
85
+ assert.equal(demo.rate, 0.5);
86
+ const other = view.rows.find((row) => row.name === "other")!;
87
+ assert.equal(other.exposed, 1);
88
+ assert.equal(other.loaded, 1);
89
+ assert.equal(other.rate, 1);
90
+ assert.equal(view.events, 5);
91
+ });
92
+
93
+ test("days 窗口:20 天前的事件在 7 天窗口外、在 30 天窗口内;`?days` 只认 7/30/90", () => {
94
+ const fixture = workspace();
95
+ makeSkill(fixture, "demo");
96
+ appendEvents(fixture, NOW - 20 * DAY, [
97
+ { kind: "advertised", sessionId: "old", skills: [{ name: "demo", disabled: false }] },
98
+ { kind: "loaded", sessionId: "old", skill: "demo", ok: true },
99
+ ]);
100
+ const week = readSkills(fixture.evolutionDir, { now: NOW, days: 7 });
101
+ assert.equal(week.rows[0]!.exposed, 0);
102
+ assert.equal(week.rows[0]!.rate, null, "没曝光写 null(页面显示「—」),不能写 0%");
103
+ const month = readSkills(fixture.evolutionDir, { now: NOW, days: 30 });
104
+ assert.equal(month.rows[0]!.exposed, 1);
105
+ assert.equal(month.rows[0]!.loaded, 1);
106
+ assert.equal(month.rows[0]!.rate, 1);
107
+
108
+ assert.equal(parseDays("7"), 7);
109
+ assert.equal(parseDays("90"), 90);
110
+ assert.equal(parseDays("9999"), DEFAULT_SKILL_DAYS, "非法窗口回落 30,不能用 URL 拉全量历史");
111
+ assert.equal(parseDays(null), DEFAULT_SKILL_DAYS);
112
+ assert.equal(parseDays(""), DEFAULT_SKILL_DAYS);
113
+ });
114
+
115
+ test("无遥测:事件数为 0,采集开关原样透传(页面据此区分「没采集」与「没数据」)", () => {
116
+ const fixture = workspace();
117
+ makeSkill(fixture, "demo");
118
+ const off = readSkills(fixture.evolutionDir, { now: NOW, telemetryOn: false });
119
+ assert.equal(off.events, 0);
120
+ assert.equal(off.telemetryOn, false);
121
+ assert.equal(off.days, DEFAULT_SKILL_DAYS);
122
+ assert.equal(off.rows.length, 1);
123
+ assert.equal(off.rows[0]!.rate, null);
124
+ assert.equal(readSkills(fixture.evolutionDir, { now: NOW, telemetryOn: true }).telemetryOn, true);
125
+ });
126
+
127
+ test("三个状态标签不合并:待批提案 / 存量违规 / 缺 .cursor stub 各自独立", () => {
128
+ const fixture = workspace();
129
+ makeSkill(fixture, "demo", { cursorStub: true });
130
+ makeSkill(fixture, "other", { cursorStub: false });
131
+ makeSkill(fixture, "third", { cursorStub: true });
132
+ fs.writeFileSync(
133
+ path.join(fixture.evolutionDir, "queue.json"),
134
+ JSON.stringify([
135
+ { runId: "run-p", level: "approval", skill: "demo", targets: [], createdAt: NOW, status: "pending" },
136
+ // 已处理的不算「有待批提案」
137
+ { runId: "run-q", level: "approval", skill: "third", targets: [], createdAt: NOW, status: "rejected" },
138
+ ]),
139
+ "utf8",
140
+ );
141
+ fs.writeFileSync(
142
+ path.join(fixture.evolutionDir, "baseline.json"),
143
+ JSON.stringify({
144
+ version: 1,
145
+ generatedAt: NOW,
146
+ bootstrap: true,
147
+ violations: [{ ruleId: "rg-over-find", level: "P0", skill: "other", line: 129, quote: "find", fingerprint: "x" }],
148
+ }),
149
+ "utf8",
150
+ );
151
+
152
+ const rows = readSkills(fixture.evolutionDir, { now: NOW });
153
+ const byName = new Map(rows.rows.map((row) => [row.name, row]));
154
+ assert.deepEqual(
155
+ [byName.get("demo")!.hasPendingProposal, byName.get("demo")!.hasBaseline, byName.get("demo")!.missingCursorStub],
156
+ [true, false, false],
157
+ );
158
+ assert.deepEqual(
159
+ [byName.get("other")!.hasPendingProposal, byName.get("other")!.hasBaseline, byName.get("other")!.missingCursorStub],
160
+ [false, true, true],
161
+ "一条技能可以同时违规又缺 stub:两件事的处置动作不同",
162
+ );
163
+ assert.equal(byName.get("third")!.hasPendingProposal, false, "status=rejected 的队列条目不算待批");
164
+ assert.equal(byName.get("third")!.missingCursorStub, false);
165
+
166
+ const tags = (name: string): string => skillTags(byName.get(name)!);
167
+ assert.match(tags("demo"), /有待批提案/);
168
+ assert.equal(tags("demo").includes("存量违规"), false);
169
+ assert.match(tags("other"), /存量违规/);
170
+ assert.match(tags("other"), /缺 \.cursor stub/);
171
+ assert.equal(tags("third"), '<span class="hint">—</span>', "一个标签都没有时给破折号,不留空白");
172
+ });
173
+
174
+ test("缺 .cursor stub 的豁免名单:nested-agents-md / pi-deepseek-search 不报缺", () => {
175
+ const fixture = workspace();
176
+ makeSkill(fixture, "nested-agents-md");
177
+ makeSkill(fixture, "pi-deepseek-search");
178
+ makeSkill(fixture, "feishu-collab");
179
+ const rows = new Map(readSkills(fixture.evolutionDir, { now: NOW }).rows.map((row) => [row.name, row]));
180
+ assert.equal(rows.get("nested-agents-md")!.missingCursorStub, false);
181
+ assert.equal(rows.get("pi-deepseek-search")!.missingCursorStub, false);
182
+ assert.equal(rows.get("feishu-collab")!.missingCursorStub, true);
183
+ });
184
+
185
+ test("最近一次相关 run:取 proposal.skill 命中的最新一条,没有就是 null", () => {
186
+ const fixture = workspace();
187
+ makeSkill(fixture, "demo");
188
+ makeSkill(fixture, "quiet");
189
+ writeRun(fixture, "run-old", {
190
+ status: "applied",
191
+ dispatchedAt: NOW - 10 * DAY,
192
+ proposal: { type: "improve", skill: "demo", status: "ok", added: 1, removed: 1, file: "02-proposal.diff" },
193
+ });
194
+ writeRun(fixture, "run-new", {
195
+ status: "rejected",
196
+ dispatchedAt: NOW - 1 * DAY,
197
+ proposal: { type: "improve", skill: "demo", status: "ok", added: 2, removed: 0, file: "02-proposal.diff" },
198
+ });
199
+ const rows = new Map(readSkills(fixture.evolutionDir, { now: NOW }).rows.map((row) => [row.name, row]));
200
+ assert.equal(rows.get("demo")!.lastRunId, "run-new");
201
+ assert.equal(rows.get("demo")!.lastRunAt, NOW - 1 * DAY);
202
+ assert.equal(rows.get("quiet")!.lastRunId, null);
203
+ });
204
+
205
+ test("技能详情:只收这个技能的历史提案,每个 run 一行,带 verdict 与 action", () => {
206
+ const fixture = workspace();
207
+ makeSkill(fixture, "demo");
208
+ makeSkill(fixture, "other");
209
+ writeRun(fixture, "run-a", {
210
+ status: "rejected",
211
+ dispatchedAt: NOW - 2 * DAY,
212
+ proposal: { type: "improve", skill: "demo", status: "ok", added: 2, removed: 1, file: "02-proposal.diff" },
213
+ review: { status: "rejected", verdict: "revise", action: "forbidden" },
214
+ });
215
+ writeRun(fixture, "run-b", {
216
+ status: "applied",
217
+ dispatchedAt: NOW - 1 * DAY,
218
+ proposal: { type: "create", skill: "demo", status: "ok", added: 9, removed: 0, file: "02-proposal.diff" },
219
+ review: { status: "reviewed", verdict: "pass", action: "auto" },
220
+ });
221
+ writeRun(fixture, "run-c", {
222
+ status: "reviewed",
223
+ dispatchedAt: NOW,
224
+ proposal: { type: "improve", skill: "other", status: "ok", added: 1, removed: 1, file: "02-proposal.diff" },
225
+ });
226
+
227
+ const detail = readSkillDetail(fixture.evolutionDir, "demo", { now: NOW });
228
+ assert.equal(detail.name, "demo");
229
+ assert.equal(detail.row?.name, "demo");
230
+ assert.deepEqual(
231
+ detail.runs.map((run) => `${run.runId}:${run.type}:${run.verdict}/${run.action}`),
232
+ ["run-b:create:pass/auto", "run-a:improve:revise/forbidden"],
233
+ );
234
+ assert.equal(detail.runs[0]!.added, 9);
235
+
236
+ const missing = readSkillDetail(fixture.evolutionDir, "gone", { now: NOW });
237
+ assert.equal(missing.row, null, "目录里没有的技能:统计为空,但仍可看历史提案");
238
+ assert.deepEqual(missing.runs, []);
239
+ });
@@ -0,0 +1,123 @@
1
+ /**
2
+ * M6 周报:术语只许写「加载率」、零加载候选按 4 周判据、样本不足要明说。
3
+ * 用例编号对应设计稿 14.2(指标公式)与 10.4(零加载候选)。
4
+ */
5
+ import assert from "node:assert/strict";
6
+ import test from "node:test";
7
+ import { buildWeeklyReport, descriptionOverlap, type WeeklyInput } from "../lib/weekly.ts";
8
+ import type { TelemetryEvent } from "../lib/telemetry.ts";
9
+
10
+ const DAY = 86_400_000;
11
+ const NOW = Date.UTC(2026, 10, 8, 12); // 2026-W45
12
+
13
+ function advertised(ts: number, names: string[]): TelemetryEvent {
14
+ return {
15
+ kind: "advertised",
16
+ ts,
17
+ iso: new Date(ts).toISOString(),
18
+ skills: names.map((name) => ({ name, filePath: `/x/${name}/SKILL.md`, disabled: false, source: "project" })),
19
+ };
20
+ }
21
+
22
+ function loaded(ts: number, skill: string): TelemetryEvent {
23
+ return { kind: "loaded", ts, iso: new Date(ts).toISOString(), skill };
24
+ }
25
+
26
+ function input(events: TelemetryEvent[], extra: Partial<WeeklyInput> = {}): WeeklyInput {
27
+ return {
28
+ cwd: "/repo",
29
+ now: NOW,
30
+ events,
31
+ runs: [],
32
+ queue: [],
33
+ skills: [{ name: "dead-skill", description: "处理各种事情" }],
34
+ ...extra,
35
+ };
36
+ }
37
+
38
+ /** 连续 4 周、每周都曝光 dead-skill 且从不加载 */
39
+ function fourWeeksNeverLoaded(): TelemetryEvent[] {
40
+ return [0, 7, 14, 21].map((offset) => advertised(NOW - offset * DAY, ["dead-skill"]));
41
+ }
42
+
43
+ test("周报:出现「加载率」,任何位置都不出现「使用率」", () => {
44
+ const text = buildWeeklyReport(input(fourWeeksNeverLoaded()));
45
+ assert.match(text, /加载率/);
46
+ assert.ok(!text.includes("使用率"), "读 ≠ 用:报告里不许出现「使用率」这个词");
47
+ });
48
+
49
+ test("周报:逐周趋势 + 汇总加载率与尝试失败率", () => {
50
+ const events = [...fourWeeksNeverLoaded(), loaded(NOW, "dead-skill"), { kind: "failed", ts: NOW, iso: "", skill: "dead-skill" }];
51
+ const text = buildWeeklyReport(input(events));
52
+ assert.match(text, /2026-W45 曝光 1 · 加载 1 · 加载率 100\.0%/);
53
+ // 4 周 × 1 曝光 = 4,加载 1 次 → 25%
54
+ assert.match(text, /加载率 25\.0%(加载 1 \/ 曝光 4)/);
55
+ assert.match(text, /尝试失败率 50\.0%(失败 1 \/ 尝试 2)/, "分母是「加载 + 尝试失败」");
56
+ });
57
+
58
+ test("周报:连续 4 周零加载 -> 候选,并附引用面清单与描述重叠", () => {
59
+ const text = buildWeeklyReport(
60
+ input(fourWeeksNeverLoaded(), {
61
+ skills: [
62
+ { name: "dead-skill", description: "处理各种事情" },
63
+ { name: "alive-skill", description: "处理各种事情的具体流程" },
64
+ ],
65
+ referenceScan: () => ({ cursorStub: false, agentsIndex: true, mentions: 2 }),
66
+ }),
67
+ );
68
+ assert.match(text, /零加载候选(近 4 周 曝光>0 且 加载=0):/);
69
+ assert.match(text, /· dead-skill {2}\.cursor stub 缺 · AGENTS 索引 有 · 其它引用 2 处 · 描述重叠 alive-skill 0\.\d\d/);
70
+ assert.ok(!/· alive-skill/.test(text), "有加载的不进候选表");
71
+ });
72
+
73
+ test("周报:不足 4 周时写「样本不足」,不列候选", () => {
74
+ const events = [advertised(NOW, ["dead-skill"]), advertised(NOW - 7 * DAY, ["dead-skill"])];
75
+ const text = buildWeeklyReport(input(events));
76
+ assert.match(text, /样本不足:当前只有 2 周遥测,需要连续 4 周/);
77
+ assert.ok(!/· dead-skill {2}\.cursor/.test(text));
78
+ });
79
+
80
+ test("周报:冲突密度把存量与新增分开数", () => {
81
+ const events = [
82
+ ...fourWeeksNeverLoaded(),
83
+ { kind: "conflict", ts: NOW, iso: "", level: "P0", stock: true },
84
+ { kind: "conflict", ts: NOW, iso: "", level: "P1", stock: false },
85
+ ];
86
+ const text = buildWeeklyReport(input(events));
87
+ assert.match(text, /冲突密度:存量 1 条 \/ 新增 1 条(P0 1 · P1 1)/);
88
+ });
89
+
90
+ test("周报:采纳率 / 回滚率 / 归因分布", () => {
91
+ const events = [
92
+ ...fourWeeksNeverLoaded(),
93
+ { kind: "attributed", ts: NOW, iso: "", skill: "dead-skill", category: "G" },
94
+ ];
95
+ const runs = [
96
+ { runId: "r1", status: "applied", dispatchedAt: NOW, sessionId: "s" },
97
+ { runId: "r2", status: "proposed", dispatchedAt: NOW, sessionId: "s" },
98
+ ];
99
+ const text = buildWeeklyReport(input(events, { runs }));
100
+ assert.match(text, /归因分布:A 0 · B 0 · C 0 · D 0 · E 0 · F 0 · G 1/);
101
+ assert.match(text, /采纳率 50\.0%(采纳 1 \/ 提案 2)/);
102
+ assert.match(text, /回滚率 0\.0%(回滚 0 \/ 采纳 1)/);
103
+ });
104
+
105
+ test("周报:没有采纳样本时进化收益写「样本不足」", () => {
106
+ const text = buildWeeklyReport(input(fourWeeksNeverLoaded()));
107
+ assert.match(text, /进化收益(采纳后 7 天加载率 Δ):样本不足/);
108
+ });
109
+
110
+ test("周报:表尾固定句与加载率盲区注解都在", () => {
111
+ const text = buildWeeklyReport(input(fourWeeksNeverLoaded()));
112
+ assert.match(text, /候选表只是提醒,不是判决;删技能要 \/skill-evolve approve <runId> --prune$/);
113
+ assert.match(text, /加载率只反映「技能文件被读取」,读 ≠ 用/);
114
+ });
115
+
116
+ test("descriptionOverlap:中文二元组 + 英文词的 Jaccard,无交集为 0", () => {
117
+ assert.equal(descriptionOverlap("处理各种事情", "处理各种事情"), 1);
118
+ assert.equal(descriptionOverlap("处理各种事情", "完全无关的描述"), 0);
119
+ const partial = descriptionOverlap("处理各种事情", "处理各种事情的具体流程");
120
+ assert.ok(partial > 0 && partial < 1);
121
+ // 英文按词取交集:deploy / to / server 三个词相同 → 3/5
122
+ assert.equal(descriptionOverlap("deploy to test server", "deploy to prod server"), 0.6);
123
+ });
package/package.json CHANGED
@@ -1,6 +1,36 @@
1
1
  {
2
2
  "name": "pi-skill-evolve",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "技能进化:采集会话中的技能曝光/加载,异步复盘、生成改进提案并独立评审,附本机 Web 管理面板",
5
+ "keywords": [
6
+ "pi-package",
7
+ "pi-extension",
8
+ "skills"
9
+ ],
10
+ "license": "MIT",
11
+ "type": "module",
12
+ "pi": {
13
+ "extensions": [
14
+ "./extensions/skill-evolve/index.ts"
15
+ ]
16
+ },
17
+ "peerDependencies": {
18
+ "@earendil-works/pi-coding-agent": "*"
19
+ },
20
+ "engines": {
21
+ "node": ">=22"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/suveng/pi-skill-evolve.git"
26
+ },
27
+ "homepage": "https://github.com/suveng/pi-skill-evolve#readme",
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "files": [
32
+ "extensions",
33
+ "README.md",
34
+ "CHANGELOG.md"
35
+ ]
36
+ }