• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

valksor / php-dev-snapshot / 21323338102

28 Dec 2025 12:41AM UTC coverage: 49.084% (-0.07%) from 49.15%
21323338102

push

github

k0d3r1s
funding coffee

375 of 764 relevant lines covered (49.08%)

2.1 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

56.92
/Util/OutputGenerator.php
1
<?php declare(strict_types = 1);
2

3
/*
4
 * This file is part of the Valksor package.
5
 *
6
 * (c) Davis Zalitis (k0d3r1s)
7
 * (c) SIA Valksor <packages@valksor.com>
8
 *
9
 * For the full copyright and license information, please view the LICENSE
10
 * file that was distributed with this source code.
11
 */
12

13
namespace ValksorDev\Snapshot\Util;
14

15
use JsonException;
16

17
use function array_column;
18
use function array_map;
19
use function array_slice;
20
use function array_sum;
21
use function arsort;
22
use function basename;
23
use function count;
24
use function end;
25
use function explode;
26
use function implode;
27
use function in_array;
28
use function json_encode;
29
use function ksort;
30
use function round;
31
use function str_repeat;
32
use function strtolower;
33
use function strtoupper;
34

35
use const JSON_PRETTY_PRINT;
36
use const JSON_THROW_ON_ERROR;
37
use const JSON_UNESCAPED_SLASHES;
38

39
/**
40
 * MCP (Markdown Context Pack) output generator for snapshot functionality.
41
 *
42
 * This utility generates AI-optimized markdown output that provides a comprehensive
43
 * overview of project structure and file contents. The output format is specifically
44
 * designed for consumption by AI assistants and code analysis tools.
45
 *
46
 * Output Features:
47
 * - Structured markdown with metadata headers
48
 * - Project hierarchy visualization
49
 * - File grouping by language/extension
50
 * - Syntax-highlighted code blocks
51
 * - Comprehensive statistics and breakdowns
52
 * - AI-optimized formatting for context understanding
53
 *
54
 * Sections Generated:
55
 * 1. Project header and description
56
 * 2. MCP metadata in JSON format
57
 * 3. Directory structure tree
58
 * 4. File contents grouped by language
59
 * 5. Statistical summary and breakdown
60
 *
61
 * Language Support:
62
 * - 25+ programming languages and file types
63
 * - Automatic language detection from file extensions
64
 * - Syntax highlighting hints for markdown renderers
65
 * - Specialized handling for configuration files
66
 */
67
final class OutputGenerator
68
{
69
    /**
70
     * Generate MCP (Markdown Context Pack) format output.
71
     *
72
     * Creates a comprehensive markdown document containing project structure,
73
     * file contents organized by language, and detailed statistics. The output
74
     * is optimized for AI consumption with proper formatting and metadata.
75
     *
76
     * @param string $projectName Name of the project being snapshotted
77
     * @param array  $files       Array of file data with 'relative_path', 'content', 'size' keys
78
     * @param array  $stats       Statistics array with 'files_processed' and 'total_size' keys
79
     *
80
     * @throws JsonException
81
     */
82
    public static function generate(
83
        string $projectName,
84
        array $files,
85
        array $stats,
86
    ): string {
87
        $output = [];
8✔
88

89
        // Header section
90
        $output[] = self::generateHeader($projectName);
8✔
91

92
        // Metadata section
93
        $output[] = self::generateMetadata($stats);
8✔
94

95
        // Project structure section
96
        $output[] = self::generateProjectStructure($files);
8✔
97

98
        // File contents section
99
        $output[] = self::generateFileContents($files);
8✔
100

101
        // Summary section
102
        $output[] = self::generateSummary($files, $stats);
8✔
103

104
        return implode("\n", $output);
8✔
105
    }
106

107
    /**
108
     * Generate the file contents section grouped by language.
109
     */
110
    private static function generateFileContents(
111
        array $files,
112
    ): string {
113
        $contents = [];
8✔
114
        $contents[] = '## Files';
8✔
115
        $contents[] = '';
8✔
116

117
        // Group files by extension for organized output
118
        $filesByExt = self::groupFilesByExtension($files);
8✔
119

120
        // Sort extensions by file count (most files first for better visibility)
121
        $fileCounts = array_map('count', $filesByExt);
8✔
122
        arsort($fileCounts);
8✔
123

124
        foreach ($fileCounts as $ext => $count) {
8✔
125
            $lang = self::getExtensionLanguage($ext);
5✔
126
            $contents[] = "### $lang Files";
5✔
127
            $contents[] = '';
5✔
128

129
            foreach ($filesByExt[$ext] as $fileInfo) {
5✔
130
                $contents[] = "#### {$fileInfo['relative_path']}";
5✔
131
                $contents[] = '';
5✔
132
                $contents[] = "```$ext";
5✔
133
                $contents[] = $fileInfo['content'];
5✔
134
                $contents[] = '```';
5✔
135
                $contents[] = '';
5✔
136
            }
137
        }
138

139
        return implode("\n", $contents);
8✔
140
    }
141

142
    /**
143
     * Generate the project header section.
144
     */
145
    private static function generateHeader(
146
        string $projectName,
147
    ): string {
148
        $header = [];
8✔
149
        $header[] = "# $projectName";
8✔
150
        $header[] = '';
8✔
151
        $header[] = 'Project snapshot generated for AI analysis and code review.';
8✔
152
        $header[] = '';
8✔
153

154
        return implode("\n", $header);
8✔
155
    }
156

157
    /**
158
     * Generate the MCP metadata section.
159
     *
160
     * @throws JsonException
161
     */
162
    private static function generateMetadata(
163
        array $stats,
164
    ): string {
165
        $metadata = [];
8✔
166
        $metadata[] = '```mcp-metadata';
8✔
167

168
        $metadataJson = [
8✔
169
            'format_version' => '1.0.0',
8✔
170
            'num_files' => $stats['files_processed'],
8✔
171
            'total_size_kb' => round($stats['total_size'] / 1024, 2),
8✔
172
            'generator' => 'Valksor Snapshot Command',
8✔
173
        ];
8✔
174

175
        $metadata[] = json_encode($metadataJson, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
8✔
176
        $metadata[] = '```';
8✔
177
        $metadata[] = '';
8✔
178

179
        return implode("\n", $metadata);
8✔
180
    }
181

182
    /**
183
     * Generate the project structure tree section.
184
     */
185
    private static function generateProjectStructure(
186
        array $files,
187
    ): string {
188
        $structure = [];
8✔
189
        $structure[] = '## Project Structure';
8✔
190
        $structure[] = '';
8✔
191
        $structure[] = '```';
8✔
192

193
        $seenDirs = [];
8✔
194

195
        foreach ($files as $fileInfo) {
8✔
196
            $relativePath = $fileInfo['relative_path'];
5✔
197
            $parts = explode('/', $relativePath);
5✔
198

199
            foreach ($parts as $i => $iValue) {
5✔
200
                $dirPath = implode('/', array_slice($parts, 0, $i + 1));
5✔
201

202
                if (!in_array($dirPath, $seenDirs, true)) {
5✔
203
                    $indent = str_repeat('  ', $i);
5✔
204
                    $isFile = $i === count($parts) - 1;
5✔
205
                    $symbol = $isFile ? '' : '/';
5✔
206
                    $structure[] = "$indent$iValue$symbol";
5✔
207
                    $seenDirs[] = $dirPath;
5✔
208
                }
209
            }
210
        }
211

212
        $structure[] = '```';
8✔
213
        $structure[] = '';
8✔
214

215
        return implode("\n", $structure);
8✔
216
    }
217

218
    /**
219
     * Generate the summary statistics section.
220
     */
221
    private static function generateSummary(
222
        array $files,
223
        array $stats,
224
    ): string {
225
        $summary = [];
8✔
226
        $summary[] = '## Summary';
8✔
227
        $summary[] = '';
8✔
228
        $summary[] = '### Statistics';
8✔
229
        $summary[] = '';
8✔
230
        $summary[] = "- **Total files**: {$stats['files_processed']}";
8✔
231
        $summary[] = '- **Total size**: ' . round($stats['total_size'] / 1024, 2) . ' KB';
8✔
232
        $summary[] = '';
8✔
233

234
        // File breakdown by type
235
        $summary[] = '### File Breakdown';
8✔
236
        $summary[] = '';
8✔
237
        $summary[] = '| Language | Extension | Files | Size (KB) |';
8✔
238
        $summary[] = '|----------|-----------|-------|-----------|';
8✔
239

240
        $filesByExt = self::groupFilesByExtension($files);
8✔
241
        $extStats = [];
8✔
242

243
        foreach ($filesByExt as $ext => $extFiles) {
8✔
244
            $totalSize = array_sum(array_column($extFiles, 'size'));
5✔
245
            $extStats[$ext] = [
5✔
246
                'count' => count($extFiles),
5✔
247
                'size_kb' => round($totalSize / 1024, 2),
5✔
248
            ];
5✔
249
        }
250

251
        ksort($extStats);
8✔
252

253
        foreach ($extStats as $ext => $stat) {
8✔
254
            $lang = self::getExtensionLanguage($ext);
5✔
255
            $summary[] = "| $lang | `$ext` | {$stat['count']} | {$stat['size_kb']} |";
5✔
256
        }
257

258
        $summary[] = '';
8✔
259

260
        return implode("\n", $summary);
8✔
261
    }
262

263
    /**
264
     * Get human-readable language name from file extension.
265
     *
266
     * Maps file extensions to their corresponding programming languages
267
     * or file types for display in the output.
268
     *
269
     * @param string $ext File extension
270
     */
271
    private static function getExtensionLanguage(
272
        string $ext,
273
    ): string {
274
        return match ($ext) {
5✔
275
            'php' => 'PHP',
5✔
276
            'javascript', 'js' => 'JavaScript',
×
277
            'typescript', 'ts' => 'TypeScript',
×
278
            'python', 'py' => 'Python',
×
279
            'html', 'htm' => 'HTML',
×
280
            'css' => 'CSS',
×
281
            'scss' => 'SCSS',
×
282
            'sass' => 'Sass',
×
283
            'less' => 'Less',
×
284
            'json' => 'JSON',
×
285
            'xml' => 'XML',
×
286
            'yaml', 'yml' => 'YAML',
1✔
287
            'markdown', 'md' => 'Markdown',
1✔
288
            'sql' => 'SQL',
×
289
            'bash', 'sh', 'zsh', 'fish' => 'Shell',
×
290
            'txt' => 'Text',
×
291
            'log' => 'Log',
×
292
            'ini' => 'INI',
×
293
            'conf' => 'Config',
×
294
            'dockerfile' => 'Docker',
×
295
            'gitignore' => 'Git',
×
296
            'eslintrc' => 'ESLint',
×
297
            'prettierrc' => 'Prettier',
×
298
            'editorconfig' => 'EditorConfig',
×
299
            'vue' => 'Vue',
×
300
            'svelte' => 'Svelte',
×
301
            'jsx', 'tsx' => 'React',
×
302
            'go' => 'Go',
×
303
            'rs' => 'Rust',
×
304
            'java' => 'Java',
×
305
            'kt' => 'Kotlin',
×
306
            'swift' => 'Swift',
×
307
            'rb' => 'Ruby',
×
308
            'scala' => 'Scala',
×
309
            'clj' => 'Clojure',
×
310
            'hs' => 'Haskell',
×
311
            'ml' => 'OCaml',
×
312
            'elm' => 'Elm',
×
313
            'dart' => 'Dart',
×
314
            'lua' => 'Lua',
×
315
            'r' => 'R',
×
316
            'm' => 'Objective-C',
×
317
            'pl' => 'Perl',
×
318
            'tcl' => 'Tcl',
×
319
            'vim' => 'Vim',
×
320
            'emacs' => 'Emacs Lisp',
×
321
            default => strtoupper($ext),
5✔
322
        };
5✔
323
    }
324

325
    /**
326
     * Get normalized file extension from file path.
327
     *
328
     * Extracts and normalizes file extensions, handling special cases
329
     * and mapping common variations to standard forms.
330
     *
331
     * @param string $path File path
332
     */
333
    private static function getFileExtension(
334
        string $path,
335
    ): string {
336
        $basename = basename($path);
5✔
337
        $parts = explode('.', $basename);
5✔
338

339
        if (1 === count($parts)) {
5✔
340
            return 'txt';
×
341
        }
342

343
        $ext = strtolower(end($parts));
5✔
344

345
        // Handle special cases and common mappings
346
        return match ($ext) {
5✔
347
            'js' => 'javascript',
×
348
            'jsx' => 'jsx',
×
349
            'ts' => 'typescript',
×
350
            'tsx' => 'tsx',
×
351
            'py' => 'python',
×
352
            'php' => 'php',
5✔
353
            'html', 'htm' => 'html',
×
354
            'css' => 'css',
×
355
            'scss' => 'scss',
×
356
            'sass' => 'sass',
×
357
            'less' => 'less',
×
358
            'json' => 'json',
×
359
            'xml' => 'xml',
×
360
            'yaml', 'yml' => 'yaml',
1✔
361
            'md' => 'markdown',
1✔
362
            'sql' => 'sql',
×
363
            'sh', 'bash', 'zsh', 'fish' => 'bash',
×
364
            'vue' => 'vue',
×
365
            'svelte' => 'svelte',
×
366
            'go' => 'go',
×
367
            'rs' => 'rust',
×
368
            'java' => 'java',
×
369
            'kt' => 'kt',
×
370
            'swift' => 'swift',
×
371
            'rb' => 'ruby',
×
372
            'scala' => 'scala',
×
373
            'clj' => 'clj',
×
374
            'hs' => 'hs',
×
375
            'ml' => 'ml',
×
376
            'elm' => 'elm',
×
377
            'dart' => 'dart',
×
378
            'lua' => 'lua',
×
379
            'r' => 'r',
×
380
            'm' => 'm',
×
381
            'pl' => 'pl',
×
382
            'tcl' => 'tcl',
×
383
            'vim' => 'vim',
×
384
            'el' => 'emacs',
×
385
            'dockerfile' => 'dockerfile',
×
386
            'gitignore' => 'gitignore',
×
387
            'eslintrc' => 'eslintrc',
×
388
            'prettierrc' => 'prettierrc',
×
389
            'editorconfig' => 'editorconfig',
×
390
            default => $ext,
5✔
391
        };
5✔
392
    }
393

394
    /**
395
     * Group files by their extension.
396
     *
397
     * @return array<string, array>
398
     */
399
    private static function groupFilesByExtension(
400
        array $files,
401
    ): array {
402
        $filesByExt = [];
8✔
403

404
        foreach ($files as $fileInfo) {
8✔
405
            $ext = self::getFileExtension($fileInfo['relative_path']);
5✔
406

407
            if (!isset($filesByExt[$ext])) {
5✔
408
                $filesByExt[$ext] = [];
5✔
409
            }
410
            $filesByExt[$ext][] = $fileInfo;
5✔
411
        }
412

413
        return $filesByExt;
8✔
414
    }
415
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc