@webpieces/dev-config 0.2.94 → 0.2.97

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 (183) hide show
  1. package/config/eslint/base.mjs +1 -1
  2. package/executors.json +6 -91
  3. package/package.json +6 -19
  4. package/{executors → src/executors}/help/executor.d.ts +4 -2
  5. package/src/executors/help/executor.js.map +1 -0
  6. package/{executors → src/executors}/validate-eslint-sync/executor.d.ts +3 -2
  7. package/src/executors/validate-eslint-sync/executor.js.map +1 -0
  8. package/{executors → src/executors}/validate-versions-locked/executor.js +5 -3
  9. package/src/executors/validate-versions-locked/executor.js.map +1 -0
  10. package/src/generators/init/generator.js.map +1 -1
  11. package/src/index.d.ts +1 -1
  12. package/src/index.js +1 -1
  13. package/src/index.js.map +1 -1
  14. package/src/plugin.d.ts +86 -0
  15. package/{plugin.js → src/plugin.js} +31 -15
  16. package/src/plugin.js.map +1 -0
  17. package/src/toError.d.ts +5 -0
  18. package/src/toError.js +37 -0
  19. package/src/toError.js.map +1 -0
  20. package/templates/eslint.webpieces.config.mjs +1 -1
  21. package/templates/webpieces.exceptions.md +15 -15
  22. package/architecture/executors/diff-utils.d.ts +0 -24
  23. package/architecture/executors/diff-utils.js +0 -119
  24. package/architecture/executors/diff-utils.js.map +0 -1
  25. package/architecture/executors/diff-utils.ts +0 -127
  26. package/architecture/executors/generate/executor.d.ts +0 -16
  27. package/architecture/executors/generate/executor.js +0 -44
  28. package/architecture/executors/generate/executor.js.map +0 -1
  29. package/architecture/executors/generate/executor.ts +0 -59
  30. package/architecture/executors/generate/schema.json +0 -14
  31. package/architecture/executors/validate-architecture-unchanged/executor.d.ts +0 -17
  32. package/architecture/executors/validate-architecture-unchanged/executor.js +0 -229
  33. package/architecture/executors/validate-architecture-unchanged/executor.js.map +0 -1
  34. package/architecture/executors/validate-architecture-unchanged/executor.ts +0 -251
  35. package/architecture/executors/validate-architecture-unchanged/schema.json +0 -14
  36. package/architecture/executors/validate-code/executor.d.ts +0 -78
  37. package/architecture/executors/validate-code/executor.js +0 -243
  38. package/architecture/executors/validate-code/executor.js.map +0 -1
  39. package/architecture/executors/validate-code/executor.ts +0 -406
  40. package/architecture/executors/validate-code/schema.json +0 -227
  41. package/architecture/executors/validate-dtos/executor.d.ts +0 -42
  42. package/architecture/executors/validate-dtos/executor.js +0 -561
  43. package/architecture/executors/validate-dtos/executor.js.map +0 -1
  44. package/architecture/executors/validate-dtos/executor.ts +0 -689
  45. package/architecture/executors/validate-dtos/schema.json +0 -33
  46. package/architecture/executors/validate-modified-files/executor.d.ts +0 -25
  47. package/architecture/executors/validate-modified-files/executor.js +0 -501
  48. package/architecture/executors/validate-modified-files/executor.js.map +0 -1
  49. package/architecture/executors/validate-modified-files/executor.ts +0 -571
  50. package/architecture/executors/validate-modified-files/schema.json +0 -25
  51. package/architecture/executors/validate-modified-methods/executor.d.ts +0 -31
  52. package/architecture/executors/validate-modified-methods/executor.js +0 -694
  53. package/architecture/executors/validate-modified-methods/executor.js.map +0 -1
  54. package/architecture/executors/validate-modified-methods/executor.ts +0 -797
  55. package/architecture/executors/validate-modified-methods/schema.json +0 -25
  56. package/architecture/executors/validate-new-methods/executor.d.ts +0 -28
  57. package/architecture/executors/validate-new-methods/executor.js +0 -513
  58. package/architecture/executors/validate-new-methods/executor.js.map +0 -1
  59. package/architecture/executors/validate-new-methods/executor.ts +0 -584
  60. package/architecture/executors/validate-new-methods/schema.json +0 -25
  61. package/architecture/executors/validate-no-any-unknown/executor.d.ts +0 -42
  62. package/architecture/executors/validate-no-any-unknown/executor.js +0 -462
  63. package/architecture/executors/validate-no-any-unknown/executor.js.map +0 -1
  64. package/architecture/executors/validate-no-any-unknown/executor.ts +0 -540
  65. package/architecture/executors/validate-no-any-unknown/schema.json +0 -24
  66. package/architecture/executors/validate-no-architecture-cycles/executor.d.ts +0 -16
  67. package/architecture/executors/validate-no-architecture-cycles/executor.js +0 -48
  68. package/architecture/executors/validate-no-architecture-cycles/executor.js.map +0 -1
  69. package/architecture/executors/validate-no-architecture-cycles/executor.ts +0 -60
  70. package/architecture/executors/validate-no-architecture-cycles/schema.json +0 -8
  71. package/architecture/executors/validate-no-destructure/executor.d.ts +0 -52
  72. package/architecture/executors/validate-no-destructure/executor.js +0 -491
  73. package/architecture/executors/validate-no-destructure/executor.js.map +0 -1
  74. package/architecture/executors/validate-no-destructure/executor.ts +0 -578
  75. package/architecture/executors/validate-no-destructure/schema.json +0 -24
  76. package/architecture/executors/validate-no-direct-api-resolver/executor.d.ts +0 -47
  77. package/architecture/executors/validate-no-direct-api-resolver/executor.js +0 -566
  78. package/architecture/executors/validate-no-direct-api-resolver/executor.js.map +0 -1
  79. package/architecture/executors/validate-no-direct-api-resolver/executor.ts +0 -666
  80. package/architecture/executors/validate-no-direct-api-resolver/schema.json +0 -29
  81. package/architecture/executors/validate-no-inline-types/executor.d.ts +0 -91
  82. package/architecture/executors/validate-no-inline-types/executor.js +0 -669
  83. package/architecture/executors/validate-no-inline-types/executor.js.map +0 -1
  84. package/architecture/executors/validate-no-inline-types/executor.ts +0 -775
  85. package/architecture/executors/validate-no-inline-types/schema.json +0 -24
  86. package/architecture/executors/validate-no-skiplevel-deps/executor.d.ts +0 -19
  87. package/architecture/executors/validate-no-skiplevel-deps/executor.js +0 -227
  88. package/architecture/executors/validate-no-skiplevel-deps/executor.js.map +0 -1
  89. package/architecture/executors/validate-no-skiplevel-deps/executor.ts +0 -267
  90. package/architecture/executors/validate-no-skiplevel-deps/schema.json +0 -8
  91. package/architecture/executors/validate-packagejson/executor.d.ts +0 -16
  92. package/architecture/executors/validate-packagejson/executor.js +0 -57
  93. package/architecture/executors/validate-packagejson/executor.js.map +0 -1
  94. package/architecture/executors/validate-packagejson/executor.ts +0 -74
  95. package/architecture/executors/validate-packagejson/schema.json +0 -8
  96. package/architecture/executors/validate-prisma-converters/executor.d.ts +0 -60
  97. package/architecture/executors/validate-prisma-converters/executor.js +0 -634
  98. package/architecture/executors/validate-prisma-converters/executor.js.map +0 -1
  99. package/architecture/executors/validate-prisma-converters/executor.ts +0 -822
  100. package/architecture/executors/validate-prisma-converters/schema.json +0 -38
  101. package/architecture/executors/validate-return-types/executor.d.ts +0 -29
  102. package/architecture/executors/validate-return-types/executor.js +0 -439
  103. package/architecture/executors/validate-return-types/executor.js.map +0 -1
  104. package/architecture/executors/validate-return-types/executor.ts +0 -524
  105. package/architecture/executors/validate-return-types/schema.json +0 -24
  106. package/architecture/executors/visualize/executor.d.ts +0 -17
  107. package/architecture/executors/visualize/executor.js +0 -49
  108. package/architecture/executors/visualize/executor.js.map +0 -1
  109. package/architecture/executors/visualize/executor.ts +0 -63
  110. package/architecture/executors/visualize/schema.json +0 -14
  111. package/architecture/index.d.ts +0 -19
  112. package/architecture/index.js +0 -23
  113. package/architecture/index.js.map +0 -1
  114. package/architecture/index.ts +0 -20
  115. package/architecture/lib/graph-comparator.d.ts +0 -39
  116. package/architecture/lib/graph-comparator.js +0 -100
  117. package/architecture/lib/graph-comparator.js.map +0 -1
  118. package/architecture/lib/graph-comparator.ts +0 -141
  119. package/architecture/lib/graph-generator.d.ts +0 -19
  120. package/architecture/lib/graph-generator.js +0 -84
  121. package/architecture/lib/graph-generator.js.map +0 -1
  122. package/architecture/lib/graph-generator.ts +0 -97
  123. package/architecture/lib/graph-loader.d.ts +0 -31
  124. package/architecture/lib/graph-loader.js +0 -98
  125. package/architecture/lib/graph-loader.js.map +0 -1
  126. package/architecture/lib/graph-loader.ts +0 -116
  127. package/architecture/lib/graph-sorter.d.ts +0 -37
  128. package/architecture/lib/graph-sorter.js +0 -110
  129. package/architecture/lib/graph-sorter.js.map +0 -1
  130. package/architecture/lib/graph-sorter.ts +0 -137
  131. package/architecture/lib/graph-visualizer.d.ts +0 -29
  132. package/architecture/lib/graph-visualizer.js +0 -217
  133. package/architecture/lib/graph-visualizer.js.map +0 -1
  134. package/architecture/lib/graph-visualizer.ts +0 -231
  135. package/architecture/lib/package-validator.d.ts +0 -38
  136. package/architecture/lib/package-validator.js +0 -126
  137. package/architecture/lib/package-validator.js.map +0 -1
  138. package/architecture/lib/package-validator.ts +0 -170
  139. package/eslint-plugin/__tests__/catch-error-pattern.test.ts +0 -359
  140. package/eslint-plugin/__tests__/max-file-lines.test.ts +0 -207
  141. package/eslint-plugin/__tests__/max-method-lines.test.ts +0 -258
  142. package/eslint-plugin/__tests__/no-unmanaged-exceptions.test.ts +0 -359
  143. package/eslint-plugin/index.d.ts +0 -23
  144. package/eslint-plugin/index.js +0 -30
  145. package/eslint-plugin/index.js.map +0 -1
  146. package/eslint-plugin/index.ts +0 -29
  147. package/eslint-plugin/rules/catch-error-pattern.d.ts +0 -11
  148. package/eslint-plugin/rules/catch-error-pattern.js +0 -196
  149. package/eslint-plugin/rules/catch-error-pattern.js.map +0 -1
  150. package/eslint-plugin/rules/catch-error-pattern.ts +0 -281
  151. package/eslint-plugin/rules/enforce-architecture.d.ts +0 -15
  152. package/eslint-plugin/rules/enforce-architecture.js +0 -476
  153. package/eslint-plugin/rules/enforce-architecture.js.map +0 -1
  154. package/eslint-plugin/rules/enforce-architecture.ts +0 -543
  155. package/eslint-plugin/rules/max-file-lines.d.ts +0 -12
  156. package/eslint-plugin/rules/max-file-lines.js +0 -257
  157. package/eslint-plugin/rules/max-file-lines.js.map +0 -1
  158. package/eslint-plugin/rules/max-file-lines.ts +0 -272
  159. package/eslint-plugin/rules/max-method-lines.d.ts +0 -12
  160. package/eslint-plugin/rules/max-method-lines.js +0 -240
  161. package/eslint-plugin/rules/max-method-lines.js.map +0 -1
  162. package/eslint-plugin/rules/max-method-lines.ts +0 -287
  163. package/eslint-plugin/rules/no-unmanaged-exceptions.d.ts +0 -22
  164. package/eslint-plugin/rules/no-unmanaged-exceptions.js +0 -160
  165. package/eslint-plugin/rules/no-unmanaged-exceptions.js.map +0 -1
  166. package/eslint-plugin/rules/no-unmanaged-exceptions.ts +0 -179
  167. package/executors/help/executor.js.map +0 -1
  168. package/executors/help/executor.ts +0 -61
  169. package/executors/validate-eslint-sync/executor.js.map +0 -1
  170. package/executors/validate-eslint-sync/executor.ts +0 -83
  171. package/executors/validate-versions-locked/executor.js.map +0 -1
  172. package/executors/validate-versions-locked/executor.ts +0 -367
  173. package/plugin/README.md +0 -243
  174. package/plugin/index.d.ts +0 -4
  175. package/plugin/index.js +0 -8
  176. package/plugin/index.js.map +0 -1
  177. package/plugin/index.ts +0 -4
  178. /package/{executors → src/executors}/help/executor.js +0 -0
  179. /package/{executors → src/executors}/help/schema.json +0 -0
  180. /package/{executors → src/executors}/validate-eslint-sync/executor.js +0 -0
  181. /package/{executors → src/executors}/validate-eslint-sync/schema.json +0 -0
  182. /package/{executors → src/executors}/validate-versions-locked/executor.d.ts +0 -0
  183. /package/{executors → src/executors}/validate-versions-locked/schema.json +0 -0
@@ -1,689 +0,0 @@
1
- /**
2
- * Validate DTOs Executor
3
- *
4
- * Validates that every non-deprecated field in a XxxDto class/interface exists
5
- * in the corresponding XxxDbo Prisma model. This catches AI agents inventing
6
- * field names that don't match the database schema.
7
- *
8
- * ============================================================================
9
- * MODES
10
- * ============================================================================
11
- * - OFF: Skip validation entirely
12
- * - MODIFIED_CLASS: Only validate Dto classes that have changed lines in the diff
13
- * - MODIFIED_FILES: Validate ALL Dto classes in files that were modified
14
- *
15
- * ============================================================================
16
- * SKIP CONDITIONS
17
- * ============================================================================
18
- * - If schema.prisma itself is modified, validation is skipped (schema in flux)
19
- * - Dto classes ending with "JoinDto" are skipped (they compose other Dtos)
20
- * - Fields marked @deprecated in a comment are exempt
21
- *
22
- * ============================================================================
23
- * MATCHING
24
- * ============================================================================
25
- * - UserDto matches UserDbo by case-insensitive prefix ("user")
26
- * - Dbo field names are converted from snake_case to camelCase for comparison
27
- * - Dto fields must be a subset of Dbo fields
28
- * - Extra Dbo fields are allowed (e.g., password)
29
- */
30
-
31
- import type { ExecutorContext } from '@nx/devkit';
32
- import { execSync } from 'child_process';
33
- import * as fs from 'fs';
34
- import * as path from 'path';
35
- import * as ts from 'typescript';
36
-
37
- export type ValidateDtosMode = 'OFF' | 'MODIFIED_CLASS' | 'MODIFIED_FILES';
38
-
39
- export interface ValidateDtosOptions {
40
- mode?: ValidateDtosMode;
41
- disableAllowed?: boolean;
42
- prismaSchemaPath?: string;
43
- dtoSourcePaths?: string[];
44
- ignoreModifiedUntilEpoch?: number;
45
- }
46
-
47
- export interface ExecutorResult {
48
- success: boolean;
49
- }
50
-
51
- interface DtoFieldInfo {
52
- name: string;
53
- line: number;
54
- deprecated: boolean;
55
- }
56
-
57
- interface DtoInfo {
58
- name: string;
59
- file: string;
60
- startLine: number;
61
- endLine: number;
62
- fields: DtoFieldInfo[];
63
- }
64
-
65
- interface DtoViolation {
66
- file: string;
67
- line: number;
68
- dtoName: string;
69
- fieldName: string;
70
- dboName: string;
71
- availableFields: string[];
72
- }
73
-
74
- interface DboEntry {
75
- name: string;
76
- fields: Set<string>;
77
- }
78
-
79
- /**
80
- * Auto-detect the base branch by finding the merge-base with origin/main.
81
- */
82
- function detectBase(workspaceRoot: string): string | null {
83
- try {
84
- const mergeBase = execSync('git merge-base HEAD origin/main', {
85
- cwd: workspaceRoot,
86
- encoding: 'utf-8',
87
- stdio: ['pipe', 'pipe', 'pipe'],
88
- }).trim();
89
-
90
- if (mergeBase) {
91
- return mergeBase;
92
- }
93
- } catch {
94
- try {
95
- const mergeBase = execSync('git merge-base HEAD main', {
96
- cwd: workspaceRoot,
97
- encoding: 'utf-8',
98
- stdio: ['pipe', 'pipe', 'pipe'],
99
- }).trim();
100
-
101
- if (mergeBase) {
102
- return mergeBase;
103
- }
104
- } catch {
105
- // Ignore
106
- }
107
- }
108
- return null;
109
- }
110
-
111
- /**
112
- * Get changed files between base and head (or working tree if head not specified).
113
- */
114
- // webpieces-disable max-lines-new-methods -- Git command handling with untracked files requires multiple code paths
115
- function getChangedFiles(workspaceRoot: string, base: string, head?: string): string[] {
116
- try {
117
- const diffTarget = head ? `${base} ${head}` : base;
118
- const output = execSync(`git diff --name-only ${diffTarget}`, {
119
- cwd: workspaceRoot,
120
- encoding: 'utf-8',
121
- });
122
- const changedFiles = output
123
- .trim()
124
- .split('\n')
125
- .filter((f) => f.length > 0);
126
-
127
- if (!head) {
128
- try {
129
- const untrackedOutput = execSync('git ls-files --others --exclude-standard', {
130
- cwd: workspaceRoot,
131
- encoding: 'utf-8',
132
- });
133
- const untrackedFiles = untrackedOutput
134
- .trim()
135
- .split('\n')
136
- .filter((f) => f.length > 0);
137
- const allFiles = new Set([...changedFiles, ...untrackedFiles]);
138
- return Array.from(allFiles);
139
- } catch {
140
- return changedFiles;
141
- }
142
- }
143
-
144
- return changedFiles;
145
- } catch {
146
- return [];
147
- }
148
- }
149
-
150
- /**
151
- * Get the diff content for a specific file.
152
- */
153
- function getFileDiff(workspaceRoot: string, file: string, base: string, head?: string): string {
154
- try {
155
- const diffTarget = head ? `${base} ${head}` : base;
156
- const diff = execSync(`git diff ${diffTarget} -- "${file}"`, {
157
- cwd: workspaceRoot,
158
- encoding: 'utf-8',
159
- });
160
-
161
- if (!diff && !head) {
162
- const fullPath = path.join(workspaceRoot, file);
163
- if (fs.existsSync(fullPath)) {
164
- const isUntracked = execSync(`git ls-files --others --exclude-standard "${file}"`, {
165
- cwd: workspaceRoot,
166
- encoding: 'utf-8',
167
- }).trim();
168
-
169
- if (isUntracked) {
170
- const content = fs.readFileSync(fullPath, 'utf-8');
171
- const lines = content.split('\n');
172
- return lines.map((line) => `+${line}`).join('\n');
173
- }
174
- }
175
- }
176
-
177
- return diff;
178
- } catch {
179
- return '';
180
- }
181
- }
182
-
183
- /**
184
- * Parse diff to extract changed line numbers (additions only - lines starting with +).
185
- */
186
- function getChangedLineNumbers(diffContent: string): Set<number> {
187
- const changedLines = new Set<number>();
188
- const lines = diffContent.split('\n');
189
- let currentLine = 0;
190
-
191
- for (const line of lines) {
192
- const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
193
- if (hunkMatch) {
194
- currentLine = parseInt(hunkMatch[1], 10);
195
- continue;
196
- }
197
-
198
- if (line.startsWith('+') && !line.startsWith('+++')) {
199
- changedLines.add(currentLine);
200
- currentLine++;
201
- } else if (line.startsWith('-') && !line.startsWith('---')) {
202
- // Deletions don't increment line number
203
- } else {
204
- currentLine++;
205
- }
206
- }
207
-
208
- return changedLines;
209
- }
210
-
211
- /**
212
- * Convert a snake_case string to camelCase.
213
- * e.g., "version_number" -> "versionNumber", "id" -> "id"
214
- */
215
- function snakeToCamel(s: string): string {
216
- return s.replace(/_([a-z])/g, (_, letter: string) => letter.toUpperCase());
217
- }
218
-
219
- /**
220
- * Parse schema.prisma to build a map of Dbo model name -> set of field names (camelCase).
221
- * Only models whose name ends with "Dbo" are included.
222
- * Field names are converted from snake_case to camelCase since Dto fields use camelCase.
223
- */
224
- function parsePrismaSchema(schemaPath: string): Map<string, Set<string>> {
225
- const models = new Map<string, Set<string>>();
226
-
227
- if (!fs.existsSync(schemaPath)) {
228
- return models;
229
- }
230
-
231
- const content = fs.readFileSync(schemaPath, 'utf-8');
232
- const lines = content.split('\n');
233
-
234
- let currentModel: string | null = null;
235
- let currentFields: Set<string> | null = null;
236
-
237
- for (const line of lines) {
238
- const trimmed = line.trim();
239
-
240
- // Match model declaration: model XxxDbo {
241
- const modelMatch = trimmed.match(/^model\s+(\w+Dbo)\s*\{/);
242
- if (modelMatch) {
243
- currentModel = modelMatch[1];
244
- currentFields = new Set<string>();
245
- continue;
246
- }
247
-
248
- // End of model block
249
- if (currentModel && trimmed === '}') {
250
- models.set(currentModel, currentFields!);
251
- currentModel = null;
252
- currentFields = null;
253
- continue;
254
- }
255
-
256
- // Inside a model block - extract field names
257
- if (currentModel && currentFields) {
258
- // Skip empty lines, comments, and model-level attributes (@@)
259
- if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('@@')) {
260
- continue;
261
- }
262
-
263
- // Field name is the first word on the line, converted to camelCase
264
- const fieldMatch = trimmed.match(/^(\w+)\s/);
265
- if (fieldMatch) {
266
- currentFields.add(snakeToCamel(fieldMatch[1]));
267
- }
268
- }
269
- }
270
-
271
- return models;
272
- }
273
-
274
- /**
275
- * Check if a field has @deprecated in a comment above it (within 3 lines).
276
- */
277
- function isFieldDeprecated(fileLines: string[], fieldLine: number): boolean {
278
- const start = Math.max(0, fieldLine - 4);
279
- for (let i = start; i <= fieldLine - 1; i++) {
280
- const line = fileLines[i]?.trim() ?? '';
281
- if (line.includes('@deprecated')) return true;
282
- }
283
- return false;
284
- }
285
-
286
- /**
287
- * Parse a TypeScript file to find Dto class/interface declarations and their fields.
288
- * Skips classes ending with "JoinDto" since they compose other Dtos.
289
- */
290
- // webpieces-disable max-lines-new-methods -- AST traversal for both class and interface Dto detection with field extraction
291
- function findDtosInFile(filePath: string, workspaceRoot: string): DtoInfo[] {
292
- const fullPath = path.join(workspaceRoot, filePath);
293
- if (!fs.existsSync(fullPath)) return [];
294
-
295
- const content = fs.readFileSync(fullPath, 'utf-8');
296
- const fileLines = content.split('\n');
297
- const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
298
-
299
- const dtos: DtoInfo[] = [];
300
-
301
- function visit(node: ts.Node): void {
302
- const isClass = ts.isClassDeclaration(node);
303
- const isInterface = ts.isInterfaceDeclaration(node);
304
-
305
- if ((isClass || isInterface) && node.name) {
306
- const name = node.name.text;
307
-
308
- // Must end with Dto but NOT with JoinDto
309
- if (name.endsWith('Dto') && !name.endsWith('JoinDto')) {
310
- const fields: DtoFieldInfo[] = [];
311
- const nodeStart = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
312
- const nodeEnd = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
313
-
314
- for (const member of node.members) {
315
- if (ts.isPropertyDeclaration(member) || ts.isPropertySignature(member)) {
316
- if (member.name && ts.isIdentifier(member.name)) {
317
- const fieldName = member.name.text;
318
- const startPos = member.getStart(sourceFile);
319
- const pos = sourceFile.getLineAndCharacterOfPosition(startPos);
320
- const line = pos.line + 1;
321
- const deprecated = isFieldDeprecated(fileLines, line);
322
-
323
- fields.push({ name: fieldName, line, deprecated });
324
- }
325
- }
326
- }
327
-
328
- dtos.push({
329
- name,
330
- file: filePath,
331
- startLine: nodeStart.line + 1,
332
- endLine: nodeEnd.line + 1,
333
- fields,
334
- });
335
- }
336
- }
337
-
338
- ts.forEachChild(node, visit);
339
- }
340
-
341
- visit(sourceFile);
342
- return dtos;
343
- }
344
-
345
- /**
346
- * Extract the prefix from a Dto/Dbo name by removing the suffix.
347
- * e.g., "UserDto" -> "user", "UserDbo" -> "user"
348
- */
349
- function extractPrefix(name: string, suffix: string): string {
350
- return name.slice(0, -suffix.length).toLowerCase();
351
- }
352
-
353
- /**
354
- * Find violations: Dto fields that don't exist in the corresponding Dbo.
355
- */
356
- function findViolations(
357
- dtos: DtoInfo[],
358
- dboModels: Map<string, Set<string>>
359
- ): DtoViolation[] {
360
- const violations: DtoViolation[] = [];
361
-
362
- // Build a lowercase prefix -> Dbo info map
363
- const dboByPrefix = new Map<string, DboEntry>();
364
- for (const [dboName, fields] of dboModels) {
365
- const prefix = extractPrefix(dboName, 'Dbo');
366
- dboByPrefix.set(prefix, { name: dboName, fields });
367
- }
368
-
369
- for (const dto of dtos) {
370
- const prefix = extractPrefix(dto.name, 'Dto');
371
- const dbo = dboByPrefix.get(prefix);
372
-
373
- if (!dbo) {
374
- // No matching Dbo found - skip (might be a Dto without a DB table)
375
- continue;
376
- }
377
-
378
- for (const field of dto.fields) {
379
- if (field.deprecated) continue;
380
-
381
- if (!dbo.fields.has(field.name)) {
382
- violations.push({
383
- file: dto.file,
384
- line: field.line,
385
- dtoName: dto.name,
386
- fieldName: field.name,
387
- dboName: dbo.name,
388
- availableFields: Array.from(dbo.fields).sort(),
389
- });
390
- }
391
- }
392
- }
393
-
394
- return violations;
395
- }
396
-
397
- /**
398
- * Compute similarity between two strings using longest common subsequence ratio.
399
- * Returns a value between 0 and 1, where 1 is an exact match.
400
- */
401
- function similarity(a: string, b: string): number {
402
- const al = a.toLowerCase();
403
- const bl = b.toLowerCase();
404
- if (al === bl) return 1;
405
-
406
- const m = al.length;
407
- const n = bl.length;
408
- const prev = new Array<number>(n + 1).fill(0);
409
- const curr = new Array<number>(n + 1).fill(0);
410
-
411
- for (let i = 1; i <= m; i++) {
412
- for (let j = 1; j <= n; j++) {
413
- if (al[i - 1] === bl[j - 1]) {
414
- curr[j] = prev[j - 1] + 1;
415
- } else {
416
- curr[j] = Math.max(prev[j], curr[j - 1]);
417
- }
418
- }
419
- for (let j = 0; j <= n; j++) {
420
- prev[j] = curr[j];
421
- curr[j] = 0;
422
- }
423
- }
424
-
425
- const lcsLen = prev[n];
426
- return (2 * lcsLen) / (m + n);
427
- }
428
-
429
- /**
430
- * Find the closest matching Dbo field for a given Dto field name.
431
- * Returns the best match or null if nothing scores above the threshold.
432
- */
433
- function suggestClosestField(fieldName: string, availableFields: string[]): string | null {
434
- let bestMatch: string | null = null;
435
- let bestScore = 0.4; // minimum threshold
436
-
437
- for (const candidate of availableFields) {
438
- const score = similarity(fieldName, candidate);
439
- if (score > bestScore) {
440
- bestScore = score;
441
- bestMatch = candidate;
442
- }
443
- }
444
-
445
- return bestMatch;
446
- }
447
-
448
- /**
449
- * Report violations to console with prominent AI warning and rename suggestions.
450
- */
451
- function reportViolations(violations: DtoViolation[]): void {
452
- console.error('');
453
- console.error('❌❌❌ DTO FIELD MISMATCH - AI AGENT: READ THIS CAREFULLY ❌❌❌');
454
- console.error('');
455
- console.error('🛑 DO NOT attempt find/replace or manual renaming. Instead:');
456
- console.error(' 1. IDE rename is 10x faster than AI renaming PLUS 100% correct vs. AI\'s 80%');
457
- console.error(' 2. Based on errors below, suggest renames to the user');
458
- console.error(' 3. Ask the human to use IDE rename-refactoring (F2 / right-click → Rename Symbol)');
459
- console.error('');
460
- console.error('📚 Every non-deprecated field in a Dto must exist in the corresponding Dbo.');
461
- console.error(' Dbo can have extra fields (e.g., password) — Dto fields cannot be invented.');
462
- console.error('');
463
- console.error(' When needing fields from multiple tables (e.g., a join), use a XxxJoinDto that');
464
- console.error(' contains YYDto and ZZDto fields from the other tables instead of flattening.');
465
- console.error(' Fields marked @deprecated in the Dto are exempt from this check.');
466
- console.error('');
467
-
468
- for (const v of violations) {
469
- console.error(` ❌ ${v.file}:${v.line}`);
470
- console.error(` ${v.dtoName}.${v.fieldName} does not exist in ${v.dboName}`);
471
-
472
- const suggestion = suggestClosestField(v.fieldName, v.availableFields);
473
- if (suggestion) {
474
- console.error(` 💡 Suggested rename: ${v.fieldName} → ${suggestion} (Use IDE rename on ${v.dtoName}.${v.fieldName})`);
475
- } else {
476
- const preview = v.availableFields.slice(0, 8).join(', ');
477
- const ellipsis = v.availableFields.length > 8 ? ', ...' : '';
478
- console.error(` 💡 No close match found. Available: ${preview}${ellipsis}`);
479
- }
480
- }
481
- console.error('');
482
-
483
- console.error('❌❌❌ AI AGENT: Suggest to the human which are the most likely renames needed and list them ❌❌❌');
484
- console.error('');
485
- }
486
-
487
- /**
488
- * Filter changed files to only TypeScript Dto source files within configured paths.
489
- */
490
- function filterDtoFiles(changedFiles: string[], dtoSourcePaths: string[]): string[] {
491
- return changedFiles.filter((f) => {
492
- if (!f.endsWith('.ts') && !f.endsWith('.tsx')) return false;
493
- if (f.includes('.spec.ts') || f.includes('.test.ts')) return false;
494
- return dtoSourcePaths.some((srcPath) => f.startsWith(srcPath));
495
- });
496
- }
497
-
498
- /**
499
- * Collect all Dto definitions from the given files.
500
- */
501
- function collectDtos(dtoFiles: string[], workspaceRoot: string): DtoInfo[] {
502
- const allDtos: DtoInfo[] = [];
503
- for (const file of dtoFiles) {
504
- const dtos = findDtosInFile(file, workspaceRoot);
505
- allDtos.push(...dtos);
506
- }
507
- return allDtos;
508
- }
509
-
510
- /**
511
- * Check if a Dto class overlaps with any changed lines in the diff.
512
- */
513
- function isDtoTouched(dto: DtoInfo, changedLines: Set<number>): boolean {
514
- for (let line = dto.startLine; line <= dto.endLine; line++) {
515
- if (changedLines.has(line)) return true;
516
- }
517
- return false;
518
- }
519
-
520
- /**
521
- * Filter Dtos to only those that have changed lines in the diff (MODIFIED_CLASS mode).
522
- */
523
- function filterTouchedDtos(
524
- dtos: DtoInfo[],
525
- workspaceRoot: string,
526
- base: string,
527
- head?: string
528
- ): DtoInfo[] {
529
- // Group dtos by file to avoid re-fetching diffs
530
- const byFile = new Map<string, DtoInfo[]>();
531
- for (const dto of dtos) {
532
- const list = byFile.get(dto.file) ?? [];
533
- list.push(dto);
534
- byFile.set(dto.file, list);
535
- }
536
-
537
- const touched: DtoInfo[] = [];
538
- for (const [file, fileDtos] of byFile) {
539
- const diff = getFileDiff(workspaceRoot, file, base, head);
540
- const changedLines = getChangedLineNumbers(diff);
541
- for (const dto of fileDtos) {
542
- if (isDtoTouched(dto, changedLines)) {
543
- touched.push(dto);
544
- }
545
- }
546
- }
547
- return touched;
548
- }
549
-
550
- /**
551
- * Resolve git base ref from env vars or auto-detection.
552
- */
553
- function resolveBase(workspaceRoot: string): string | undefined {
554
- const envBase = process.env['NX_BASE'];
555
- if (envBase) return envBase;
556
- return detectBase(workspaceRoot) ?? undefined;
557
- }
558
-
559
- /**
560
- * Run the core validation after early-exit checks have passed.
561
- */
562
- // webpieces-disable max-lines-new-methods -- Core validation orchestration with multiple early-exit checks
563
- function validateDtoFiles(
564
- workspaceRoot: string,
565
- prismaSchemaPath: string,
566
- changedFiles: string[],
567
- dtoSourcePaths: string[],
568
- mode: ValidateDtosMode,
569
- base: string,
570
- head?: string
571
- ): ExecutorResult {
572
- if (changedFiles.some((f) => f.endsWith(prismaSchemaPath))) {
573
- console.log('⏭️ Skipping validate-dtos (schema.prisma is modified - schema in flux)');
574
- console.log('');
575
- return { success: true };
576
- }
577
-
578
- const dtoFiles = filterDtoFiles(changedFiles, dtoSourcePaths);
579
-
580
- if (dtoFiles.length === 0) {
581
- console.log('✅ No Dto files changed');
582
- return { success: true };
583
- }
584
-
585
- console.log(`📂 Checking ${dtoFiles.length} changed file(s) for Dto definitions...`);
586
-
587
- const fullSchemaPath = path.join(workspaceRoot, prismaSchemaPath);
588
- const dboModels = parsePrismaSchema(fullSchemaPath);
589
-
590
- if (dboModels.size === 0) {
591
- console.log('⏭️ No Dbo models found in schema.prisma');
592
- console.log('');
593
- return { success: true };
594
- }
595
-
596
- console.log(` Found ${dboModels.size} Dbo model(s) in schema.prisma`);
597
-
598
- let allDtos = collectDtos(dtoFiles, workspaceRoot);
599
-
600
- if (allDtos.length === 0) {
601
- console.log('✅ No Dto definitions found in changed files');
602
- return { success: true };
603
- }
604
-
605
- // In MODIFIED_CLASS mode, narrow to only Dtos with changed lines
606
- if (mode === 'MODIFIED_CLASS') {
607
- allDtos = filterTouchedDtos(allDtos, workspaceRoot, base, head);
608
- if (allDtos.length === 0) {
609
- console.log('✅ No Dto classes were modified');
610
- return { success: true };
611
- }
612
- }
613
-
614
- console.log(` Validating ${allDtos.length} Dto definition(s)`);
615
-
616
- const violations = findViolations(allDtos, dboModels);
617
-
618
- if (violations.length === 0) {
619
- console.log('✅ All Dto fields match their Dbo models');
620
- return { success: true };
621
- }
622
-
623
- reportViolations(violations);
624
- return { success: false };
625
- }
626
-
627
- /**
628
- * Resolve mode considering ignoreModifiedUntilEpoch override.
629
- * When active, downgrades to OFF. When expired, logs a warning.
630
- */
631
- function resolveMode(normalMode: ValidateDtosMode, epoch: number | undefined): ValidateDtosMode {
632
- if (epoch === undefined || normalMode === 'OFF') {
633
- return normalMode;
634
- }
635
- const nowSeconds = Date.now() / 1000;
636
- if (nowSeconds < epoch) {
637
- const expiresDate = new Date(epoch * 1000).toISOString().split('T')[0];
638
- console.log(`\n⏭️ Skipping validate-dtos (ignoreModifiedUntilEpoch active, expires: ${expiresDate})`);
639
- console.log('');
640
- return 'OFF';
641
- }
642
- return normalMode;
643
- }
644
-
645
- export default async function runExecutor(
646
- options: ValidateDtosOptions,
647
- context: ExecutorContext
648
- ): Promise<ExecutorResult> {
649
- const workspaceRoot = context.root;
650
- const mode = resolveMode(options.mode ?? 'OFF', options.ignoreModifiedUntilEpoch);
651
-
652
- if (mode === 'OFF') {
653
- console.log('\n⏭️ Skipping validate-dtos (mode: OFF)');
654
- console.log('');
655
- return { success: true };
656
- }
657
-
658
- const prismaSchemaPath = options.prismaSchemaPath;
659
- const dtoSourcePaths = options.dtoSourcePaths ?? [];
660
-
661
- if (!prismaSchemaPath || dtoSourcePaths.length === 0) {
662
- const reason = !prismaSchemaPath ? 'no prismaSchemaPath configured' : 'no dtoSourcePaths configured';
663
- console.log(`\n⏭️ Skipping validate-dtos (${reason})`);
664
- console.log('');
665
- return { success: true };
666
- }
667
-
668
- console.log('\n📏 Validating DTOs match Prisma Dbo models\n');
669
- console.log(` Mode: ${mode}`);
670
- console.log(` Schema: ${prismaSchemaPath}`);
671
- console.log(` Dto paths: ${dtoSourcePaths.join(', ')}`);
672
-
673
- const base = resolveBase(workspaceRoot);
674
- const head = process.env['NX_HEAD'];
675
-
676
- if (!base) {
677
- console.log('\n⏭️ Skipping validate-dtos (could not detect base branch)');
678
- console.log('');
679
- return { success: true };
680
- }
681
-
682
- console.log(` Base: ${base}`);
683
- console.log(` Head: ${head ?? 'working tree (includes uncommitted changes)'}`);
684
- console.log('');
685
-
686
- const changedFiles = getChangedFiles(workspaceRoot, base, head);
687
-
688
- return validateDtoFiles(workspaceRoot, prismaSchemaPath, changedFiles, dtoSourcePaths, mode, base, head);
689
- }