wogiflow 2.1.2 → 2.2.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 (49) hide show
  1. package/.claude/commands/wogi-audit.md +189 -3
  2. package/.claude/commands/wogi-bulk.md +1 -1
  3. package/.claude/commands/wogi-help.md +1 -1
  4. package/.claude/commands/{wogi-compact.md → wogi-pre-compact.md} +6 -2
  5. package/.claude/commands/wogi-review.md +86 -13
  6. package/.claude/commands/wogi-setup-stack.md +1 -1
  7. package/.claude/commands/wogi-skill-learn.md +1 -1
  8. package/.claude/commands/wogi-start.md +65 -20
  9. package/.claude/docs/claude-code-compatibility.md +28 -0
  10. package/.claude/docs/commands.md +1 -1
  11. package/.claude/docs/knowledge-base/02-task-execution/04-completion.md +1 -1
  12. package/.claude/docs/knowledge-base/04-memory-context/README.md +2 -2
  13. package/.claude/docs/knowledge-base/04-memory-context/context-management.md +1 -1
  14. package/.claude/rules/_internal/README.md +64 -0
  15. package/.claude/rules/_internal/document-structure.md +77 -0
  16. package/.claude/rules/_internal/dual-repo-management.md +174 -0
  17. package/.claude/rules/_internal/feature-refactoring-cleanup.md +87 -0
  18. package/.claude/rules/_internal/github-releases.md +71 -0
  19. package/.claude/rules/_internal/model-management.md +35 -0
  20. package/.claude/rules/_internal/self-maintenance.md +87 -0
  21. package/.claude/rules/architecture/component-reuse.md +38 -0
  22. package/.claude/rules/code-style/naming-conventions.md +52 -0
  23. package/.claude/rules/operations/git-workflows.md +92 -0
  24. package/.claude/rules/operations/scratch-directory.md +54 -0
  25. package/.claude/rules/security/security-patterns.md +176 -0
  26. package/.claude/skills/figma-analyzer/knowledge/learnings.md +11 -0
  27. package/.workflow/bridges/claude-bridge.js +1 -1
  28. package/.workflow/models/registry.json +1 -1
  29. package/.workflow/specs/architecture.md.template +24 -0
  30. package/.workflow/specs/stack.md.template +33 -0
  31. package/.workflow/specs/testing.md.template +36 -0
  32. package/.workflow/templates/claude-md.hbs +33 -3
  33. package/README.md +1 -1
  34. package/package.json +1 -1
  35. package/scripts/flow-audit.js +158 -1
  36. package/scripts/flow-context-compact/index.js +1 -1
  37. package/scripts/flow-context-monitor.js +2 -2
  38. package/scripts/flow-loop-retry-learning.js +1 -1
  39. package/scripts/flow-proactive-compact.js +3 -3
  40. package/scripts/flow-progress-tracker.js +289 -0
  41. package/scripts/flow-prompt-capture.js +263 -170
  42. package/scripts/flow-standards-learner.js +167 -3
  43. package/scripts/flow-task-checkpoint.js +2 -0
  44. package/scripts/flow-version-check.js +1 -0
  45. package/scripts/hooks/core/commit-log-gate.js +146 -0
  46. package/scripts/hooks/core/post-compact.js +109 -4
  47. package/scripts/hooks/core/task-completed.js +19 -0
  48. package/scripts/hooks/entry/claude-code/post-tool-use.js +60 -0
  49. package/scripts/hooks/entry/claude-code/pre-tool-use.js +27 -0
@@ -254,6 +254,134 @@ function calculateHealthScore(scores) {
254
254
  };
255
255
  }
256
256
 
257
+ // ============================================================
258
+ // Pattern Promotion (Audit → Learning Pipeline)
259
+ // ============================================================
260
+
261
+ // Severity classification thresholds for audit patterns
262
+ const SYSTEMIC_THRESHOLD = 5; // 5+ files = HIGH severity / systemic
263
+ const MEDIUM_THRESHOLD = 3; // 3-4 files = MEDIUM severity
264
+
265
+ /**
266
+ * Process AI-clustered audit findings through the learning pipeline.
267
+ *
268
+ * For each clustered pattern:
269
+ * 1. Check if a rule already exists in decisions.md → ENFORCEMENT_GAP
270
+ * 2. Record/increment in feedback-patterns.md
271
+ * 3. Check promotion threshold → auto-promote if met
272
+ * 4. Check last-audit.json → detect RECURRING patterns
273
+ *
274
+ * @param {Object[]} clusters - AI-clustered patterns from audit
275
+ * @param {Object} [previousAudit] - Previous audit data for recurrence detection
276
+ * @returns {Object} Promotion results per pattern
277
+ */
278
+ function promoteAuditPatterns(clusters, previousAudit) {
279
+ const {
280
+ recordAuditPattern,
281
+ checkEnforcementGap,
282
+ promoteToDecisions,
283
+ syncToRulesDir,
284
+ mapAuditCategoryToLearnerCategory,
285
+ buildAuditRuleTemplate
286
+ } = require('./flow-standards-learner');
287
+
288
+ const previousPatternIds = new Set(
289
+ (previousAudit?.patterns || []).map(p => p.patternId)
290
+ );
291
+
292
+ const results = {
293
+ patterns: [],
294
+ summary: {
295
+ total: clusters.length,
296
+ promoted: 0,
297
+ promotionFailed: 0,
298
+ tracking: 0,
299
+ enforcementGaps: 0,
300
+ newPatterns: 0,
301
+ recurring: 0
302
+ }
303
+ };
304
+
305
+ for (const cluster of clusters) {
306
+ const patternResult = {
307
+ patternId: cluster.patternId,
308
+ category: cluster.category,
309
+ description: cluster.description,
310
+ instanceCount: cluster.instanceCount,
311
+ severity: cluster.severity || (cluster.instanceCount >= SYSTEMIC_THRESHOLD ? 'HIGH' : cluster.instanceCount >= MEDIUM_THRESHOLD ? 'MEDIUM' : 'LOW'),
312
+ isSystemic: cluster.isSystemic || cluster.instanceCount >= SYSTEMIC_THRESHOLD,
313
+ status: 'NEW',
314
+ count: 0,
315
+ rootCause: null,
316
+ recommendation: null
317
+ };
318
+
319
+ // Step 1: Check for enforcement gap
320
+ const gapCheck = checkEnforcementGap(cluster.patternId, cluster.description);
321
+ if (gapCheck.exists) {
322
+ patternResult.status = 'ENFORCEMENT_GAP';
323
+ patternResult.ruleLocation = gapCheck.section;
324
+ patternResult.ruleText = gapCheck.ruleText;
325
+ results.summary.enforcementGaps++;
326
+ } else {
327
+ // Step 2: Record/increment in feedback-patterns
328
+ const recordResult = recordAuditPattern(cluster);
329
+
330
+ if (recordResult.recorded) {
331
+ patternResult.count = recordResult.newCount;
332
+
333
+ // Step 3: Check promotion threshold
334
+ if (recordResult.shouldPromote) {
335
+ // Build a learning object for promotion (reuse learner functions)
336
+ const learning = {
337
+ canLearn: true,
338
+ violationType: cluster.category,
339
+ category: mapAuditCategoryToLearnerCategory(cluster.category),
340
+ patternName: cluster.description.slice(0, 100),
341
+ message: `${cluster.instanceCount} instances found (audit source, ${recordResult.newCount} occurrences)`,
342
+ ruleTemplate: buildAuditRuleTemplate(cluster)
343
+ };
344
+
345
+ const promoteResult = promoteToDecisions(learning, recordResult.newCount);
346
+ if (promoteResult.promoted) {
347
+ patternResult.status = 'PROMOTED';
348
+ results.summary.promoted++;
349
+
350
+ // Also sync to rules dir
351
+ const syncLearning = {
352
+ ...learning,
353
+ subcategory: cluster.patternId
354
+ };
355
+ syncToRulesDir(syncLearning);
356
+ } else {
357
+ // Distinguish "not yet at threshold" from "promotion failed"
358
+ patternResult.status = 'PROMOTION_FAILED';
359
+ patternResult.failureReason = promoteResult.reason || 'Unknown promotion failure';
360
+ results.summary.promotionFailed++;
361
+ }
362
+ } else {
363
+ patternResult.status = `TRACKING (${recordResult.newCount}/${recordResult.threshold})`;
364
+ results.summary.tracking++;
365
+ }
366
+ }
367
+ }
368
+
369
+ // Step 4: Check recurrence
370
+ if (previousPatternIds.has(cluster.patternId)) {
371
+ if (patternResult.status !== 'ENFORCEMENT_GAP' && patternResult.status !== 'PROMOTED') {
372
+ patternResult.status = `RECURRING — ${patternResult.status}`;
373
+ }
374
+ results.summary.recurring++;
375
+ } else if (patternResult.status === 'NEW' || patternResult.status.startsWith('TRACKING')) {
376
+ results.summary.newPatterns++;
377
+ }
378
+
379
+ results.patterns.push(patternResult);
380
+ }
381
+
382
+ return results;
383
+ }
384
+
257
385
  // ============================================================
258
386
  // CLI Interface
259
387
  // ============================================================
@@ -307,6 +435,30 @@ function main() {
307
435
  break;
308
436
  }
309
437
 
438
+ case 'promote': {
439
+ // Process AI-clustered findings through the learning pipeline
440
+ // Usage: node scripts/flow-audit.js promote '<clusters-json>'
441
+ const clustersArg = process.argv[3];
442
+ if (!clustersArg) {
443
+ console.error('Usage: flow-audit.js promote \'[{patternId, category, description, instanceCount, instances}]\'');
444
+ process.exit(1);
445
+ }
446
+
447
+ const clusters = safeJsonParseString(clustersArg, null);
448
+ if (!Array.isArray(clusters)) {
449
+ console.error('Invalid JSON clusters argument — expected an array');
450
+ process.exit(1);
451
+ }
452
+
453
+ // Load previous audit for recurrence detection
454
+ const lastAuditPath = path.join(PATHS.state, 'last-audit.json');
455
+ const previousAudit = safeJsonParse(lastAuditPath, {});
456
+
457
+ const promoteResults = promoteAuditPatterns(clusters, previousAudit);
458
+ console.log(JSON.stringify(promoteResults, null, 2));
459
+ break;
460
+ }
461
+
310
462
  default: {
311
463
  console.log(`
312
464
  Wogi Flow - Project Audit Helpers
@@ -319,9 +471,13 @@ Commands:
319
471
  outdated Run npm outdated (structured JSON output)
320
472
  audit Run npm audit (structured JSON output)
321
473
  score Calculate weighted health score from agent grades
474
+ promote Process AI-clustered findings through learning pipeline
322
475
 
323
476
  Score usage:
324
477
  node scripts/flow-audit.js score '{"architecture":"B+","dependencies":"A-"}'
478
+
479
+ Promote usage:
480
+ node scripts/flow-audit.js promote '[{"patternId":"missing-error-handling","category":"security","description":"...","instanceCount":7}]'
325
481
  `);
326
482
  break;
327
483
  }
@@ -333,7 +489,8 @@ module.exports = {
333
489
  findTodos,
334
490
  getOutdatedDeps,
335
491
  getAuditResults,
336
- calculateHealthScore
492
+ calculateHealthScore,
493
+ promoteAuditPatterns
337
494
  };
338
495
 
339
496
  if (require.main === module) {
@@ -291,7 +291,7 @@ function clearAll() {
291
291
  function getSerializedTree(level = 1) {
292
292
  const tree = summaryTree.loadTree();
293
293
  if (!tree) {
294
- return '# No Context Saved\n\nRun /wogi-compact to save session context.';
294
+ return '# No Context Saved\n\nRun /wogi-pre-compact to save session context.';
295
295
  }
296
296
 
297
297
  return summaryTree.serializeTree(tree, level);
@@ -271,10 +271,10 @@ function checkContextHealth() {
271
271
 
272
272
  if (usage >= config.criticalAt) {
273
273
  status = 'critical';
274
- recommendation = 'Run /wogi-compact NOW to avoid context overflow';
274
+ recommendation = 'Run /wogi-pre-compact NOW to avoid context overflow';
275
275
  } else if (usage >= config.warnAt) {
276
276
  status = 'warning';
277
- recommendation = 'Consider running /wogi-compact soon';
277
+ recommendation = 'Consider running /wogi-pre-compact soon';
278
278
  } else {
279
279
  status = 'healthy';
280
280
  recommendation = null;
@@ -99,7 +99,7 @@ const ROOT_CAUSE_CATEGORIES = {
99
99
  },
100
100
  CONTEXT_OVERFLOW: {
101
101
  ...FailureCategory.CONTEXT_OVERFLOW,
102
- suggestion: 'Use /wogi-compact before large tasks',
102
+ suggestion: 'Use /wogi-pre-compact before large tasks',
103
103
  targetFile: 'config.json'
104
104
  },
105
105
  CAPABILITY_MISMATCH: {
@@ -68,7 +68,7 @@ function getProactiveCompactionConfig() {
68
68
  * Check whether proactive compaction should trigger at a phase boundary.
69
69
  *
70
70
  * This is called by /wogi-start at each phase transition.
71
- * The actual compaction is performed by the AI agent using /wogi-compact.
71
+ * The actual compaction is performed by the AI agent using /wogi-pre-compact.
72
72
  *
73
73
  * @param {Object} params - Check parameters
74
74
  * @param {string} params.phase - The phase that just completed
@@ -161,7 +161,7 @@ async function handlePhaseBoundary(params) {
161
161
 
162
162
  /**
163
163
  * Generate a compaction summary for the current task state.
164
- * Used by /wogi-compact when proactive compaction triggers.
164
+ * Used by /wogi-pre-compact when proactive compaction triggers.
165
165
  *
166
166
  * @param {Object} checkpoint - Current checkpoint data
167
167
  * @returns {string} Formatted compaction summary
@@ -253,7 +253,7 @@ function formatCompactionMessage(result, contextPercent) {
253
253
  lines.push(`Context at ${pct}%. Compacting before next phase...`);
254
254
  lines.push(`Reason: ${result.reason}`);
255
255
  lines.push('');
256
- lines.push('Task state has been checkpointed. Run /wogi-compact to compact now.');
256
+ lines.push('Task state has been checkpointed. Run /wogi-pre-compact to compact now.');
257
257
  lines.push('After compaction, read task-checkpoint.json to restore context.');
258
258
 
259
259
  return lines.join('\n');
@@ -0,0 +1,289 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Progress Tracker
5
+ *
6
+ * Manages progress state for long-running tasks (reviews, audits, multi-criteria).
7
+ * Writes to .workflow/state/task-progress.json for hook/status line consumption.
8
+ * Optionally updates task title in ready.json with progress prefix for status line.
9
+ *
10
+ * Usage:
11
+ * flow-progress-tracker.js update <json> Update progress state
12
+ * flow-progress-tracker.js get Get current progress
13
+ * flow-progress-tracker.js clear Clear progress state
14
+ * flow-progress-tracker.js format <json> Format a progress bar string
15
+ *
16
+ * The AI calls this at natural checkpoints during execution.
17
+ */
18
+
19
+ const fs = require('node:fs');
20
+ const path = require('node:path');
21
+ const { PATHS, safeJsonParse, safeJsonParseString, getReadyData, saveReadyData } = require('./flow-utils');
22
+
23
+ const PROGRESS_PATH = path.join(PATHS.state, 'task-progress.json');
24
+
25
+ // ============================================================
26
+ // Progress State Management
27
+ // ============================================================
28
+
29
+ /**
30
+ * Update the progress state file.
31
+ *
32
+ * @param {Object} progress
33
+ * @param {string} progress.taskId - Current task ID
34
+ * @param {string} progress.command - Command running (e.g., "/wogi-review")
35
+ * @param {string} progress.phase - Current phase name
36
+ * @param {number} progress.phaseNum - Current phase number (1-based)
37
+ * @param {number} progress.totalPhases - Total phases
38
+ * @param {string} [progress.step] - Current sub-step description
39
+ * @param {number} [progress.stepNum] - Current sub-step number
40
+ * @param {number} [progress.totalSteps] - Total sub-steps in this phase
41
+ * @param {boolean} [progress.updateTitle] - Update task title in ready.json
42
+ * @returns {{ saved: boolean }}
43
+ */
44
+ function updateProgress(progress) {
45
+ const state = {
46
+ taskId: progress.taskId,
47
+ command: progress.command,
48
+ phase: progress.phase,
49
+ phaseNum: progress.phaseNum || 0,
50
+ totalPhases: progress.totalPhases || 0,
51
+ step: progress.step || null,
52
+ stepNum: progress.stepNum || 0,
53
+ totalSteps: progress.totalSteps || 0,
54
+ percentage: calculatePercentage(progress),
55
+ startedAt: getExistingStartTime() || new Date().toISOString(),
56
+ lastUpdate: new Date().toISOString()
57
+ };
58
+
59
+ try {
60
+ fs.mkdirSync(path.dirname(PROGRESS_PATH), { recursive: true });
61
+ fs.writeFileSync(PROGRESS_PATH, JSON.stringify(state, null, 2));
62
+ } catch (err) {
63
+ return { saved: false, reason: err.message };
64
+ }
65
+
66
+ // Update task title in ready.json for status line visibility (opt-in)
67
+ if (progress.updateTitle === true && state.taskId) {
68
+ updateTaskTitle(state);
69
+ }
70
+
71
+ return { saved: true, state };
72
+ }
73
+
74
+ /**
75
+ * Calculate progress percentage from phase/step numbers.
76
+ */
77
+ function calculatePercentage(progress) {
78
+ const { phaseNum, totalPhases, stepNum, totalSteps } = progress;
79
+ if (!totalPhases || !phaseNum) return 0;
80
+
81
+ // Phase-level progress
82
+ const phaseProgress = ((phaseNum - 1) / totalPhases) * 100;
83
+
84
+ // Step-level progress within the current phase
85
+ const phaseWeight = 100 / totalPhases;
86
+ const stepProgress = (totalSteps && stepNum)
87
+ ? (stepNum / totalSteps) * phaseWeight
88
+ : 0;
89
+
90
+ return Math.min(100, Math.round(phaseProgress + stepProgress));
91
+ }
92
+
93
+ /**
94
+ * Get existing start time to preserve across updates.
95
+ */
96
+ function getExistingStartTime() {
97
+ try {
98
+ const existing = safeJsonParse(PROGRESS_PATH, null);
99
+ return existing?.startedAt || null;
100
+ } catch (err) {
101
+ return null;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Update task title in ready.json with progress prefix.
107
+ * Format: "[2/5] Original title"
108
+ */
109
+ function updateTaskTitle(state) {
110
+ try {
111
+ const data = getReadyData();
112
+ const task = data.inProgress.find(t => t.id === state.taskId);
113
+ if (!task) return;
114
+
115
+ // Strip any existing progress prefix
116
+ const cleanTitle = task.title.replace(/^\[\d+\/\d+\]\s*/, '');
117
+
118
+ // Add new prefix
119
+ task.title = `[${state.phaseNum}/${state.totalPhases}] ${cleanTitle}`;
120
+ saveReadyData(data);
121
+ } catch (err) {
122
+ // Non-fatal — title update is cosmetic
123
+ if (process.env.DEBUG) {
124
+ console.error(`[progress-tracker] Title update failed: ${err.message}`);
125
+ }
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Get current progress state.
131
+ * @returns {Object|null}
132
+ */
133
+ function getProgress() {
134
+ return safeJsonParse(PROGRESS_PATH, null);
135
+ }
136
+
137
+ /**
138
+ * Clear progress state (called on task completion).
139
+ *
140
+ * NOTE: Title restoration (stripping [N/M] prefix) is handled inside the
141
+ * task-completed hook's withLock() callback to avoid race conditions on ready.json.
142
+ * This function only deletes the progress state file.
143
+ */
144
+ function clearProgress() {
145
+ try {
146
+ fs.unlinkSync(PROGRESS_PATH);
147
+ return { cleared: true };
148
+ } catch (err) {
149
+ if (err.code === 'ENOENT') return { cleared: true };
150
+ return { cleared: false, reason: err.message };
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Format a progress bar string for conversation output.
156
+ *
157
+ * @param {Object} opts
158
+ * @param {number} opts.current - Current step
159
+ * @param {number} opts.total - Total steps
160
+ * @param {string} opts.label - Label text
161
+ * @param {number} [opts.width=20] - Bar width
162
+ * @returns {string} Formatted progress line
163
+ */
164
+ function formatProgressBar(opts) {
165
+ const { current, total, label, width = 20 } = opts;
166
+ const pct = total > 0 ? Math.round((current / total) * 100) : 0;
167
+ const filled = total > 0 ? Math.round((current / total) * width) : 0;
168
+ const empty = width - filled;
169
+
170
+ const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(empty);
171
+ return `[${bar}] ${pct}% ${label} (${current}/${total})`;
172
+ }
173
+
174
+ /**
175
+ * Format a multi-level progress display for conversation output.
176
+ *
177
+ * @param {Object} opts
178
+ * @param {string} opts.command - Command name
179
+ * @param {string} opts.phase - Current phase
180
+ * @param {number} opts.phaseNum
181
+ * @param {number} opts.totalPhases
182
+ * @param {string} [opts.step] - Current step within phase
183
+ * @param {number} [opts.stepNum]
184
+ * @param {number} [opts.totalSteps]
185
+ * @returns {string} Multi-line progress display
186
+ */
187
+ function formatProgress(opts) {
188
+ const lines = [];
189
+ const pct = calculatePercentage(opts);
190
+
191
+ // Phase-level bar
192
+ lines.push(formatProgressBar({
193
+ current: opts.phaseNum,
194
+ total: opts.totalPhases,
195
+ label: opts.phase
196
+ }));
197
+
198
+ // Step-level detail (if applicable)
199
+ if (opts.step && opts.totalSteps) {
200
+ lines.push(` ${opts.step} (${opts.stepNum}/${opts.totalSteps})`);
201
+ }
202
+
203
+ return lines.join('\n');
204
+ }
205
+
206
+ // ============================================================
207
+ // CLI Interface
208
+ // ============================================================
209
+
210
+ function main() {
211
+ const command = process.argv[2];
212
+ const arg = process.argv[3];
213
+
214
+ switch (command) {
215
+ case 'update': {
216
+ if (!arg) {
217
+ console.error('Usage: flow-progress-tracker.js update \'{"taskId":"...","phase":"...","phaseNum":1,"totalPhases":5}\'');
218
+ process.exit(1);
219
+ }
220
+ const progress = safeJsonParseString(arg, null);
221
+ if (!progress) {
222
+ console.error('Invalid JSON argument');
223
+ process.exit(1);
224
+ }
225
+ const result = updateProgress(progress);
226
+ console.log(JSON.stringify(result, null, 2));
227
+ break;
228
+ }
229
+
230
+ case 'get': {
231
+ const state = getProgress();
232
+ if (state) {
233
+ console.log(JSON.stringify(state, null, 2));
234
+ } else {
235
+ console.log(JSON.stringify({ active: false }));
236
+ }
237
+ break;
238
+ }
239
+
240
+ case 'clear': {
241
+ const result = clearProgress();
242
+ console.log(JSON.stringify(result, null, 2));
243
+ break;
244
+ }
245
+
246
+ case 'format': {
247
+ if (!arg) {
248
+ console.error('Usage: flow-progress-tracker.js format \'{"current":2,"total":5,"label":"Phase"}\'');
249
+ process.exit(1);
250
+ }
251
+ const opts = safeJsonParseString(arg, null);
252
+ if (!opts) {
253
+ console.error('Invalid JSON argument');
254
+ process.exit(1);
255
+ }
256
+ console.log(formatProgressBar(opts));
257
+ break;
258
+ }
259
+
260
+ default:
261
+ console.log(`
262
+ Wogi Flow - Progress Tracker
263
+
264
+ Usage: flow-progress-tracker.js <command> [args]
265
+
266
+ Commands:
267
+ update <json> Update progress state + task title
268
+ get Get current progress state
269
+ clear Clear progress state
270
+ format <json> Format a progress bar string
271
+
272
+ Update format:
273
+ {"taskId":"wf-xxx","command":"/wogi-review","phase":"AI Review","phaseNum":2,"totalPhases":5}
274
+ `);
275
+ }
276
+ }
277
+
278
+ module.exports = {
279
+ updateProgress,
280
+ getProgress,
281
+ clearProgress,
282
+ formatProgressBar,
283
+ formatProgress,
284
+ calculatePercentage
285
+ };
286
+
287
+ if (require.main === module) {
288
+ main();
289
+ }