@andares/pi-toolkits 0.10.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +88 -2
  2. package/README.zh-CN.md +72 -2
  3. package/package.json +7 -3
  4. package/src/features/ask/index.ts +2 -1
  5. package/src/features/ask/prompt.ts +2 -1
  6. package/src/features/init/analyze.ts +162 -0
  7. package/src/features/init/apply.test.ts +264 -0
  8. package/src/features/init/apply.ts +693 -0
  9. package/src/features/init/audit.test.ts +292 -0
  10. package/src/features/init/audit.ts +586 -0
  11. package/src/features/init/coverage.test.ts +93 -0
  12. package/src/features/init/coverage.ts +163 -0
  13. package/src/features/init/detect.test.ts +237 -0
  14. package/src/features/init/detect.ts +808 -0
  15. package/src/features/init/dispatch.ts +135 -0
  16. package/src/features/init/index.test.ts +331 -0
  17. package/src/features/init/index.ts +569 -0
  18. package/src/features/init/plan.test.ts +261 -0
  19. package/src/features/init/plan.ts +867 -0
  20. package/src/features/init/render.ts +131 -0
  21. package/src/features/init/spec.ts +429 -0
  22. package/src/features/init/templates/agents-skeleton.md +44 -0
  23. package/src/features/init/templates/memory-readme.md +99 -0
  24. package/src/features/init/templates/signpost.md +15 -0
  25. package/src/features/init/templates/state-gitignore +6 -0
  26. package/src/features/init/templates.test.ts +58 -0
  27. package/src/features/init/templates.ts +79 -0
  28. package/src/features/init/test-helpers.ts +89 -0
  29. package/src/features/init/tool.ts +72 -0
  30. package/src/features/init/types.ts +284 -0
  31. package/src/features/init/ui.ts +160 -0
  32. package/src/features/permission/audit.test.ts +10 -10
  33. package/src/features/permission/constants.test.ts +28 -2
  34. package/src/features/permission/constants.ts +28 -3
  35. package/src/features/permission/index.test.ts +76 -33
  36. package/src/features/permission/policy.test.ts +48 -11
  37. package/src/features/permission/policy.ts +16 -1
  38. package/src/features/permission/units.test.ts +25 -9
  39. package/src/features/permission/units.ts +49 -24
  40. package/src/index.ts +3 -0
@@ -0,0 +1,292 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { mkdirSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { audit, countFindings, latestCoverageReport, renderFindings } from "./audit.js";
5
+ import { detect } from "./detect.js";
6
+ import { AUDIT_CODES, REPORTS_DIR } from "./spec.js";
7
+ import { gitProject, installedProject, project, remove } from "./test-helpers.js";
8
+
9
+ function codes(root: string): string[] {
10
+ return audit(detect(root)).map((finding) => finding.code);
11
+ }
12
+
13
+ const CLEAN: Record<string, string> = {
14
+ "AGENTS.md": [
15
+ "# demo — project guide",
16
+ "",
17
+ "> Memory contract (agent-framework v1).",
18
+ "",
19
+ "## Plan and memory index",
20
+ "",
21
+ "| File | Owns |",
22
+ "| --- | --- |",
23
+ "| `specs/PLAN.md` | route |",
24
+ "| `.agents/memory/FINISHED.md` | closed work |",
25
+ "| `.agents/memory/README.md` | contract |",
26
+ "| `.agents/memory/FUTURE.md` | deferred |",
27
+ "| `.agents/memory/ARCHITECTURE.md` | decisions |",
28
+ "",
29
+ ].join("\n"),
30
+ "specs/PLAN.md": "# Plan\n\n## Route\n\n- next\n",
31
+ ".agents/memory/README.md": "# Agent memory framework v1\n",
32
+ ".agents/memory/FINISHED.md":
33
+ "# Finished\n\n## Summary\n\n- init — `.agents/implementations/2026-01-01_init.md`\n",
34
+ ".agents/memory/FUTURE.md": "# Future\n\n- eval idea, 触发条件: a real need appears\n",
35
+ ".agents/memory/ARCHITECTURE.md": "# Architecture\n\n## Layering\n\n- one layer\n",
36
+ ".agents/implementations/2026-01-01_init.md": "# init\n\n完成时间:2026-01-01 00:00:00\n",
37
+ ".agents/framework/state.toml": 'version = 1\ntool = "test"\ncreated = "2026-01-01T00:00:00+08:00"\nupdated = "2026-01-01T00:00:00+08:00"\nprofile = "core"\nmodules = []\nlang = "en"\n',
38
+ };
39
+
40
+ describe("audit", () => {
41
+ it("passes a clean installed project", () => {
42
+ const root = project(CLEAN);
43
+ try {
44
+ const findings = audit(detect(root));
45
+ expect(countFindings(findings).errors).toBe(0);
46
+ expect(countFindings(findings).warnings).toBe(0);
47
+ } finally {
48
+ remove(root);
49
+ }
50
+ });
51
+
52
+ it("says so when the framework is not installed", () => {
53
+ const root = project({ "README.md": "# demo\n" });
54
+ try {
55
+ expect(codes(root)).toContain(AUDIT_CODES.notInstalled);
56
+ } finally {
57
+ remove(root);
58
+ }
59
+ });
60
+
61
+ it("reports bare legacy references", () => {
62
+ const root = project({ ...CLEAN, "src/a.ts": "// see memory/DESIGN.md\n" });
63
+ try {
64
+ expect(codes(root)).toContain(AUDIT_CODES.pathBareRef);
65
+ } finally {
66
+ remove(root);
67
+ }
68
+ });
69
+
70
+ it("reports agent-only directories, retired todos and empty root files", () => {
71
+ const root = project({
72
+ ...CLEAN,
73
+ "memory/PLAN.md": "# old\n",
74
+ "todos/2026-01-01-x.md": "# x\n",
75
+ "crash-dump": "",
76
+ });
77
+ try {
78
+ const all = codes(root);
79
+ expect(all).toContain(AUDIT_CODES.rootAgentDir);
80
+ expect(all).toContain(AUDIT_CODES.todosRemain);
81
+ expect(all).toContain(AUDIT_CODES.rootEmptyFile);
82
+ } finally {
83
+ remove(root);
84
+ }
85
+ });
86
+
87
+ it("reports a missing contract", () => {
88
+ const root = project({
89
+ ...CLEAN,
90
+ ".agents/memory/README.md": "",
91
+ });
92
+ try {
93
+ // An empty file still counts as present; remove it instead.
94
+ remove(join(root, ".agents/memory/README.md"));
95
+ expect(codes(root)).toContain(AUDIT_CODES.contractMissing);
96
+ } finally {
97
+ remove(root);
98
+ }
99
+ });
100
+
101
+ it("reports a missing plan/memory index", () => {
102
+ const root = project({
103
+ ...CLEAN,
104
+ "AGENTS.md": "# demo\n\nNo index here.\n",
105
+ });
106
+ try {
107
+ expect(codes(root)).toContain(AUDIT_CODES.indexMismatch);
108
+ } finally {
109
+ remove(root);
110
+ }
111
+ });
112
+
113
+ it("reports a broken internal link", () => {
114
+ const root = project({
115
+ ...CLEAN,
116
+ "AGENTS.md": `${CLEAN["AGENTS.md"]}\nSee \`specs/missing.md\`.\n`,
117
+ });
118
+ try {
119
+ expect(codes(root)).toContain(AUDIT_CODES.brokenLink);
120
+ } finally {
121
+ remove(root);
122
+ }
123
+ });
124
+
125
+ it("reports duplicated sections and appendices that are not last", () => {
126
+ const root = project({
127
+ ...CLEAN,
128
+ "AGENTS.md": [
129
+ "# demo",
130
+ "",
131
+ "## Plan and memory index",
132
+ "",
133
+ "`specs/PLAN.md` `.agents/memory/FINISHED.md` `.agents/memory/README.md`",
134
+ "",
135
+ "## 6. 架构决定",
136
+ "",
137
+ "- a",
138
+ "",
139
+ "## 附录 A",
140
+ "",
141
+ "- old",
142
+ "",
143
+ "## 2026-10-11 追加",
144
+ "",
145
+ "- later",
146
+ "",
147
+ ].join("\n"),
148
+ ".agents/memory/ARCHITECTURE.md": "# Architecture\n\n## 6. 架构决定\n\n- b\n",
149
+ });
150
+ try {
151
+ const all = codes(root);
152
+ expect(all).toContain(AUDIT_CODES.duplicateFact);
153
+ expect(all).toContain(AUDIT_CODES.appendixOrder);
154
+ } finally {
155
+ remove(root);
156
+ }
157
+ });
158
+
159
+ it("reports AGENTS.md over the hard budget with the largest sections", () => {
160
+ const bulk = Array.from(
161
+ { length: 420 },
162
+ (_, index) => `- 规则 ${index}:这是一段用来撑大文件的中文说明,内容并不重要但字数很多。`,
163
+ ).join("\n");
164
+ const root = project({
165
+ ...CLEAN,
166
+ "AGENTS.md": `# demo\n\n## Plan and memory index\n\n\`specs/PLAN.md\` \`.agents/memory/README.md\`\n\n## 规则\n\n${bulk}\n`,
167
+ });
168
+ try {
169
+ const finding = audit(detect(root)).find(
170
+ (item) => item.code === AUDIT_CODES.budgetAgents,
171
+ );
172
+ expect(finding?.severity).toBe("warning");
173
+ expect(finding?.message).toMatch(/规则 \(/);
174
+ } finally {
175
+ remove(root);
176
+ }
177
+ });
178
+
179
+ it("reports open coverage rows from the newest report", () => {
180
+ const root = project(CLEAN);
181
+ try {
182
+ mkdirSync(join(root, REPORTS_DIR), { recursive: true });
183
+ writeFileSync(
184
+ join(root, REPORTS_DIR, "coverage-2026-01-01.md"),
185
+ ["| Source | Section | Status | Destination | Reason |", "| --- | --- | --- | --- | --- |", "| AGENTS.md | 定位 | OPEN | | |"].join("\n"),
186
+ "utf8",
187
+ );
188
+ expect(latestCoverageReport(root)).toBe(`${REPORTS_DIR}/coverage-2026-01-01.md`);
189
+ expect(codes(root)).toContain(AUDIT_CODES.coverageOpen);
190
+ } finally {
191
+ remove(root);
192
+ }
193
+ });
194
+
195
+ it("reports memory that git ignores", () => {
196
+ const root = gitProject({
197
+ ...CLEAN,
198
+ ".gitignore": ".agents/memory/\n",
199
+ });
200
+ try {
201
+ expect(codes(root)).toContain(AUDIT_CODES.stateUntracked);
202
+ } finally {
203
+ remove(root);
204
+ }
205
+ });
206
+
207
+ it("does not report tracked memory as untracked", () => {
208
+ const root = gitProject(CLEAN);
209
+ try {
210
+ writeFileSync(join(root, ".agents/memory/TEMP.md"), "# new\n", "utf8");
211
+ expect(codes(root)).not.toContain(AUDIT_CODES.stateUntracked);
212
+ } finally {
213
+ remove(root);
214
+ }
215
+ });
216
+
217
+ it("reports scaffold leftovers and renders grouped findings", () => {
218
+ const root = project({
219
+ ...CLEAN,
220
+ "AGENTS.md": `${CLEAN["AGENTS.md"]}\n<!-- TODO(ask-init): fill this -->\n`,
221
+ "src/a.ts": "// memory/X.md\n",
222
+ });
223
+ try {
224
+ const findings = audit(detect(root));
225
+ expect(findings.map((item) => item.code)).toContain(AUDIT_CODES.scaffoldLeftover);
226
+ const text = renderFindings(findings, { root, frameworkVersion: 1 });
227
+ expect(text).toMatch(/errors/);
228
+ expect(text).toContain(AUDIT_CODES.pathBareRef);
229
+ } finally {
230
+ remove(root);
231
+ }
232
+ });
233
+
234
+ it("reports a newer state version as an error", () => {
235
+ const root = installedProject({
236
+ ".agents/framework/state.toml": 'version = 99\ntool = "future"\n',
237
+ });
238
+ try {
239
+ const finding = audit(detect(root)).find(
240
+ (item) => item.code === AUDIT_CODES.stateVersion,
241
+ );
242
+ expect(finding?.severity).toBe("error");
243
+ } finally {
244
+ remove(root);
245
+ }
246
+ });
247
+ });
248
+
249
+
250
+ describe("review regressions", () => {
251
+ it("treats not-yet-committed memory as a warning, not an error", () => {
252
+ const root = gitProject({ "README.md": "# demo\n" });
253
+ try {
254
+ mkdirSync(join(root, ".agents/memory"), { recursive: true });
255
+ writeFileSync(
256
+ join(root, ".agents/memory/FINISHED.md"),
257
+ "# Finished\n\n## Summary\n\n_None._\n",
258
+ "utf8",
259
+ );
260
+ const findings = audit(detect(root));
261
+ const finding = findings.find((item) => item.code === AUDIT_CODES.stateUncommitted);
262
+ expect(finding?.severity).toBe("warning");
263
+ expect(findings.map((item) => item.code)).not.toContain(AUDIT_CODES.stateUntracked);
264
+ } finally {
265
+ remove(root);
266
+ }
267
+ });
268
+
269
+ it("does not demand evidence for a placeholder FINISHED section", () => {
270
+ const root = project({
271
+ ...CLEAN,
272
+ ".agents/memory/FINISHED.md": "# Finished\n\n## Summary\n\n_Nothing closed yet._\n",
273
+ });
274
+ try {
275
+ expect(codes(root)).not.toContain(AUDIT_CODES.finishedNoEvidence);
276
+ } finally {
277
+ remove(root);
278
+ }
279
+ });
280
+
281
+ it("still flags a real finished entry without an evidence pointer", () => {
282
+ const root = project({
283
+ ...CLEAN,
284
+ ".agents/memory/FINISHED.md": "# Finished\n\n## M1\n\n- shipped the importer\n",
285
+ });
286
+ try {
287
+ expect(codes(root)).toContain(AUDIT_CODES.finishedNoEvidence);
288
+ } finally {
289
+ remove(root);
290
+ }
291
+ });
292
+ });