aios-core 3.1.0 → 3.3.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.
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Validator - Verifies IDE sync status
3
+ * @story 6.19 - IDE Command Auto-Sync System
4
+ */
5
+
6
+ const fs = require('fs-extra');
7
+ const path = require('path');
8
+ const crypto = require('crypto');
9
+ const { getRedirectFilenames } = require('./redirect-generator');
10
+
11
+ /**
12
+ * Calculate content hash for comparison
13
+ * @param {string} content - File content
14
+ * @returns {string} - SHA256 hash
15
+ */
16
+ function hashContent(content) {
17
+ // Normalize line endings for cross-platform consistency
18
+ const normalized = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
19
+ return crypto.createHash('sha256').update(normalized, 'utf8').digest('hex');
20
+ }
21
+
22
+ /**
23
+ * Check if a file exists at target path
24
+ * @param {string} filePath - Path to check
25
+ * @returns {boolean} - True if file exists
26
+ */
27
+ function fileExists(filePath) {
28
+ return fs.existsSync(filePath);
29
+ }
30
+
31
+ /**
32
+ * Read file content if it exists
33
+ * @param {string} filePath - Path to read
34
+ * @returns {string|null} - File content or null
35
+ */
36
+ function readFileIfExists(filePath) {
37
+ try {
38
+ if (fs.existsSync(filePath)) {
39
+ return fs.readFileSync(filePath, 'utf8');
40
+ }
41
+ } catch (error) {
42
+ // Ignore read errors
43
+ }
44
+ return null;
45
+ }
46
+
47
+ /**
48
+ * Validate sync status for a single IDE
49
+ * @param {object[]} expectedFiles - Array of {filename, content} expected
50
+ * @param {string} targetDir - Target directory to check
51
+ * @param {object} redirectsConfig - Redirects configuration
52
+ * @returns {object} - Validation result
53
+ */
54
+ function validateIdeSync(expectedFiles, targetDir, redirectsConfig) {
55
+ const result = {
56
+ targetDir,
57
+ missing: [],
58
+ drift: [],
59
+ orphaned: [],
60
+ synced: [],
61
+ total: {
62
+ expected: expectedFiles.length,
63
+ missing: 0,
64
+ drift: 0,
65
+ orphaned: 0,
66
+ synced: 0,
67
+ },
68
+ };
69
+
70
+ // Track expected filenames
71
+ const expectedFilenames = new Set(expectedFiles.map(f => f.filename));
72
+
73
+ // Add redirect filenames to expected
74
+ const redirectFilenames = getRedirectFilenames(redirectsConfig);
75
+ for (const rf of redirectFilenames) {
76
+ expectedFilenames.add(rf);
77
+ }
78
+
79
+ // Check each expected file
80
+ for (const expected of expectedFiles) {
81
+ const targetPath = path.join(targetDir, expected.filename);
82
+ const actualContent = readFileIfExists(targetPath);
83
+
84
+ if (actualContent === null) {
85
+ // File is missing
86
+ result.missing.push({
87
+ filename: expected.filename,
88
+ path: targetPath,
89
+ });
90
+ result.total.missing++;
91
+ } else {
92
+ // Compare content
93
+ const expectedHash = hashContent(expected.content);
94
+ const actualHash = hashContent(actualContent);
95
+
96
+ if (expectedHash !== actualHash) {
97
+ // Content differs (drift)
98
+ result.drift.push({
99
+ filename: expected.filename,
100
+ path: targetPath,
101
+ expectedHash,
102
+ actualHash,
103
+ });
104
+ result.total.drift++;
105
+ } else {
106
+ // File is synced
107
+ result.synced.push({
108
+ filename: expected.filename,
109
+ path: targetPath,
110
+ });
111
+ result.total.synced++;
112
+ }
113
+ }
114
+ }
115
+
116
+ // Check for orphaned files (files in target not in expected)
117
+ if (fs.existsSync(targetDir)) {
118
+ try {
119
+ const actualFiles = fs.readdirSync(targetDir).filter(f => f.endsWith('.md'));
120
+
121
+ for (const file of actualFiles) {
122
+ if (!expectedFilenames.has(file)) {
123
+ result.orphaned.push({
124
+ filename: file,
125
+ path: path.join(targetDir, file),
126
+ });
127
+ result.total.orphaned++;
128
+ }
129
+ }
130
+ } catch (error) {
131
+ // Ignore directory read errors
132
+ }
133
+ }
134
+
135
+ return result;
136
+ }
137
+
138
+ /**
139
+ * Validate sync status for all IDEs
140
+ * @param {object} ideConfigs - Map of IDE name to {expectedFiles, targetDir}
141
+ * @param {object} redirectsConfig - Redirects configuration
142
+ * @returns {object} - Full validation result
143
+ */
144
+ function validateAllIdes(ideConfigs, redirectsConfig) {
145
+ const results = {
146
+ ides: {},
147
+ summary: {
148
+ total: 0,
149
+ synced: 0,
150
+ missing: 0,
151
+ drift: 0,
152
+ orphaned: 0,
153
+ pass: true,
154
+ },
155
+ };
156
+
157
+ for (const [ideName, config] of Object.entries(ideConfigs)) {
158
+ const ideResult = validateIdeSync(
159
+ config.expectedFiles,
160
+ config.targetDir,
161
+ redirectsConfig
162
+ );
163
+
164
+ results.ides[ideName] = ideResult;
165
+
166
+ // Update summary
167
+ results.summary.total += ideResult.total.expected;
168
+ results.summary.synced += ideResult.total.synced;
169
+ results.summary.missing += ideResult.total.missing;
170
+ results.summary.drift += ideResult.total.drift;
171
+ results.summary.orphaned += ideResult.total.orphaned;
172
+ }
173
+
174
+ // Pass if no missing or drift
175
+ results.summary.pass =
176
+ results.summary.missing === 0 && results.summary.drift === 0;
177
+
178
+ return results;
179
+ }
180
+
181
+ /**
182
+ * Format validation result as report string
183
+ * @param {object} results - Validation results
184
+ * @param {boolean} verbose - Include detailed file lists
185
+ * @returns {string} - Formatted report
186
+ */
187
+ function formatValidationReport(results, verbose = false) {
188
+ const lines = [];
189
+
190
+ lines.push('# IDE Sync Validation Report');
191
+ lines.push('');
192
+
193
+ // Summary
194
+ lines.push('## Summary');
195
+ lines.push('');
196
+ lines.push(`| Metric | Count |`);
197
+ lines.push(`|--------|-------|`);
198
+ lines.push(`| Total Expected | ${results.summary.total} |`);
199
+ lines.push(`| Synced | ${results.summary.synced} |`);
200
+ lines.push(`| Missing | ${results.summary.missing} |`);
201
+ lines.push(`| Drift | ${results.summary.drift} |`);
202
+ lines.push(`| Orphaned | ${results.summary.orphaned} |`);
203
+ lines.push('');
204
+
205
+ const status = results.summary.pass ? '✅ PASS' : '❌ FAIL';
206
+ lines.push(`**Status:** ${status}`);
207
+ lines.push('');
208
+
209
+ // Per-IDE details
210
+ if (verbose) {
211
+ lines.push('## IDE Details');
212
+ lines.push('');
213
+
214
+ for (const [ideName, ideResult] of Object.entries(results.ides)) {
215
+ lines.push(`### ${ideName}`);
216
+ lines.push('');
217
+ lines.push(`- Target: \`${ideResult.targetDir}\``);
218
+ lines.push(`- Synced: ${ideResult.total.synced}`);
219
+ lines.push(`- Missing: ${ideResult.total.missing}`);
220
+ lines.push(`- Drift: ${ideResult.total.drift}`);
221
+ lines.push(`- Orphaned: ${ideResult.total.orphaned}`);
222
+ lines.push('');
223
+
224
+ if (ideResult.missing.length > 0) {
225
+ lines.push('**Missing Files:**');
226
+ for (const f of ideResult.missing) {
227
+ lines.push(`- ${f.filename}`);
228
+ }
229
+ lines.push('');
230
+ }
231
+
232
+ if (ideResult.drift.length > 0) {
233
+ lines.push('**Drifted Files:**');
234
+ for (const f of ideResult.drift) {
235
+ lines.push(`- ${f.filename}`);
236
+ }
237
+ lines.push('');
238
+ }
239
+
240
+ if (ideResult.orphaned.length > 0) {
241
+ lines.push('**Orphaned Files:**');
242
+ for (const f of ideResult.orphaned) {
243
+ lines.push(`- ${f.filename}`);
244
+ }
245
+ lines.push('');
246
+ }
247
+ }
248
+ }
249
+
250
+ // Fix instructions
251
+ if (!results.summary.pass) {
252
+ lines.push('## How to Fix');
253
+ lines.push('');
254
+ lines.push('Run the following command to sync IDE files:');
255
+ lines.push('');
256
+ lines.push('```bash');
257
+ lines.push('npm run sync:ide');
258
+ lines.push('```');
259
+ lines.push('');
260
+ lines.push('Then commit the generated files.');
261
+ }
262
+
263
+ return lines.join('\n');
264
+ }
265
+
266
+ module.exports = {
267
+ hashContent,
268
+ fileExists,
269
+ readFileIfExists,
270
+ validateIdeSync,
271
+ validateAllIdes,
272
+ formatValidationReport,
273
+ };