agent-eslint-config 0.1.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +176 -79
  2. package/package.json +8 -1
package/README.md CHANGED
@@ -177,20 +177,16 @@ const registry: RegisteredUser[] = []
177
177
 
178
178
  async function processUserRegistration(input: unknown): Promise<RegisteredUser> {
179
179
  const data = input as any
180
- if (!data.email || typeof data.email !== 'string') throw new Error('email is required')
181
- if (!data.password || typeof data.password !== 'string') throw new Error('password is required')
182
- const email = data.email.trim().toLowerCase()
183
- if (!email.includes('@')) throw new Error('invalid email')
180
+ if (!data.email || typeof data.email !== 'string') throw new Error('email is required');
181
+ if (!data.password || typeof data.password !== 'string') throw new Error('password is required');
182
+ const email = data.email.trim().toLowerCase();
183
+ if (!email.includes('@')) throw new Error('invalid email');
184
184
  let hash = ''
185
185
  for (const character of data.password) {
186
- if (typeof character === 'string') {
187
- hash = hash + String(character.charCodeAt(0) * 31 % 255)
188
- }
186
+ if (typeof character === 'string') { hash = hash + String(character.charCodeAt(0) * 31 % 255); }
189
187
  }
190
188
  for (const existing of registry) {
191
- if (existing.email === email) {
192
- throw new Error('email already registered')
193
- }
189
+ if (existing.email === email) { throw new Error('email already registered'); }
194
190
  }
195
191
  const user = { id: String(registry.length + 1), email, passwordHash: hash }
196
192
  registry.push(user)
@@ -299,91 +295,136 @@ function notifyRegistration(user: RegisteredUser): void {
299
295
  Bad code:
300
296
 
301
297
  ```ts
302
- interface Account {
303
- email: string | null
304
- age: number
305
- roles: string[]
298
+ interface User {
299
+ role: string
300
+ isDeleted: boolean
301
+ emailVerified: boolean
306
302
  }
307
303
 
308
- function validateUser(account: Account | null): string {
309
- if (account !== null) {
310
- if (account.email !== null) {
311
- if (account.email.includes('@')) { // unicorn/no-lonely-if: lone `if` inside an `if`
312
- if (account.age >= 18) {
313
- if (account.roles.length > 0) {
314
- return 'ok'
304
+ declare const db: { users: { findById: (id: string) => Promise<User | null> } }
305
+
306
+ async function validateUser(userId: string, role: string): Promise<User> { // jsdoc/require-jsdoc + sonarjs/cognitive-complexity + max-statements
307
+ if (userId) {
308
+ const user = await db.users.findById(userId)
309
+ if (user) {
310
+ if (!user.isDeleted) { // max-depth + sonarjs/nested-control-flow: nested beyond 2 levels
311
+ if (user.role === role) {
312
+ if (user.emailVerified) {
313
+ // happy path buried 5 levels deep
314
+ return user
315
+ } else {
316
+ throw new Error('Email not verified')
315
317
  }
318
+ } else {
319
+ throw new Error('Insufficient role')
316
320
  }
321
+ } else {
322
+ throw new Error('User is deleted')
317
323
  }
324
+ } else {
325
+ throw new Error('User not found')
318
326
  }
327
+ } else {
328
+ throw new Error('User ID is required')
319
329
  }
320
-
321
- return 'invalid'
322
330
  }
323
331
  ```
324
332
 
325
333
  Good code:
326
334
 
327
335
  ```ts
328
- interface Account {
329
- email: string | null
330
- age: number
331
- roles: string[]
336
+ interface User {
337
+ role: string
338
+ isDeleted: boolean
339
+ emailVerified: boolean
332
340
  }
333
341
 
334
342
  /**
335
- * Validates an account using flat guard clauses.
336
- * @param account The account to validate, or null.
337
- * @returns 'ok' when every requirement is met, otherwise 'invalid'.
343
+ * Loads a user and validates it with flat guard clauses.
344
+ * @param userId The id of the user to load.
345
+ * @param role The role the user must hold.
346
+ * @returns The validated, active user.
338
347
  */
339
- export function validateUser(account: Account | null): string {
340
- if (account === null) {
341
- return 'invalid'
348
+ export async function validateUser(userId: string, role: string): Promise<User> {
349
+ if (!userId) {
350
+ throw new Error('User ID is required')
342
351
  }
343
352
 
344
- if (!hasValidEmail(account.email)) {
345
- return 'invalid'
346
- }
347
-
348
- if (account.age < 18) {
349
- return 'invalid'
350
- }
353
+ const user = await database.users.findById(userId)
351
354
 
352
- if (account.roles.length === 0) {
353
- return 'invalid'
354
- }
355
+ assertActiveUser(user, role)
355
356
 
356
- return 'ok'
357
+ return user
357
358
  }
358
359
 
359
360
  /**
360
- * Reports whether an email is present and well-formed.
361
- * @param email The email to check, or null.
362
- * @returns True when the email contains an '@'.
361
+ * Asserts the loaded user exists and is eligible for the role.
362
+ * @param user The loaded user, or null when none was found.
363
+ * @param role The role the user must hold.
363
364
  */
364
- function hasValidEmail(email: string | null): boolean {
365
- return email !== null && email.includes('@')
365
+ function assertActiveUser(user: User | null, role: string): asserts user is User {
366
+ if (!user) {
367
+ throw new Error('User not found')
368
+ }
369
+
370
+ if (user.isDeleted) {
371
+ throw new Error('User is deleted')
372
+ }
373
+
374
+ if (user.role !== role) {
375
+ throw new Error('Insufficient role')
376
+ }
377
+
378
+ if (!user.emailVerified) {
379
+ throw new Error('Email not verified')
380
+ }
366
381
  }
367
382
  ```
368
383
 
369
- ### 3. Replace a static-only "helper" class
384
+ ### 3. Untangle a complex static-only "helper" class
370
385
 
371
386
  Bad code:
372
387
 
373
388
  ```ts
374
- class SubscriptionHelper {
375
- static subscribers: string[] = [] // no-restricted-syntax: static property
389
+ class PricingHelper { // unicorn/no-static-only-class + sonarjs/class-name (vague "Helper") + jsdoc/require-jsdoc
390
+ static VAT = 0.2 // no-restricted-syntax: static property
391
+
392
+ static calc(t: string, qty: number, code: string, member: boolean): number { // complexity + sonarjs/cognitive-complexity + no-restricted-syntax (static method) + id-length ('t')
393
+ let price = 0
394
+
395
+ if (t === 'book') {
396
+ price = 10
397
+ } else if (t === 'game') {
398
+ price = 40
399
+ } else if (t === 'film') {
400
+ price = 20
401
+ } else {
402
+ price = 5
403
+ }
376
404
 
377
- static add(u: string): void { // no-restricted-syntax: static method; id-length: `u`
378
- SubscriptionHelper.subscribers.push(u)
379
- }
405
+ let total = price * qty
380
406
 
381
- static remove(u: string): void {
382
- SubscriptionHelper.subscribers = SubscriptionHelper.subscribers.filter(x => x !== u)
383
- }
407
+ if (qty > 100) {
408
+ total = total * 0.8
409
+ } else if (qty > 50) {
410
+ total = total * 0.9
411
+ } else if (qty > 10) {
412
+ total = total * 0.95
413
+ }
384
414
 
385
- static count(): number {
386
- return SubscriptionHelper.subscribers.length
415
+ if (member) {
416
+ if (code === 'GOLD') {
417
+ total = total * 0.85
418
+ } else if (code === 'SILVER') {
419
+ total = total * 0.9
420
+ }
421
+ }
422
+
423
+ if (total > 1000) {
424
+ total = total - 50
425
+ }
426
+
427
+ return total + total * PricingHelper.VAT
387
428
  }
388
429
  }
389
430
  ```
@@ -391,38 +432,94 @@ class SubscriptionHelper {
391
432
  Good code:
392
433
 
393
434
  ```ts
394
- /** Tracks newsletter subscribers in memory. */
395
- export class SubscriptionRegistry {
396
- private readonly subscribers = new Set<string>()
435
+ interface Order {
436
+ type: string
437
+ quantity: number
438
+ couponCode: string
439
+ isMember: boolean
440
+ }
441
+
442
+ /** Prices catalogue orders including quantity, membership discounts, and VAT. */
443
+ export class PricingCalculator {
444
+ private readonly vatRate = 0.2
445
+
446
+ private readonly basePrices: Record<string, number> = { book: 10, game: 40, film: 20 }
447
+
448
+ private readonly memberCoupons: Record<string, number> = { GOLD: 0.85, SILVER: 0.9 }
449
+
450
+ private readonly bulkTiers = [
451
+ { min: 100, rate: 0.8 },
452
+ { min: 50, rate: 0.9 },
453
+ { min: 10, rate: 0.95 },
454
+ ]
455
+
456
+ /**
457
+ * Computes the final price of an order including discounts and VAT.
458
+ * @param order The order to price.
459
+ * @returns The final price with VAT applied.
460
+ */
461
+ price(order: Order): number {
462
+ const subtotal = this.subtotal(order)
463
+ const discounted = this.applyDiscounts(subtotal, order)
464
+
465
+ return this.withVat(discounted)
466
+ }
397
467
 
398
468
  /**
399
- * Adds a subscriber by email.
400
- * @param email The subscriber email.
401
- * @returns The subscriber count after adding.
469
+ * Computes the pre-discount subtotal for an order.
470
+ * @param order The order to price.
471
+ * @returns The base price multiplied by quantity.
402
472
  */
403
- add(email: string): number {
404
- this.subscribers.add(email)
473
+ private subtotal(order: Order): number {
474
+ const base = this.basePrices[order.type] ?? 5
405
475
 
406
- return this.subscribers.size
476
+ return base * order.quantity
407
477
  }
408
478
 
409
479
  /**
410
- * Removes a subscriber by email.
411
- * @param email The subscriber email.
412
- * @returns The subscriber count after removing.
480
+ * Applies quantity and membership discounts to a subtotal.
481
+ * @param subtotal The pre-discount subtotal.
482
+ * @param order The order being priced.
483
+ * @returns The discounted amount.
413
484
  */
414
- remove(email: string): number {
415
- this.subscribers.delete(email)
485
+ private applyDiscounts(subtotal: number, order: Order): number {
486
+ const afterQuantity = subtotal * this.quantityRate(order.quantity)
487
+ const afterMember = afterQuantity * this.memberRate(order)
488
+
489
+ return afterMember > 1000 ? afterMember - 50 : afterMember
490
+ }
491
+
492
+ /**
493
+ * Resolves the quantity discount rate for an order size.
494
+ * @param quantity The number of items ordered.
495
+ * @returns A multiplier between 0 and 1.
496
+ */
497
+ private quantityRate(quantity: number): number {
498
+ const tier = this.bulkTiers.find(entry => quantity > entry.min)
499
+
500
+ return tier?.rate ?? 1
501
+ }
502
+
503
+ /**
504
+ * Resolves the membership coupon rate for an order.
505
+ * @param order The order being priced.
506
+ * @returns A multiplier between 0 and 1.
507
+ */
508
+ private memberRate(order: Order): number {
509
+ if (!order.isMember) {
510
+ return 1
511
+ }
416
512
 
417
- return this.subscribers.size
513
+ return this.memberCoupons[order.couponCode] ?? 1
418
514
  }
419
515
 
420
516
  /**
421
- * Counts the current subscribers.
422
- * @returns The number of subscribers.
517
+ * Adds VAT to an amount.
518
+ * @param amount The pre-VAT amount.
519
+ * @returns The amount including VAT.
423
520
  */
424
- count(): number {
425
- return this.subscribers.size
521
+ private withVat(amount: number): number {
522
+ return amount + amount * this.vatRate
426
523
  }
427
524
  }
428
525
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-eslint-config",
3
- "version": "0.1.0",
3
+ "version": "1.0.2",
4
4
  "description": "Overly opionated eslint config for agents. Forces them to write low complexity and highly readable code. Supports: Claude, Gemini, Codex and many more.",
5
5
  "author": "Vlad Goncharov <vlad.goncharov@neolab.com>",
6
6
  "license": "MIT",
@@ -48,14 +48,21 @@
48
48
  "eslint": "^9.10.0 || ^10.0.0"
49
49
  },
50
50
  "devDependencies": {
51
+ "@semantic-release/git": "^10.0.1",
51
52
  "@types/node": "^25.6.2",
52
53
  "@typescript-eslint/rule-tester": "^8.62.0",
53
54
  "@typescript/native-preview": "7.0.0-dev.20260509.2",
54
55
  "bumpp": "^11.1.0",
55
56
  "concurrently": "^10.0.3",
57
+ "cz-conventional-changelog": "^3.3.0",
56
58
  "eslint": "^10.5.0",
57
59
  "tsdown": "^0.22.0",
58
60
  "typescript": "^6.0.3",
59
61
  "vitest": "^4.1.5"
62
+ },
63
+ "config": {
64
+ "commitizen": {
65
+ "path": "./node_modules/cz-conventional-changelog"
66
+ }
60
67
  }
61
68
  }