@zhuan-ai/zhuanspec 2.2.5 โ†’ 2.5.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 (103) hide show
  1. package/bin/zhuanspec-hook.js +3 -0
  2. package/dist/cli/hooks.d.ts +14 -0
  3. package/dist/cli/hooks.js +465 -0
  4. package/dist/cli/index.js +100 -0
  5. package/dist/commands/artifact-workflow.js +15 -34
  6. package/dist/commands/design.d.ts +42 -0
  7. package/dist/commands/design.js +337 -0
  8. package/dist/commands/progress.d.ts +38 -0
  9. package/dist/commands/progress.js +349 -0
  10. package/dist/commands/review.d.ts +32 -0
  11. package/dist/commands/review.js +472 -0
  12. package/dist/commands/validate.d.ts +14 -0
  13. package/dist/commands/validate.js +161 -10
  14. package/dist/core/archive.d.ts +1 -0
  15. package/dist/core/archive.js +45 -3
  16. package/dist/core/completions/command-registry.js +67 -0
  17. package/dist/core/configurators/slash/amazon-q.js +32 -2
  18. package/dist/core/configurators/slash/antigravity.js +8 -2
  19. package/dist/core/configurators/slash/auggie.js +16 -1
  20. package/dist/core/configurators/slash/base.js +1 -1
  21. package/dist/core/configurators/slash/claude.d.ts +4 -0
  22. package/dist/core/configurators/slash/claude.js +56 -1
  23. package/dist/core/configurators/slash/cline.js +8 -2
  24. package/dist/core/configurators/slash/codebuddy.js +22 -1
  25. package/dist/core/configurators/slash/codex.js +21 -0
  26. package/dist/core/configurators/slash/costrict.js +15 -0
  27. package/dist/core/configurators/slash/crush.js +22 -1
  28. package/dist/core/configurators/slash/cursor.js +22 -1
  29. package/dist/core/configurators/slash/factory.js +16 -1
  30. package/dist/core/configurators/slash/gemini.js +8 -2
  31. package/dist/core/configurators/slash/github-copilot.js +19 -1
  32. package/dist/core/configurators/slash/iflow.js +22 -1
  33. package/dist/core/configurators/slash/kilocode.js +4 -1
  34. package/dist/core/configurators/slash/opencode.js +27 -0
  35. package/dist/core/configurators/slash/qoder.d.ts +4 -0
  36. package/dist/core/configurators/slash/qoder.js +59 -1
  37. package/dist/core/configurators/slash/qwen.js +8 -2
  38. package/dist/core/configurators/slash/roocode.js +8 -2
  39. package/dist/core/configurators/slash/windsurf.js +8 -2
  40. package/dist/core/dashboard/metrics.d.ts +33 -0
  41. package/dist/core/dashboard/metrics.js +114 -0
  42. package/dist/core/hooks/collect-knowledge.d.ts +16 -0
  43. package/dist/core/hooks/collect-knowledge.js +203 -0
  44. package/dist/core/hooks/context-load-hook.d.ts +25 -0
  45. package/dist/core/hooks/context-load-hook.js +159 -0
  46. package/dist/core/hooks/deviation-check.d.ts +27 -0
  47. package/dist/core/hooks/deviation-check.js +403 -0
  48. package/dist/core/hooks/deviation-handler.d.ts +43 -0
  49. package/dist/core/hooks/deviation-handler.js +98 -0
  50. package/dist/core/hooks/init.d.ts +14 -0
  51. package/dist/core/hooks/init.js +244 -0
  52. package/dist/core/hooks/notify-milestone.d.ts +14 -0
  53. package/dist/core/hooks/notify-milestone.js +170 -0
  54. package/dist/core/hooks/post-apply.d.ts +29 -0
  55. package/dist/core/hooks/post-apply.js +173 -0
  56. package/dist/core/hooks/post-archive.d.ts +7 -0
  57. package/dist/core/hooks/post-archive.js +208 -0
  58. package/dist/core/hooks/pre-apply.d.ts +34 -0
  59. package/dist/core/hooks/pre-apply.js +139 -0
  60. package/dist/core/hooks/pre-archive.d.ts +7 -0
  61. package/dist/core/hooks/pre-archive.js +50 -0
  62. package/dist/core/hooks/record-progress.d.ts +49 -0
  63. package/dist/core/hooks/record-progress.js +609 -0
  64. package/dist/core/hooks/review-hooks.d.ts +89 -0
  65. package/dist/core/hooks/review-hooks.js +345 -0
  66. package/dist/core/hooks/review-orchestrator.d.ts +40 -0
  67. package/dist/core/hooks/review-orchestrator.js +146 -0
  68. package/dist/core/hooks/summarize.d.ts +15 -0
  69. package/dist/core/hooks/summarize.js +282 -0
  70. package/dist/core/hooks/user-input-hook.d.ts +25 -0
  71. package/dist/core/hooks/user-input-hook.js +179 -0
  72. package/dist/core/init.d.ts +8 -0
  73. package/dist/core/init.js +251 -23
  74. package/dist/core/parsers/requirement-blocks.js +13 -10
  75. package/dist/core/templates/agents-template.d.ts +1 -1
  76. package/dist/core/templates/agents-template.js +513 -244
  77. package/dist/core/templates/index.d.ts +1 -0
  78. package/dist/core/templates/index.js +1 -0
  79. package/dist/core/templates/skill-templates.js +46 -156
  80. package/dist/core/templates/slash-command-templates.d.ts +1 -1
  81. package/dist/core/templates/slash-command-templates.js +352 -21
  82. package/dist/core/templates/tasks-template.d.ts +7 -0
  83. package/dist/core/templates/tasks-template.js +121 -25
  84. package/dist/core/templates/tdd-tasks-template.d.ts +3 -0
  85. package/dist/core/templates/tdd-tasks-template.js +91 -38
  86. package/dist/core/templates/test-cases-template.d.ts +41 -0
  87. package/dist/core/templates/test-cases-template.js +128 -0
  88. package/dist/core/validation/strict-rules.d.ts +44 -5
  89. package/dist/core/validation/strict-rules.js +302 -8
  90. package/dist/core/validation/validator.js +52 -2
  91. package/dist/core/view.d.ts +1 -0
  92. package/dist/core/view.js +60 -2
  93. package/dist/mcp/index.d.ts +28 -0
  94. package/dist/mcp/index.js +31 -0
  95. package/dist/utils/file-system.d.ts +1 -0
  96. package/dist/utils/file-system.js +11 -0
  97. package/dist/utils/item-discovery.js +24 -2
  98. package/dist/utils/phase-utils.d.ts +36 -0
  99. package/dist/utils/phase-utils.js +117 -0
  100. package/package.json +3 -2
  101. package/schemas/spec-driven/schema.yaml +45 -31
  102. package/schemas/spec-driven/templates/spec.md +142 -5
  103. package/schemas/spec-driven/templates/tasks.md +73 -9
@@ -0,0 +1,349 @@
1
+ /**
2
+ * Progress Command - Display current execution status
3
+ *
4
+ * This command reads progress.json and tasks.md to show:
5
+ * - Current phase and wave status
6
+ * - Task completion percentage with visual progress bar
7
+ * - Estimated remaining time
8
+ */
9
+ import path from 'path';
10
+ import { promises as fs } from 'fs';
11
+ import { FileSystemUtils } from '../utils/file-system.js';
12
+ import { PHASE_ORDER } from '../utils/phase-utils.js';
13
+ import { initializeProgress } from '../core/hooks/record-progress.js';
14
+ export class ProgressCommand {
15
+ /**
16
+ * Recover damaged progress.json using three-tier fallback strategy
17
+ * @param filePath - Path to progress.json file
18
+ * @returns Recovered ProgressData or null if unrecoverable
19
+ */
20
+ async recoverProgressJson(filePath) {
21
+ const content = await FileSystemUtils.readFile(filePath);
22
+ // Layer 1: Standard JSON parse
23
+ try {
24
+ return JSON.parse(content);
25
+ }
26
+ catch {
27
+ // Continue to Layer 2
28
+ }
29
+ // Layer 2: Extract first top-level JSON object (handle trailing corruption)
30
+ try {
31
+ // Find the first complete JSON object
32
+ const objectStart = content.indexOf('{');
33
+ if (objectStart === -1)
34
+ return null;
35
+ // Track brace depth to find object boundary
36
+ let depth = 0;
37
+ let objectEnd = -1;
38
+ for (let i = objectStart; i < content.length; i++) {
39
+ if (content[i] === '{')
40
+ depth++;
41
+ else if (content[i] === '}') {
42
+ depth--;
43
+ if (depth === 0) {
44
+ objectEnd = i + 1;
45
+ break;
46
+ }
47
+ }
48
+ }
49
+ if (objectEnd > objectStart) {
50
+ const objectContent = content.slice(objectStart, objectEnd);
51
+ return JSON.parse(objectContent);
52
+ }
53
+ }
54
+ catch {
55
+ // Continue to Layer 3
56
+ }
57
+ // Layer 3: Minimal field extraction using regex
58
+ const phaseMatch = content.match(/"phase"\s*:\s*"([^"]+)"/);
59
+ const currentTaskMatch = content.match(/"currentTask"\s*:\s*"([^"]+)"/);
60
+ const totalTasksMatch = content.match(/"totalTasks"\s*:\s*(\d+)/);
61
+ const changeIdMatch = content.match(/"changeId"\s*:\s*"([^"]+)"/);
62
+ const sessionIdMatch = content.match(/"sessionId"\s*:\s*"([^"]+)"/);
63
+ if (phaseMatch || changeIdMatch) {
64
+ return {
65
+ changeId: changeIdMatch?.[1] || '',
66
+ sessionId: sessionIdMatch?.[1] || '',
67
+ startedAt: '',
68
+ lastUpdatedAt: '',
69
+ phase: phaseMatch?.[1] || 'unknown',
70
+ currentNode: phaseMatch?.[1] || 'unknown',
71
+ currentTask: currentTaskMatch?.[1] || '',
72
+ completedTasks: [],
73
+ totalTasks: totalTasksMatch ? parseInt(totalTasksMatch[1], 10) : 0,
74
+ };
75
+ }
76
+ return null;
77
+ }
78
+ /**
79
+ * Show progress for a specific change
80
+ * Usage: zhuanspec progress show <change-id>
81
+ */
82
+ async show(changeId, options) {
83
+ const cwd = process.cwd();
84
+ const zhuanspecDir = path.join(cwd, 'zhuanspec');
85
+ const changeDir = path.join(zhuanspecDir, 'changes', changeId);
86
+ const metricsDir = path.join(changeDir, 'metrics');
87
+ if (!await FileSystemUtils.directoryExists(changeDir)) {
88
+ throw new Error(`Change '${changeId}' not found`);
89
+ }
90
+ // Read progress.json with recovery support
91
+ const progressPath = path.join(metricsDir, 'progress.json');
92
+ let progress = null;
93
+ if (await FileSystemUtils.fileExists(progressPath)) {
94
+ progress = await this.recoverProgressJson(progressPath);
95
+ }
96
+ // Read tasks.md
97
+ const tasksPath = path.join(changeDir, 'tasks.md');
98
+ let tasks = [];
99
+ let waves = [];
100
+ if (await FileSystemUtils.fileExists(tasksPath)) {
101
+ const tasksContent = await FileSystemUtils.readFile(tasksPath);
102
+ tasks = this.parseTasks(tasksContent);
103
+ waves = this.computeWaves(tasks);
104
+ }
105
+ if (options?.json) {
106
+ this.outputJson(changeId, progress, tasks, waves);
107
+ }
108
+ else {
109
+ this.outputFormatted(changeId, progress, tasks, waves);
110
+ }
111
+ }
112
+ async execute(options) {
113
+ const cwd = process.cwd();
114
+ const zhuanspecDir = path.join(cwd, 'zhuanspec');
115
+ // Check if zhuanspec directory exists
116
+ if (!await FileSystemUtils.fileExists(zhuanspecDir)) {
117
+ console.log('โ„น๏ธ No ZhuanSpec project found in current directory');
118
+ return;
119
+ }
120
+ // Get active change from environment or find most recent
121
+ const changeId = process.env.ZHUANSPEC_CHANGE_ID || await this.findMostRecentChange(zhuanspecDir);
122
+ if (!changeId) {
123
+ console.log('โ„น๏ธ No active ZhuanSpec change');
124
+ return;
125
+ }
126
+ const changeDir = path.join(zhuanspecDir, 'changes', changeId);
127
+ const metricsDir = path.join(changeDir, 'metrics');
128
+ // Read progress.json with recovery support
129
+ const progressPath = path.join(metricsDir, 'progress.json');
130
+ let progress = null;
131
+ if (await FileSystemUtils.fileExists(progressPath)) {
132
+ progress = await this.recoverProgressJson(progressPath);
133
+ }
134
+ // Read tasks.md
135
+ const tasksPath = path.join(changeDir, 'tasks.md');
136
+ let tasks = [];
137
+ let waves = [];
138
+ if (await FileSystemUtils.fileExists(tasksPath)) {
139
+ const tasksContent = await FileSystemUtils.readFile(tasksPath);
140
+ tasks = this.parseTasks(tasksContent);
141
+ waves = this.computeWaves(tasks);
142
+ }
143
+ if (options?.json) {
144
+ this.outputJson(changeId, progress, tasks, waves);
145
+ }
146
+ else {
147
+ this.outputFormatted(changeId, progress, tasks, waves);
148
+ }
149
+ }
150
+ async findMostRecentChange(zhuanspecDir) {
151
+ const changesDir = path.join(zhuanspecDir, 'changes');
152
+ if (!await FileSystemUtils.fileExists(changesDir)) {
153
+ return null;
154
+ }
155
+ const entries = await fs.readdir(changesDir, { withFileTypes: true });
156
+ const changeDirs = entries
157
+ .filter(e => e.isDirectory() && !e.name.startsWith('.') && e.name !== 'archive')
158
+ .map(e => e.name);
159
+ if (changeDirs.length === 0) {
160
+ return null;
161
+ }
162
+ // Return most recently modified
163
+ const changeStats = await Promise.all(changeDirs.map(async (name) => {
164
+ const dirPath = path.join(changesDir, name);
165
+ const stats = await fs.stat(dirPath);
166
+ return { name, mtime: stats.mtime.getTime() };
167
+ }));
168
+ changeStats.sort((a, b) => b.mtime - a.mtime);
169
+ return changeStats[0].name;
170
+ }
171
+ parseTasks(content) {
172
+ const tasks = [];
173
+ const lines = content.split('\n');
174
+ let currentWave = 0;
175
+ for (const line of lines) {
176
+ // Match wave headers: ### Wave 1, ### Wave 2, etc.
177
+ const waveMatch = line.match(/^###\s*Wave\s*(\d+)/i);
178
+ if (waveMatch) {
179
+ currentWave = parseInt(waveMatch[1], 10);
180
+ continue;
181
+ }
182
+ // Match new format task lines: - [ ] X.Y description or - [x] X.Y description
183
+ const newFormatMatch = line.match(/^-\s*\[([x ])\]\s*(\d+\.\d+)\s*(.+)/);
184
+ if (newFormatMatch) {
185
+ const statusChar = newFormatMatch[1];
186
+ const taskId = newFormatMatch[2];
187
+ const description = newFormatMatch[3].trim();
188
+ tasks.push({
189
+ taskId,
190
+ status: statusChar === 'x' ? 'completed' : 'pending',
191
+ wave: currentWave,
192
+ description: description.split('@')[0].trim(), // Remove @skill/@depends tags
193
+ });
194
+ continue;
195
+ }
196
+ // Match legacy format task headers: #### T1: description or #### T2: description
197
+ const legacyMatch = line.match(/^####\s*(T\d+)\s*:\s*(.+)$/);
198
+ if (legacyMatch) {
199
+ const taskId = legacyMatch[1];
200
+ const rawDescription = legacyMatch[2].trim();
201
+ // Check if description ends with โœ… (completed marker)
202
+ const isCompleted = rawDescription.endsWith('โœ…');
203
+ const description = isCompleted
204
+ ? rawDescription.slice(0, -1).trim()
205
+ : rawDescription;
206
+ tasks.push({
207
+ taskId,
208
+ status: isCompleted ? 'completed' : 'pending',
209
+ wave: currentWave,
210
+ description: description.split('@')[0].trim(), // Remove @skill/@depends tags
211
+ });
212
+ }
213
+ }
214
+ return tasks;
215
+ }
216
+ computeWaves(tasks) {
217
+ const wavesMap = new Map();
218
+ for (const task of tasks) {
219
+ if (!wavesMap.has(task.wave)) {
220
+ wavesMap.set(task.wave, {
221
+ waveId: task.wave,
222
+ tasks: [],
223
+ completedTasks: [],
224
+ status: 'pending',
225
+ });
226
+ }
227
+ const wave = wavesMap.get(task.wave);
228
+ wave.tasks.push(task);
229
+ if (task.status === 'completed') {
230
+ wave.completedTasks.push(task);
231
+ }
232
+ }
233
+ // Determine wave status
234
+ for (const [, wave] of wavesMap) {
235
+ if (wave.completedTasks.length === wave.tasks.length) {
236
+ wave.status = 'completed';
237
+ }
238
+ else if (wave.completedTasks.length > 0) {
239
+ wave.status = 'in_progress';
240
+ }
241
+ else {
242
+ wave.status = 'pending';
243
+ }
244
+ }
245
+ return Array.from(wavesMap.values()).sort((a, b) => a.waveId - b.waveId);
246
+ }
247
+ outputJson(changeId, progress, tasks, waves) {
248
+ const output = {
249
+ changeId,
250
+ phase: progress?.phase || 'unknown',
251
+ currentTask: progress?.currentTask || null,
252
+ tasks: {
253
+ total: tasks.length,
254
+ completed: tasks.filter(t => t.status === 'completed').length,
255
+ pending: tasks.filter(t => t.status === 'pending').length,
256
+ },
257
+ waves: waves.map(w => ({
258
+ waveId: w.waveId,
259
+ status: w.status,
260
+ totalTasks: w.tasks.length,
261
+ completedTasks: w.completedTasks.length,
262
+ })),
263
+ progress: progress,
264
+ };
265
+ console.log(JSON.stringify(output, null, 2));
266
+ }
267
+ outputFormatted(changeId, progress, tasks, waves) {
268
+ const phase = progress?.phase || 'unknown';
269
+ const currentTask = progress?.currentTask || '';
270
+ const completedCount = tasks.filter(t => t.status === 'completed').length;
271
+ const totalTasks = tasks.length;
272
+ // Phase index
273
+ const phases = ['idle', 'propose', 'apply', 'review', 'archive'];
274
+ const phaseIndex = phases.indexOf(phase) + 1;
275
+ // Progress percentage
276
+ const percentage = totalTasks > 0 ? (completedCount / totalTasks) * 100 : 0;
277
+ const progressBar = this.generateProgressBar(percentage, 12);
278
+ // Duration
279
+ const durationMs = progress?.stats?.durationMs?.[phase] || 0;
280
+ const durationMin = Math.round(durationMs / 60000);
281
+ const estimatedRemaining = totalTasks > 0 && completedCount > 0
282
+ ? Math.round((durationMin / completedCount) * (totalTasks - completedCount))
283
+ : 0;
284
+ console.log('');
285
+ console.log(`๐Ÿ“Š ๅ˜ๆ›ด่ฟ›ๅบฆ: ${changeId}`);
286
+ console.log('โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”');
287
+ console.log('');
288
+ console.log(`Phase: ${phase} (${phaseIndex}/${phases.length})`);
289
+ console.log('');
290
+ // Wave status
291
+ for (const wave of waves) {
292
+ const waveStatusEmoji = wave.status === 'completed' ? 'โœ…' : wave.status === 'in_progress' ? '๐Ÿ”„' : 'โณ';
293
+ const waveStatusText = wave.status === 'completed' ? 'ๅฎŒๆˆ' : wave.status === 'in_progress' ? '่ฟ›่กŒไธญ' : 'ๅพ…ๅผ€ๅง‹';
294
+ console.log(`Wave ${wave.waveId}: ${waveStatusEmoji} ${waveStatusText}`);
295
+ for (const task of wave.tasks) {
296
+ const taskStatusEmoji = task.status === 'completed' ? 'โœ…' : task.status === 'in_progress' ? '๐Ÿ”„' : 'โณ';
297
+ const isCurrent = task.taskId === currentTask;
298
+ const currentIndicator = isCurrent ? ' โ† ๅฝ“ๅ‰' : '';
299
+ console.log(` ${taskStatusEmoji} ${task.taskId} ${task.description}${currentIndicator}`);
300
+ }
301
+ console.log('');
302
+ }
303
+ // Overall progress
304
+ console.log(`ๆ•ดไฝ“: ${progressBar} ${percentage.toFixed(1)}% (${completedCount}/${totalTasks})`);
305
+ // Duration info
306
+ if (durationMin > 0) {
307
+ console.log(`่€—ๆ—ถ: ${durationMin}m | ้ข„่ฎกๅ‰ฉไฝ™: ~${estimatedRemaining}m`);
308
+ }
309
+ console.log('โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”');
310
+ console.log('');
311
+ // Next task
312
+ const nextTask = tasks.find(t => t.status === 'pending');
313
+ if (nextTask) {
314
+ console.log(`โžก๏ธ ไธ‹ไธ€ไธชไปปๅŠก: ${nextTask.taskId}`);
315
+ }
316
+ else if (completedCount === totalTasks && totalTasks > 0) {
317
+ console.log('โœ… ๅ…จ้ƒจไปปๅŠกๅฎŒๆˆ๏ผŒๅ‡†ๅค‡่ฟ›ๅ…ฅ Review ้˜ถๆฎต');
318
+ }
319
+ }
320
+ generateProgressBar(percentage, blocks) {
321
+ const filled = Math.round((percentage / 100) * blocks);
322
+ const empty = blocks - filled;
323
+ return 'โ–ˆ'.repeat(filled) + 'โ–‘'.repeat(empty);
324
+ }
325
+ /**
326
+ * Set phase for a change
327
+ * Usage: zhuanspec progress set-phase <change-id> <phase>
328
+ */
329
+ async setPhase(changeId, phase) {
330
+ const cwd = process.cwd();
331
+ const changeDir = path.join(cwd, 'zhuanspec', 'changes', changeId);
332
+ if (!await FileSystemUtils.directoryExists(changeDir)) {
333
+ throw new Error(`Change '${changeId}' not found`);
334
+ }
335
+ // Validate phase
336
+ const normalizedPhase = phase.toLowerCase();
337
+ if (!PHASE_ORDER.includes(normalizedPhase)) {
338
+ throw new Error(`Invalid phase: ${phase}. Valid phases: ${PHASE_ORDER.join(', ')}`);
339
+ }
340
+ // Update progress.json
341
+ await initializeProgress(changeId, normalizedPhase);
342
+ // Read and display result
343
+ const progressPath = path.join(changeDir, 'metrics', 'progress.json');
344
+ const progress = JSON.parse(await FileSystemUtils.readFile(progressPath));
345
+ console.log(`\nโœ“ Phase set: ${changeId} -> ${normalizedPhase}`);
346
+ console.log(`โœ“ Progress file: ${progressPath}\n`);
347
+ }
348
+ }
349
+ //# sourceMappingURL=progress.js.map
@@ -0,0 +1,32 @@
1
+ export declare class ReviewCommand {
2
+ execute(changeName?: string, options?: {
3
+ json?: boolean;
4
+ testCommand?: string;
5
+ coverageThreshold?: string;
6
+ }): Promise<void>;
7
+ private selectChangeInteractively;
8
+ private collectRequirementNames;
9
+ private collectTouchedFiles;
10
+ private collectTouchedFileContent;
11
+ private runTests;
12
+ private buildSummary;
13
+ private buildSpecConsistency;
14
+ private printSummary;
15
+ /**
16
+ * Fetch Sonar issues using MCP
17
+ */
18
+ private fetchSonarIssues;
19
+ /**
20
+ * Collect all Scenarios from delta specs
21
+ */
22
+ private collectScenarios;
23
+ /**
24
+ * Find test files in the project
25
+ */
26
+ private findTestFiles;
27
+ /**
28
+ * Convert Scenario name to possible test method names
29
+ */
30
+ private scenarioToTestMethodNames;
31
+ }
32
+ //# sourceMappingURL=review.d.ts.map