@unrdf/project-engine 5.0.1 → 26.4.2
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.
- package/package.json +16 -15
- package/src/golden-structure.mjs +2 -2
- package/src/materialize-apply.mjs +2 -2
- package/README.md +0 -53
- package/src/api-contract-validator.mjs +0 -711
- package/src/auto-test-generator.mjs +0 -444
- package/src/autonomic-mapek.mjs +0 -511
- package/src/capabilities-manifest.mjs +0 -125
- package/src/code-complexity-js.mjs +0 -368
- package/src/dependency-graph.mjs +0 -276
- package/src/doc-drift-checker.mjs +0 -172
- package/src/doc-generator.mjs +0 -229
- package/src/domain-infer.mjs +0 -966
- package/src/drift-snapshot.mjs +0 -775
- package/src/file-roles.mjs +0 -94
- package/src/fs-scan.mjs +0 -305
- package/src/gap-finder.mjs +0 -376
- package/src/hotspot-analyzer.mjs +0 -412
- package/src/index.mjs +0 -151
- package/src/initialize.mjs +0 -957
- package/src/lens/project-structure.mjs +0 -74
- package/src/mapek-orchestration.mjs +0 -665
- package/src/materialize-plan.mjs +0 -422
- package/src/materialize.mjs +0 -137
- package/src/policy-derivation.mjs +0 -869
- package/src/project-config.mjs +0 -142
- package/src/project-diff.mjs +0 -28
- package/src/project-engine/build-utils.mjs +0 -237
- package/src/project-engine/code-analyzer.mjs +0 -248
- package/src/project-engine/doc-generator.mjs +0 -407
- package/src/project-engine/infrastructure.mjs +0 -213
- package/src/project-engine/metrics.mjs +0 -146
- package/src/project-model.mjs +0 -111
- package/src/project-report.mjs +0 -348
- package/src/refactoring-guide.mjs +0 -242
- package/src/stack-detect.mjs +0 -102
- package/src/stack-linter.mjs +0 -213
- package/src/template-infer.mjs +0 -674
- package/src/type-auditor.mjs +0 -609
package/src/type-auditor.mjs
DELETED
|
@@ -1,609 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Type-safety auditor - validates Zod schemas match TypeScript types
|
|
3
|
-
* @module project-engine/type-auditor
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { promises as fs } from 'fs';
|
|
7
|
-
import path from 'path';
|
|
8
|
-
import { z } from 'zod';
|
|
9
|
-
|
|
10
|
-
/* ========================================================================= */
|
|
11
|
-
/* Zod Schemas */
|
|
12
|
-
/* ========================================================================= */
|
|
13
|
-
|
|
14
|
-
const FieldInfoSchema = z.object({
|
|
15
|
-
name: z.string(),
|
|
16
|
-
type: z.string(),
|
|
17
|
-
optional: z.boolean(),
|
|
18
|
-
array: z.boolean().default(false),
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
const EntityFieldsSchema = z.object({
|
|
22
|
-
file: z.string(),
|
|
23
|
-
fields: z.record(FieldInfoSchema),
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
const MismatchSchema = z.object({
|
|
27
|
-
entity: z.string(),
|
|
28
|
-
zod: EntityFieldsSchema.optional(),
|
|
29
|
-
typescript: EntityFieldsSchema.optional(),
|
|
30
|
-
issues: z.array(z.string()),
|
|
31
|
-
severity: z.enum(['low', 'medium', 'high', 'critical']),
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const AuditResultSchema = z.object({
|
|
35
|
-
mismatches: z.array(MismatchSchema),
|
|
36
|
-
summary: z.string(),
|
|
37
|
-
recommendation: z.string(),
|
|
38
|
-
score: z.number().min(0).max(100),
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
const AuditOptionsSchema = z.object({
|
|
42
|
-
domainStore: z.any().optional(),
|
|
43
|
-
fsStore: z.any().optional(),
|
|
44
|
-
stackProfile: z
|
|
45
|
-
.object({
|
|
46
|
-
hasZod: z.boolean().default(false),
|
|
47
|
-
hasTypescript: z.boolean().default(false),
|
|
48
|
-
})
|
|
49
|
-
.passthrough()
|
|
50
|
-
.optional(),
|
|
51
|
-
projectRoot: z.string().optional(),
|
|
52
|
-
schemaDir: z.string().default('src/schemas'),
|
|
53
|
-
typesDir: z.string().default('src/types'),
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const CompareTypesResultSchema = z.object({
|
|
57
|
-
added: z.array(z.string()),
|
|
58
|
-
removed: z.array(z.string()),
|
|
59
|
-
modified: z.array(
|
|
60
|
-
z.object({
|
|
61
|
-
field: z.string(),
|
|
62
|
-
zodInfo: FieldInfoSchema.optional(),
|
|
63
|
-
tsInfo: FieldInfoSchema.optional(),
|
|
64
|
-
issue: z.string(),
|
|
65
|
-
})
|
|
66
|
-
),
|
|
67
|
-
consistent: z.array(z.string()),
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* @typedef {z.infer<typeof FieldInfoSchema>} FieldInfo
|
|
72
|
-
* @typedef {z.infer<typeof EntityFieldsSchema>} EntityFields
|
|
73
|
-
* @typedef {z.infer<typeof MismatchSchema>} Mismatch
|
|
74
|
-
* @typedef {z.infer<typeof AuditResultSchema>} AuditResult
|
|
75
|
-
* @typedef {z.infer<typeof CompareTypesResultSchema>} CompareTypesResult
|
|
76
|
-
*/
|
|
77
|
-
|
|
78
|
-
/* ========================================================================= */
|
|
79
|
-
/* File reading utilities */
|
|
80
|
-
/* ========================================================================= */
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Read file content from filesystem
|
|
84
|
-
* @param {string} filePath
|
|
85
|
-
* @returns {Promise<string|null>}
|
|
86
|
-
*/
|
|
87
|
-
async function readFileContent(filePath) {
|
|
88
|
-
try {
|
|
89
|
-
return await fs.readFile(filePath, 'utf-8');
|
|
90
|
-
} catch {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Find files in directory matching pattern
|
|
97
|
-
* @param {string} dirPath
|
|
98
|
-
* @param {string[]} extensions
|
|
99
|
-
* @returns {Promise<string[]>}
|
|
100
|
-
*/
|
|
101
|
-
async function findFilesInDir(dirPath, extensions = ['.ts', '.tsx', '.mjs', '.js']) {
|
|
102
|
-
try {
|
|
103
|
-
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
104
|
-
const files = [];
|
|
105
|
-
|
|
106
|
-
for (const entry of entries) {
|
|
107
|
-
if (entry.isFile()) {
|
|
108
|
-
const ext = path.extname(entry.name);
|
|
109
|
-
if (extensions.includes(ext)) {
|
|
110
|
-
files.push(path.join(dirPath, entry.name));
|
|
111
|
-
}
|
|
112
|
-
} else if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
113
|
-
const subFiles = await findFilesInDir(path.join(dirPath, entry.name), extensions);
|
|
114
|
-
files.push(...subFiles);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return files;
|
|
119
|
-
} catch {
|
|
120
|
-
return [];
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/* ========================================================================= */
|
|
125
|
-
/* Zod schema parsing */
|
|
126
|
-
/* ========================================================================= */
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Parse Zod schema fields from file content
|
|
130
|
-
* @param {string} content
|
|
131
|
-
* @returns {Map<string, Record<string, FieldInfo>>}
|
|
132
|
-
*/
|
|
133
|
-
function parseZodSchemas(content) {
|
|
134
|
-
const schemas = new Map();
|
|
135
|
-
|
|
136
|
-
// Match: export const XxxSchema = z.object({ ... })
|
|
137
|
-
const schemaPattern =
|
|
138
|
-
/(?:export\s+)?(?:const|let)\s+(\w+)Schema\s*=\s*z\.object\(\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}\s*\)/g;
|
|
139
|
-
|
|
140
|
-
let match;
|
|
141
|
-
while ((match = schemaPattern.exec(content)) !== null) {
|
|
142
|
-
const entityName = match[1];
|
|
143
|
-
const fieldsBlock = match[2];
|
|
144
|
-
const fields = parseZodFieldsDetailed(fieldsBlock);
|
|
145
|
-
|
|
146
|
-
if (Object.keys(fields).length > 0) {
|
|
147
|
-
schemas.set(entityName, fields);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return schemas;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Parse Zod fields with detailed type info
|
|
156
|
-
* @param {string} fieldsBlock
|
|
157
|
-
* @returns {Record<string, FieldInfo>}
|
|
158
|
-
*/
|
|
159
|
-
function parseZodFieldsDetailed(fieldsBlock) {
|
|
160
|
-
/** @type {Record<string, FieldInfo>} */
|
|
161
|
-
const fields = {};
|
|
162
|
-
|
|
163
|
-
// Match field definitions more carefully
|
|
164
|
-
const lines = fieldsBlock
|
|
165
|
-
.split(/[,\n]/)
|
|
166
|
-
.map(l => l.trim())
|
|
167
|
-
.filter(Boolean);
|
|
168
|
-
|
|
169
|
-
for (const line of lines) {
|
|
170
|
-
// Match: fieldName: z.type()
|
|
171
|
-
const fieldMatch = line.match(/^(\w+)\s*:\s*z\.(\w+)/);
|
|
172
|
-
if (!fieldMatch) continue;
|
|
173
|
-
|
|
174
|
-
const fieldName = fieldMatch[1];
|
|
175
|
-
const zodType = fieldMatch[2];
|
|
176
|
-
|
|
177
|
-
// Check for .optional() or .nullable()
|
|
178
|
-
const isOptional = line.includes('.optional()') || line.includes('.nullable()');
|
|
179
|
-
const isArray = zodType === 'array' || line.includes('.array()');
|
|
180
|
-
|
|
181
|
-
// Map zod types
|
|
182
|
-
let type = 'string';
|
|
183
|
-
if (zodType === 'number' || zodType === 'bigint') type = 'number';
|
|
184
|
-
else if (zodType === 'boolean') type = 'boolean';
|
|
185
|
-
else if (zodType === 'date') type = 'date';
|
|
186
|
-
else if (zodType === 'array') type = 'array';
|
|
187
|
-
else if (zodType === 'enum') type = 'enum';
|
|
188
|
-
else if (zodType === 'object') type = 'object';
|
|
189
|
-
|
|
190
|
-
fields[fieldName] = {
|
|
191
|
-
name: fieldName,
|
|
192
|
-
type,
|
|
193
|
-
optional: isOptional,
|
|
194
|
-
array: isArray,
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return fields;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/* ========================================================================= */
|
|
202
|
-
/* TypeScript type parsing */
|
|
203
|
-
/* ========================================================================= */
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Parse TypeScript interfaces and types from file content
|
|
207
|
-
* @param {string} content
|
|
208
|
-
* @returns {Map<string, Record<string, FieldInfo>>}
|
|
209
|
-
*/
|
|
210
|
-
function parseTypeScriptTypes(content) {
|
|
211
|
-
const types = new Map();
|
|
212
|
-
|
|
213
|
-
// Match interface declarations
|
|
214
|
-
const interfacePattern =
|
|
215
|
-
/(?:export\s+)?interface\s+(\w+)(?:\s+extends\s+[\w,\s]+)?\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/g;
|
|
216
|
-
|
|
217
|
-
let match;
|
|
218
|
-
while ((match = interfacePattern.exec(content)) !== null) {
|
|
219
|
-
const typeName = match[1];
|
|
220
|
-
const fieldsBlock = match[2];
|
|
221
|
-
|
|
222
|
-
// Skip utility types
|
|
223
|
-
if (typeName.endsWith('Props') || typeName.endsWith('Options') || typeName.endsWith('Config')) {
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const fields = parseTypeScriptFieldsDetailed(fieldsBlock);
|
|
228
|
-
if (Object.keys(fields).length > 0) {
|
|
229
|
-
types.set(typeName, fields);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Match type declarations
|
|
234
|
-
const typePattern = /(?:export\s+)?type\s+(\w+)\s*=\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/g;
|
|
235
|
-
|
|
236
|
-
while ((match = typePattern.exec(content)) !== null) {
|
|
237
|
-
const typeName = match[1];
|
|
238
|
-
const fieldsBlock = match[2];
|
|
239
|
-
|
|
240
|
-
// Skip utility types
|
|
241
|
-
if (typeName.endsWith('Props') || typeName.endsWith('Options') || typeName.endsWith('Config')) {
|
|
242
|
-
continue;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const fields = parseTypeScriptFieldsDetailed(fieldsBlock);
|
|
246
|
-
if (Object.keys(fields).length > 0) {
|
|
247
|
-
types.set(typeName, fields);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return types;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Parse TypeScript fields with detailed info
|
|
256
|
-
* @param {string} fieldsBlock
|
|
257
|
-
* @returns {Record<string, FieldInfo>}
|
|
258
|
-
*/
|
|
259
|
-
function parseTypeScriptFieldsDetailed(fieldsBlock) {
|
|
260
|
-
/** @type {Record<string, FieldInfo>} */
|
|
261
|
-
const fields = {};
|
|
262
|
-
|
|
263
|
-
// Match: fieldName: Type, fieldName?: Type
|
|
264
|
-
const fieldPattern = /(?:readonly\s+)?(\w+)(\?)?:\s*([^;,\n]+)/g;
|
|
265
|
-
|
|
266
|
-
let match;
|
|
267
|
-
while ((match = fieldPattern.exec(fieldsBlock)) !== null) {
|
|
268
|
-
const fieldName = match[1];
|
|
269
|
-
const isOptional = !!match[2];
|
|
270
|
-
let rawType = match[3].trim();
|
|
271
|
-
|
|
272
|
-
// Check for array types
|
|
273
|
-
const isArray = rawType.endsWith('[]') || rawType.startsWith('Array<');
|
|
274
|
-
|
|
275
|
-
// Clean up the type
|
|
276
|
-
let baseType = rawType
|
|
277
|
-
.replace(/\[\]$/, '')
|
|
278
|
-
.replace(/^Array<(.+)>$/, '$1')
|
|
279
|
-
.replace(/\s*\|\s*null$/, '')
|
|
280
|
-
.replace(/\s*\|\s*undefined$/, '')
|
|
281
|
-
.trim();
|
|
282
|
-
|
|
283
|
-
// Normalize type names
|
|
284
|
-
let type = 'string';
|
|
285
|
-
const normalizedType = baseType.toLowerCase();
|
|
286
|
-
if (normalizedType === 'number' || normalizedType === 'bigint') type = 'number';
|
|
287
|
-
else if (normalizedType === 'boolean') type = 'boolean';
|
|
288
|
-
else if (normalizedType === 'date') type = 'date';
|
|
289
|
-
else if (normalizedType === 'object' || normalizedType.startsWith('{')) type = 'object';
|
|
290
|
-
|
|
291
|
-
fields[fieldName] = {
|
|
292
|
-
name: fieldName,
|
|
293
|
-
type,
|
|
294
|
-
optional: isOptional,
|
|
295
|
-
array: isArray,
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
return fields;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/* ========================================================================= */
|
|
303
|
-
/* Type comparison */
|
|
304
|
-
/* ========================================================================= */
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Compare Zod fields with TypeScript fields
|
|
308
|
-
*
|
|
309
|
-
* @param {Record<string, FieldInfo>} zodFields
|
|
310
|
-
* @param {Record<string, FieldInfo>} tsFields
|
|
311
|
-
* @returns {CompareTypesResult}
|
|
312
|
-
*/
|
|
313
|
-
export function compareTypes(zodFields, tsFields) {
|
|
314
|
-
const zodFieldNames = new Set(Object.keys(zodFields));
|
|
315
|
-
const tsFieldNames = new Set(Object.keys(tsFields));
|
|
316
|
-
|
|
317
|
-
/** @type {string[]} */
|
|
318
|
-
const added = []; // In Zod but not in TS
|
|
319
|
-
/** @type {string[]} */
|
|
320
|
-
const removed = []; // In TS but not in Zod
|
|
321
|
-
/** @type {CompareTypesResult['modified']} */
|
|
322
|
-
const modified = [];
|
|
323
|
-
/** @type {string[]} */
|
|
324
|
-
const consistent = [];
|
|
325
|
-
|
|
326
|
-
// Fields in Zod but not in TS
|
|
327
|
-
for (const fieldName of zodFieldNames) {
|
|
328
|
-
if (!tsFieldNames.has(fieldName)) {
|
|
329
|
-
added.push(fieldName);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Fields in TS but not in Zod
|
|
334
|
-
for (const fieldName of tsFieldNames) {
|
|
335
|
-
if (!zodFieldNames.has(fieldName)) {
|
|
336
|
-
removed.push(fieldName);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Compare common fields
|
|
341
|
-
for (const fieldName of zodFieldNames) {
|
|
342
|
-
if (!tsFieldNames.has(fieldName)) continue;
|
|
343
|
-
|
|
344
|
-
const zodField = zodFields[fieldName];
|
|
345
|
-
const tsField = tsFields[fieldName];
|
|
346
|
-
|
|
347
|
-
const issues = [];
|
|
348
|
-
|
|
349
|
-
// Check optionality mismatch
|
|
350
|
-
if (zodField.optional && !tsField.optional) {
|
|
351
|
-
issues.push(`${fieldName}: optional in Zod, required in TS`);
|
|
352
|
-
} else if (!zodField.optional && tsField.optional) {
|
|
353
|
-
issues.push(`${fieldName}: required in Zod, optional in TS`);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// Check array mismatch
|
|
357
|
-
if (zodField.array !== tsField.array) {
|
|
358
|
-
issues.push(`${fieldName}: array=${zodField.array} in Zod, array=${tsField.array} in TS`);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Check type mismatch (simple check)
|
|
362
|
-
if (zodField.type !== tsField.type) {
|
|
363
|
-
issues.push(`${fieldName}: type=${zodField.type} in Zod, type=${tsField.type} in TS`);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
if (issues.length > 0) {
|
|
367
|
-
modified.push({
|
|
368
|
-
field: fieldName,
|
|
369
|
-
zodInfo: zodField,
|
|
370
|
-
tsInfo: tsField,
|
|
371
|
-
issue: issues.join('; '),
|
|
372
|
-
});
|
|
373
|
-
} else {
|
|
374
|
-
consistent.push(fieldName);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
return CompareTypesResultSchema.parse({
|
|
379
|
-
added,
|
|
380
|
-
removed,
|
|
381
|
-
modified,
|
|
382
|
-
consistent,
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/* ========================================================================= */
|
|
387
|
-
/* Main audit API */
|
|
388
|
-
/* ========================================================================= */
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* Audit type consistency between Zod schemas and TypeScript types
|
|
392
|
-
*
|
|
393
|
-
* @param {Object} options
|
|
394
|
-
* @param {any} [options.domainStore] - Domain model RDF store (optional)
|
|
395
|
-
* @param {any} [options.fsStore] - Filesystem RDF store (optional)
|
|
396
|
-
* @param {Object} [options.stackProfile] - Stack detection profile
|
|
397
|
-
* @param {string} [options.projectRoot] - Project root directory
|
|
398
|
-
* @param {string} [options.schemaDir] - Directory containing Zod schemas
|
|
399
|
-
* @param {string} [options.typesDir] - Directory containing TypeScript types
|
|
400
|
-
* @returns {Promise<AuditResult>}
|
|
401
|
-
*/
|
|
402
|
-
export async function auditTypeConsistency(options) {
|
|
403
|
-
const validated = AuditOptionsSchema.parse(options);
|
|
404
|
-
const { projectRoot, schemaDir, typesDir } = validated;
|
|
405
|
-
|
|
406
|
-
/** @type {Mismatch[]} */
|
|
407
|
-
const mismatches = [];
|
|
408
|
-
|
|
409
|
-
if (!projectRoot) {
|
|
410
|
-
return AuditResultSchema.parse({
|
|
411
|
-
mismatches: [],
|
|
412
|
-
summary: 'No project root specified',
|
|
413
|
-
recommendation: 'Provide projectRoot to enable type auditing',
|
|
414
|
-
score: 100,
|
|
415
|
-
});
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
const schemaPath = path.join(projectRoot, schemaDir);
|
|
419
|
-
const typesPath = path.join(projectRoot, typesDir);
|
|
420
|
-
|
|
421
|
-
// Find all schema files
|
|
422
|
-
const schemaFiles = await findFilesInDir(schemaPath, ['.ts', '.tsx', '.mjs', '.js']);
|
|
423
|
-
const typeFiles = await findFilesInDir(typesPath, ['.ts', '.tsx', '.d.ts']);
|
|
424
|
-
|
|
425
|
-
// Parse all Zod schemas
|
|
426
|
-
/** @type {Map<string, {file: string, fields: Record<string, FieldInfo>}>} */
|
|
427
|
-
const zodSchemas = new Map();
|
|
428
|
-
|
|
429
|
-
for (const file of schemaFiles) {
|
|
430
|
-
const content = await readFileContent(file);
|
|
431
|
-
if (!content) continue;
|
|
432
|
-
|
|
433
|
-
// Skip if not a Zod file
|
|
434
|
-
if (!content.includes("from 'zod'") && !content.includes('from "zod"')) continue;
|
|
435
|
-
|
|
436
|
-
const schemas = parseZodSchemas(content);
|
|
437
|
-
for (const [name, fields] of schemas.entries()) {
|
|
438
|
-
zodSchemas.set(name, { file: path.relative(projectRoot, file), fields });
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// Parse all TypeScript types
|
|
443
|
-
/** @type {Map<string, {file: string, fields: Record<string, FieldInfo>}>} */
|
|
444
|
-
const tsTypes = new Map();
|
|
445
|
-
|
|
446
|
-
for (const file of typeFiles) {
|
|
447
|
-
const content = await readFileContent(file);
|
|
448
|
-
if (!content) continue;
|
|
449
|
-
|
|
450
|
-
const types = parseTypeScriptTypes(content);
|
|
451
|
-
for (const [name, fields] of types.entries()) {
|
|
452
|
-
tsTypes.set(name, { file: path.relative(projectRoot, file), fields });
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// Compare matching entities
|
|
457
|
-
const allEntities = new Set([...zodSchemas.keys(), ...tsTypes.keys()]);
|
|
458
|
-
|
|
459
|
-
for (const entityName of allEntities) {
|
|
460
|
-
const zodInfo = zodSchemas.get(entityName);
|
|
461
|
-
const tsInfo = tsTypes.get(entityName);
|
|
462
|
-
|
|
463
|
-
/** @type {string[]} */
|
|
464
|
-
const issues = [];
|
|
465
|
-
let severity = /** @type {'low' | 'medium' | 'high' | 'critical'} */ ('low');
|
|
466
|
-
|
|
467
|
-
if (zodInfo && !tsInfo) {
|
|
468
|
-
issues.push(`Entity "${entityName}" exists in Zod but not in TypeScript`);
|
|
469
|
-
severity = 'medium';
|
|
470
|
-
} else if (!zodInfo && tsInfo) {
|
|
471
|
-
issues.push(`Entity "${entityName}" exists in TypeScript but not in Zod`);
|
|
472
|
-
severity = 'medium';
|
|
473
|
-
} else if (zodInfo && tsInfo) {
|
|
474
|
-
const comparison = compareTypes(zodInfo.fields, tsInfo.fields);
|
|
475
|
-
|
|
476
|
-
// Report added fields (in Zod but not TS)
|
|
477
|
-
for (const fieldName of comparison.added) {
|
|
478
|
-
issues.push(`Field "${fieldName}" exists in Zod but not in TypeScript`);
|
|
479
|
-
severity = severity === 'low' ? 'medium' : severity;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// Report removed fields (in TS but not Zod)
|
|
483
|
-
for (const fieldName of comparison.removed) {
|
|
484
|
-
issues.push(`Field "${fieldName}" removed from TS but exists in Zod`);
|
|
485
|
-
severity = 'high'; // More critical - TS code expects field that Zod won't validate
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// Report modified fields
|
|
489
|
-
for (const mod of comparison.modified) {
|
|
490
|
-
issues.push(mod.issue);
|
|
491
|
-
// Optionality mismatch is high severity
|
|
492
|
-
if (mod.issue.includes('optional') && mod.issue.includes('required')) {
|
|
493
|
-
severity = 'high';
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
if (issues.length > 0) {
|
|
499
|
-
mismatches.push({
|
|
500
|
-
entity: entityName,
|
|
501
|
-
zod: zodInfo ? { file: zodInfo.file, fields: zodInfo.fields } : undefined,
|
|
502
|
-
typescript: tsInfo ? { file: tsInfo.file, fields: tsInfo.fields } : undefined,
|
|
503
|
-
issues,
|
|
504
|
-
severity,
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// Calculate score
|
|
510
|
-
const totalEntities = allEntities.size;
|
|
511
|
-
const entitiesWithIssues = mismatches.length;
|
|
512
|
-
const score =
|
|
513
|
-
totalEntities > 0
|
|
514
|
-
? Math.round(((totalEntities - entitiesWithIssues) / totalEntities) * 100)
|
|
515
|
-
: 100;
|
|
516
|
-
|
|
517
|
-
// Generate summary and recommendation
|
|
518
|
-
const summary =
|
|
519
|
-
entitiesWithIssues === 0
|
|
520
|
-
? 'All Zod schemas and TypeScript types are consistent'
|
|
521
|
-
: `${entitiesWithIssues} entities with type mismatches out of ${totalEntities} total`;
|
|
522
|
-
|
|
523
|
-
const criticalCount = mismatches.filter(m => m.severity === 'critical').length;
|
|
524
|
-
const highCount = mismatches.filter(m => m.severity === 'high').length;
|
|
525
|
-
|
|
526
|
-
let recommendation = 'No action needed';
|
|
527
|
-
if (criticalCount > 0) {
|
|
528
|
-
recommendation = 'Critical type mismatches detected - fix immediately before production';
|
|
529
|
-
} else if (highCount > 0) {
|
|
530
|
-
recommendation = 'High severity mismatches - update TypeScript types to match Zod schemas';
|
|
531
|
-
} else if (entitiesWithIssues > 0) {
|
|
532
|
-
recommendation = 'Minor type mismatches - consider synchronizing schemas and types';
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
return AuditResultSchema.parse({
|
|
536
|
-
mismatches,
|
|
537
|
-
summary,
|
|
538
|
-
recommendation,
|
|
539
|
-
score,
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
/**
|
|
544
|
-
* Audit a single entity's type consistency
|
|
545
|
-
*
|
|
546
|
-
* @param {string} zodContent - Content of file containing Zod schema
|
|
547
|
-
* @param {string} tsContent - Content of file containing TypeScript type
|
|
548
|
-
* @param {string} entityName - Name of entity to audit
|
|
549
|
-
* @returns {Mismatch | null}
|
|
550
|
-
*/
|
|
551
|
-
export function auditEntityTypes(zodContent, tsContent, entityName) {
|
|
552
|
-
const zodSchemas = parseZodSchemas(zodContent);
|
|
553
|
-
const tsTypes = parseTypeScriptTypes(tsContent);
|
|
554
|
-
|
|
555
|
-
const zodFields = zodSchemas.get(entityName);
|
|
556
|
-
const tsFields = tsTypes.get(entityName);
|
|
557
|
-
|
|
558
|
-
if (!zodFields && !tsFields) {
|
|
559
|
-
return null;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/** @type {string[]} */
|
|
563
|
-
const issues = [];
|
|
564
|
-
let severity = /** @type {'low' | 'medium' | 'high' | 'critical'} */ ('low');
|
|
565
|
-
|
|
566
|
-
if (zodFields && !tsFields) {
|
|
567
|
-
issues.push(`Entity "${entityName}" exists in Zod but not in TypeScript`);
|
|
568
|
-
severity = 'medium';
|
|
569
|
-
} else if (!zodFields && tsFields) {
|
|
570
|
-
issues.push(`Entity "${entityName}" exists in TypeScript but not in Zod`);
|
|
571
|
-
severity = 'medium';
|
|
572
|
-
} else if (zodFields && tsFields) {
|
|
573
|
-
const comparison = compareTypes(zodFields, tsFields);
|
|
574
|
-
|
|
575
|
-
for (const fieldName of comparison.added) {
|
|
576
|
-
issues.push(`Field "${fieldName}" exists in Zod but not in TypeScript`);
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
for (const fieldName of comparison.removed) {
|
|
580
|
-
issues.push(`Field "${fieldName}" removed from TS but exists in Zod`);
|
|
581
|
-
severity = 'high';
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
for (const mod of comparison.modified) {
|
|
585
|
-
issues.push(mod.issue);
|
|
586
|
-
if (mod.issue.includes('optional') && mod.issue.includes('required')) {
|
|
587
|
-
severity = 'high';
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
if (issues.length === 0) {
|
|
593
|
-
return null;
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
return MismatchSchema.parse({
|
|
597
|
-
entity: entityName,
|
|
598
|
-
zod: zodFields ? { file: 'inline', fields: zodFields } : undefined,
|
|
599
|
-
typescript: tsFields ? { file: 'inline', fields: tsFields } : undefined,
|
|
600
|
-
issues,
|
|
601
|
-
severity,
|
|
602
|
-
});
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
/* ========================================================================= */
|
|
606
|
-
/* Exports for module */
|
|
607
|
-
/* ========================================================================= */
|
|
608
|
-
|
|
609
|
-
export { FieldInfoSchema, MismatchSchema, AuditResultSchema, CompareTypesResultSchema };
|