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.
- package/README.md +176 -79
- 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
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
298
|
+
interface User {
|
|
299
|
+
role: string
|
|
300
|
+
isDeleted: boolean
|
|
301
|
+
emailVerified: boolean
|
|
306
302
|
}
|
|
307
303
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
336
|
+
interface User {
|
|
337
|
+
role: string
|
|
338
|
+
isDeleted: boolean
|
|
339
|
+
emailVerified: boolean
|
|
332
340
|
}
|
|
333
341
|
|
|
334
342
|
/**
|
|
335
|
-
*
|
|
336
|
-
* @param
|
|
337
|
-
* @
|
|
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(
|
|
340
|
-
if (
|
|
341
|
-
|
|
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
|
-
|
|
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
|
-
|
|
353
|
-
return 'invalid'
|
|
354
|
-
}
|
|
355
|
+
assertActiveUser(user, role)
|
|
355
356
|
|
|
356
|
-
return
|
|
357
|
+
return user
|
|
357
358
|
}
|
|
358
359
|
|
|
359
360
|
/**
|
|
360
|
-
*
|
|
361
|
-
* @param
|
|
362
|
-
* @
|
|
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
|
|
365
|
-
|
|
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.
|
|
384
|
+
### 3. Untangle a complex static-only "helper" class
|
|
370
385
|
|
|
371
386
|
Bad code:
|
|
372
387
|
|
|
373
388
|
```ts
|
|
374
|
-
class
|
|
375
|
-
static
|
|
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
|
-
|
|
378
|
-
SubscriptionHelper.subscribers.push(u)
|
|
379
|
-
}
|
|
405
|
+
let total = price * qty
|
|
380
406
|
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
386
|
-
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
*
|
|
400
|
-
* @param
|
|
401
|
-
* @returns The
|
|
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
|
-
|
|
404
|
-
this.
|
|
473
|
+
private subtotal(order: Order): number {
|
|
474
|
+
const base = this.basePrices[order.type] ?? 5
|
|
405
475
|
|
|
406
|
-
return
|
|
476
|
+
return base * order.quantity
|
|
407
477
|
}
|
|
408
478
|
|
|
409
479
|
/**
|
|
410
|
-
*
|
|
411
|
-
* @param
|
|
412
|
-
* @
|
|
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
|
-
|
|
415
|
-
this.
|
|
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.
|
|
513
|
+
return this.memberCoupons[order.couponCode] ?? 1
|
|
418
514
|
}
|
|
419
515
|
|
|
420
516
|
/**
|
|
421
|
-
*
|
|
422
|
-
* @
|
|
517
|
+
* Adds VAT to an amount.
|
|
518
|
+
* @param amount The pre-VAT amount.
|
|
519
|
+
* @returns The amount including VAT.
|
|
423
520
|
*/
|
|
424
|
-
|
|
425
|
-
return this.
|
|
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": "
|
|
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
|
}
|