docguard-cli 0.5.1 → 0.6.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.
- package/README.md +37 -15
- package/cli/commands/generate.mjs +432 -82
- package/cli/commands/publish.mjs +246 -0
- package/cli/docguard.mjs +19 -3
- package/cli/scanners/doc-tools.mjs +351 -0
- package/cli/scanners/routes.mjs +461 -0
- package/cli/scanners/schemas.mjs +567 -0
- package/docs/ai-integration.md +7 -7
- package/docs/commands.md +40 -40
- package/docs/faq.md +1 -1
- package/docs/profiles.md +6 -6
- package/docs/quickstart.md +10 -10
- package/package.json +1 -1
- package/templates/ci/github-actions.yml +1 -1
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep Schema Scanner
|
|
3
|
+
* Parses schema definitions from ORM/validation libraries.
|
|
4
|
+
* Supports: Prisma, Drizzle, Zod, Mongoose, TypeORM, OpenAPI schemas
|
|
5
|
+
*
|
|
6
|
+
* Priority: OpenAPI schemas > ORM schemas > Validation schemas
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
10
|
+
import { resolve, join, relative, basename, extname } from 'node:path';
|
|
11
|
+
|
|
12
|
+
const IGNORE_DIRS = new Set([
|
|
13
|
+
'node_modules', '.git', '.next', 'dist', 'build', 'coverage',
|
|
14
|
+
'.cache', '__pycache__', '.venv', 'vendor', '.turbo',
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Deep scan schemas from ORM definitions, validation libraries, and OpenAPI specs.
|
|
19
|
+
* @param {string} dir - Project root
|
|
20
|
+
* @param {object} stack - Detected tech stack
|
|
21
|
+
* @param {object} docTools - Detected doc tools (may include OpenAPI)
|
|
22
|
+
* @returns {object} { entities: [...], relationships: [...], source: string }
|
|
23
|
+
*/
|
|
24
|
+
export function scanSchemasDeep(dir, stack, docTools) {
|
|
25
|
+
// Priority 1: OpenAPI schemas
|
|
26
|
+
if (docTools?.openapi?.found && docTools.openapi.schemas?.length > 0) {
|
|
27
|
+
return {
|
|
28
|
+
entities: docTools.openapi.schemas.map(s => ({
|
|
29
|
+
name: s.name,
|
|
30
|
+
fields: s.fields,
|
|
31
|
+
file: docTools.openapi.path,
|
|
32
|
+
source: 'openapi',
|
|
33
|
+
description: s.description,
|
|
34
|
+
})),
|
|
35
|
+
relationships: extractOpenAPIRelationships(docTools.openapi.schemas),
|
|
36
|
+
source: 'openapi',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Priority 2: ORM-specific scanning
|
|
41
|
+
const orm = stack?.orm || '';
|
|
42
|
+
const entities = [];
|
|
43
|
+
const relationships = [];
|
|
44
|
+
|
|
45
|
+
// Prisma
|
|
46
|
+
const prismaResult = scanPrismaDeep(dir);
|
|
47
|
+
if (prismaResult.entities.length > 0) {
|
|
48
|
+
entities.push(...prismaResult.entities);
|
|
49
|
+
relationships.push(...prismaResult.relationships);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Drizzle
|
|
53
|
+
const drizzleResult = scanDrizzleSchemas(dir);
|
|
54
|
+
if (drizzleResult.entities.length > 0) {
|
|
55
|
+
entities.push(...drizzleResult.entities);
|
|
56
|
+
relationships.push(...drizzleResult.relationships);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Zod (if no ORM found, Zod schemas are the data model)
|
|
60
|
+
if (entities.length === 0) {
|
|
61
|
+
const zodResult = scanZodSchemas(dir);
|
|
62
|
+
entities.push(...zodResult.entities);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Mongoose
|
|
66
|
+
const mongooseResult = scanMongooseSchemas(dir);
|
|
67
|
+
if (mongooseResult.entities.length > 0) {
|
|
68
|
+
entities.push(...mongooseResult.entities);
|
|
69
|
+
relationships.push(...mongooseResult.relationships);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
entities,
|
|
74
|
+
relationships,
|
|
75
|
+
source: entities.length > 0 ? entities[0].source : 'none',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Prisma Deep Parser ──────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
function scanPrismaDeep(dir) {
|
|
82
|
+
const entities = [];
|
|
83
|
+
const relationships = [];
|
|
84
|
+
|
|
85
|
+
const schemaPath = resolve(dir, 'prisma/schema.prisma');
|
|
86
|
+
if (!existsSync(schemaPath)) return { entities, relationships };
|
|
87
|
+
|
|
88
|
+
const content = readFileSync(schemaPath, 'utf-8');
|
|
89
|
+
|
|
90
|
+
// Parse all models
|
|
91
|
+
const modelRegex = /model\s+(\w+)\s*\{([^}]+)\}/g;
|
|
92
|
+
let modelMatch;
|
|
93
|
+
|
|
94
|
+
while ((modelMatch = modelRegex.exec(content)) !== null) {
|
|
95
|
+
const modelName = modelMatch[1];
|
|
96
|
+
const body = modelMatch[2];
|
|
97
|
+
const fields = [];
|
|
98
|
+
|
|
99
|
+
const lines = body.split('\n');
|
|
100
|
+
for (const line of lines) {
|
|
101
|
+
const trimmed = line.trim();
|
|
102
|
+
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('@@')) continue;
|
|
103
|
+
|
|
104
|
+
// Parse field: name Type @modifiers
|
|
105
|
+
const fieldMatch = trimmed.match(/^(\w+)\s+([\w\[\]?]+)(\s+.*)?$/);
|
|
106
|
+
if (!fieldMatch) continue;
|
|
107
|
+
|
|
108
|
+
const fieldName = fieldMatch[1];
|
|
109
|
+
const rawType = fieldMatch[2];
|
|
110
|
+
const modifiers = fieldMatch[3] || '';
|
|
111
|
+
|
|
112
|
+
// Skip relation fields for field list (but track relationships)
|
|
113
|
+
const isRelation = rawType.endsWith('[]') || modifiers.includes('@relation');
|
|
114
|
+
|
|
115
|
+
if (isRelation && rawType.endsWith('[]')) {
|
|
116
|
+
relationships.push({
|
|
117
|
+
from: modelName,
|
|
118
|
+
to: rawType.replace('[]', '').replace('?', ''),
|
|
119
|
+
type: 'one-to-many',
|
|
120
|
+
field: fieldName,
|
|
121
|
+
});
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (isRelation && !rawType.endsWith('[]') && modifiers.includes('@relation')) {
|
|
126
|
+
relationships.push({
|
|
127
|
+
from: modelName,
|
|
128
|
+
to: rawType.replace('?', ''),
|
|
129
|
+
type: 'many-to-one',
|
|
130
|
+
field: fieldName,
|
|
131
|
+
});
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
fields.push({
|
|
136
|
+
name: fieldName,
|
|
137
|
+
type: mapPrismaType(rawType),
|
|
138
|
+
required: !rawType.includes('?'),
|
|
139
|
+
primaryKey: modifiers.includes('@id'),
|
|
140
|
+
unique: modifiers.includes('@unique'),
|
|
141
|
+
default: extractPrismaDefault(modifiers),
|
|
142
|
+
description: extractInlineComment(line),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
entities.push({
|
|
147
|
+
name: modelName,
|
|
148
|
+
fields,
|
|
149
|
+
file: 'prisma/schema.prisma',
|
|
150
|
+
source: 'prisma',
|
|
151
|
+
description: '',
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Parse enums
|
|
156
|
+
const enumRegex = /enum\s+(\w+)\s*\{([^}]+)\}/g;
|
|
157
|
+
let enumMatch;
|
|
158
|
+
while ((enumMatch = enumRegex.exec(content)) !== null) {
|
|
159
|
+
const enumName = enumMatch[1];
|
|
160
|
+
const values = enumMatch[2].trim().split('\n')
|
|
161
|
+
.map(l => l.trim())
|
|
162
|
+
.filter(l => l && !l.startsWith('//'));
|
|
163
|
+
|
|
164
|
+
entities.push({
|
|
165
|
+
name: enumName,
|
|
166
|
+
fields: values.map(v => ({ name: v, type: 'enum_value', required: true })),
|
|
167
|
+
file: 'prisma/schema.prisma',
|
|
168
|
+
source: 'prisma-enum',
|
|
169
|
+
description: `Enum with ${values.length} values`,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { entities, relationships };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function mapPrismaType(rawType) {
|
|
177
|
+
const type = rawType.replace('?', '').replace('[]', '');
|
|
178
|
+
const map = {
|
|
179
|
+
'String': 'string', 'Int': 'integer', 'BigInt': 'bigint',
|
|
180
|
+
'Float': 'float', 'Decimal': 'decimal', 'Boolean': 'boolean',
|
|
181
|
+
'DateTime': 'datetime', 'Json': 'json', 'Bytes': 'bytes',
|
|
182
|
+
};
|
|
183
|
+
return map[type] || type;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function extractPrismaDefault(modifiers) {
|
|
187
|
+
const match = modifiers.match(/@default\(([^)]+)\)/);
|
|
188
|
+
if (!match) return '—';
|
|
189
|
+
return match[1];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function extractInlineComment(line) {
|
|
193
|
+
const match = line.match(/\/\/\s*(.+)$/);
|
|
194
|
+
return match ? match[1].trim() : '';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── Drizzle Scanner ─────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
function scanDrizzleSchemas(dir) {
|
|
200
|
+
const entities = [];
|
|
201
|
+
const relationships = [];
|
|
202
|
+
|
|
203
|
+
const schemaDirs = ['src/db', 'src/schema', 'db', 'schema', 'drizzle', 'src/drizzle', 'src'];
|
|
204
|
+
const tablePattern = /(?:export\s+(?:const|let)\s+)?(\w+)\s*=\s*(?:pg|mysql|sqlite)Table\s*\(\s*['"`](\w+)['"`]\s*,\s*\{([^}]+)\}/g;
|
|
205
|
+
|
|
206
|
+
for (const schemaDir of schemaDirs) {
|
|
207
|
+
const fullDir = resolve(dir, schemaDir);
|
|
208
|
+
if (!existsSync(fullDir)) continue;
|
|
209
|
+
|
|
210
|
+
walkDir(fullDir, (filePath) => {
|
|
211
|
+
const content = readFileSafe(filePath);
|
|
212
|
+
if (!content || !content.includes('Table(')) return;
|
|
213
|
+
|
|
214
|
+
let match;
|
|
215
|
+
const regex = new RegExp(tablePattern.source, 'g');
|
|
216
|
+
while ((match = regex.exec(content)) !== null) {
|
|
217
|
+
const varName = match[1];
|
|
218
|
+
const tableName = match[2];
|
|
219
|
+
const body = match[3];
|
|
220
|
+
const fields = parseDrizzleColumns(body);
|
|
221
|
+
|
|
222
|
+
// Look for references (foreign keys)
|
|
223
|
+
for (const field of fields) {
|
|
224
|
+
if (field._ref) {
|
|
225
|
+
relationships.push({
|
|
226
|
+
from: tableName,
|
|
227
|
+
to: field._ref,
|
|
228
|
+
type: 'many-to-one',
|
|
229
|
+
field: field.name,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
entities.push({
|
|
235
|
+
name: tableName,
|
|
236
|
+
fields: fields.map(f => ({ ...f, _ref: undefined })),
|
|
237
|
+
file: relative(dir, filePath),
|
|
238
|
+
source: 'drizzle',
|
|
239
|
+
description: '',
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { entities, relationships };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function parseDrizzleColumns(body) {
|
|
249
|
+
const fields = [];
|
|
250
|
+
const lines = body.split('\n');
|
|
251
|
+
|
|
252
|
+
for (const line of lines) {
|
|
253
|
+
const trimmed = line.trim().replace(/,$/, '');
|
|
254
|
+
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
255
|
+
|
|
256
|
+
// Match: fieldName: type('column_name')
|
|
257
|
+
const colMatch = trimmed.match(/(\w+)\s*:\s*(\w+)\s*\(\s*['"`]?(\w+)?['"`]?\s*\)/);
|
|
258
|
+
if (!colMatch) continue;
|
|
259
|
+
|
|
260
|
+
const fieldName = colMatch[1];
|
|
261
|
+
const drizzleType = colMatch[2];
|
|
262
|
+
|
|
263
|
+
const field = {
|
|
264
|
+
name: fieldName,
|
|
265
|
+
type: mapDrizzleType(drizzleType),
|
|
266
|
+
required: !trimmed.includes('.notNull()') ? false : true,
|
|
267
|
+
primaryKey: trimmed.includes('.primaryKey()') || drizzleType === 'serial',
|
|
268
|
+
unique: trimmed.includes('.unique()'),
|
|
269
|
+
default: extractDrizzleDefault(trimmed),
|
|
270
|
+
description: '',
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// Check for references
|
|
274
|
+
const refMatch = trimmed.match(/\.references\(\s*\(\)\s*=>\s*(\w+)\.\w+\)/);
|
|
275
|
+
if (refMatch) {
|
|
276
|
+
field._ref = refMatch[1];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
fields.push(field);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return fields;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function mapDrizzleType(type) {
|
|
286
|
+
const map = {
|
|
287
|
+
'serial': 'integer (auto)', 'integer': 'integer', 'bigint': 'bigint',
|
|
288
|
+
'smallint': 'smallint', 'text': 'string', 'varchar': 'string',
|
|
289
|
+
'char': 'string', 'boolean': 'boolean', 'timestamp': 'datetime',
|
|
290
|
+
'date': 'date', 'time': 'time', 'json': 'json', 'jsonb': 'json',
|
|
291
|
+
'real': 'float', 'doublePrecision': 'double', 'numeric': 'decimal',
|
|
292
|
+
'uuid': 'uuid',
|
|
293
|
+
};
|
|
294
|
+
return map[type] || type;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function extractDrizzleDefault(line) {
|
|
298
|
+
const match = line.match(/\.default\(([^)]+)\)/);
|
|
299
|
+
if (match) return match[1].replace(/['"`]/g, '');
|
|
300
|
+
if (line.includes('.defaultNow()')) return 'now()';
|
|
301
|
+
if (line.includes('.defaultRandom()')) return 'random()';
|
|
302
|
+
return '—';
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ── Zod Scanner ─────────────────────────────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
function scanZodSchemas(dir) {
|
|
308
|
+
const entities = [];
|
|
309
|
+
|
|
310
|
+
const schemaDirs = ['src/schema', 'src/schemas', 'schema', 'schemas', 'src/types', 'src/validation', 'src'];
|
|
311
|
+
const zodPattern = /(?:export\s+(?:const|let)\s+)(\w+(?:Schema|Validator|Input|Output))\s*=\s*z\.object\s*\(\s*\{([^}]+)\}\s*\)/g;
|
|
312
|
+
|
|
313
|
+
for (const schemaDir of schemaDirs) {
|
|
314
|
+
const fullDir = resolve(dir, schemaDir);
|
|
315
|
+
if (!existsSync(fullDir)) continue;
|
|
316
|
+
|
|
317
|
+
walkDir(fullDir, (filePath) => {
|
|
318
|
+
const content = readFileSafe(filePath);
|
|
319
|
+
if (!content || !content.includes('z.object')) return;
|
|
320
|
+
|
|
321
|
+
let match;
|
|
322
|
+
const regex = new RegExp(zodPattern.source, 'g');
|
|
323
|
+
while ((match = regex.exec(content)) !== null) {
|
|
324
|
+
const schemaName = match[1].replace(/Schema$|Validator$/, '');
|
|
325
|
+
const body = match[2];
|
|
326
|
+
const fields = parseZodFields(body);
|
|
327
|
+
|
|
328
|
+
entities.push({
|
|
329
|
+
name: schemaName,
|
|
330
|
+
fields,
|
|
331
|
+
file: relative(dir, filePath),
|
|
332
|
+
source: 'zod',
|
|
333
|
+
description: '',
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return { entities };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function parseZodFields(body) {
|
|
343
|
+
const fields = [];
|
|
344
|
+
const lines = body.split('\n');
|
|
345
|
+
|
|
346
|
+
for (const line of lines) {
|
|
347
|
+
const trimmed = line.trim().replace(/,$/, '');
|
|
348
|
+
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
349
|
+
|
|
350
|
+
// Match: fieldName: z.type()
|
|
351
|
+
const fieldMatch = trimmed.match(/(\w+)\s*:\s*z\.\s*(\w+)\s*\(/);
|
|
352
|
+
if (!fieldMatch) continue;
|
|
353
|
+
|
|
354
|
+
const fieldName = fieldMatch[1];
|
|
355
|
+
const zodType = fieldMatch[2];
|
|
356
|
+
|
|
357
|
+
fields.push({
|
|
358
|
+
name: fieldName,
|
|
359
|
+
type: mapZodType(zodType),
|
|
360
|
+
required: !trimmed.includes('.optional()') && !trimmed.includes('.nullable()'),
|
|
361
|
+
primaryKey: false,
|
|
362
|
+
unique: false,
|
|
363
|
+
default: trimmed.includes('.default(') ? 'has default' : '—',
|
|
364
|
+
description: '',
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return fields;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function mapZodType(type) {
|
|
372
|
+
const map = {
|
|
373
|
+
'string': 'string', 'number': 'number', 'boolean': 'boolean',
|
|
374
|
+
'date': 'date', 'bigint': 'bigint', 'array': 'array',
|
|
375
|
+
'object': 'object', 'enum': 'enum', 'union': 'union',
|
|
376
|
+
'literal': 'literal', 'record': 'record', 'tuple': 'tuple',
|
|
377
|
+
'any': 'any', 'unknown': 'unknown', 'null': 'null',
|
|
378
|
+
'undefined': 'undefined', 'void': 'void', 'never': 'never',
|
|
379
|
+
'coerce': 'coerced',
|
|
380
|
+
};
|
|
381
|
+
return map[type] || type;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ── Mongoose Scanner ────────────────────────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
function scanMongooseSchemas(dir) {
|
|
387
|
+
const entities = [];
|
|
388
|
+
const relationships = [];
|
|
389
|
+
|
|
390
|
+
const schemaDirs = ['src/models', 'models', 'src/schema', 'schema'];
|
|
391
|
+
const schemaPattern = /(?:const|let|var)\s+(\w+)(?:Schema)?\s*=\s*new\s+(?:mongoose\.)?Schema\s*\(\s*\{([^}]+)\}/g;
|
|
392
|
+
|
|
393
|
+
for (const schemaDir of schemaDirs) {
|
|
394
|
+
const fullDir = resolve(dir, schemaDir);
|
|
395
|
+
if (!existsSync(fullDir)) continue;
|
|
396
|
+
|
|
397
|
+
walkDir(fullDir, (filePath) => {
|
|
398
|
+
const content = readFileSafe(filePath);
|
|
399
|
+
if (!content || !content.includes('Schema(')) return;
|
|
400
|
+
|
|
401
|
+
let match;
|
|
402
|
+
const regex = new RegExp(schemaPattern.source, 'g');
|
|
403
|
+
while ((match = regex.exec(content)) !== null) {
|
|
404
|
+
const schemaName = match[1].replace(/Schema$/i, '');
|
|
405
|
+
const body = match[2];
|
|
406
|
+
const fields = parseMongooseFields(body);
|
|
407
|
+
|
|
408
|
+
// Check for refs (relationships)
|
|
409
|
+
for (const field of fields) {
|
|
410
|
+
if (field._ref) {
|
|
411
|
+
relationships.push({
|
|
412
|
+
from: schemaName,
|
|
413
|
+
to: field._ref,
|
|
414
|
+
type: 'many-to-one',
|
|
415
|
+
field: field.name,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
entities.push({
|
|
421
|
+
name: schemaName.charAt(0).toUpperCase() + schemaName.slice(1),
|
|
422
|
+
fields: fields.map(f => ({ ...f, _ref: undefined })),
|
|
423
|
+
file: relative(dir, filePath),
|
|
424
|
+
source: 'mongoose',
|
|
425
|
+
description: '',
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return { entities, relationships };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function parseMongooseFields(body) {
|
|
435
|
+
const fields = [];
|
|
436
|
+
const lines = body.split('\n');
|
|
437
|
+
|
|
438
|
+
for (const line of lines) {
|
|
439
|
+
const trimmed = line.trim().replace(/,$/, '');
|
|
440
|
+
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
441
|
+
|
|
442
|
+
// Match: fieldName: Type or fieldName: { type: Type }
|
|
443
|
+
const simpleMatch = trimmed.match(/(\w+)\s*:\s*(\w+)/);
|
|
444
|
+
if (!simpleMatch) continue;
|
|
445
|
+
|
|
446
|
+
const fieldName = simpleMatch[1];
|
|
447
|
+
const typeOrKey = simpleMatch[2];
|
|
448
|
+
|
|
449
|
+
if (typeOrKey === 'type') {
|
|
450
|
+
// Complex field: { type: String, required: true }
|
|
451
|
+
const typeMatch = trimmed.match(/type\s*:\s*(\w+)/);
|
|
452
|
+
const required = trimmed.includes('required: true') || trimmed.includes("required: 'true'");
|
|
453
|
+
const unique = trimmed.includes('unique: true');
|
|
454
|
+
const refMatch = trimmed.match(/ref\s*:\s*['"](\w+)['"]/);
|
|
455
|
+
|
|
456
|
+
fields.push({
|
|
457
|
+
name: fieldName,
|
|
458
|
+
type: mapMongooseType(typeMatch ? typeMatch[1] : 'Mixed'),
|
|
459
|
+
required,
|
|
460
|
+
primaryKey: fieldName === '_id',
|
|
461
|
+
unique,
|
|
462
|
+
default: '—',
|
|
463
|
+
description: '',
|
|
464
|
+
_ref: refMatch ? refMatch[1] : null,
|
|
465
|
+
});
|
|
466
|
+
} else {
|
|
467
|
+
// Simple field: fieldName: String
|
|
468
|
+
fields.push({
|
|
469
|
+
name: fieldName,
|
|
470
|
+
type: mapMongooseType(typeOrKey),
|
|
471
|
+
required: false,
|
|
472
|
+
primaryKey: fieldName === '_id',
|
|
473
|
+
unique: false,
|
|
474
|
+
default: '—',
|
|
475
|
+
description: '',
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return fields;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function mapMongooseType(type) {
|
|
484
|
+
const map = {
|
|
485
|
+
'String': 'string', 'Number': 'number', 'Boolean': 'boolean',
|
|
486
|
+
'Date': 'date', 'Buffer': 'buffer', 'ObjectId': 'ObjectId',
|
|
487
|
+
'Array': 'array', 'Map': 'map', 'Mixed': 'mixed',
|
|
488
|
+
'Schema': 'embedded',
|
|
489
|
+
};
|
|
490
|
+
return map[type] || type;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
494
|
+
|
|
495
|
+
function extractOpenAPIRelationships(schemas) {
|
|
496
|
+
const relationships = [];
|
|
497
|
+
for (const schema of schemas) {
|
|
498
|
+
for (const field of schema.fields) {
|
|
499
|
+
if (field.type !== 'string' && field.type !== 'number' && field.type !== 'boolean' && field.type !== 'integer') {
|
|
500
|
+
// Likely a reference to another schema
|
|
501
|
+
const target = schemas.find(s => s.name.toLowerCase() === field.type.toLowerCase());
|
|
502
|
+
if (target) {
|
|
503
|
+
relationships.push({
|
|
504
|
+
from: schema.name,
|
|
505
|
+
to: target.name,
|
|
506
|
+
type: 'reference',
|
|
507
|
+
field: field.name,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return relationships;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function readFileSafe(path) {
|
|
517
|
+
try { return readFileSync(path, 'utf-8'); } catch { return null; }
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function walkDir(dir, callback) {
|
|
521
|
+
if (!existsSync(dir)) return;
|
|
522
|
+
try {
|
|
523
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
524
|
+
for (const entry of entries) {
|
|
525
|
+
if (IGNORE_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
|
|
526
|
+
const fullPath = join(dir, entry.name);
|
|
527
|
+
if (entry.isDirectory()) {
|
|
528
|
+
walkDir(fullPath, callback);
|
|
529
|
+
} else if (entry.isFile() && /\.(js|mjs|cjs|ts|tsx|jsx|py)$/.test(entry.name)) {
|
|
530
|
+
callback(fullPath);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
} catch { /* skip */ }
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Generate mermaid ER diagram from entities and relationships.
|
|
538
|
+
*/
|
|
539
|
+
export function generateERDiagram(entities, relationships) {
|
|
540
|
+
if (entities.length === 0) return '';
|
|
541
|
+
|
|
542
|
+
const lines = ['erDiagram'];
|
|
543
|
+
|
|
544
|
+
// Add entities with fields
|
|
545
|
+
for (const entity of entities) {
|
|
546
|
+
if (entity.source === 'prisma-enum') continue; // Skip enums in ER
|
|
547
|
+
const fieldLines = entity.fields
|
|
548
|
+
.slice(0, 8) // Limit fields shown
|
|
549
|
+
.map(f => {
|
|
550
|
+
const pk = f.primaryKey ? ' PK' : '';
|
|
551
|
+
const uk = f.unique ? ' UK' : '';
|
|
552
|
+
return ` ${f.type.replace(/[^a-zA-Z0-9]/g, '_')} ${f.name}${pk}${uk}`;
|
|
553
|
+
});
|
|
554
|
+
lines.push(` ${entity.name} {`);
|
|
555
|
+
lines.push(...fieldLines);
|
|
556
|
+
lines.push(` }`);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Add relationships
|
|
560
|
+
for (const rel of relationships) {
|
|
561
|
+
const arrow = rel.type === 'one-to-many' ? '||--o{' :
|
|
562
|
+
rel.type === 'many-to-one' ? '}o--||' : '||--||';
|
|
563
|
+
lines.push(` ${rel.from} ${arrow} ${rel.to} : "${rel.field}"`);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return lines.join('\n');
|
|
567
|
+
}
|
package/docs/ai-integration.md
CHANGED
|
@@ -39,7 +39,7 @@ DocGuard works with any AI coding agent that can read CLI output:
|
|
|
39
39
|
### Step 1: Diagnose
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
npx docguard diagnose
|
|
42
|
+
npx docguard-cli diagnose
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
Output:
|
|
@@ -66,7 +66,7 @@ Output:
|
|
|
66
66
|
The AI reads the remediation plan and executes `docguard fix --doc <name>` for each issue. Each fix command outputs research instructions:
|
|
67
67
|
|
|
68
68
|
```bash
|
|
69
|
-
npx docguard fix --doc architecture
|
|
69
|
+
npx docguard-cli fix --doc architecture
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
Output:
|
|
@@ -90,7 +90,7 @@ WRITE THE DOCUMENT:
|
|
|
90
90
|
### Step 3: Verify
|
|
91
91
|
|
|
92
92
|
```bash
|
|
93
|
-
npx docguard guard
|
|
93
|
+
npx docguard-cli guard
|
|
94
94
|
```
|
|
95
95
|
|
|
96
96
|
If all checks pass → done. If issues remain → repeat from Step 1.
|
|
@@ -100,7 +100,7 @@ If all checks pass → done. If issues remain → repeat from Step 1.
|
|
|
100
100
|
For programmatic integration:
|
|
101
101
|
|
|
102
102
|
```bash
|
|
103
|
-
npx docguard diagnose --format json
|
|
103
|
+
npx docguard-cli diagnose --format json
|
|
104
104
|
```
|
|
105
105
|
|
|
106
106
|
```json
|
|
@@ -124,7 +124,7 @@ npx docguard diagnose --format json
|
|
|
124
124
|
```
|
|
125
125
|
|
|
126
126
|
```bash
|
|
127
|
-
npx docguard guard --format json
|
|
127
|
+
npx docguard-cli guard --format json
|
|
128
128
|
```
|
|
129
129
|
|
|
130
130
|
```json
|
|
@@ -157,7 +157,7 @@ jobs:
|
|
|
157
157
|
- uses: actions/checkout@v4
|
|
158
158
|
- uses: actions/setup-node@v4
|
|
159
159
|
with: { node-version: '20' }
|
|
160
|
-
- run: npx docguard ci --format json --threshold 70
|
|
160
|
+
- run: npx docguard-cli ci --format json --threshold 70
|
|
161
161
|
```
|
|
162
162
|
|
|
163
163
|
Or copy `templates/ci/github-actions.yml` from this repo.
|
|
@@ -165,7 +165,7 @@ Or copy `templates/ci/github-actions.yml` from this repo.
|
|
|
165
165
|
### Pre-commit Hook
|
|
166
166
|
|
|
167
167
|
```bash
|
|
168
|
-
npx docguard hooks
|
|
168
|
+
npx docguard-cli hooks
|
|
169
169
|
```
|
|
170
170
|
|
|
171
171
|
Automatically runs `guard` before every commit.
|