pi-skill-evolve 0.0.0-stage → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +81 -2
- package/extensions/skill-evolve/fixtures/selftest/delete-ban/after.md +10 -0
- package/extensions/skill-evolve/fixtures/selftest/delete-ban/change.diff +8 -0
- package/extensions/skill-evolve/fixtures/selftest/delete-ban/fixture.json +6 -0
- package/extensions/skill-evolve/fixtures/selftest/rg-to-find/after.md +9 -0
- package/extensions/skill-evolve/fixtures/selftest/rg-to-find/change.diff +8 -0
- package/extensions/skill-evolve/fixtures/selftest/rg-to-find/fixture.json +6 -0
- package/extensions/skill-evolve/fixtures/selftest/weak-description/after.md +8 -0
- package/extensions/skill-evolve/fixtures/selftest/weak-description/change.diff +8 -0
- package/extensions/skill-evolve/fixtures/selftest/weak-description/fixture.json +6 -0
- package/extensions/skill-evolve/index.ts +300 -0
- package/extensions/skill-evolve/lib/apply-gates.ts +204 -0
- package/extensions/skill-evolve/lib/apply-targets.ts +90 -0
- package/extensions/skill-evolve/lib/apply.ts +141 -0
- package/extensions/skill-evolve/lib/attribution.ts +138 -0
- package/extensions/skill-evolve/lib/audit.ts +221 -0
- package/extensions/skill-evolve/lib/baseline.ts +176 -0
- package/extensions/skill-evolve/lib/child-env.ts +10 -0
- package/extensions/skill-evolve/lib/classify-action.ts +161 -0
- package/extensions/skill-evolve/lib/command.ts +100 -0
- package/extensions/skill-evolve/lib/config-command.ts +38 -0
- package/extensions/skill-evolve/lib/config.ts +194 -0
- package/extensions/skill-evolve/lib/detect.ts +62 -0
- package/extensions/skill-evolve/lib/diff-apply.ts +90 -0
- package/extensions/skill-evolve/lib/diff-gen.ts +78 -0
- package/extensions/skill-evolve/lib/dispatch.ts +300 -0
- package/extensions/skill-evolve/lib/evolution-apply.ts +265 -0
- package/extensions/skill-evolve/lib/evolution-model.ts +72 -0
- package/extensions/skill-evolve/lib/evolution-observe.ts +82 -0
- package/extensions/skill-evolve/lib/evolution-views.ts +89 -0
- package/extensions/skill-evolve/lib/evolution.ts +289 -0
- package/extensions/skill-evolve/lib/flags.ts +44 -0
- package/extensions/skill-evolve/lib/gate0.ts +78 -0
- package/extensions/skill-evolve/lib/hard-rules.ts +167 -0
- package/extensions/skill-evolve/lib/lock.ts +215 -0
- package/extensions/skill-evolve/lib/logfile.ts +52 -0
- package/extensions/skill-evolve/lib/mechanical-report.ts +95 -0
- package/extensions/skill-evolve/lib/mechanical.ts +136 -0
- package/extensions/skill-evolve/lib/metrics.ts +172 -0
- package/extensions/skill-evolve/lib/model-config.ts +95 -0
- package/extensions/skill-evolve/lib/notify.ts +132 -0
- package/extensions/skill-evolve/lib/panel.ts +174 -0
- package/extensions/skill-evolve/lib/pending.ts +34 -0
- package/extensions/skill-evolve/lib/proc.ts +65 -0
- package/extensions/skill-evolve/lib/prompt.ts +80 -0
- package/extensions/skill-evolve/lib/proposal.ts +227 -0
- package/extensions/skill-evolve/lib/prune-plan.ts +62 -0
- package/extensions/skill-evolve/lib/queue.ts +136 -0
- package/extensions/skill-evolve/lib/replay-fixture.ts +80 -0
- package/extensions/skill-evolve/lib/replay.ts +246 -0
- package/extensions/skill-evolve/lib/report.ts +181 -0
- package/extensions/skill-evolve/lib/review-post.ts +101 -0
- package/extensions/skill-evolve/lib/review-prompt.ts +82 -0
- package/extensions/skill-evolve/lib/review-record.ts +63 -0
- package/extensions/skill-evolve/lib/review-run.ts +131 -0
- package/extensions/skill-evolve/lib/review-spawn.ts +83 -0
- package/extensions/skill-evolve/lib/review-supervisor.ts +267 -0
- package/extensions/skill-evolve/lib/round.ts +116 -0
- package/extensions/skill-evolve/lib/run-watch.ts +34 -0
- package/extensions/skill-evolve/lib/runs.ts +208 -0
- package/extensions/skill-evolve/lib/selftest.ts +201 -0
- package/extensions/skill-evolve/lib/session-index.ts +145 -0
- package/extensions/skill-evolve/lib/shutdown-dispatch.ts +177 -0
- package/extensions/skill-evolve/lib/single-flight.ts +111 -0
- package/extensions/skill-evolve/lib/spawn-plan.ts +103 -0
- package/extensions/skill-evolve/lib/stale.ts +217 -0
- package/extensions/skill-evolve/lib/telemetry.ts +205 -0
- package/extensions/skill-evolve/lib/ui-gate.ts +34 -0
- package/extensions/skill-evolve/lib/web/actions.ts +299 -0
- package/extensions/skill-evolve/lib/web/activity.ts +57 -0
- package/extensions/skill-evolve/lib/web/applied.ts +64 -0
- package/extensions/skill-evolve/lib/web/approvals.ts +58 -0
- package/extensions/skill-evolve/lib/web/audit-log.ts +115 -0
- package/extensions/skill-evolve/lib/web/autostart.ts +177 -0
- package/extensions/skill-evolve/lib/web/data.ts +298 -0
- package/extensions/skill-evolve/lib/web/diff-lines.ts +141 -0
- package/extensions/skill-evolve/lib/web/filter.ts +203 -0
- package/extensions/skill-evolve/lib/web/http.ts +68 -0
- package/extensions/skill-evolve/lib/web/json-read.ts +33 -0
- package/extensions/skill-evolve/lib/web/panel-pid.ts +99 -0
- package/extensions/skill-evolve/lib/web/render-controls.ts +156 -0
- package/extensions/skill-evolve/lib/web/render-rows.ts +252 -0
- package/extensions/skill-evolve/lib/web/render-run.ts +188 -0
- package/extensions/skill-evolve/lib/web/render-skills.ts +164 -0
- package/extensions/skill-evolve/lib/web/render.ts +136 -0
- package/extensions/skill-evolve/lib/web/reports.ts +160 -0
- package/extensions/skill-evolve/lib/web/run-detail.ts +234 -0
- package/extensions/skill-evolve/lib/web/run-path.ts +21 -0
- package/extensions/skill-evolve/lib/web/server.ts +268 -0
- package/extensions/skill-evolve/lib/web/shell.ts +87 -0
- package/extensions/skill-evolve/lib/web/skills.ts +202 -0
- package/extensions/skill-evolve/lib/web/style.ts +107 -0
- package/extensions/skill-evolve/lib/web/text.ts +52 -0
- package/extensions/skill-evolve/lib/web/trash.ts +36 -0
- package/extensions/skill-evolve/lib/weekly.ts +285 -0
- package/extensions/skill-evolve/prompts//345/244/215/347/233/230.md +172 -0
- package/extensions/skill-evolve/scripts/replay.ts +93 -0
- package/extensions/skill-evolve/test/apply.test.ts +278 -0
- package/extensions/skill-evolve/test/approve.test.ts +248 -0
- package/extensions/skill-evolve/test/attribution.test.ts +133 -0
- package/extensions/skill-evolve/test/audit.test.ts +151 -0
- package/extensions/skill-evolve/test/baseline.test.ts +132 -0
- package/extensions/skill-evolve/test/classify-action.test.ts +133 -0
- package/extensions/skill-evolve/test/command.test.ts +267 -0
- package/extensions/skill-evolve/test/config-command.test.ts +73 -0
- package/extensions/skill-evolve/test/config.test.ts +154 -0
- package/extensions/skill-evolve/test/detect.test.ts +71 -0
- package/extensions/skill-evolve/test/diff-apply.test.ts +46 -0
- package/extensions/skill-evolve/test/dispatch-busy.test.ts +223 -0
- package/extensions/skill-evolve/test/dispatch-supervisor.test.ts +88 -0
- package/extensions/skill-evolve/test/dispatch.test.ts +306 -0
- package/extensions/skill-evolve/test/evolution.test.ts +378 -0
- package/extensions/skill-evolve/test/fake-review-spawn.ts +60 -0
- package/extensions/skill-evolve/test/hard-rules.test.ts +123 -0
- package/extensions/skill-evolve/test/lock.test.ts +138 -0
- package/extensions/skill-evolve/test/logfile.test.ts +85 -0
- package/extensions/skill-evolve/test/mechanical-wiring.test.ts +217 -0
- package/extensions/skill-evolve/test/metrics.test.ts +129 -0
- package/extensions/skill-evolve/test/model-config.test.ts +84 -0
- package/extensions/skill-evolve/test/notify.test.ts +133 -0
- package/extensions/skill-evolve/test/observe.test.ts +100 -0
- package/extensions/skill-evolve/test/panel-pid.test.ts +84 -0
- package/extensions/skill-evolve/test/panel.test.ts +115 -0
- package/extensions/skill-evolve/test/proposal.test.ts +159 -0
- package/extensions/skill-evolve/test/queue.test.ts +110 -0
- package/extensions/skill-evolve/test/reject.test.ts +134 -0
- package/extensions/skill-evolve/test/replay.test.ts +111 -0
- package/extensions/skill-evolve/test/report.test.ts +66 -0
- package/extensions/skill-evolve/test/review-post.test.ts +92 -0
- package/extensions/skill-evolve/test/review-prompt.test.ts +105 -0
- package/extensions/skill-evolve/test/review-spawn.test.ts +81 -0
- package/extensions/skill-evolve/test/review-supervisor.test.ts +243 -0
- package/extensions/skill-evolve/test/selftest.test.ts +113 -0
- package/extensions/skill-evolve/test/session-index.test.ts +128 -0
- package/extensions/skill-evolve/test/single-flight.test.ts +122 -0
- package/extensions/skill-evolve/test/stale.test.ts +272 -0
- package/extensions/skill-evolve/test/telemetry.test.ts +145 -0
- package/extensions/skill-evolve/test/ui-gate.test.ts +77 -0
- package/extensions/skill-evolve/test/web-actions.test.ts +262 -0
- package/extensions/skill-evolve/test/web-applied.test.ts +100 -0
- package/extensions/skill-evolve/test/web-audit.test.ts +87 -0
- package/extensions/skill-evolve/test/web-autostart.test.ts +268 -0
- package/extensions/skill-evolve/test/web-data.test.ts +420 -0
- package/extensions/skill-evolve/test/web-diff-lines.test.ts +155 -0
- package/extensions/skill-evolve/test/web-filter.test.ts +212 -0
- package/extensions/skill-evolve/test/web-panel-pid.test.ts +97 -0
- package/extensions/skill-evolve/test/web-render-run.test.ts +132 -0
- package/extensions/skill-evolve/test/web-render-skills.test.ts +134 -0
- package/extensions/skill-evolve/test/web-render.test.ts +300 -0
- package/extensions/skill-evolve/test/web-run-detail.test.ts +256 -0
- package/extensions/skill-evolve/test/web-server-views.test.ts +113 -0
- package/extensions/skill-evolve/test/web-server.test.ts +465 -0
- package/extensions/skill-evolve/test/web-shell.test.ts +67 -0
- package/extensions/skill-evolve/test/web-skills.test.ts +239 -0
- package/extensions/skill-evolve/test/weekly.test.ts +123 -0
- 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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.2",
|
|
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
|
+
}
|