@webpieces/dev-config 0.2.61 → 0.2.62

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.
@@ -14,7 +14,9 @@
14
14
  * Usage:
15
15
  * nx affected --target=validate-modified-methods --base=origin/main
16
16
  *
17
- * Escape hatch: Add webpieces-disable max-lines-new-and-modified comment with justification
17
+ * Escape hatch: Add webpieces-disable max-lines-new-and-modified comment with date and justification
18
+ * Format: // webpieces-disable max-lines-new-and-modified 2025/01/15 -- [reason]
19
+ * The disable expires after 1 month from the date specified.
18
20
  */
19
21
 
20
22
  import type { ExecutorContext } from '@nx/devkit';
@@ -36,6 +38,8 @@ interface MethodViolation {
36
38
  methodName: string;
37
39
  line: number;
38
40
  lines: number;
41
+ expiredDisable?: boolean;
42
+ expiredDate?: string;
39
43
  }
40
44
 
41
45
  const TMP_DIR = 'tmp/webpieces';
@@ -140,15 +144,19 @@ const result = this.buildResultObject(data);
140
144
 
141
145
  Sometimes methods genuinely need to be longer (complex algorithms, state machines, etc.).
142
146
 
143
- **Escape hatch**: Add a webpieces-disable comment with justification:
147
+ **Escape hatch**: Add a webpieces-disable comment with DATE and justification:
144
148
 
145
149
  \`\`\`typescript
146
- // webpieces-disable max-lines-new-and-modified -- Complex state machine, splitting reduces clarity
150
+ // webpieces-disable max-lines-new-and-modified 2025/01/15 -- Complex state machine, splitting reduces clarity
147
151
  async complexStateMachine(): Promise<void> {
148
152
  // ... longer method with justification
149
153
  }
150
154
  \`\`\`
151
155
 
156
+ **IMPORTANT**: The date format is yyyy/mm/dd. The disable will EXPIRE after 1 month from this date.
157
+ After expiration, you must either fix the method or update the date to get another month.
158
+ This ensures that disable comments are reviewed periodically.
159
+
152
160
  ## AI Agent Action Steps
153
161
 
154
162
  1. **READ** the method to understand its logical sections
@@ -289,14 +297,64 @@ function getChangedLineNumbers(diffContent: string): Set<number> {
289
297
  return changedLines;
290
298
  }
291
299
 
300
+ /**
301
+ * Parse a date string in yyyy/mm/dd format and return a Date object.
302
+ * Returns null if the format is invalid.
303
+ */
304
+ function parseDisableDate(dateStr: string): Date | null {
305
+ // Match yyyy/mm/dd format
306
+ const match = dateStr.match(/^(\d{4})\/(\d{2})\/(\d{2})$/);
307
+ if (!match) return null;
308
+
309
+ const year = parseInt(match[1], 10);
310
+ const month = parseInt(match[2], 10) - 1; // JS months are 0-indexed
311
+ const day = parseInt(match[3], 10);
312
+
313
+ const date = new Date(year, month, day);
314
+
315
+ // Validate the date is valid (e.g., not Feb 30)
316
+ if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) {
317
+ return null;
318
+ }
319
+
320
+ return date;
321
+ }
322
+
323
+ /**
324
+ * Check if a date is within the last month (not expired).
325
+ */
326
+ function isDateWithinMonth(date: Date): boolean {
327
+ const now = new Date();
328
+ const oneMonthAgo = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate());
329
+ return date >= oneMonthAgo;
330
+ }
331
+
332
+ /**
333
+ * Get today's date in yyyy/mm/dd format for error messages
334
+ */
335
+ function getTodayDateString(): string {
336
+ const now = new Date();
337
+ const year = now.getFullYear();
338
+ const month = String(now.getMonth() + 1).padStart(2, '0');
339
+ const day = String(now.getDate()).padStart(2, '0');
340
+ return `${year}/${month}/${day}`;
341
+ }
342
+
343
+ interface DisableInfo {
344
+ type: 'full' | 'new-only' | 'none';
345
+ isExpired: boolean;
346
+ date?: string;
347
+ }
348
+
292
349
  /**
293
350
  * Check what kind of webpieces-disable comment is present for a method.
294
- * Returns: 'full' | 'new-only' | 'none'
351
+ * Returns: DisableInfo with type, expiration status, and date
295
352
  * - 'full': max-lines-new-and-modified (ultimate escape, skips both validators)
296
353
  * - 'new-only': max-lines-new-methods (escaped 30-line check, still needs 80-line check)
297
354
  * - 'none': no escape hatch
298
355
  */
299
- function getDisableType(lines: string[], lineNumber: number): 'full' | 'new-only' | 'none' {
356
+ // webpieces-disable max-lines-new-methods -- Complex validation logic with multiple escape hatch types
357
+ function getDisableInfo(lines: string[], lineNumber: number): DisableInfo {
300
358
  const startCheck = Math.max(0, lineNumber - 5);
301
359
  for (let i = lineNumber - 2; i >= startCheck; i--) {
302
360
  const line = lines[i]?.trim() ?? '';
@@ -305,24 +363,83 @@ function getDisableType(lines: string[], lineNumber: number): 'full' | 'new-only
305
363
  }
306
364
  if (line.includes('webpieces-disable')) {
307
365
  if (line.includes('max-lines-new-and-modified')) {
308
- return 'full';
366
+ // Check for date in format: max-lines-new-and-modified yyyy/mm/dd
367
+ const dateMatch = line.match(/max-lines-new-and-modified\s+(\d{4}\/\d{2}\/\d{2}|XXXX\/XX\/XX)/);
368
+
369
+ if (!dateMatch) {
370
+ // No date found - treat as expired (invalid)
371
+ return { type: 'full', isExpired: true, date: undefined };
372
+ }
373
+
374
+ const dateStr = dateMatch[1];
375
+
376
+ // Secret permanent disable
377
+ if (dateStr === 'XXXX/XX/XX') {
378
+ return { type: 'full', isExpired: false, date: dateStr };
379
+ }
380
+
381
+ const date = parseDisableDate(dateStr);
382
+ if (!date) {
383
+ // Invalid date format - treat as expired
384
+ return { type: 'full', isExpired: true, date: dateStr };
385
+ }
386
+
387
+ if (!isDateWithinMonth(date)) {
388
+ // Date is expired (older than 1 month)
389
+ return { type: 'full', isExpired: true, date: dateStr };
390
+ }
391
+
392
+ // Valid and not expired
393
+ return { type: 'full', isExpired: false, date: dateStr };
309
394
  }
310
395
  if (line.includes('max-lines-new-methods')) {
311
- return 'new-only';
396
+ // Check for date in format: max-lines-new-methods yyyy/mm/dd
397
+ const dateMatch = line.match(/max-lines-new-methods\s+(\d{4}\/\d{2}\/\d{2}|XXXX\/XX\/XX)/);
398
+
399
+ if (!dateMatch) {
400
+ // No date found - treat as expired (invalid)
401
+ return { type: 'new-only', isExpired: true, date: undefined };
402
+ }
403
+
404
+ const dateStr = dateMatch[1];
405
+
406
+ // Secret permanent disable
407
+ if (dateStr === 'XXXX/XX/XX') {
408
+ return { type: 'new-only', isExpired: false, date: dateStr };
409
+ }
410
+
411
+ const date = parseDisableDate(dateStr);
412
+ if (!date) {
413
+ // Invalid date format - treat as expired
414
+ return { type: 'new-only', isExpired: true, date: dateStr };
415
+ }
416
+
417
+ if (!isDateWithinMonth(date)) {
418
+ // Date is expired (older than 1 month)
419
+ return { type: 'new-only', isExpired: true, date: dateStr };
420
+ }
421
+
422
+ // Valid and not expired
423
+ return { type: 'new-only', isExpired: false, date: dateStr };
312
424
  }
313
425
  }
314
426
  }
315
- return 'none';
427
+ return { type: 'none', isExpired: false };
428
+ }
429
+
430
+ interface MethodInfo {
431
+ name: string;
432
+ line: number;
433
+ endLine: number;
434
+ lines: number;
435
+ disableInfo: DisableInfo;
316
436
  }
317
437
 
318
438
  /**
319
439
  * Parse a TypeScript file and find methods with their line counts
320
440
  */
321
441
  // webpieces-disable max-lines-new-methods -- AST traversal requires inline visitor function
322
- function findMethodsInFile(
323
- filePath: string,
324
- workspaceRoot: string
325
- ): Array<{ name: string; line: number; endLine: number; lines: number; disableType: 'full' | 'new-only' | 'none' }> {
442
+ function findMethodsInFile(filePath: string, workspaceRoot: string): MethodInfo[] {
326
443
  const fullPath = path.join(workspaceRoot, filePath);
327
444
  if (!fs.existsSync(fullPath)) return [];
328
445
 
@@ -330,8 +447,7 @@ function findMethodsInFile(
330
447
  const fileLines = content.split('\n');
331
448
  const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
332
449
 
333
- const methods: Array<{ name: string; line: number; endLine: number; lines: number; disableType: 'full' | 'new-only' | 'none' }> =
334
- [];
450
+ const methods: MethodInfo[] = [];
335
451
 
336
452
  // webpieces-disable max-lines-new-methods -- AST visitor pattern requires handling multiple node types
337
453
  function visit(node: ts.Node): void {
@@ -367,7 +483,7 @@ function findMethodsInFile(
367
483
  line: startLine,
368
484
  endLine: endLine,
369
485
  lines: endLine - startLine + 1,
370
- disableType: getDisableType(fileLines, startLine),
486
+ disableInfo: getDisableInfo(fileLines, startLine),
371
487
  });
372
488
  }
373
489
 
@@ -387,9 +503,9 @@ function findMethodsInFile(
387
503
  *
388
504
  * Skips:
389
505
  * - NEW methods without any escape (let validate-new-methods handle them first)
390
- * - Methods with `max-lines-new-and-modified` escape (ultimate escape hatch)
506
+ * - Methods with valid, non-expired `max-lines-new-and-modified` escape (ultimate escape hatch)
391
507
  */
392
- // webpieces-disable max-lines-new-methods -- Core validation logic with multiple file operations
508
+ // webpieces-disable max-lines-new-and-modified 2025/12/20 -- Core validation logic with multiple file operations
393
509
  function findViolations(
394
510
  workspaceRoot: string,
395
511
  changedFiles: string[],
@@ -414,26 +530,52 @@ function findViolations(
414
530
 
415
531
  for (const method of methods) {
416
532
  const isNewMethod = newMethodNames.has(method.name);
533
+ const { type: disableType, isExpired, date: disableDate } = method.disableInfo;
417
534
 
418
- // Skip methods with full escape (max-lines-new-and-modified)
419
- if (method.disableType === 'full') continue;
535
+ // Skip methods with valid, non-expired full escape (max-lines-new-and-modified)
536
+ if (disableType === 'full' && !isExpired) continue;
420
537
 
421
538
  // Skip methods under the limit
422
539
  if (method.lines <= maxLines) continue;
423
540
 
424
541
  if (isNewMethod) {
425
542
  // For NEW methods:
426
- // - If has 'new-only' escape → check (they escaped 30-line, now need 80-line check)
543
+ // - If has 'new-only' escape (non-expired) → check (they escaped 30-line, now need 80-line check)
427
544
  // - If has 'none' → skip (let validate-new-methods handle first)
428
- if (method.disableType !== 'new-only') continue;
429
-
430
- // New method with max-lines-new-methods escape - check against 80-line limit
431
- violations.push({
432
- file,
433
- methodName: method.name,
434
- line: method.line,
435
- lines: method.lines,
436
- });
545
+ // - If has expired disable → report as violation
546
+ if (disableType === 'full' && isExpired) {
547
+ // Expired full disable - report with expired info
548
+ violations.push({
549
+ file,
550
+ methodName: method.name,
551
+ line: method.line,
552
+ lines: method.lines,
553
+ expiredDisable: true,
554
+ expiredDate: disableDate,
555
+ });
556
+ continue;
557
+ }
558
+ if (disableType !== 'new-only') continue;
559
+
560
+ if (isExpired) {
561
+ // Expired new-only disable - report with expired info
562
+ violations.push({
563
+ file,
564
+ methodName: method.name,
565
+ line: method.line,
566
+ lines: method.lines,
567
+ expiredDisable: true,
568
+ expiredDate: disableDate,
569
+ });
570
+ } else {
571
+ // New method with valid max-lines-new-methods escape - check against 80-line limit
572
+ violations.push({
573
+ file,
574
+ methodName: method.name,
575
+ line: method.line,
576
+ lines: method.lines,
577
+ });
578
+ }
437
579
  } else {
438
580
  // For MODIFIED methods: check if any changed line falls within method's range
439
581
  let hasChanges = false;
@@ -445,12 +587,24 @@ function findViolations(
445
587
  }
446
588
 
447
589
  if (hasChanges) {
448
- violations.push({
449
- file,
450
- methodName: method.name,
451
- line: method.line,
452
- lines: method.lines,
453
- });
590
+ if (disableType === 'full' && isExpired) {
591
+ // Expired full disable - report with expired info
592
+ violations.push({
593
+ file,
594
+ methodName: method.name,
595
+ line: method.line,
596
+ lines: method.lines,
597
+ expiredDisable: true,
598
+ expiredDate: disableDate,
599
+ });
600
+ } else {
601
+ violations.push({
602
+ file,
603
+ methodName: method.name,
604
+ line: method.line,
605
+ lines: method.lines,
606
+ });
607
+ }
454
608
  }
455
609
  }
456
610
  }
@@ -491,6 +645,46 @@ function detectBase(workspaceRoot: string): string | null {
491
645
  return null;
492
646
  }
493
647
 
648
+ /**
649
+ * Report violations to console
650
+ */
651
+ // webpieces-disable max-lines-new-methods -- Error output formatting with multiple message sections
652
+ function reportViolations(violations: MethodViolation[], maxLines: number): void {
653
+ console.error('');
654
+ console.error('❌ Modified methods exceed ' + maxLines + ' lines!');
655
+ console.error('');
656
+ console.error('📚 When you modify a method, you must bring it under ' + maxLines + ' lines.');
657
+ console.error(' This rule encourages GRADUAL cleanup so even though you did not cause it,');
658
+ console.error(' you touched it, so you should fix now as part of your PR');
659
+ console.error(' (this is for vibe coding and AI to fix as it touches things).');
660
+ console.error(' You can refactor to stay under the limit 50% of the time. If not feasible, use the escape hatch.');
661
+ console.error('');
662
+ console.error(
663
+ '⚠️ *** READ tmp/webpieces/webpieces.methodsize.md for detailed guidance on how to fix this easily *** ⚠️'
664
+ );
665
+ console.error('');
666
+
667
+ for (const v of violations) {
668
+ if (v.expiredDisable) {
669
+ console.error(` ❌ ${v.file}:${v.line}`);
670
+ console.error(` Method: ${v.methodName} (${v.lines} lines, max: ${maxLines})`);
671
+ console.error(` ⏰ EXPIRED DISABLE: Your disable comment dated ${v.expiredDate ?? 'unknown'} has expired (>1 month old).`);
672
+ console.error(` You must either FIX the method or UPDATE the date to get another month.`);
673
+ } else {
674
+ console.error(` ❌ ${v.file}:${v.line}`);
675
+ console.error(` Method: ${v.methodName} (${v.lines} lines, max: ${maxLines})`);
676
+ }
677
+ }
678
+ console.error('');
679
+
680
+ console.error(' You can disable this error, but you will be forced to fix again in 1 month');
681
+ console.error(' since 99% of methods can be less than ' + maxLines + ' lines of code.');
682
+ console.error('');
683
+ console.error(' Use escape with DATE (expires in 1 month):');
684
+ console.error(` // webpieces-disable max-lines-new-and-modified ${getTodayDateString()} -- [your reason]`);
685
+ console.error('');
686
+ }
687
+
494
688
  export default async function runExecutor(
495
689
  options: ValidateModifiedMethodsOptions,
496
690
  context: ExecutorContext
@@ -537,30 +731,8 @@ export default async function runExecutor(
537
731
  return { success: true };
538
732
  }
539
733
 
540
- // Write instructions file
541
734
  writeTmpInstructions(workspaceRoot);
542
-
543
- // Report violations
544
- console.error('');
545
- console.error('❌ Modified methods exceed ' + maxLines + ' lines!');
546
- console.error('');
547
- console.error('📚 When you modify a method, you must bring it under ' + maxLines + ' lines.');
548
- console.error(' This rule encourages GRADUAL cleanup so even though you did not cause it,');
549
- console.error(' you touched it, so you should fix now as part of your PR');
550
- console.error(' (this is for vibe coding and AI to fix as it touches things).');
551
- console.error(' You can refactor to stay under the limit 50% of the time. If not feasible, use the escape hatch.');
552
- console.error('');
553
- console.error(
554
- '⚠️ *** READ tmp/webpieces/webpieces.methodsize.md for detailed guidance on how to fix this easily *** ⚠️'
555
- );
556
- console.error('');
557
-
558
- for (const v of violations) {
559
- console.error(` ❌ ${v.file}:${v.line}`);
560
- console.error(` Method: ${v.methodName} (${v.lines} lines, max: ${maxLines})`);
561
- }
562
- console.error('');
563
-
735
+ reportViolations(violations, maxLines);
564
736
  return { success: false };
565
737
  } catch (err: unknown) {
566
738
  const error = err instanceof Error ? err : new Error(String(err));
package/executors.json CHANGED
@@ -54,6 +54,11 @@
54
54
  "implementation": "./executors/validate-versions-locked/executor",
55
55
  "schema": "./executors/validate-versions-locked/schema.json",
56
56
  "description": "Validate package.json versions are locked (no semver ranges) and npm ci compatible"
57
+ },
58
+ "validate-modified-files": {
59
+ "implementation": "./architecture/executors/validate-modified-files/executor",
60
+ "schema": "./architecture/executors/validate-modified-files/schema.json",
61
+ "description": "Validate modified files don't exceed max line count"
57
62
  }
58
63
  }
59
64
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/dev-config",
3
- "version": "0.2.61",
3
+ "version": "0.2.62",
4
4
  "description": "Development configuration, scripts, and patterns for WebPieces projects",
5
5
  "type": "commonjs",
6
6
  "bin": {
package/plugin.js CHANGED
@@ -37,9 +37,11 @@ const DEFAULT_OPTIONS = {
37
37
  validatePackageJson: true,
38
38
  validateNewMethods: true,
39
39
  validateModifiedMethods: true,
40
+ validateModifiedFiles: true,
40
41
  validateVersionsLocked: true,
41
42
  newMethodsMaxLines: 30,
42
43
  modifiedAndNewMethodsMaxLines: 80,
44
+ modifiedFilesMaxLines: 900,
43
45
  },
44
46
  features: {
45
47
  generate: true,
@@ -157,6 +159,8 @@ function buildValidationTargetsList(validations) {
157
159
  targets.push('validate-new-methods');
158
160
  if (validations.validateModifiedMethods)
159
161
  targets.push('validate-modified-methods');
162
+ if (validations.validateModifiedFiles)
163
+ targets.push('validate-modified-files');
160
164
  if (validations.validateVersionsLocked)
161
165
  targets.push('validate-versions-locked');
162
166
  return targets;
@@ -195,6 +199,9 @@ function createWorkspaceTargetsWithoutPrefix(opts) {
195
199
  if (validations.validateModifiedMethods) {
196
200
  targets['validate-modified-methods'] = createValidateModifiedMethodsTarget(validations.modifiedAndNewMethodsMaxLines);
197
201
  }
202
+ if (validations.validateModifiedFiles) {
203
+ targets['validate-modified-files'] = createValidateModifiedFilesTarget(validations.modifiedFilesMaxLines);
204
+ }
198
205
  if (validations.validateVersionsLocked) {
199
206
  targets['validate-versions-locked'] = createValidateVersionsLockedTarget();
200
207
  }
@@ -239,6 +246,9 @@ function createWorkspaceTargets(opts) {
239
246
  if (opts.workspace.validations.validateModifiedMethods) {
240
247
  targets[`${prefix}validate-modified-methods`] = createValidateModifiedMethodsTarget(opts.workspace.validations.modifiedAndNewMethodsMaxLines);
241
248
  }
249
+ if (opts.workspace.validations.validateModifiedFiles) {
250
+ targets[`${prefix}validate-modified-files`] = createValidateModifiedFilesTarget(opts.workspace.validations.modifiedFilesMaxLines);
251
+ }
242
252
  return targets;
243
253
  }
244
254
  function createGenerateTarget(graphPath) {
@@ -345,6 +355,18 @@ function createValidateModifiedMethodsTarget(maxLines) {
345
355
  },
346
356
  };
347
357
  }
358
+ function createValidateModifiedFilesTarget(maxLines) {
359
+ return {
360
+ executor: '@webpieces/dev-config:validate-modified-files',
361
+ cache: false, // Don't cache - depends on git state
362
+ inputs: ['default'],
363
+ options: { max: maxLines },
364
+ metadata: {
365
+ technologies: ['nx'],
366
+ description: `Validate modified files do not exceed ${maxLines} lines (encourages keeping files small)`,
367
+ },
368
+ };
369
+ }
348
370
  function createValidateVersionsLockedTarget() {
349
371
  return {
350
372
  executor: '@webpieces/dev-config:validate-versions-locked',