@boyingliu01/xp-gate 0.9.0 → 0.9.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 (75) hide show
  1. package/mock-policy/AGENTS.md +2 -2
  2. package/mutation/AGENTS.md +2 -2
  3. package/package.json +1 -1
  4. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  5. package/plugins/claude-code/skills/admin-template-guidelines/SKILL.md +6 -797
  6. package/plugins/claude-code/skills/admin-template-guidelines/references/rule-1.md +91 -0
  7. package/plugins/claude-code/skills/admin-template-guidelines/references/rule-2.md +155 -0
  8. package/plugins/claude-code/skills/admin-template-guidelines/references/rule-3.md +70 -0
  9. package/plugins/claude-code/skills/admin-template-guidelines/references/rule-4.md +154 -0
  10. package/plugins/claude-code/skills/admin-template-guidelines/references/rule-5.md +126 -0
  11. package/plugins/claude-code/skills/admin-template-guidelines/references/rule-6.md +180 -0
  12. package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
  13. package/plugins/claude-code/skills/delphi-review/SKILL.md +59 -361
  14. package/plugins/claude-code/skills/delphi-review/references/orchestrator-dispatch.md +64 -0
  15. package/plugins/claude-code/skills/delphi-review/references/round-templates.md +67 -0
  16. package/plugins/claude-code/skills/ralph-loop/SKILL.md +87 -429
  17. package/plugins/claude-code/skills/ralph-loop/references/components.md +113 -0
  18. package/plugins/claude-code/skills/ralph-loop/references/workflow-details.md +99 -0
  19. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
  20. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
  21. package/plugins/claude-code/skills/test-specification-alignment/SKILL.md +97 -599
  22. package/plugins/opencode/package.json +1 -1
  23. package/plugins/opencode/skills/admin-template-guidelines/SKILL.md +6 -797
  24. package/plugins/opencode/skills/admin-template-guidelines/references/rule-1.md +91 -0
  25. package/plugins/opencode/skills/admin-template-guidelines/references/rule-2.md +155 -0
  26. package/plugins/opencode/skills/admin-template-guidelines/references/rule-3.md +70 -0
  27. package/plugins/opencode/skills/admin-template-guidelines/references/rule-4.md +154 -0
  28. package/plugins/opencode/skills/admin-template-guidelines/references/rule-5.md +126 -0
  29. package/plugins/opencode/skills/admin-template-guidelines/references/rule-6.md +180 -0
  30. package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
  31. package/plugins/opencode/skills/delphi-review/SKILL.md +59 -361
  32. package/plugins/opencode/skills/delphi-review/references/orchestrator-dispatch.md +64 -0
  33. package/plugins/opencode/skills/delphi-review/references/round-templates.md +67 -0
  34. package/plugins/opencode/skills/ralph-loop/SKILL.md +87 -429
  35. package/plugins/opencode/skills/ralph-loop/references/components.md +113 -0
  36. package/plugins/opencode/skills/ralph-loop/references/workflow-details.md +99 -0
  37. package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
  38. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
  39. package/plugins/opencode/skills/test-specification-alignment/SKILL.md +97 -599
  40. package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
  41. package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
  42. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
  43. package/principles/AGENTS.md +2 -2
  44. package/skills/admin-template-guidelines/SKILL.md +6 -797
  45. package/skills/admin-template-guidelines/references/rule-1.md +91 -0
  46. package/skills/admin-template-guidelines/references/rule-2.md +155 -0
  47. package/skills/admin-template-guidelines/references/rule-3.md +70 -0
  48. package/skills/admin-template-guidelines/references/rule-4.md +154 -0
  49. package/skills/admin-template-guidelines/references/rule-5.md +126 -0
  50. package/skills/admin-template-guidelines/references/rule-6.md +180 -0
  51. package/skills/delphi-review/AGENTS.md +2 -2
  52. package/skills/delphi-review/SKILL.md +59 -361
  53. package/skills/delphi-review/references/orchestrator-dispatch.md +64 -0
  54. package/skills/delphi-review/references/round-templates.md +67 -0
  55. package/skills/ralph-loop/SKILL.md +87 -429
  56. package/skills/ralph-loop/references/components.md +113 -0
  57. package/skills/ralph-loop/references/workflow-details.md +99 -0
  58. package/skills/sprint-flow/AGENTS.md +2 -2
  59. package/skills/test-specification-alignment/AGENTS.md +2 -2
  60. package/skills/test-specification-alignment/SKILL.md +97 -599
  61. package/plugins/claude-code/skills/ralph-loop/references/components/memory.md +0 -55
  62. package/plugins/claude-code/skills/ralph-loop/references/components/middleware.md +0 -54
  63. package/plugins/claude-code/skills/ralph-loop/references/components/skill-invocations.md +0 -39
  64. package/plugins/claude-code/skills/ralph-loop/references/components/system-prompt.md +0 -24
  65. package/plugins/claude-code/skills/ralph-loop/references/components/tool-descriptions.md +0 -32
  66. package/plugins/opencode/skills/ralph-loop/references/components/memory.md +0 -55
  67. package/plugins/opencode/skills/ralph-loop/references/components/middleware.md +0 -54
  68. package/plugins/opencode/skills/ralph-loop/references/components/skill-invocations.md +0 -39
  69. package/plugins/opencode/skills/ralph-loop/references/components/system-prompt.md +0 -24
  70. package/plugins/opencode/skills/ralph-loop/references/components/tool-descriptions.md +0 -32
  71. package/skills/ralph-loop/references/components/memory.md +0 -55
  72. package/skills/ralph-loop/references/components/middleware.md +0 -54
  73. package/skills/ralph-loop/references/components/skill-invocations.md +0 -39
  74. package/skills/ralph-loop/references/components/system-prompt.md +0 -24
  75. package/skills/ralph-loop/references/components/tool-descriptions.md +0 -32
@@ -36,95 +36,7 @@ maturity: alpha
36
36
  | `admin/routes/tree.ts` | 技能树/目录管理 |
37
37
  | `admin/routes/admin-shared.ts` | 共享中间件、通用 helper、布局渲染 |
38
38
 
39
- ### 错误示例:单文件膨胀(800+ 行)
40
-
41
- ```typescript
42
- // admin/routes/index.ts — 反模式
43
- import { FastifyInstance } from 'fastify';
44
-
45
- export async function adminRoutes(fastify: FastifyInstance) {
46
- // 模板列表 — 第 10 行
47
- fastify.get('/admin/templates', async (req, reply) => { ... });
48
- // 模板创建 — 第 45 行
49
- fastify.post('/admin/templates', async (req, reply) => { ... });
50
- // 模板编辑 — 第 80 行
51
- fastify.get('/admin/templates/:id/edit', async (req, reply) => { ... });
52
- // 模板删除 — 第 115 行
53
- fastify.delete('/admin/templates/:id', async (req, reply) => { ... });
54
- // 计划列表 — 第 150 行
55
- fastify.get('/admin/plans', async (req, reply) => { ... });
56
- // 计划创建 — 第 190 行
57
- fastify.post('/admin/plans', async (req, reply) => { ... });
58
- // ... 持续膨胀到 800+ 行 ...
59
- }
60
- ```
61
-
62
- ### ✅ 正确示例:按模块拆分
63
-
64
- ```typescript
65
- // admin/routes/admin-shared.ts
66
- import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
67
-
68
- export function registerAdminShared(fastify: FastifyInstance) {
69
- // 共享中间件:认证
70
- fastify.addHook('onRequest', authenticateAdmin);
71
-
72
- // 共享 helper:渲染带布局的页面
73
- fastify.decorate('renderAdmin', async function (
74
- this: FastifyInstance,
75
- req: FastifyRequest,
76
- reply: FastifyReply,
77
- template: string,
78
- data: Record<string, unknown>
79
- ) {
80
- return reply.view(`admin/layouts/main.njk`, {
81
- ...data,
82
- _content: await reply.view(`admin/${template}.njk`, data),
83
- });
84
- });
85
- }
86
-
87
- // admin/routes/templates.ts
88
- import { FastifyInstance } from 'fastify';
89
-
90
- export async function registerTemplateRoutes(fastify: FastifyInstance) {
91
- fastify.get('/admin/templates', { onRequest: [authenticateAdmin] }, listTemplates);
92
- fastify.get('/admin/templates/new', { onRequest: [authenticateAdmin] }, showNewTemplate);
93
- fastify.post('/admin/templates', { onRequest: [authenticateAdmin] }, createTemplate);
94
- fastify.get('/admin/templates/:id/edit', { onRequest: [authenticateAdmin] }, showEditTemplate);
95
- fastify.put('/admin/templates/:id', { onRequest: [authenticateAdmin] }, updateTemplate);
96
- fastify.delete('/admin/templates/:id', { onRequest: [authenticateAdmin] }, deleteTemplate);
97
- }
98
-
99
- // admin/routes/plans.ts
100
- import { FastifyInstance } from 'fastify';
101
-
102
- export async function registerPlanRoutes(fastify: FastifyInstance) {
103
- fastify.get('/admin/plans', { onRequest: [authenticateAdmin] }, listPlans);
104
- fastify.post('/admin/plans', { onRequest: [authenticateAdmin] }, createPlan);
105
- fastify.post('/admin/plans/:id/start', { onRequest: [authenticateAdmin] }, startPlan);
106
- fastify.post('/admin/plans/:id/pause', { onRequest: [authenticateAdmin] }, pausePlan);
107
- fastify.post('/admin/plans/:id/complete', { onRequest: [authenticateAdmin] }, completePlan);
108
- }
109
-
110
- // admin/routes/index.ts — 仅负责聚合注册
111
- import { FastifyInstance } from 'fastify';
112
- import { registerAdminShared } from './admin-shared';
113
- import { registerTemplateRoutes } from './templates';
114
- import { registerPlanRoutes } from './plans';
115
- import { registerReportRoutes } from './reports';
116
- import { registerAnalyticsRoutes } from './analytics';
117
- import { registerTreeRoutes } from './tree';
118
-
119
- export async function registerAllAdminRoutes(fastify: FastifyInstance) {
120
- registerAdminShared(fastify);
121
- await registerTemplateRoutes(fastify);
122
- await registerPlanRoutes(fastify);
123
- await registerReportRoutes(fastify);
124
- await registerAnalyticsRoutes(fastify);
125
- await registerTreeRoutes(fastify);
126
- }
127
- ```
39
+ **Examples:** see `references/rule-1.md`
128
40
 
129
41
  ---
130
42
 
@@ -132,159 +44,7 @@ export async function registerAllAdminRoutes(fastify: FastifyInstance) {
132
44
 
133
45
  **Constraint:** All admin route tests MUST use a shared `createAdminTestApp()` helper — never hand-roll mini Fastify instances per test file.
134
46
 
135
- ### 错误示例:每个测试文件自建实例
136
-
137
- ```typescript
138
- // test/admin/templates.test.ts — 反模式
139
- import Fastify from 'fastify';
140
- import { registerTemplateRoutes } from '../../admin/routes/templates';
141
-
142
- describe('Template Routes', () => {
143
- let fastify: any;
144
-
145
- beforeEach(async () => {
146
- fastify = Fastify();
147
- await fastify.register(prismaPlugin, { datasource: {} }); // 每次自建
148
- await fastify.register(registerTemplateRoutes);
149
- await fastify.ready();
150
- });
151
-
152
- afterEach(async () => {
153
- await fastify.close(); // 每个文件重复 teardown 逻辑
154
- });
155
-
156
- it('should list templates', async () => {
157
- const res = await fastify.inject({ method: 'GET', url: '/admin/templates' });
158
- expect(res.statusCode).toBe(200);
159
- });
160
- });
161
- ```
162
-
163
- ```typescript
164
- // test/admin/plans.test.ts — 同样重复,但细节不同
165
- import Fastify from 'fastify';
166
- import { registerPlanRoutes } from '../../admin/routes/plans';
167
-
168
- describe('Plan Routes', () => {
169
- let app: any;
170
-
171
- beforeAll(async () => {
172
- app = Fastify({ logger: false }); // 不一致的配置
173
- await app.register(prismaPlugin, { datasource: {}, skipMigrate: true });
174
- await app.register(registerPlanRoutes);
175
- await app.listen({ port: 0 });
176
- });
177
-
178
- afterAll(async () => {
179
- await app.server.close(); // 与 templates.test.ts teardown 方式不同
180
- });
181
- });
182
- ```
183
-
184
- ### ✅ 正确示例:共享 Helper
185
-
186
- ```typescript
187
- // test/helpers/create-admin-test-app.ts
188
- import Fastify, { FastifyInstance } from 'fastify';
189
- import { prisma } from '@prisma/client';
190
- import { registerAllAdminRoutes } from '../../admin/routes';
191
-
192
- export interface AdminTestApp {
193
- fastify: FastifyInstance;
194
- inject: FastifyInstance['inject'];
195
- close: () => Promise<void>;
196
- }
197
-
198
- export async function createAdminTestApp(): Promise<AdminTestApp> {
199
- const fastify = Fastify({
200
- logger: false,
201
- trustProxy: true,
202
- });
203
-
204
- // 注册 Prisma 插件
205
- await fastify.register(async (instance) => {
206
- instance.decorate('prisma', prisma);
207
- });
208
-
209
- // 注册所有 Admin 路由
210
- await registerAllAdminRoutes(fastify);
211
-
212
- await fastify.ready();
213
-
214
- return {
215
- fastify,
216
- inject: fastify.inject.bind(fastify),
217
- close: async () => {
218
- await fastify.close();
219
- },
220
- };
221
- }
222
- ```
223
-
224
- ```typescript
225
- // test/admin/templates.test.ts
226
- import { createAdminTestApp, AdminTestApp } from '../helpers/create-admin-test-app';
227
-
228
- describe('Template Routes', () => {
229
- let app: AdminTestApp;
230
-
231
- beforeEach(async () => {
232
- app = await createAdminTestApp();
233
- });
234
-
235
- afterEach(async () => {
236
- await app.close();
237
- });
238
-
239
- it('should list templates', async () => {
240
- const res = await app.inject({
241
- method: 'GET',
242
- url: '/admin/templates',
243
- });
244
- expect(res.statusCode).toBe(200);
245
- const body = JSON.parse(res.body);
246
- expect(Array.isArray(body.templates)).toBe(true);
247
- });
248
-
249
- it('should create a template', async () => {
250
- const res = await app.inject({
251
- method: 'POST',
252
- url: '/admin/templates',
253
- payload: {
254
- name: 'New Template',
255
- content: '<h1>Hello</h1>',
256
- },
257
- });
258
- expect(res.statusCode).toBe(302);
259
- expect(res.headers.location).toContain('/admin/templates');
260
- });
261
- });
262
- ```
263
-
264
- ```typescript
265
- // test/admin/plans.test.ts — 使用同一个 helper,零重复
266
- import { createAdminTestApp, AdminTestApp } from '../helpers/create-admin-test-app';
267
-
268
- describe('Plan Routes', () => {
269
- let app: AdminTestApp;
270
-
271
- beforeEach(async () => {
272
- app = await createAdminTestApp();
273
- });
274
-
275
- afterEach(async () => {
276
- await app.close();
277
- });
278
-
279
- it('should list plans', async () => {
280
- const res = await app.inject({
281
- method: 'GET',
282
- url: '/admin/plans',
283
- });
284
- expect(res.statusCode).toBe(200);
285
- });
286
- });
287
- ```
47
+ **Examples:** see `references/rule-2.md`
288
48
 
289
49
  ---
290
50
 
@@ -292,74 +52,7 @@ describe('Plan Routes', () => {
292
52
 
293
53
  **Constraint:** Comparison expressions in Nunjucks MUST be wrapped in parentheses when used with filters. The Nunjucks filter operator (`|`) has **higher precedence** than comparison operators, causing silent incorrect output.
294
54
 
295
- ### 优先级规则
296
-
297
- ```
298
- 表达式: A <= B | lower
299
- 实际解析: A <= (B | lower) ← filter 先执行,比较的是 A 和 "lower" 后的 B
300
- 预期语义: (A <= B) | lower ← 先比较,结果再经 filter
301
- ```
302
-
303
- ### ❌ 错误示例
304
-
305
- ```nunjucks
306
- {# admin/views/plans.njk — 反模式 #}
307
-
308
- {# 比较在 filter 之后执行 → 逻辑错误 #}
309
- {% if plan._interviews.length <= 15 | lower %}
310
- <span class="badge badge-small">Small Plan</span>
311
- {% endif %}
312
-
313
- {# 实际等价于: plan._interviews.length <= "lower" → 始终 true #}
314
-
315
- {# 条件渲染中的陷阱 #}
316
- {% if plan.status == 'completed' | title %}
317
- <span class="status-done">Done</span>
318
- {% endif %}
319
-
320
- {# 字符串格式化错误 #}
321
- {{ plan.completionRate * 100 | round | default(0) }}
322
- {# 当 completionRate 为 null 时,null * 100 = NaN → round(NaN) → 错误 #}
323
- ```
324
-
325
- ### ✅ 正确示例
326
-
327
- ```nunjucks
328
- {# admin/views/plans.njk #}
329
-
330
- {# 括号确保比较先执行 #}
331
- {% if (plan._interviews.length <= 15) | lower %}
332
- <span class="badge badge-small">Small Plan</span>
333
- {% endif %}
334
-
335
- {# 条件判断中的正确使用 #}
336
- {% if (plan.status == 'completed') | title %}
337
- <span class="status-done">Done</span>
338
- {% endif %}
339
-
340
- {# 数值计算中先处理 null #}
341
- {{ (plan.completionRate ?? 0) * 100 | round(1) | default('0.0') }}%
342
-
343
- {# 多层比较与 filter 组合 #}
344
- {% if (plan._interviews.length >= 15 and plan._interviews.length <= 60) | bool %}
345
- <span class="badge badge-medium">Medium Plan</span>
346
- {% elif (plan._interviews.length > 60) | bool %}
347
- <span class="badge badge-large">Large Plan</span>
348
- {% endif %}
349
-
350
- {# 安全渲染可选字段 #}
351
- {% if (plan.description | length) > 0 %}
352
- <p class="description">{{ plan.description | truncate(120) }}</p>
353
- {% else %}
354
- <p class="description text-muted">No description provided.</p>
355
- {% endif %}
356
- ```
357
-
358
- ### 验证清单
359
-
360
- - [ ] 所有 `{{ A <= B | filter }}` 改为 `{{ (A <= B) | filter }}`
361
- - [ ] 所有 `{% if A == B | filter %}` 改为 `{% if (A == B) | filter %}`
362
- - [ ] 使用 `| bool` 而非裸布尔表达式配合逻辑运算符
55
+ **Examples & Checklist:** → see `references/rule-3.md`
363
56
 
364
57
  ---
365
58
 
@@ -376,158 +69,7 @@ describe('Plan Routes', () => {
376
69
  | 完成率计算 | plans.ts, analytics.ts, tree.ts | `calculateCompletionRate()` |
377
70
  | 完整 ViewModel 转换 | 所有列表页路由 | `mapTemplateToViewModel()` |
378
71
 
379
- ### 错误示例:路由内联转换
380
-
381
- ```typescript
382
- // admin/routes/plans.ts — 反模式
383
- fastify.get('/admin/plans', async (req, reply) => {
384
- const plans = await prisma.plan.findMany({
385
- include: { _count: { select: { interviews: true } } },
386
- });
387
-
388
- // 每个路由都在做同样的转换
389
- const viewModels = plans.map((plan) => ({
390
- id: plan.id,
391
- name: plan.name,
392
- status: plan.status,
393
- interviewCount: plan._count?.interviews ?? 0,
394
- completedCount: plan.interviews?.filter((i) => i.status === 'COMPLETED').length ?? 0,
395
- completionRate: plan.interviews?.filter((i) => i.status === 'COMPLETED').length ?? 0
396
- / (plan._count?.interviews ?? 1) * 100,
397
- createdAt: plan.createdAt.toISOString().split('T')[0],
398
- }));
399
-
400
- return reply.view('admin/plans/list.njk', { plans: viewModels });
401
- });
402
- ```
403
-
404
- ### ✅ 正确示例:提取 ViewModel 模块
405
-
406
- ```typescript
407
- // admin/view-models.ts
408
- import { Plan, Interview, Template } from '@prisma/client';
409
-
410
- export interface PlanViewModel {
411
- id: string;
412
- name: string;
413
- status: string;
414
- interviewCount: number;
415
- completedCount: number;
416
- completionRate: number;
417
- createdAt: string;
418
- }
419
-
420
- export interface TemplateViewModel {
421
- id: string;
422
- name: string;
423
- content: string;
424
- interviewCount: number;
425
- lastUsed: string | null;
426
- }
427
-
428
- /**
429
- * 将 Prisma Plan 记录转换为视图模型
430
- */
431
- export function mapPlanToViewModel(
432
- plan: Plan & { _count?: { interviews: number } },
433
- interviews?: Interview[]
434
- ): PlanViewModel {
435
- const completedCount = interviews?.filter((i) => i.status === 'COMPLETED').length ?? 0;
436
- const interviewCount = plan._count?.interviews ?? interviews?.length ?? 0;
437
-
438
- return {
439
- id: plan.id,
440
- name: plan.name,
441
- status: plan.status,
442
- interviewCount,
443
- completedCount,
444
- completionRate: interviewCount > 0 ? (completedCount / interviewCount) * 100 : 0,
445
- createdAt: plan.createdAt.toISOString().split('T')[0],
446
- };
447
- }
448
-
449
- /**
450
- * 批量转换 Plan 列表
451
- */
452
- export function mapPlansToViewModels(
453
- plans: (Plan & { _count?: { interviews: number } })[],
454
- interviewsMap?: Map<string, Interview[]>
455
- ): PlanViewModel[] {
456
- return plans.map((plan) =>
457
- mapPlanToViewModel(plan, interviewsMap?.get(plan.id))
458
- );
459
- }
460
-
461
- /**
462
- * 将 Prisma Template 记录转换为视图模型
463
- */
464
- export function mapTemplateToViewModel(
465
- template: Template & { _count?: { interviews: number }; interviews?: Interview[] }
466
- ): TemplateViewModel {
467
- const interviewCount = template._count?.interviews ?? template.interviews?.length ?? 0;
468
- const lastUsed = template.interviews?.length
469
- ? Math.max(...template.interviews.map((i) => i.createdAt.getTime()))
470
- : null;
471
-
472
- return {
473
- id: template.id,
474
- name: template.name,
475
- content: template.content,
476
- interviewCount,
477
- lastUsed: lastUsed ? new Date(lastUsed).toISOString().split('T')[0] : null,
478
- };
479
- }
480
-
481
- /**
482
- * 过滤出已完成的计划
483
- */
484
- export function filterCompletedPlans(plans: PlanViewModel[]): PlanViewModel[] {
485
- return plans.filter((p) => p.status === 'COMPLETED');
486
- }
487
-
488
- /**
489
- * 计算统计摘要
490
- */
491
- export function calculatePlanStats(plans: PlanViewModel[]) {
492
- const total = plans.length;
493
- const completed = plans.filter((p) => p.status === 'COMPLETED').length;
494
- const inProgress = plans.filter((p) => p.status === 'IN_PROGRESS').length;
495
- const avgCompletionRate =
496
- total > 0 ? plans.reduce((sum, p) => sum + p.completionRate, 0) / total : 0;
497
-
498
- return { total, completed, inProgress, avgCompletionRate: Math.round(avgCompletionRate) };
499
- }
500
- ```
501
-
502
- ```typescript
503
- // admin/routes/plans.ts — 使用 ViewModel
504
- import { mapPlansToViewModels, filterCompletedPlans, calculatePlanStats } from '../view-models';
505
-
506
- fastify.get('/admin/plans', async (req, reply) => {
507
- const plans = await prisma.plan.findMany({
508
- include: { _count: { select: { interviews: true } } },
509
- });
510
-
511
- const viewModels = mapPlansToViewModels(plans);
512
-
513
- return reply.view('admin/plans/list.njk', {
514
- plans: viewModels,
515
- stats: calculatePlanStats(viewModels),
516
- });
517
- });
518
-
519
- fastify.get('/admin/plans/completed', async (req, reply) => {
520
- const plans = await prisma.plan.findMany({
521
- where: { status: 'COMPLETED' },
522
- include: { _count: { select: { interviews: true } } },
523
- });
524
-
525
- const viewModels = mapPlansToViewModels(plans);
526
- const completed = filterCompletedPlans(viewModels);
527
-
528
- return reply.view('admin/plans/completed.njk', { plans: completed });
529
- });
530
- ```
72
+ **Examples:** see `references/rule-4.md`
531
73
 
532
74
  ---
533
75
 
@@ -544,130 +86,7 @@ fastify.get('/admin/plans/completed', async (req, reply) => {
544
86
  | Admin API (HTMX endpoints) | ✅ 必须 | 部分更新接口 |
545
87
  | Public GET | ❌ 不需要 | 面向用户的公开页面 |
546
88
 
547
- ### 错误示例:GET 路由未认证
548
-
549
- ```typescript
550
- // admin/routes/plans.ts — 反模式
551
- import { FastifyInstance } from 'fastify';
552
-
553
- export async function registerPlanRoutes(fastify: FastifyInstance) {
554
- // GET 路由缺少认证 — 任何人都可查看所有计划
555
- fastify.get('/admin/plans', async (req, reply) => {
556
- const plans = await prisma.plan.findMany();
557
- return reply.view('admin/plans/list.njk', { plans });
558
- });
559
-
560
- // 只有 POST 有认证
561
- fastify.post('/admin/plans', { onRequest: [authenticateAdmin] }, async (req, reply) => {
562
- // ...
563
- });
564
- }
565
- ```
566
-
567
- ```typescript
568
- // admin/routes/analytics.ts — 使用不安全的 API Key
569
- import { FastifyInstance } from 'fastify';
570
-
571
- export async function registerAnalyticsRoutes(fastify: FastifyInstance) {
572
- fastify.get('/admin/analytics', {
573
- onRequest: [
574
- // 通过 Header 传递 API Key — 易泄露、不安全
575
- async (req: FastifyRequest, reply: FastifyReply) => {
576
- const apiKey = req.headers['x-api-key'];
577
- if (apiKey !== process.env.ADMIN_API_KEY) {
578
- return reply.code(401).send({ error: 'Unauthorized' });
579
- }
580
- },
581
- ],
582
- }, async (req, reply) => {
583
- // ...
584
- });
585
- }
586
- ```
587
-
588
- ### ✅ 正确示例:所有路由统一认证
589
-
590
- ```typescript
591
- // admin/middleware/authenticate-admin.ts
592
- import { FastifyRequest, FastifyReply, HookHandlerDoneFunction } from 'fastify';
593
-
594
- export async function authenticateAdmin(
595
- req: FastifyRequest,
596
- reply: FastifyReply,
597
- done: HookHandlerDoneFunction
598
- ) {
599
- const session = req.session;
600
-
601
- if (!session?.userId) {
602
- return reply
603
- .code(302)
604
- .header('Location', '/login?redirect=' + encodeURIComponent(req.url))
605
- .send();
606
- }
607
-
608
- // 验证用户具有 Admin 角色
609
- const user = await prisma.user.findUnique({
610
- where: { id: session.userId },
611
- select: { role: true },
612
- });
613
-
614
- if (user?.role !== 'ADMIN') {
615
- return reply.code(403).view('admin/errors/forbidden.njk', {
616
- message: 'You do not have permission to access the admin panel.',
617
- });
618
- }
619
-
620
- // 将用户信息附加到请求
621
- req.user = user;
622
- done();
623
- }
624
- ```
625
-
626
- ```typescript
627
- // admin/routes/admin-shared.ts — 全局注册认证中间件
628
- import { FastifyInstance } from 'fastify';
629
- import { authenticateAdmin } from '../middleware/authenticate-admin';
630
-
631
- export function registerAdminShared(fastify: FastifyInstance) {
632
- // 为所有 /admin/* 路由注册认证 hook
633
- fastify.addHook('onRequest', authenticateAdmin);
634
- }
635
- ```
636
-
637
- ```typescript
638
- // admin/routes/plans.ts — 继承全局认证
639
- import { FastifyInstance } from 'fastify';
640
-
641
- export async function registerPlanRoutes(fastify: FastifyInstance) {
642
- // 所有路由自动继承 authenticateAdmin
643
- // 无需在每个 route 上重复声明
644
- fastify.get('/admin/plans', async (req, reply) => {
645
- const plans = await prisma.plan.findMany();
646
- return reply.view('admin/plans/list.njk', { plans });
647
- });
648
-
649
- fastify.post('/admin/plans', async (req, reply) => {
650
- await prisma.plan.create({ data: req.body as any });
651
- return reply.redirect('/admin/plans');
652
- });
653
- }
654
- ```
655
-
656
- ```typescript
657
- // admin/routes/analytics.ts — 同样保护
658
- import { FastifyInstance } from 'fastify';
659
-
660
- export async function registerAnalyticsRoutes(fastify: FastifyInstance) {
661
- // GET 路由同样受认证保护
662
- fastify.get('/admin/analytics', async (req, reply) => {
663
- const stats = await prisma.plan.aggregate({
664
- _count: { id: true },
665
- _avg: { completionRate: true },
666
- });
667
- return reply.view('admin/analytics/dashboard.njk', { stats });
668
- });
669
- }
670
- ```
89
+ **Examples:** see `references/rule-5.md`
671
90
 
672
91
  ---
673
92
 
@@ -689,216 +108,6 @@ export async function registerAnalyticsRoutes(fastify: FastifyInstance) {
689
108
  | Toast 通知本地状态 | ❌ | ✅ |
690
109
  | 按钮 loading 状态切换 | ❌ | ✅ |
691
110
 
692
- ### 错误示例:HTMX Alpine 混用
693
-
694
- ```nunjucks
695
- {# admin/views/plans.njk — 反模式 #}
696
-
697
- {# 使用 Alpine 做服务端请求 — 职责错误 #}
698
- <div x-data="{ plans: [], loading: false }"
699
- x-init="loading = true; fetch('/admin/api/plans').then(r => r.json()).then(d => plans = d); loading = false">
700
- <template x-for="plan in plans" :key="plan.id">
701
- <div>
702
- <span x-text="plan.name"></span>
703
- {# 删除操作使用 Alpine 而非 HTMX — 失去服务端渲染优势 #}
704
- <button @click="fetch('/admin/plans/' + plan.id, { method: 'DELETE' })
705
- .then(() => plans = plans.filter(p => p.id !== plan.id))">
706
- Delete
707
- </button>
708
- </div>
709
- </template>
710
- </div>
711
-
712
- {# HTMX 驱动下拉菜单状态 — 职责错误 #}
713
- <div hx-get="/admin/plans" hx-swap="innerHTML">
714
- <button hx-on::after-request="this.nextElementSibling.style.display = 'block'">
715
- Show Filters
716
- </button>
717
- <div style="display: none">
718
- <input type="text" name="search" placeholder="Search..."
719
- hx-get="/admin/plans" hx-trigger="keyup changed delay:300ms" />
720
- </div>
721
- </div>
722
- ```
723
-
724
- ### ✅ 正确示例:职责分离
725
-
726
- ```nunjucks
727
- {# admin/views/plans.njk #}
728
-
729
- {# 整体布局与内容列表由 HTMX 驱动 #}
730
- <div id="plans-content" hx-boost="true">
731
- <div class="header-bar" x-data="{ showFilters: false }">
732
- <h1>Interview Plans</h1>
733
-
734
- {# 下拉菜单状态由 Alpine 管理 #}
735
- <button type="button" @click="showFilters = !showFilters"
736
- :class="{ 'active': showFilters }">
737
- Filters
738
- </button>
739
-
740
- {# Alpine 控制本地显示/隐藏 #}
741
- <div x-show="showFilters" x-transition class="filter-panel">
742
- {# 搜索输入通过 HTMX 触发服务端请求 #}
743
- <input type="text" name="search" placeholder="Search plans..."
744
- hx-get="/admin/plans"
745
- hx-target="#plans-list"
746
- hx-trigger="keyup changed delay:300ms"
747
- hx-indicator=".search-spinner" />
748
- </div>
749
- </div>
750
-
751
- {# 列表内容完全由 HTMX 服务端渲染 #}
752
- <div id="plans-list">
753
- {% for plan in plans %}
754
- <div class="plan-card" id="plan-{{ plan.id }}">
755
- <h3>{{ plan.name }}</h3>
756
- <p>Status: {{ plan.status | upper }}</p>
757
- <p>Completion: {{ plan.completionRate | round(1) }}%</p>
758
-
759
- <div class="actions">
760
- {# 删除操作通过 HTMX 提交,服务端响应更新 DOM #}
761
- <button class="btn-danger"
762
- hx-delete="/admin/plans/{{ plan.id }}"
763
- hx-target="#plan-{{ plan.id }}"
764
- hx-swap="outerHTML"
765
- hx-confirm="Are you sure you want to delete this plan?"
766
- x-data="{ deleting: false }"
767
- hx-on::before-request="deleting = true"
768
- hx-on::after-request="deleting = false">
769
- <span x-show="!deleting">Delete</span>
770
- <span x-show="deleting" class="spinner">Deleting...</span>
771
- </button>
772
- </div>
773
- </div>
774
- {% endfor %}
775
- </div>
776
- </div>
777
- ```
778
-
779
- ```nunjucks
780
- {# admin/views/modals/confirm-delete.njk — Modal 由 Alpine 管理 #}
781
-
782
- <div x-data="{ open: false, targetId: null, targetName: '' }"
783
- @open-modal.window="open = true; targetId = $event.detail.id; targetName = $event.detail.name"
784
- x-show="open"
785
- x-cloak
786
- class="modal-backdrop"
787
- @click.self="open = false">
788
-
789
- <div class="modal-content" x-transition>
790
- <h2>Confirm Delete</h2>
791
- <p>Are you sure you want to delete "<span x-text="targetName"></span>"?</p>
792
-
793
- <div class="modal-actions">
794
- <button @click="open = false">Cancel</button>
795
-
796
- {# 真正的删除操作通过 HTMX 提交 #}
797
- <button class="btn-danger"
798
- hx-delete="/admin/plans/"
799
- hx-vals='js:{ id: targetId }'
800
- hx-target="#plans-list"
801
- @click="open = false">
802
- Confirm Delete
803
- </button>
804
- </div>
805
- </div>
806
- </div>
807
- ```
808
-
809
- ```nunjucks
810
- {# admin/views/templates/edit.njk — 表单字段本地验证由 Alpine 管理 #}
811
-
812
- <form x-data="{
813
- name: '',
814
- nameError: '',
815
- content: '',
816
- contentError: '',
817
- get isValid() {
818
- return this.name.length > 0 && this.content.length > 0 && !this.nameError && !this.contentError;
819
- },
820
- validateName(value) {
821
- this.name = value;
822
- this.nameError = value.length < 3 ? 'Name must be at least 3 characters.' : '';
823
- },
824
- validateContent(value) {
825
- this.content = value;
826
- this.contentError = value.trim().length === 0 ? 'Content cannot be empty.' : '';
827
- }
828
- }" hx-post="/admin/templates/{{ template.id }}"
829
- hx-target="#form-result"
830
- hx-swap="innerHTML"
831
- hx-on::before-request="if (!isValid) { $event.preventDefault(); }">
832
-
833
- <div class="form-group">
834
- <label for="name">Template Name</label>
835
- <input type="text" id="name" name="name"
836
- x-model="name"
837
- @input="validateName(name)"
838
- :class="{ 'error': nameError }"
839
- value="{{ template.name }}" />
840
- <span x-show="nameError" x-text="nameError" class="field-error"></span>
841
- </div>
842
-
843
- <div class="form-group">
844
- <label for="content">Template Content</label>
845
- <textarea id="content" name="content" rows="20"
846
- x-model="content"
847
- @input="validateContent(content)"
848
- :class="{ 'error': contentError }">{{ template.content }}</textarea>
849
- <span x-show="contentError" x-text="contentError" class="field-error"></span>
850
- </div>
851
-
852
- <div class="form-actions">
853
- <button type="submit" :disabled="!isValid" class="btn-primary">
854
- Save Template
855
- </button>
856
- <a href="/admin/templates" class="btn-secondary">Cancel</a>
857
- </div>
858
- </form>
859
-
860
- {# HTMX 响应结果区域 #}
861
- <div id="form-result"></div>
862
- ```
863
-
864
- ### 分离原则
865
-
866
- 1. **HTMX 是服务端与 DOM 之间的桥梁** — 所有数据变更、页面导航、内容更新必须通过 HTMX
867
- 2. **Alpine 是组件状态的管理者** — 只管理不需要服务端参与的 UI 状态(开关、显隐、验证)
868
- 3. **HTMX 属性不应操作 DOM 状态** — `hx-on::after-request` 里不应出现 `element.style.display`
869
- 4. **Alpine 方法不应发起 fetch** — `@click="fetch('/api/...')"` 是反模式,应用 `hx-get/hx-post`
111
+ **Examples & Principles:** see `references/rule-6.md`
870
112
 
871
- ---
872
113
 
873
- ## 输出契约
874
-
875
- 当 sprint-flow BUILD phase 加载此 skill 时,生成的 admin 代码必须满足:
876
-
877
- ```json
878
- {
879
- "rule_1_route_splitting": {
880
- "status": "pass",
881
- "evidence": "admin/routes/ 目录下存在 templates.ts, plans.ts 等分模块文件,无单文件 >200 行"
882
- },
883
- "rule_2_test_consistency": {
884
- "status": "pass",
885
- "evidence": "所有 admin 测试文件 import createAdminTestApp(),无自建 Fastify 实例"
886
- },
887
- "rule_3_nunjucks_parens": {
888
- "status": "pass",
889
- "evidence": "所有 .*njk 文件中 filter 前的比较表达式均有括号包裹"
890
- },
891
- "rule_4_view_model_mapper": {
892
- "status": "pass",
893
- "evidence": "存在 admin/view-models.ts,路由文件中无重复的 .map() 数据转换"
894
- },
895
- "rule_5_auth_protection": {
896
- "status": "pass",
897
- "evidence": "所有 admin 路由继承全局 authenticateAdmin hook,无 GET 豁免"
898
- },
899
- "rule_6_htmx_alpine_separation": {
900
- "status": "pass",
901
- "evidence": ".njk 文件中无 Alpine fetch() 调用,无 HTMX 直接操作 DOM 状态"
902
- }
903
- }
904
- ```