@andares/pi-toolkits 0.9.8 → 0.11.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 (39) hide show
  1. package/README.md +117 -5
  2. package/README.zh-CN.md +102 -5
  3. package/package.json +8 -3
  4. package/src/features/init/analyze.ts +162 -0
  5. package/src/features/init/apply.test.ts +264 -0
  6. package/src/features/init/apply.ts +693 -0
  7. package/src/features/init/audit.test.ts +292 -0
  8. package/src/features/init/audit.ts +586 -0
  9. package/src/features/init/coverage.test.ts +93 -0
  10. package/src/features/init/coverage.ts +163 -0
  11. package/src/features/init/detect.test.ts +237 -0
  12. package/src/features/init/detect.ts +808 -0
  13. package/src/features/init/dispatch.ts +135 -0
  14. package/src/features/init/index.test.ts +331 -0
  15. package/src/features/init/index.ts +569 -0
  16. package/src/features/init/plan.test.ts +261 -0
  17. package/src/features/init/plan.ts +867 -0
  18. package/src/features/init/render.ts +131 -0
  19. package/src/features/init/spec.ts +429 -0
  20. package/src/features/init/templates/agents-skeleton.md +44 -0
  21. package/src/features/init/templates/memory-readme.md +99 -0
  22. package/src/features/init/templates/signpost.md +15 -0
  23. package/src/features/init/templates/state-gitignore +6 -0
  24. package/src/features/init/templates.test.ts +58 -0
  25. package/src/features/init/templates.ts +79 -0
  26. package/src/features/init/test-helpers.ts +89 -0
  27. package/src/features/init/tool.ts +72 -0
  28. package/src/features/init/types.ts +284 -0
  29. package/src/features/init/ui.ts +160 -0
  30. package/src/features/permission/audit.test.ts +159 -0
  31. package/src/features/permission/audit.ts +145 -0
  32. package/src/features/permission/index.test.ts +138 -3
  33. package/src/features/permission/index.ts +108 -12
  34. package/src/features/permission/policy.ts +39 -3
  35. package/src/index.ts +9 -2
  36. package/src/lib/approval-log.test.ts +289 -0
  37. package/src/lib/approval-log.ts +724 -0
  38. package/src/lib/gates.audit.test.ts +343 -0
  39. package/src/lib/gates.ts +291 -10
@@ -0,0 +1,261 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { join } from "node:path";
3
+ import { buildPlan } from "./plan.js";
4
+ import { detect } from "./detect.js";
5
+ import { installedProject, project, remove } from "./test-helpers.js";
6
+
7
+ const AGENTS = [
8
+ "# demo — 项目指南",
9
+ "",
10
+ "> 本文件只保留每次会话都需要的内容。细节记忆按主题放在 `memory/`,用到再读。",
11
+ "",
12
+ "## 1. 定位",
13
+ "",
14
+ "演示项目。",
15
+ "",
16
+ "## 6. 已确定的架构决定",
17
+ "",
18
+ "- 渲染走原生。",
19
+ "",
20
+ "## 8. 必须处理的问题清单",
21
+ "",
22
+ "已迁至 `memory/PLAN.md` §5。",
23
+ "",
24
+ "## 9. 初始化状态",
25
+ "",
26
+ "项目初始化已完成(M0)。记录见 `memory/FINISHED.md`。",
27
+ "",
28
+ "## 附录 A:历史路线",
29
+ "",
30
+ "旧规划原文。",
31
+ "",
32
+ "## 2026-10-11 追加",
33
+ "",
34
+ "后来补的一节。",
35
+ "",
36
+ ].join("\n");
37
+
38
+ const LEGACY = {
39
+ "AGENTS.md": AGENTS,
40
+ "memory/PLAN.md": "# 计划\n\n## 路线\n\n- M1 未完成\n",
41
+ "memory/FINISHED.md": "# 已完成\n\n## 摘要\n\n- M0:见 `implementations/2026-01-01_a.md`\n",
42
+ "memory/ARCHITECTURE.md": "# 架构\n\n## 6. 已确定的架构决定\n\n- 分层。\n",
43
+ "memory/DESIGN.md": "# 视觉\n\n## 令牌\n",
44
+ "implementations/2026-01-01_a.md": "# a\n\n见 `memory/ARCHITECTURE.md`。\n",
45
+ "specs/a.md": "# a\n\n证据:`implementations/2026-01-01_a.md`。\n",
46
+ "specs/editor-presets.md": "# editor presets\n\n## 验收\n\n- [x] done\n",
47
+ "todos/2026-01-02-editor-presets.md": "# 原始需求\n\n把预设做出来。\n",
48
+ "src/a.ts": "// 见 `memory/DESIGN.md`\n",
49
+ };
50
+
51
+ describe("buildPlan", () => {
52
+ it("plans a fresh install without inventing content", () => {
53
+ const root = project({ "README.md": "# demo\n" });
54
+ try {
55
+ const plan = buildPlan(detect(root), { toolVersion: "test", now: new Date("2026-10-26T10:00:00Z") });
56
+ expect(plan.mode).toBe("fresh");
57
+ const created = plan.actions
58
+ .filter((action) => action.kind === "create-file")
59
+ .map((action) => (action.kind === "create-file" ? action.path : ""));
60
+ expect(created).toContain("AGENTS.md");
61
+ expect(created).toContain(".agents/memory/README.md");
62
+ expect(created).toContain(".agents/README.md");
63
+ expect(created).toContain("specs/PLAN.md");
64
+ expect(created).toContain(".agents/memory/ARCHITECTURE.md");
65
+ // Optional modules are not created as empty shells.
66
+ expect(created).not.toContain(".agents/memory/DESIGN.md");
67
+ expect(plan.tasks.map((task) => task.id)).toEqual(["survey", "fill-agents"]);
68
+ expect(plan.actions.some((action) => action.kind === "move-dir")).toBe(false);
69
+ } finally {
70
+ remove(root);
71
+ }
72
+ });
73
+
74
+ it("plans a conservative adoption of a legacy layout", () => {
75
+ const root = project(LEGACY);
76
+ try {
77
+ const facts = detect(root);
78
+ const plan = buildPlan(facts, { toolVersion: "test", now: new Date("2026-10-26T10:00:00Z") });
79
+ expect(plan.mode).toBe("adopt");
80
+ const moves = plan.actions.filter((action) => action.kind === "move-dir");
81
+ expect(moves.map((action) => (action.kind === "move-dir" ? action.from : ""))).toEqual([
82
+ "memory",
83
+ "implementations",
84
+ ]);
85
+ const fileMove = plan.actions.find((action) => action.kind === "move-file");
86
+ expect(fileMove && fileMove.kind === "move-file" ? fileMove.to : "").toBe("specs/PLAN.md");
87
+ const rewrite = plan.actions.find((action) => action.kind === "rewrite-refs");
88
+ expect(rewrite && rewrite.kind === "rewrite-refs" ? rewrite.hits : 0).toBe(facts.refs.hits);
89
+ expect(rewrite && rewrite.kind === "rewrite-refs" ? rewrite.pairs[0] : undefined).toEqual({
90
+ from: "memory/PLAN.md",
91
+ to: "specs/PLAN.md",
92
+ });
93
+ const ids = plan.tasks.map((task) => task.id);
94
+ expect(ids).toContain("survey");
95
+ expect(ids).toContain("edit-agents");
96
+ expect(ids).toContain("triage-todos");
97
+ expect(ids).toContain("coverage");
98
+ const edit = plan.tasks.find((task) => task.id === "edit-agents");
99
+ expect(edit?.detail).toMatch(/tombstone|§8|§9/);
100
+ expect(edit?.detail).toMatch(/appendices|附录/i);
101
+ const triage = plan.tasks.find((task) => task.id === "triage-todos");
102
+ expect(triage?.detail).toMatch(/specs\/editor-presets\.md/);
103
+ // A budget overrun is recorded as debt, not paid silently in this pass.
104
+ expect(ids).not.toContain("budget-debt");
105
+ } finally {
106
+ remove(root);
107
+ }
108
+ });
109
+
110
+ it("records AGENTS.md budget overflow as debt with the largest sections", () => {
111
+ const big = Array.from({ length: 900 }, (_, index) => `- 规则 ${index}:这是一条很长的说明文字,用来把文件撑过硬预算。`).join("\n");
112
+ const root = project({ "AGENTS.md": `# demo\n\n## 规则\n\n${big}\n` });
113
+ try {
114
+ const plan = buildPlan(detect(root), { toolVersion: "test", now: new Date("2026-10-26T10:00:00Z") });
115
+ const debt = plan.tasks.find((task) => task.id === "budget-debt");
116
+ expect(debt).toBeDefined();
117
+ expect(debt?.detail).toMatch(/hard/);
118
+ expect(debt?.detail).toMatch(/§规则/);
119
+ } finally {
120
+ remove(root);
121
+ }
122
+ });
123
+
124
+ it("reports an installed framework as audit-only", () => {
125
+ const root = installedProject();
126
+ try {
127
+ const plan = buildPlan(detect(root), { toolVersion: "test" });
128
+ expect(plan.mode).toBe("audit-only");
129
+ expect(plan.actions).toEqual([]);
130
+ expect(plan.tasks).toEqual([]);
131
+ } finally {
132
+ remove(root);
133
+ }
134
+ });
135
+
136
+ it("refuses a framework newer than the tool", () => {
137
+ const root = installedProject({
138
+ ".agents/framework/state.toml": 'version = 99\ntool = "future"\n',
139
+ });
140
+ try {
141
+ const plan = buildPlan(detect(root), { toolVersion: "test" });
142
+ expect(plan.mode).toBe("unsupported");
143
+ } finally {
144
+ remove(root);
145
+ }
146
+ });
147
+
148
+ it("never auto-merges two copies of the same directory", () => {
149
+ const root = installedProject({
150
+ "memory/PLAN.md": "# 旧计划\n\n见 `memory/PLAN.md`\n",
151
+ "implementations/2026-01-01_old.md": "# old\n",
152
+ });
153
+ try {
154
+ const plan = buildPlan(detect(root), { toolVersion: "test" });
155
+ expect(plan.mode).toBe("reconcile");
156
+ const moves = plan.actions.filter((action) => action.kind === "move-dir");
157
+ expect(moves).toHaveLength(0);
158
+ expect(plan.tasks.map((task) => task.id)).toContain("merge-legacy");
159
+ expect(plan.warnings.join("\n")).toMatch(/both exist/);
160
+ } finally {
161
+ remove(root);
162
+ }
163
+ });
164
+
165
+ it("asks for a CLAUDE.md dedupe when the two entries overlap", () => {
166
+ const shared = [
167
+ "# demo",
168
+ "",
169
+ "## Rules",
170
+ "",
171
+ "- Package manager is pnpm; never run npm install in this repo.",
172
+ "- Tests must stay green before a commit is reported.",
173
+ "- Never invent a fact; read the code instead of guessing.",
174
+ "",
175
+ ].join("\n");
176
+ const root = project({ "AGENTS.md": shared, "CLAUDE.md": shared });
177
+ try {
178
+ const plan = buildPlan(detect(root), { toolVersion: "test" });
179
+ const task = plan.tasks.find((item) => item.id === "dedupe-claude");
180
+ expect(task).toBeDefined();
181
+ expect(task?.detail).toMatch(/duplicates/);
182
+ } finally {
183
+ remove(root);
184
+ }
185
+ });
186
+
187
+ it("adds the deep pass only when asked", () => {
188
+ const root = project(LEGACY);
189
+ try {
190
+ const shallow = buildPlan(detect(root), { toolVersion: "test" });
191
+ const deep = buildPlan(detect(root), { toolVersion: "test", deep: true });
192
+ const shallowEdit = shallow.tasks.find((task) => task.id === "edit-agents")?.detail ?? "";
193
+ const deepEdit = deep.tasks.find((task) => task.id === "edit-agents")?.detail ?? "";
194
+ expect(shallowEdit).not.toMatch(/Deep pass/);
195
+ expect(deepEdit).toMatch(/Deep pass/);
196
+ } finally {
197
+ remove(root);
198
+ }
199
+ });
200
+ });
201
+
202
+
203
+ describe("review regressions", () => {
204
+ it("moves a canonical memory PLAN.md when specs has no plan", () => {
205
+ const root = installedProject({
206
+ ".agents/memory/PLAN.md": "# Plan\n\n## Route\n\n- open\n",
207
+ });
208
+ try {
209
+ remove(join(root, "specs/PLAN.md"));
210
+ const plan = buildPlan(detect(root), { toolVersion: "test" });
211
+ const move = plan.actions.find((action) => action.kind === "move-file");
212
+ expect(move && move.kind === "move-file" ? move.from : "").toBe(
213
+ ".agents/memory/PLAN.md",
214
+ );
215
+ expect(plan.tasks.some((task) => task.id === "edit-plan")).toBe(true);
216
+ } finally {
217
+ remove(root);
218
+ }
219
+ });
220
+
221
+ it("does not ask to rewrite paths that are already canonical", () => {
222
+ const root = project({
223
+ "AGENTS.md": [
224
+ "# demo",
225
+ "",
226
+ "> Memory contract (agent-framework v1).",
227
+ "",
228
+ "## Plan and memory index",
229
+ "",
230
+ "`specs/PLAN.md` `.agents/memory/` `.agents/implementations/`",
231
+ "",
232
+ ].join("\n"),
233
+ });
234
+ try {
235
+ const plan = buildPlan(detect(root), { toolVersion: "test" });
236
+ const edit = plan.tasks.find((task) => task.id === "edit-agents");
237
+ expect(edit?.detail ?? "").not.toMatch(/Update every path/);
238
+ } finally {
239
+ remove(root);
240
+ }
241
+ });
242
+ });
243
+
244
+
245
+ it("assigns a destination to non-plan legacy documents", () => {
246
+ const root = project({
247
+ "AGENTS.md": "# demo\n",
248
+ "TODO.md": "# TODO\n\n- open item\n",
249
+ "docs/adr/0001-storage.md": "# Storage decision\n",
250
+ });
251
+ try {
252
+ const plan = buildPlan(detect(root), { toolVersion: "test" });
253
+ const task = plan.tasks.find((item) => item.id === "merge-legacy-docs");
254
+ expect(task).toBeDefined();
255
+ expect(task?.detail).toContain("TODO.md");
256
+ expect(task?.detail).toContain("docs/adr/0001-storage.md");
257
+ expect(task?.targets).toContain("TODO.md");
258
+ } finally {
259
+ remove(root);
260
+ }
261
+ });