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

valksor / php-dev-snapshot / 19634210989

24 Nov 2025 12:21PM UTC coverage: 61.538%. First build
19634210989

push

github

k0d3r1s
add valksor-dev snapshot

336 of 546 new or added lines in 4 files covered. (61.54%)

336 of 546 relevant lines covered (61.54%)

2.88 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 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

34
use const JSON_PRETTY_PRINT;
35
use const JSON_UNESCAPED_SLASHES;
36

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

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

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

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

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

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

102
        return implode("\n", $output);
8✔
103
    }
104

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

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

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

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

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

137
        return implode("\n", $contents);
8✔
138
    }
139

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

152
        return implode("\n", $header);
8✔
153
    }
154

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

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

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

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

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

192
        $seenDirs = [];
8✔
193

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

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

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

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

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

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

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

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

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

250
        ksort($extStats);
8✔
251

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

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

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

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

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

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

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

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

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

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

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

412
        return $filesByExt;
8✔
413
    }
414
}
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