@titannio/webtoolkit-cli 1.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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +639 -0
  3. package/dist/bin.d.ts +2 -0
  4. package/dist/bin.js +268 -0
  5. package/dist/bundle-audit.d.ts +7 -0
  6. package/dist/bundle-audit.js +111 -0
  7. package/dist/cleaner.d.ts +15 -0
  8. package/dist/cleaner.js +359 -0
  9. package/dist/config-reference.d.ts +8 -0
  10. package/dist/config-reference.js +805 -0
  11. package/dist/config.d.ts +306 -0
  12. package/dist/config.js +173 -0
  13. package/dist/dev-grid.d.ts +7 -0
  14. package/dist/dev-grid.js +181 -0
  15. package/dist/dev-watch.d.ts +13 -0
  16. package/dist/dev-watch.js +184 -0
  17. package/dist/environment.d.ts +10 -0
  18. package/dist/environment.js +172 -0
  19. package/dist/guard-registry.d.ts +1 -0
  20. package/dist/guard-registry.js +17 -0
  21. package/dist/guard-runner.d.ts +4 -0
  22. package/dist/guard-runner.js +36 -0
  23. package/dist/guards/any-guard.d.ts +16 -0
  24. package/dist/guards/any-guard.js +121 -0
  25. package/dist/guards/assert-no-tests-in-dist.d.ts +1 -0
  26. package/dist/guards/assert-no-tests-in-dist.js +56 -0
  27. package/dist/guards/check-mojibake.d.ts +52 -0
  28. package/dist/guards/check-mojibake.js +378 -0
  29. package/dist/guards/code-pattern-guard.d.ts +71 -0
  30. package/dist/guards/code-pattern-guard.js +654 -0
  31. package/dist/guards/dal-service-repository-check.d.ts +13 -0
  32. package/dist/guards/dal-service-repository-check.js +264 -0
  33. package/dist/guards/dependency-cruiser-guard.d.ts +14 -0
  34. package/dist/guards/dependency-cruiser-guard.js +69 -0
  35. package/dist/guards/documentation-guard.d.ts +3 -0
  36. package/dist/guards/documentation-guard.js +370 -0
  37. package/dist/guards/guard-config.d.ts +19 -0
  38. package/dist/guards/guard-config.js +87 -0
  39. package/dist/guards/internal-link-guard.d.ts +12 -0
  40. package/dist/guards/internal-link-guard.js +272 -0
  41. package/dist/guards/package-surface-guard.d.ts +37 -0
  42. package/dist/guards/package-surface-guard.js +234 -0
  43. package/dist/guards/pnpm-workspace-config.d.ts +2 -0
  44. package/dist/guards/pnpm-workspace-config.js +40 -0
  45. package/dist/guards/rebuild-preflight.d.ts +29 -0
  46. package/dist/guards/rebuild-preflight.js +137 -0
  47. package/dist/guards/repository-hygiene-guard.d.ts +12 -0
  48. package/dist/guards/repository-hygiene-guard.js +70 -0
  49. package/dist/guards/schema-guard.d.ts +10 -0
  50. package/dist/guards/schema-guard.js +160 -0
  51. package/dist/guards/singleton-deps-guard.d.ts +21 -0
  52. package/dist/guards/singleton-deps-guard.js +183 -0
  53. package/dist/guards/tsconfig-guard.d.ts +5 -0
  54. package/dist/guards/tsconfig-guard.js +105 -0
  55. package/dist/guards/workspace-manifest-guard.d.ts +21 -0
  56. package/dist/guards/workspace-manifest-guard.js +210 -0
  57. package/dist/jsdoc-report.d.ts +7 -0
  58. package/dist/jsdoc-report.js +456 -0
  59. package/dist/process.d.ts +20 -0
  60. package/dist/process.js +86 -0
  61. package/dist/ready-service.d.ts +7 -0
  62. package/dist/ready-service.js +135 -0
  63. package/dist/release-gate.d.ts +7 -0
  64. package/dist/release-gate.js +35 -0
  65. package/dist/repo-check.d.ts +7 -0
  66. package/dist/repo-check.js +121 -0
  67. package/dist/tasks.d.ts +17 -0
  68. package/dist/tasks.js +195 -0
  69. package/dist/upgrade.d.ts +10 -0
  70. package/dist/upgrade.js +674 -0
  71. package/dist/validate.d.ts +7 -0
  72. package/dist/validate.js +51 -0
  73. package/dist/workspace-tests.d.ts +33 -0
  74. package/dist/workspace-tests.js +529 -0
  75. package/package.json +57 -0
@@ -0,0 +1,654 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Code Pattern Guard
4
+ *
5
+ * Detects prohibited or undesirable code patterns that are too specific for the
6
+ * global lint configuration, but important enough to be enforced in the
7
+ * architecture maintenance pipeline.
8
+ *
9
+ * This guard is intentionally conservative:
10
+ * - it blocks patterns that are known to be dangerous for our runtime/build
11
+ * - it keeps explicit allowlists for legacy or operational exceptions that are
12
+ * currently considered valid in the healthy system
13
+ *
14
+ * If a rule starts failing on the current mainline without a recent code change,
15
+ * the rule itself should be reviewed before the code is considered broken.
16
+ */
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import ts from 'typescript';
20
+ import { BASE_ARTIFACT_EXCLUDE_PATTERNS, BASE_SOURCE_EXTENSIONS, assertConfiguredScanScope, compilePatterns, hasExtension, isMainModule, loadGuardConfig, resolveProjectPath, } from './guard-config.js';
21
+ /**
22
+ * Terminal presentation helpers.
23
+ */
24
+ const colors = {
25
+ reset: '\x1b[0m',
26
+ bright: '\x1b[1m',
27
+ red: '\x1b[31m',
28
+ green: '\x1b[32m',
29
+ yellow: '\x1b[33m',
30
+ blue: '\x1b[34m',
31
+ cyan: '\x1b[36m',
32
+ gray: '\x1b[90m',
33
+ };
34
+ /**
35
+ * Repository and backend path anchors used by multiple rules.
36
+ */
37
+ /**
38
+ * Generic file-system exclusions for this guard.
39
+ */
40
+ const EXCLUDE_PATTERNS = compilePatterns([], BASE_ARTIFACT_EXCLUDE_PATTERNS);
41
+ /**
42
+ * Generic path and file collection helpers.
43
+ */
44
+ function normalizeFilePath(filePath) {
45
+ return filePath.replace(/\\/g, '/');
46
+ }
47
+ function toRelativeFilePath(filePath, rootDir = process.cwd()) {
48
+ return normalizeFilePath(path.relative(rootDir, filePath));
49
+ }
50
+ function isInsideDirectory(directory, candidate) {
51
+ const relativePath = path.relative(directory, candidate);
52
+ return relativePath !== '..' && !relativePath.startsWith(`..${path.sep}`) && !path.isAbsolute(relativePath);
53
+ }
54
+ function shouldSkipEntry(entryPath) {
55
+ return EXCLUDE_PATTERNS.some((pattern) => pattern.test(entryPath));
56
+ }
57
+ function matchesAnyPattern(filePath, patterns, rootDir = process.cwd()) {
58
+ if (!patterns || patterns.length === 0)
59
+ return false;
60
+ const relativePath = toRelativeFilePath(filePath, rootDir);
61
+ return patterns.some((pattern) => pattern.test(relativePath));
62
+ }
63
+ function collectFiles(rootDir, includeDirs) {
64
+ const fileSet = new Set();
65
+ const walk = (dir) => {
66
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory() || shouldSkipEntry(dir))
67
+ return;
68
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
69
+ const fullPath = path.join(dir, entry.name);
70
+ if (shouldSkipEntry(fullPath)) {
71
+ continue;
72
+ }
73
+ if (entry.isDirectory()) {
74
+ walk(fullPath);
75
+ continue;
76
+ }
77
+ /* v8 ignore next -- non-file directory entries are intentionally ignored */
78
+ if (!entry.isFile())
79
+ continue;
80
+ if (!hasExtension(fullPath, BASE_SOURCE_EXTENSIONS)) {
81
+ continue;
82
+ }
83
+ fileSet.add(path.resolve(fullPath));
84
+ }
85
+ };
86
+ for (const includeDir of includeDirs) {
87
+ walk(path.resolve(rootDir, includeDir));
88
+ }
89
+ return Array.from(fileSet).sort((a, b) => a.localeCompare(b));
90
+ }
91
+ /**
92
+ * TypeScript parsing and module-resolution helpers.
93
+ */
94
+ function createSourceFile(filePath, content) {
95
+ const extension = path.extname(filePath);
96
+ const scriptKind = extension === '.tsx' || extension === '.jsx' ? ts.ScriptKind.TSX :
97
+ extension === '.js' ? ts.ScriptKind.JS :
98
+ extension === '.mjs' ? ts.ScriptKind.JS :
99
+ extension === '.cjs' ? ts.ScriptKind.JS :
100
+ ts.ScriptKind.TS;
101
+ return ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, scriptKind);
102
+ }
103
+ function readParsedConfig(tsconfigPath) {
104
+ let configResult;
105
+ try {
106
+ configResult = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
107
+ }
108
+ catch (error) {
109
+ throw new Error(`Failed to read backend tsconfig: ${error.message}`);
110
+ }
111
+ if (configResult.error) {
112
+ const message = ts.flattenDiagnosticMessageText(configResult.error.messageText, '\n');
113
+ throw new Error(`Failed to read backend tsconfig: ${message}`);
114
+ }
115
+ return ts.parseJsonConfigFileContent(configResult.config, ts.sys, path.dirname(tsconfigPath));
116
+ }
117
+ function resolveImportTarget(sourceFileName, moduleSpecifier, compilerOptions) {
118
+ const resolved = ts.resolveModuleName(moduleSpecifier, sourceFileName, compilerOptions, ts.sys);
119
+ return resolved.resolvedModule?.resolvedFileName ?? null;
120
+ }
121
+ function createViolation(sourceFile, absoluteFilePath, node, rule, message, snippet, severityOverride) {
122
+ const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
123
+ return {
124
+ filePath: toRelativeFilePath(absoluteFilePath, rule.rootDir),
125
+ line: line + 1,
126
+ ruleId: rule.id,
127
+ severity: severityOverride ?? rule.severity,
128
+ message,
129
+ snippet: snippet ?? node.getText(sourceFile),
130
+ };
131
+ }
132
+ /**
133
+ * AST pattern helpers used by multiple rule stages.
134
+ */
135
+ function isProcessEnvObject(node) {
136
+ return !!node &&
137
+ ts.isPropertyAccessExpression(node) &&
138
+ ts.isIdentifier(node.expression) &&
139
+ node.expression.text === 'process' &&
140
+ node.name.text === 'env';
141
+ }
142
+ function isMongoosePropertyCall(node, propertyName) {
143
+ return ts.isPropertyAccessExpression(node.expression) &&
144
+ ts.isIdentifier(node.expression.expression) &&
145
+ node.expression.expression.text === 'mongoose' &&
146
+ node.expression.name.text === propertyName;
147
+ }
148
+ function isSchemaTypesMixed(node) {
149
+ return ts.isPropertyAccessExpression(node) &&
150
+ node.name.text === 'Mixed' &&
151
+ ts.isPropertyAccessExpression(node.expression) &&
152
+ ts.isIdentifier(node.expression.expression) &&
153
+ node.expression.expression.text === 'Schema' &&
154
+ node.expression.name.text === 'Types';
155
+ }
156
+ function unwrapTypeAssertionExpression(node) {
157
+ let current = node;
158
+ while (ts.isAsExpression(current) || ts.isTypeAssertionExpression(current)) {
159
+ current = current.expression;
160
+ }
161
+ return current;
162
+ }
163
+ function getReqValidationPropertyAccessTarget(node) {
164
+ if (ts.isPropertyAccessExpression(node)) {
165
+ if (ts.isIdentifier(node.expression) && node.expression.text === 'req') {
166
+ const propertyName = node.name.text;
167
+ if (propertyName === 'body' || propertyName === 'query' || propertyName === 'params') {
168
+ return propertyName;
169
+ }
170
+ }
171
+ return getReqValidationPropertyAccessTarget(node.expression);
172
+ }
173
+ if (ts.isElementAccessExpression(node)) {
174
+ return getReqValidationPropertyAccessTarget(node.expression);
175
+ }
176
+ return null;
177
+ }
178
+ function isMapperFile(filePath, rootDir = process.cwd()) {
179
+ return /^apps\/backend\/src\/mappers\//.test(toRelativeFilePath(filePath, rootDir));
180
+ }
181
+ export function collectImportBindingIdentifiers(importClause) {
182
+ const bindings = [];
183
+ if (importClause.name) {
184
+ bindings.push(importClause.name);
185
+ }
186
+ const namedBindings = importClause.namedBindings;
187
+ if (!namedBindings) {
188
+ return bindings;
189
+ }
190
+ if (ts.isNamespaceImport(namedBindings)) {
191
+ bindings.push(namedBindings.name);
192
+ return bindings;
193
+ }
194
+ for (const element of namedBindings.elements) {
195
+ bindings.push(element.name);
196
+ }
197
+ return bindings;
198
+ }
199
+ export function isTypeOnlyUsage(identifier) {
200
+ let current = identifier;
201
+ while (current.parent) {
202
+ const parent = current.parent;
203
+ if (ts.isExpressionWithTypeArguments(parent) && parent.expression === current) {
204
+ const heritageClause = parent.parent;
205
+ if (heritageClause.token === ts.SyntaxKind.ImplementsKeyword) {
206
+ return true;
207
+ }
208
+ return ts.isInterfaceDeclaration(heritageClause.parent);
209
+ }
210
+ if (ts.isTypeQueryNode(parent)) {
211
+ return false;
212
+ }
213
+ if (ts.isTypeNode(parent)) {
214
+ return true;
215
+ }
216
+ if (ts.isQualifiedName(parent) || ts.isPropertyAccessExpression(parent)) {
217
+ current = parent;
218
+ continue;
219
+ }
220
+ current = parent;
221
+ }
222
+ return false;
223
+ }
224
+ export function shouldPreferTypeImport(importDeclaration, sourceFile, typeChecker) {
225
+ const importClause = importDeclaration.importClause;
226
+ if (!importClause || importClause.isTypeOnly) {
227
+ return false;
228
+ }
229
+ const bindings = collectImportBindingIdentifiers(importClause);
230
+ if (bindings.length === 0) {
231
+ return false;
232
+ }
233
+ for (const binding of bindings) {
234
+ const bindingSymbol = typeChecker.getSymbolAtLocation(binding);
235
+ if (!bindingSymbol) {
236
+ return false;
237
+ }
238
+ let foundUsage = false;
239
+ let foundRuntimeUsage = false;
240
+ const visit = (node) => {
241
+ if (foundRuntimeUsage) {
242
+ return;
243
+ }
244
+ if (ts.isIdentifier(node) && node.text === binding.text && node !== binding) {
245
+ const usageSymbol = typeChecker.getSymbolAtLocation(node);
246
+ if (usageSymbol === bindingSymbol) {
247
+ foundUsage = true;
248
+ if (!isTypeOnlyUsage(node)) {
249
+ foundRuntimeUsage = true;
250
+ return;
251
+ }
252
+ }
253
+ }
254
+ ts.forEachChild(node, visit);
255
+ };
256
+ visit(sourceFile);
257
+ if (!foundUsage || foundRuntimeUsage) {
258
+ return false;
259
+ }
260
+ }
261
+ return true;
262
+ }
263
+ /**
264
+ * Rule catalog.
265
+ *
266
+ * Read this list as a sequence of architecture-check stages. Each block below
267
+ * exists for a specific class of production/build safety issue.
268
+ */
269
+ export const RULES = [
270
+ // Step 1: block the known-bad `models` named import from mongoose.
271
+ {
272
+ id: 'backend-mongoose-no-direct-models-import',
273
+ severity: 'forbidden',
274
+ summary: 'Do not import `models` directly from `mongoose`',
275
+ rationale: 'This named import can break at runtime under Node ESM/CommonJS interop even when type-check and build pass. Use `import mongoose, { ... } from "mongoose"` plus `mongoose.models` instead.',
276
+ includeDirs: [],
277
+ check(sourceFile, absoluteFilePath, _context, rule) {
278
+ const violations = [];
279
+ for (const statement of sourceFile.statements) {
280
+ if (!ts.isImportDeclaration(statement))
281
+ continue;
282
+ if (!ts.isStringLiteral(statement.moduleSpecifier))
283
+ continue;
284
+ if (statement.moduleSpecifier.text !== 'mongoose')
285
+ continue;
286
+ const namedBindings = statement.importClause?.namedBindings;
287
+ if (!namedBindings || !ts.isNamedImports(namedBindings))
288
+ continue;
289
+ for (const element of namedBindings.elements) {
290
+ const importedName = element.propertyName?.text ?? element.name.text;
291
+ if (importedName !== 'models')
292
+ continue;
293
+ violations.push(createViolation(sourceFile, absoluteFilePath, element, rule, 'Use o default import `mongoose` e acesse `mongoose.models` em vez de importar `models` diretamente.', statement.getText(sourceFile)));
294
+ }
295
+ }
296
+ return violations;
297
+ },
298
+ },
299
+ // Step 2: keep new environment access centralized by default.
300
+ {
301
+ id: 'backend-no-direct-process-env',
302
+ severity: 'forbidden',
303
+ summary: 'Do not access `process.env` directly outside the allowed config/ops surface',
304
+ rationale: 'Direct environment reads scattered through runtime code make build-time and runtime behavior harder to reason about. New usage must stay centralized or be explicitly allowlisted.',
305
+ includeDirs: [],
306
+ check(sourceFile, absoluteFilePath, _context, rule) {
307
+ const violations = [];
308
+ const visit = (node) => {
309
+ if (ts.isPropertyAccessExpression(node) && isProcessEnvObject(node.expression)) {
310
+ violations.push(createViolation(sourceFile, absoluteFilePath, node, rule, 'Centralize environment access via config/helpers or add an explicit allowlist entry for a real operational exception.'));
311
+ }
312
+ else if (ts.isElementAccessExpression(node) &&
313
+ isProcessEnvObject(node.expression) &&
314
+ node.argumentExpression &&
315
+ (ts.isStringLiteral(node.argumentExpression) || ts.isNoSubstitutionTemplateLiteral(node.argumentExpression))) {
316
+ violations.push(createViolation(sourceFile, absoluteFilePath, node, rule, 'Centralize environment access via config/helpers or add an explicit allowlist entry for a real operational exception.'));
317
+ }
318
+ ts.forEachChild(node, visit);
319
+ };
320
+ visit(sourceFile);
321
+ return violations;
322
+ },
323
+ },
324
+ // Step 2.5: keep parameter objects named instead of inline in production code.
325
+ {
326
+ id: 'no-inline-parameter-object-types-in-production',
327
+ severity: 'forbidden',
328
+ summary: 'Do not use inline object type literals directly in parameter signatures',
329
+ rationale: 'Anonymous parameter object types increase cognitive load, duplicate shapes across layers and make shared contracts harder to evolve consistently. Prefer named Input/Dto/Params/Options/Props aliases.',
330
+ includeDirs: [],
331
+ check(sourceFile, absoluteFilePath, _context, rule) {
332
+ const violations = [];
333
+ const visit = (node) => {
334
+ if ((ts.isFunctionDeclaration(node)
335
+ || ts.isMethodDeclaration(node)
336
+ || ts.isConstructorDeclaration(node)
337
+ || ts.isArrowFunction(node)
338
+ || ts.isFunctionExpression(node))) {
339
+ for (const parameter of node.parameters) {
340
+ if (!parameter.type || !ts.isTypeLiteralNode(parameter.type))
341
+ continue;
342
+ violations.push(createViolation(sourceFile, absoluteFilePath, parameter.type, rule, 'Extraia o shape para um tipo nomeado local ou compartilhado em vez de manter um object type inline na assinatura.', parameter.getText(sourceFile)));
343
+ }
344
+ }
345
+ ts.forEachChild(node, visit);
346
+ };
347
+ visit(sourceFile);
348
+ return violations;
349
+ },
350
+ },
351
+ // Step 3: restrict mongoose connection lifecycle to operational/test entrypoints.
352
+ {
353
+ id: 'backend-mongoose-connect-only-in-ops-and-test-bootstrap',
354
+ severity: 'forbidden',
355
+ summary: 'Keep `mongoose.connect` and `mongoose.disconnect` out of regular runtime modules',
356
+ rationale: 'Connection lifecycle must stay in app bootstrap, operational scripts or dedicated test bootstrap. New runtime usage is a strong sign of layering drift.',
357
+ includeDirs: [],
358
+ check(sourceFile, absoluteFilePath, _context, rule) {
359
+ const violations = [];
360
+ const visit = (node) => {
361
+ if (ts.isCallExpression(node) && (isMongoosePropertyCall(node, 'connect') || isMongoosePropertyCall(node, 'disconnect'))) {
362
+ const action = node.expression.name.text;
363
+ violations.push(createViolation(sourceFile, absoluteFilePath, node, rule, `Use de \`mongoose.${action}()\` fora de scripts operacionais ou bootstrap de teste precisa de revisao arquitetural explicita.`));
364
+ }
365
+ ts.forEachChild(node, visit);
366
+ };
367
+ visit(sourceFile);
368
+ return violations;
369
+ },
370
+ },
371
+ // Step 4: stop runtime layers from bypassing the backend model/DAL boundary,
372
+ // while reporting type-only mapper imports as improvement opportunities.
373
+ {
374
+ id: 'backend-no-direct-model-imports-outside-allowed-zones',
375
+ severity: 'forbidden',
376
+ summary: 'Do not import backend models directly outside model/db/tests/scripts zones',
377
+ rationale: 'Controllers, services, routes and middlewares should not bind directly to Mongoose models. Mappers are allowed to depend on persisted shapes, but should prefer `import type` when a model import is used only for typing.',
378
+ includeDirs: [],
379
+ check(sourceFile, absoluteFilePath, context, rule) {
380
+ const violations = [];
381
+ const mapperFile = isMapperFile(absoluteFilePath, context.rootDir);
382
+ for (const statement of sourceFile.statements) {
383
+ if (!ts.isImportDeclaration(statement))
384
+ continue;
385
+ if (!ts.isStringLiteral(statement.moduleSpecifier))
386
+ continue;
387
+ const targetFile = resolveImportTarget(absoluteFilePath, statement.moduleSpecifier.text, context.backendCompilerOptions);
388
+ if (!targetFile)
389
+ continue;
390
+ if (!isInsideDirectory(context.modelsDirectory, path.resolve(targetFile)))
391
+ continue;
392
+ if (mapperFile) {
393
+ if (shouldPreferTypeImport(statement, sourceFile, context.backendTypeChecker)) {
394
+ violations.push(createViolation(sourceFile, absoluteFilePath, statement, rule, 'Neste mapper, o import do model parece ser usado apenas para tipagem. Prefira `import type` para evitar acoplamento de runtime desnecessario.', undefined, 'undesirable'));
395
+ }
396
+ continue;
397
+ }
398
+ violations.push(createViolation(sourceFile, absoluteFilePath, statement, rule, 'Nao importe models do backend diretamente nesta camada. Passe pelo DAL ou mova a necessidade para uma zona explicitamente autorizada.'));
399
+ }
400
+ return violations;
401
+ },
402
+ },
403
+ // Step 5: restrict dynamic mongoose model lookup/registration to narrow zones.
404
+ {
405
+ id: 'backend-mongoose-model-only-in-model-layer-or-allowlist',
406
+ severity: 'forbidden',
407
+ summary: 'Do not call `mongoose.model(...)` outside the model layer or explicit exceptions',
408
+ rationale: 'Dynamic model lookup/registration outside the model layer is hard to reason about and can hide coupling to Mongoose internals. Exceptions must stay narrow and explicit.',
409
+ includeDirs: [],
410
+ check(sourceFile, absoluteFilePath, _context, rule) {
411
+ const violations = [];
412
+ const visit = (node) => {
413
+ if (ts.isCallExpression(node) && isMongoosePropertyCall(node, 'model')) {
414
+ violations.push(createViolation(sourceFile, absoluteFilePath, node, rule, 'Restrinja `mongoose.model(...)` a models, testes ou excecoes operacionais explicitamente autorizadas.'));
415
+ }
416
+ ts.forEachChild(node, visit);
417
+ };
418
+ visit(sourceFile);
419
+ return violations;
420
+ },
421
+ },
422
+ // Step 6: flag destructive bulk operations outside tests and maintenance flows.
423
+ {
424
+ id: 'backend-no-destructive-bulk-ops-outside-tests-and-maintenance',
425
+ severity: 'forbidden',
426
+ summary: 'Keep destructive bulk operations out of regular runtime code',
427
+ rationale: 'Operations like `deleteMany({})`, `dropDatabase()` and `dropCollection()` are valid in tests and very specific maintenance flows, but dangerous as a casual pattern elsewhere.',
428
+ includeDirs: [],
429
+ check(sourceFile, absoluteFilePath, _context, rule) {
430
+ const violations = [];
431
+ const visit = (node) => {
432
+ if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) {
433
+ ts.forEachChild(node, visit);
434
+ return;
435
+ }
436
+ const methodName = node.expression.name.text;
437
+ if (methodName === 'deleteMany') {
438
+ const [firstArg] = node.arguments;
439
+ if (firstArg && ts.isObjectLiteralExpression(firstArg) && firstArg.properties.length === 0) {
440
+ violations.push(createViolation(sourceFile, absoluteFilePath, node, rule, 'Bulk destructive cleanup com `deleteMany({})` so deve existir em testes ou manutenção explicitamente autorizada.'));
441
+ }
442
+ }
443
+ else if (methodName === 'dropDatabase' || methodName === 'dropCollection') {
444
+ violations.push(createViolation(sourceFile, absoluteFilePath, node, rule, `Operação destrutiva \`${methodName}()\` so deve existir em testes ou manutenção explicitamente autorizada.`));
445
+ }
446
+ ts.forEachChild(node, visit);
447
+ };
448
+ visit(sourceFile);
449
+ return violations;
450
+ },
451
+ },
452
+ // Step 7: prevent new flexible-schema escape hatches without explicit review.
453
+ {
454
+ id: 'backend-no-new-schema-mixed-usage',
455
+ severity: 'forbidden',
456
+ summary: 'Do not introduce new `Schema.Types.Mixed` usage outside the explicit allowlist',
457
+ rationale: 'Mixed weakens schema guarantees and should stay limited to the few places where the current system intentionally accepts flexible payloads.',
458
+ includeDirs: [],
459
+ check(sourceFile, absoluteFilePath, _context, rule) {
460
+ const violations = [];
461
+ const visit = (node) => {
462
+ if (isSchemaTypesMixed(node)) {
463
+ violations.push(createViolation(sourceFile, absoluteFilePath, node, rule, 'Novo uso de `Schema.Types.Mixed` exige revisao arquitetural e allowlist explicita.'));
464
+ }
465
+ ts.forEachChild(node, visit);
466
+ };
467
+ visit(sourceFile);
468
+ return violations;
469
+ },
470
+ },
471
+ // Step 8: keep Express controllers typed from route validation instead of request casts.
472
+ {
473
+ id: 'backend-controllers-no-validated-request-casts',
474
+ severity: 'forbidden',
475
+ summary: 'Do not cast `req.body`, `req.query` or `req.params` inside controllers',
476
+ rationale: 'Controllers should receive typed request shapes from validation middleware via `handleAsync<TRequest>`, keeping validation and type inference aligned at the route boundary.',
477
+ includeDirs: [],
478
+ check(sourceFile, absoluteFilePath, _context, rule) {
479
+ const violations = [];
480
+ const visit = (node) => {
481
+ if (ts.isAsExpression(node)) {
482
+ const parent = node.parent;
483
+ if (!ts.isAsExpression(parent)) {
484
+ const target = getReqValidationPropertyAccessTarget(unwrapTypeAssertionExpression(node.expression));
485
+ if (target) {
486
+ violations.push(createViolation(sourceFile, absoluteFilePath, node, rule, `Tipo de \`req.${target}\` deve vir de \`handleAsync<TRequest>\` usando RequestFromSchemas/ValidatedRequest, sem cast manual no controller.`));
487
+ }
488
+ }
489
+ }
490
+ ts.forEachChild(node, visit);
491
+ };
492
+ visit(sourceFile);
493
+ return violations;
494
+ },
495
+ },
496
+ {
497
+ id: 'frontend-no-direct-axios-imports-outside-http-boundary',
498
+ severity: 'forbidden',
499
+ summary: 'Do not import `axios` directly outside the shared HTTP boundary',
500
+ rationale: 'Axios-specific knowledge should stay inside the shared transport boundary so apps and UI code depend on canonical contract clients and generic error helpers instead of transport details.',
501
+ includeDirs: [],
502
+ check(sourceFile, absoluteFilePath, _context, rule) {
503
+ const violations = [];
504
+ for (const statement of sourceFile.statements) {
505
+ if (!ts.isImportDeclaration(statement))
506
+ continue;
507
+ if (!ts.isStringLiteral(statement.moduleSpecifier))
508
+ continue;
509
+ if (statement.moduleSpecifier.text !== 'axios')
510
+ continue;
511
+ violations.push(createViolation(sourceFile, absoluteFilePath, statement, rule, 'Importe helpers/clients compartilhados em vez de usar `axios` diretamente fora do boundary HTTP.'));
512
+ }
513
+ return violations;
514
+ },
515
+ },
516
+ {
517
+ id: 'frontend-no-services-api-imports-outside-shared-service-boundary',
518
+ severity: 'forbidden',
519
+ summary: 'Keep `services/api/*` imports inside the shared service boundary',
520
+ rationale: 'The internal `services/api/*` transport/config surface should not leak into apps or shared UI modules. Only the shared service layer and explicit test helpers may depend on it directly.',
521
+ includeDirs: [],
522
+ check(sourceFile, absoluteFilePath, _context, rule) {
523
+ const violations = [];
524
+ for (const statement of sourceFile.statements) {
525
+ const moduleSpecifier = ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement)
526
+ ? statement.moduleSpecifier
527
+ : undefined;
528
+ if (!moduleSpecifier || !ts.isStringLiteral(moduleSpecifier))
529
+ continue;
530
+ if (!moduleSpecifier.text.includes('services/api/'))
531
+ continue;
532
+ violations.push(createViolation(sourceFile, absoluteFilePath, statement, rule, 'Consuma a API publica compartilhada em vez de importar `services/api/*` fora do boundary de services.'));
533
+ }
534
+ return violations;
535
+ },
536
+ },
537
+ ];
538
+ /**
539
+ * Output helpers and main execution flow.
540
+ */
541
+ function printHeader() {
542
+ console.log(`${colors.bright}${colors.blue}Code Pattern Guard${colors.reset}`);
543
+ console.log(`${colors.gray}Detecta padroes de codigo proibidos ou indesejados fora do escopo ideal do lint global.${colors.reset}`);
544
+ console.log();
545
+ }
546
+ export async function runCodePatternGuard(options = {}) {
547
+ const rootDir = options.rootDir ?? process.cwd();
548
+ const config = options.config ?? await loadGuardConfig('codePattern', rootDir);
549
+ const availableRuleIds = new Set(RULES.map((rule) => rule.id));
550
+ const configuredRuleIds = Object.keys(config.rules);
551
+ const unknownRuleIds = configuredRuleIds.filter((ruleId) => !availableRuleIds.has(ruleId));
552
+ /* v8 ignore next -- both outcomes are asserted; V8 omits the fallthrough branch */
553
+ if (unknownRuleIds.length > 0) {
554
+ throw new Error(`Unknown code-pattern rule(s): ${unknownRuleIds.join(', ')}`);
555
+ }
556
+ if (configuredRuleIds.length === 0) {
557
+ throw new Error('guards.codePattern.rules must not be empty.');
558
+ }
559
+ const rules = RULES
560
+ .filter((rule) => config.rules[rule.id])
561
+ .map((rule) => ({
562
+ ...rule,
563
+ rootDir,
564
+ includeDirs: config.rules[rule.id].includePaths,
565
+ allowedPathPatterns: compilePatterns(config.rules[rule.id].allowedPathPatterns),
566
+ }));
567
+ for (const includeDir of rules.flatMap((rule) => rule.includeDirs))
568
+ resolveProjectPath(rootDir, includeDir);
569
+ printHeader();
570
+ const parsedBackendConfig = readParsedConfig(resolveProjectPath(rootDir, config.tsconfig));
571
+ const backendProgram = ts.createProgram(parsedBackendConfig.fileNames, parsedBackendConfig.options);
572
+ const context = {
573
+ rootDir,
574
+ backendProgram,
575
+ backendTypeChecker: backendProgram.getTypeChecker(),
576
+ backendCompilerOptions: parsedBackendConfig.options,
577
+ modelsDirectory: resolveProjectPath(rootDir, config.modelsDirectory),
578
+ };
579
+ const allIncludeDirs = Array.from(new Set(rules.flatMap((rule) => rule.includeDirs)));
580
+ console.log(`${colors.cyan}Rules loaded:${colors.reset} ${rules.length}`);
581
+ console.log(`${colors.cyan}Scope:${colors.reset} ${allIncludeDirs.join(', ')}`);
582
+ console.log();
583
+ const files = collectFiles(rootDir, allIncludeDirs);
584
+ for (const rule of rules) {
585
+ const eligibleFiles = files.filter((filePath) => rule.includeDirs.some((includeDir) => (isInsideDirectory(path.resolve(rootDir, includeDir), filePath))));
586
+ assertConfiguredScanScope({
587
+ root: rootDir,
588
+ guardName: 'code-pattern',
589
+ configPath: `guards.codePattern.rules.${rule.id}.includePaths`,
590
+ configuredPaths: rule.includeDirs,
591
+ eligibleFiles,
592
+ });
593
+ }
594
+ const violations = [];
595
+ for (const filePath of files) {
596
+ const sourceFile = context.backendProgram.getSourceFile(filePath) ?? createSourceFile(filePath, fs.readFileSync(filePath, 'utf8'));
597
+ for (const rule of rules) {
598
+ const isInScope = rule.includeDirs.some((includeDir) => {
599
+ const absoluteIncludeDir = path.resolve(rootDir, includeDir);
600
+ return normalizeFilePath(filePath).startsWith(normalizeFilePath(absoluteIncludeDir));
601
+ });
602
+ if (!isInScope)
603
+ continue;
604
+ if (matchesAnyPattern(filePath, rule.allowedPathPatterns, rootDir))
605
+ continue;
606
+ violations.push(...rule.check(sourceFile, filePath, context, rule));
607
+ }
608
+ }
609
+ if (violations.length === 0) {
610
+ console.log(`${colors.green}${colors.bright}No prohibited or undesirable code patterns found.${colors.reset}`);
611
+ console.log();
612
+ return 0;
613
+ }
614
+ const byRule = new Map();
615
+ for (const violation of violations) {
616
+ const list = byRule.get(violation.ruleId) ?? [];
617
+ list.push(violation);
618
+ byRule.set(violation.ruleId, list);
619
+ }
620
+ const hasForbiddenViolations = violations.some((violation) => violation.severity === 'forbidden');
621
+ const headingColor = hasForbiddenViolations ? colors.red : colors.yellow;
622
+ const headingLabel = hasForbiddenViolations ? 'Violations found' : 'Improvement opportunities found';
623
+ console.log(`${colors.bright}${headingColor}${headingLabel}${colors.reset}`);
624
+ console.log(`${colors.gray}Cada item abaixo representa um padrao que deve ser corrigido ou explicitamente reavaliado.${colors.reset}`);
625
+ console.log();
626
+ for (const rule of rules) {
627
+ const matches = byRule.get(rule.id) ?? [];
628
+ if (matches.length === 0)
629
+ continue;
630
+ const ruleHasForbidden = matches.some((match) => match.severity === 'forbidden');
631
+ const ruleSeverityLabel = ruleHasForbidden ? 'forbidden' : 'undesirable';
632
+ console.log(`${colors.bright}${rule.id}${colors.reset} (${matches.length})`);
633
+ console.log(`- Severity: ${ruleSeverityLabel}`);
634
+ console.log(`- Summary: ${rule.summary}`);
635
+ console.log(`- Rationale: ${rule.rationale}`);
636
+ for (const violation of matches) {
637
+ console.log(` ${colors.gray}${violation.filePath}:${violation.line}${colors.reset}`);
638
+ console.log(` ${colors.red}->${colors.reset} ${violation.message}`);
639
+ console.log(` ${colors.gray}${violation.snippet}${colors.reset}`);
640
+ }
641
+ console.log();
642
+ }
643
+ return hasForbiddenViolations ? 1 : 0;
644
+ }
645
+ /* v8 ignore start -- executable adapter */
646
+ if (isMainModule(import.meta.url)) {
647
+ runCodePatternGuard().then((code) => {
648
+ process.exitCode = code;
649
+ }).catch((error) => {
650
+ console.error(error.message);
651
+ process.exitCode = 1;
652
+ });
653
+ }
654
+ /* v8 ignore stop */
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * DAL + Service + Repository Compliance Report
4
+ *
5
+ * Analisa a camada backend e identifica violacoes de fronteira arquitetural
6
+ * entre Controllers, Services, Repositories, Routes, Middlewares e o agregador
7
+ * central de DAL.
8
+ */
9
+ import type { DalServiceRepositoryGuardConfig } from '../config.js';
10
+ export declare function runDalServiceRepositoryGuard(options?: {
11
+ rootDir?: string;
12
+ config?: DalServiceRepositoryGuardConfig;
13
+ }): Promise<number>;