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

valksor / php-dev-snapshot / 19705546221

26 Nov 2025 01:33PM UTC coverage: 49.15% (-12.4%) from 61.538%
19705546221

push

github

k0d3r1s
generic binary provider

376 of 765 relevant lines covered (49.15%)

2.11 hits per line

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

57.14
/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 date;
25
use function end;
26
use function explode;
27
use function implode;
28
use function in_array;
29
use function json_encode;
30
use function ksort;
31
use function round;
32
use function str_repeat;
33
use function strtolower;
34
use function strtoupper;
35

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

181
        return implode("\n", $metadata);
8✔
182
    }
183

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

195
        $seenDirs = [];
8✔
196

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

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

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

214
        $structure[] = '```';
8✔
215
        $structure[] = '';
8✔
216

217
        return implode("\n", $structure);
8✔
218
    }
219

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

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

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

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

253
        ksort($extStats);
8✔
254

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

260
        $summary[] = '';
8✔
261

262
        return implode("\n", $summary);
8✔
263
    }
264

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

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

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

345
        $ext = strtolower(end($parts));
5✔
346

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

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

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

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

415
        return $filesByExt;
8✔
416
    }
417
}
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