tdk-api-wrapper 1.2.0 → 1.2.1

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 CHANGED
@@ -68,7 +68,7 @@ Aşağıdaki metotlar `TDK` sınıfı üzerinden statik olarak erişilebilir dur
68
68
  - **`TDK.syllabicate(word)`**: Kelimeyi Türkçe heceleme kurallarına göre doğru hecelerine ayırır (Örn: `['mu', 'vaf', 'fa', 'ki', 'yet']`). API isteği atmaz, çok hızlıdır.
69
69
  - **`TDK.checkVowelHarmony(word)`**: Kelimenin büyük ünlü uyumuna uyup uymadığını (boolean) kontrol eder.
70
70
  - **`TDK.getPartOfSpeech(word)`**: Kelimenin sözcük türünü (isim, sıfat, zarf vb.) döndürür.
71
- - **`TDK.checkSpelling(word)`**: Sıkça yapılan yanlışlar listesini ve TDK veritabanını kullanarak kelimenin doğru yazılıp yazılmadığını kontrol eder. Yanlışsa doğrusunu önerir; tam eşleşme yoksa, aynı listedeki kelimeler arasında edit-distance (Levenshtein) ile en yakınını önerir (not: tüm sözlükte değil, yalnızca bu küçük havuzda arama yapar).
71
+ - **`TDK.checkSpelling(word)`**: Kelimenin doğru yazılıp yazılmadığını kontrol eder. Önce TDK'nin "sık yapılan yanlışlar" listesinde tam eşleşme arar; bulamazsa TDK'nin ~81 bin kelimelik tam madde listesi üzerinde edit-distance (Levenshtein) ile en yakın kelimeyi önerir (örn. `herkez` `herkes`, `mektub` `mektup`).
72
72
  - **`TDK.getCompoundWords(word)`**: Aranan kelime ile oluşturulmuş birleşik kelimeleri (Örn: dolma kalem) listeler.
73
73
 
74
74
  ### 3. Edebi ve Kültürel Analiz
@@ -390,29 +390,37 @@ var TDK = class {
390
390
  if (mixMatch) {
391
391
  return { isCorrect: false, word, suggestion: mixMatch.dogru };
392
392
  }
393
- const candidates = [
394
- ...daily.syyd.map((s) => s.dogrukelime),
395
- ...daily.karistirma.flatMap((s) => [s.yanlis, s.dogru]),
396
- ...daily.kelime.map((k) => k.madde)
397
- ];
398
- let best = null;
399
- for (const candidate of candidates) {
400
- const distance = this.levenshtein(word.toLocaleLowerCase("tr-TR"), candidate.toLocaleLowerCase("tr-TR"));
401
- if (distance > 0 && (!best || distance < best.distance)) {
402
- best = { candidate, distance };
403
- }
404
- }
405
- if (best && best.distance <= 2) {
406
- return { isCorrect: false, word, suggestion: best.candidate };
393
+ }
394
+ if (this.autocompleteCache.length === 0) {
395
+ this.autocompleteCache = await this.fetchAutocompleteData();
396
+ }
397
+ const cleanWord = word.trim().toLocaleLowerCase("tr-TR");
398
+ let best = null;
399
+ for (const candidate of this.autocompleteCache) {
400
+ if (candidate.includes(" ") || candidate !== candidate.toLocaleLowerCase("tr-TR"))
401
+ continue;
402
+ const distance = this.levenshtein(cleanWord, candidate);
403
+ if (distance > 0 && (!best || distance < best.distance)) {
404
+ best = { candidate, distance };
405
+ if (distance === 1)
406
+ break;
407
407
  }
408
408
  }
409
+ if (best && best.distance <= 2) {
410
+ return { isCorrect: false, word, suggestion: best.candidate };
411
+ }
409
412
  return { isCorrect: false, word };
410
413
  }
411
414
  /**
412
415
  * Fetches daily content (word of the day, proverbs, rules, etc).
416
+ * `bypassCache` skips both reading and writing `dailyContentCache` even
417
+ * when `enableCache(true)` is on — used by `getRule()`'s retry loop, which
418
+ * needs a fresh random `/icerik` draw on every attempt; without it, once
419
+ * caching is enabled the loop would just re-read the same cached response
420
+ * 25 times and could never find a rule outside that first random draw.
413
421
  */
414
- static async getDailyContent() {
415
- if (this.isCacheEnabled && this.dailyContentCache)
422
+ static async getDailyContent(bypassCache = false) {
423
+ if (!bypassCache && this.isCacheEnabled && this.dailyContentCache)
416
424
  return this.dailyContentCache;
417
425
  try {
418
426
  const response = await fetch(`${this.BASE_URL}/icerik`, {
@@ -420,7 +428,7 @@ var TDK = class {
420
428
  });
421
429
  if (response.ok) {
422
430
  const data = await response.json();
423
- if (this.isCacheEnabled)
431
+ if (!bypassCache && this.isCacheEnabled)
424
432
  this.dailyContentCache = data;
425
433
  return data;
426
434
  }
@@ -461,10 +469,12 @@ var TDK = class {
461
469
  * `/icerik` daily-content feed, e.g. `{ adi: "Kısaltmalar", url: "https://..." }`.
462
470
  * Note: like `getRandomWord()`, this is NOT a fixed catalog — `/icerik`
463
471
  * appears to hand back a single randomly-rotated rule per request, so two
464
- * calls a second apart can return entirely different rules.
472
+ * calls a second apart can return entirely different rules. `bypassCache`
473
+ * (used internally by `getRule()`'s retry loop) forces a fresh `/icerik`
474
+ * draw even when `enableCache(true)` is on.
465
475
  */
466
- static async getKurallar() {
467
- const daily = await this.getDailyContent();
476
+ static async getKurallar(bypassCache = false) {
477
+ const daily = await this.getDailyContent(bypassCache);
468
478
  return daily?.kural ?? [];
469
479
  }
470
480
  /**
@@ -473,16 +483,19 @@ var TDK = class {
473
483
  * hands back a single randomly-rotated rule per request (out of a pool of
474
484
  * roughly twenty) rather than a fixed catalog, a single `getKurallar()`
475
485
  * draw would rarely match a given name — this re-draws (bounded, with a
476
- * short delay) until it finds a match or gives up. Returns `null` if no
477
- * match turns up within the attempt budget or the matched page can't be
478
- * parsed.
486
+ * short delay) until it finds a match or gives up. Every attempt bypasses
487
+ * `dailyContentCache` without that, once `enableCache(true)` is on, all
488
+ * 25 attempts would just re-read the same cached `/icerik` response and
489
+ * could never find a rule outside whatever the first draw happened to be.
490
+ * Returns `null` if no match turns up within the attempt budget or the
491
+ * matched page can't be parsed.
479
492
  */
480
493
  static async getRule(name) {
481
494
  if (!name || name.trim() === "")
482
495
  return null;
483
496
  const target = name.trim().toLocaleLowerCase("tr-TR");
484
497
  for (let attempt = 0; attempt < 25; attempt++) {
485
- const rules = await this.getKurallar();
498
+ const rules = await this.getKurallar(true);
486
499
  const match = rules.find((r) => r.adi.toLocaleLowerCase("tr-TR").includes(target));
487
500
  if (match)
488
501
  return this.fetchRuleText(match.url);
package/dist/cli.js CHANGED
@@ -415,29 +415,37 @@ var TDK = class {
415
415
  if (mixMatch) {
416
416
  return { isCorrect: false, word: word2, suggestion: mixMatch.dogru };
417
417
  }
418
- const candidates = [
419
- ...daily.syyd.map((s) => s.dogrukelime),
420
- ...daily.karistirma.flatMap((s) => [s.yanlis, s.dogru]),
421
- ...daily.kelime.map((k) => k.madde)
422
- ];
423
- let best = null;
424
- for (const candidate of candidates) {
425
- const distance = this.levenshtein(word2.toLocaleLowerCase("tr-TR"), candidate.toLocaleLowerCase("tr-TR"));
426
- if (distance > 0 && (!best || distance < best.distance)) {
427
- best = { candidate, distance };
428
- }
429
- }
430
- if (best && best.distance <= 2) {
431
- return { isCorrect: false, word: word2, suggestion: best.candidate };
418
+ }
419
+ if (this.autocompleteCache.length === 0) {
420
+ this.autocompleteCache = await this.fetchAutocompleteData();
421
+ }
422
+ const cleanWord = word2.trim().toLocaleLowerCase("tr-TR");
423
+ let best = null;
424
+ for (const candidate of this.autocompleteCache) {
425
+ if (candidate.includes(" ") || candidate !== candidate.toLocaleLowerCase("tr-TR"))
426
+ continue;
427
+ const distance = this.levenshtein(cleanWord, candidate);
428
+ if (distance > 0 && (!best || distance < best.distance)) {
429
+ best = { candidate, distance };
430
+ if (distance === 1)
431
+ break;
432
432
  }
433
433
  }
434
+ if (best && best.distance <= 2) {
435
+ return { isCorrect: false, word: word2, suggestion: best.candidate };
436
+ }
434
437
  return { isCorrect: false, word: word2 };
435
438
  }
436
439
  /**
437
440
  * Fetches daily content (word of the day, proverbs, rules, etc).
441
+ * `bypassCache` skips both reading and writing `dailyContentCache` even
442
+ * when `enableCache(true)` is on — used by `getRule()`'s retry loop, which
443
+ * needs a fresh random `/icerik` draw on every attempt; without it, once
444
+ * caching is enabled the loop would just re-read the same cached response
445
+ * 25 times and could never find a rule outside that first random draw.
438
446
  */
439
- static async getDailyContent() {
440
- if (this.isCacheEnabled && this.dailyContentCache)
447
+ static async getDailyContent(bypassCache = false) {
448
+ if (!bypassCache && this.isCacheEnabled && this.dailyContentCache)
441
449
  return this.dailyContentCache;
442
450
  try {
443
451
  const response = await fetch(`${this.BASE_URL}/icerik`, {
@@ -445,7 +453,7 @@ var TDK = class {
445
453
  });
446
454
  if (response.ok) {
447
455
  const data = await response.json();
448
- if (this.isCacheEnabled)
456
+ if (!bypassCache && this.isCacheEnabled)
449
457
  this.dailyContentCache = data;
450
458
  return data;
451
459
  }
@@ -486,10 +494,12 @@ var TDK = class {
486
494
  * `/icerik` daily-content feed, e.g. `{ adi: "Kısaltmalar", url: "https://..." }`.
487
495
  * Note: like `getRandomWord()`, this is NOT a fixed catalog — `/icerik`
488
496
  * appears to hand back a single randomly-rotated rule per request, so two
489
- * calls a second apart can return entirely different rules.
497
+ * calls a second apart can return entirely different rules. `bypassCache`
498
+ * (used internally by `getRule()`'s retry loop) forces a fresh `/icerik`
499
+ * draw even when `enableCache(true)` is on.
490
500
  */
491
- static async getKurallar() {
492
- const daily = await this.getDailyContent();
501
+ static async getKurallar(bypassCache = false) {
502
+ const daily = await this.getDailyContent(bypassCache);
493
503
  return daily?.kural ?? [];
494
504
  }
495
505
  /**
@@ -498,16 +508,19 @@ var TDK = class {
498
508
  * hands back a single randomly-rotated rule per request (out of a pool of
499
509
  * roughly twenty) rather than a fixed catalog, a single `getKurallar()`
500
510
  * draw would rarely match a given name — this re-draws (bounded, with a
501
- * short delay) until it finds a match or gives up. Returns `null` if no
502
- * match turns up within the attempt budget or the matched page can't be
503
- * parsed.
511
+ * short delay) until it finds a match or gives up. Every attempt bypasses
512
+ * `dailyContentCache` without that, once `enableCache(true)` is on, all
513
+ * 25 attempts would just re-read the same cached `/icerik` response and
514
+ * could never find a rule outside whatever the first draw happened to be.
515
+ * Returns `null` if no match turns up within the attempt budget or the
516
+ * matched page can't be parsed.
504
517
  */
505
518
  static async getRule(name) {
506
519
  if (!name || name.trim() === "")
507
520
  return null;
508
521
  const target = name.trim().toLocaleLowerCase("tr-TR");
509
522
  for (let attempt = 0; attempt < 25; attempt++) {
510
- const rules = await this.getKurallar();
523
+ const rules = await this.getKurallar(true);
511
524
  const match = rules.find((r) => r.adi.toLocaleLowerCase("tr-TR").includes(target));
512
525
  if (match)
513
526
  return this.fetchRuleText(match.url);
package/dist/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  TDK
4
- } from "./chunk-2TA5PMVZ.mjs";
4
+ } from "./chunk-6BTOGV2M.mjs";
5
5
 
6
6
  // src/cli.ts
7
7
  var rawArgs = process.argv.slice(2);
package/dist/index.d.mts CHANGED
@@ -244,8 +244,13 @@ declare class TDK {
244
244
  static checkSpelling(word: string): Promise<SpellCheckResult>;
245
245
  /**
246
246
  * Fetches daily content (word of the day, proverbs, rules, etc).
247
+ * `bypassCache` skips both reading and writing `dailyContentCache` even
248
+ * when `enableCache(true)` is on — used by `getRule()`'s retry loop, which
249
+ * needs a fresh random `/icerik` draw on every attempt; without it, once
250
+ * caching is enabled the loop would just re-read the same cached response
251
+ * 25 times and could never find a rule outside that first random draw.
247
252
  */
248
- static getDailyContent(): Promise<DailyContent | null>;
253
+ static getDailyContent(bypassCache?: boolean): Promise<DailyContent | null>;
249
254
  /**
250
255
  * Returns today's word of the day along with all of its listed meanings.
251
256
  */
@@ -260,18 +265,23 @@ declare class TDK {
260
265
  * `/icerik` daily-content feed, e.g. `{ adi: "Kısaltmalar", url: "https://..." }`.
261
266
  * Note: like `getRandomWord()`, this is NOT a fixed catalog — `/icerik`
262
267
  * appears to hand back a single randomly-rotated rule per request, so two
263
- * calls a second apart can return entirely different rules.
268
+ * calls a second apart can return entirely different rules. `bypassCache`
269
+ * (used internally by `getRule()`'s retry loop) forces a fresh `/icerik`
270
+ * draw even when `enableCache(true)` is on.
264
271
  */
265
- static getKurallar(): Promise<TDKRule[]>;
272
+ static getKurallar(bypassCache?: boolean): Promise<TDKRule[]>;
266
273
  /**
267
274
  * Fetches the full plain-text content of a named spelling rule (matched
268
275
  * case-insensitively, substring match) from `tdk.gov.tr`. Since `/icerik`
269
276
  * hands back a single randomly-rotated rule per request (out of a pool of
270
277
  * roughly twenty) rather than a fixed catalog, a single `getKurallar()`
271
278
  * draw would rarely match a given name — this re-draws (bounded, with a
272
- * short delay) until it finds a match or gives up. Returns `null` if no
273
- * match turns up within the attempt budget or the matched page can't be
274
- * parsed.
279
+ * short delay) until it finds a match or gives up. Every attempt bypasses
280
+ * `dailyContentCache` without that, once `enableCache(true)` is on, all
281
+ * 25 attempts would just re-read the same cached `/icerik` response and
282
+ * could never find a rule outside whatever the first draw happened to be.
283
+ * Returns `null` if no match turns up within the attempt budget or the
284
+ * matched page can't be parsed.
275
285
  */
276
286
  static getRule(name: string): Promise<string | null>;
277
287
  /**
package/dist/index.d.ts CHANGED
@@ -244,8 +244,13 @@ declare class TDK {
244
244
  static checkSpelling(word: string): Promise<SpellCheckResult>;
245
245
  /**
246
246
  * Fetches daily content (word of the day, proverbs, rules, etc).
247
+ * `bypassCache` skips both reading and writing `dailyContentCache` even
248
+ * when `enableCache(true)` is on — used by `getRule()`'s retry loop, which
249
+ * needs a fresh random `/icerik` draw on every attempt; without it, once
250
+ * caching is enabled the loop would just re-read the same cached response
251
+ * 25 times and could never find a rule outside that first random draw.
247
252
  */
248
- static getDailyContent(): Promise<DailyContent | null>;
253
+ static getDailyContent(bypassCache?: boolean): Promise<DailyContent | null>;
249
254
  /**
250
255
  * Returns today's word of the day along with all of its listed meanings.
251
256
  */
@@ -260,18 +265,23 @@ declare class TDK {
260
265
  * `/icerik` daily-content feed, e.g. `{ adi: "Kısaltmalar", url: "https://..." }`.
261
266
  * Note: like `getRandomWord()`, this is NOT a fixed catalog — `/icerik`
262
267
  * appears to hand back a single randomly-rotated rule per request, so two
263
- * calls a second apart can return entirely different rules.
268
+ * calls a second apart can return entirely different rules. `bypassCache`
269
+ * (used internally by `getRule()`'s retry loop) forces a fresh `/icerik`
270
+ * draw even when `enableCache(true)` is on.
264
271
  */
265
- static getKurallar(): Promise<TDKRule[]>;
272
+ static getKurallar(bypassCache?: boolean): Promise<TDKRule[]>;
266
273
  /**
267
274
  * Fetches the full plain-text content of a named spelling rule (matched
268
275
  * case-insensitively, substring match) from `tdk.gov.tr`. Since `/icerik`
269
276
  * hands back a single randomly-rotated rule per request (out of a pool of
270
277
  * roughly twenty) rather than a fixed catalog, a single `getKurallar()`
271
278
  * draw would rarely match a given name — this re-draws (bounded, with a
272
- * short delay) until it finds a match or gives up. Returns `null` if no
273
- * match turns up within the attempt budget or the matched page can't be
274
- * parsed.
279
+ * short delay) until it finds a match or gives up. Every attempt bypasses
280
+ * `dailyContentCache` without that, once `enableCache(true)` is on, all
281
+ * 25 attempts would just re-read the same cached `/icerik` response and
282
+ * could never find a rule outside whatever the first draw happened to be.
283
+ * Returns `null` if no match turns up within the attempt budget or the
284
+ * matched page can't be parsed.
275
285
  */
276
286
  static getRule(name: string): Promise<string | null>;
277
287
  /**
package/dist/index.js CHANGED
@@ -429,29 +429,37 @@ var TDK = class {
429
429
  if (mixMatch) {
430
430
  return { isCorrect: false, word, suggestion: mixMatch.dogru };
431
431
  }
432
- const candidates = [
433
- ...daily.syyd.map((s) => s.dogrukelime),
434
- ...daily.karistirma.flatMap((s) => [s.yanlis, s.dogru]),
435
- ...daily.kelime.map((k) => k.madde)
436
- ];
437
- let best = null;
438
- for (const candidate of candidates) {
439
- const distance = this.levenshtein(word.toLocaleLowerCase("tr-TR"), candidate.toLocaleLowerCase("tr-TR"));
440
- if (distance > 0 && (!best || distance < best.distance)) {
441
- best = { candidate, distance };
442
- }
443
- }
444
- if (best && best.distance <= 2) {
445
- return { isCorrect: false, word, suggestion: best.candidate };
432
+ }
433
+ if (this.autocompleteCache.length === 0) {
434
+ this.autocompleteCache = await this.fetchAutocompleteData();
435
+ }
436
+ const cleanWord = word.trim().toLocaleLowerCase("tr-TR");
437
+ let best = null;
438
+ for (const candidate of this.autocompleteCache) {
439
+ if (candidate.includes(" ") || candidate !== candidate.toLocaleLowerCase("tr-TR"))
440
+ continue;
441
+ const distance = this.levenshtein(cleanWord, candidate);
442
+ if (distance > 0 && (!best || distance < best.distance)) {
443
+ best = { candidate, distance };
444
+ if (distance === 1)
445
+ break;
446
446
  }
447
447
  }
448
+ if (best && best.distance <= 2) {
449
+ return { isCorrect: false, word, suggestion: best.candidate };
450
+ }
448
451
  return { isCorrect: false, word };
449
452
  }
450
453
  /**
451
454
  * Fetches daily content (word of the day, proverbs, rules, etc).
455
+ * `bypassCache` skips both reading and writing `dailyContentCache` even
456
+ * when `enableCache(true)` is on — used by `getRule()`'s retry loop, which
457
+ * needs a fresh random `/icerik` draw on every attempt; without it, once
458
+ * caching is enabled the loop would just re-read the same cached response
459
+ * 25 times and could never find a rule outside that first random draw.
452
460
  */
453
- static async getDailyContent() {
454
- if (this.isCacheEnabled && this.dailyContentCache)
461
+ static async getDailyContent(bypassCache = false) {
462
+ if (!bypassCache && this.isCacheEnabled && this.dailyContentCache)
455
463
  return this.dailyContentCache;
456
464
  try {
457
465
  const response = await fetch(`${this.BASE_URL}/icerik`, {
@@ -459,7 +467,7 @@ var TDK = class {
459
467
  });
460
468
  if (response.ok) {
461
469
  const data = await response.json();
462
- if (this.isCacheEnabled)
470
+ if (!bypassCache && this.isCacheEnabled)
463
471
  this.dailyContentCache = data;
464
472
  return data;
465
473
  }
@@ -500,10 +508,12 @@ var TDK = class {
500
508
  * `/icerik` daily-content feed, e.g. `{ adi: "Kısaltmalar", url: "https://..." }`.
501
509
  * Note: like `getRandomWord()`, this is NOT a fixed catalog — `/icerik`
502
510
  * appears to hand back a single randomly-rotated rule per request, so two
503
- * calls a second apart can return entirely different rules.
511
+ * calls a second apart can return entirely different rules. `bypassCache`
512
+ * (used internally by `getRule()`'s retry loop) forces a fresh `/icerik`
513
+ * draw even when `enableCache(true)` is on.
504
514
  */
505
- static async getKurallar() {
506
- const daily = await this.getDailyContent();
515
+ static async getKurallar(bypassCache = false) {
516
+ const daily = await this.getDailyContent(bypassCache);
507
517
  return daily?.kural ?? [];
508
518
  }
509
519
  /**
@@ -512,16 +522,19 @@ var TDK = class {
512
522
  * hands back a single randomly-rotated rule per request (out of a pool of
513
523
  * roughly twenty) rather than a fixed catalog, a single `getKurallar()`
514
524
  * draw would rarely match a given name — this re-draws (bounded, with a
515
- * short delay) until it finds a match or gives up. Returns `null` if no
516
- * match turns up within the attempt budget or the matched page can't be
517
- * parsed.
525
+ * short delay) until it finds a match or gives up. Every attempt bypasses
526
+ * `dailyContentCache` without that, once `enableCache(true)` is on, all
527
+ * 25 attempts would just re-read the same cached `/icerik` response and
528
+ * could never find a rule outside whatever the first draw happened to be.
529
+ * Returns `null` if no match turns up within the attempt budget or the
530
+ * matched page can't be parsed.
518
531
  */
519
532
  static async getRule(name) {
520
533
  if (!name || name.trim() === "")
521
534
  return null;
522
535
  const target = name.trim().toLocaleLowerCase("tr-TR");
523
536
  for (let attempt = 0; attempt < 25; attempt++) {
524
- const rules = await this.getKurallar();
537
+ const rules = await this.getKurallar(true);
525
538
  const match = rules.find((r) => r.adi.toLocaleLowerCase("tr-TR").includes(target));
526
539
  if (match)
527
540
  return this.fetchRuleText(match.url);
package/dist/index.mjs CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  TDKError,
4
4
  TDKNetworkError,
5
5
  TDKValidationError
6
- } from "./chunk-2TA5PMVZ.mjs";
6
+ } from "./chunk-6BTOGV2M.mjs";
7
7
  export {
8
8
  TDK,
9
9
  TDKError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tdk-api-wrapper",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "TDK (Türk Dil Kurumu) unofficial live data API wrapper for Node.js",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
package/src/tdk.ts CHANGED
@@ -389,8 +389,10 @@ export class TDK {
389
389
  if (results.length > 0) {
390
390
  return { isCorrect: true, word };
391
391
  }
392
-
393
- // 2. If not, check "sıkça yapılan yanlışlar" from DailyContent
392
+
393
+ // 2. If not, check "sıkça yapılan yanlışlar" from DailyContent — an exact
394
+ // match here is TDK explicitly saying "X is often confused with Y", so
395
+ // it's authoritative when it hits (but only 2-3 rotating entries per call).
394
396
  const daily = await this.getDailyContent();
395
397
  if (daily) {
396
398
  const syydMatch = daily.syyd.find(s => s.yanliskelime.toLocaleLowerCase("tr-TR") === word.toLocaleLowerCase("tr-TR"));
@@ -401,43 +403,49 @@ export class TDK {
401
403
  if (mixMatch) {
402
404
  return { isCorrect: false, word, suggestion: mixMatch.dogru };
403
405
  }
406
+ }
404
407
 
405
- // 3. No exact match in TDK's fixed lists: fall back to the closest word
406
- // (by edit distance) within that same small pool. This is NOT a search
407
- // over the full dictionary TDK exposes no such lookup — just a
408
- // best-effort nudge using the "sık yapılan yanlışlar" data we already have.
409
- const candidates = [
410
- ...daily.syyd.map((s) => s.dogrukelime),
411
- ...daily.karistirma.flatMap((s) => [s.yanlis, s.dogru]),
412
- ...daily.kelime.map((k) => k.madde),
413
- ];
414
- let best: { candidate: string; distance: number } | null = null;
415
- for (const candidate of candidates) {
416
- const distance = this.levenshtein(word.toLocaleLowerCase("tr-TR"), candidate.toLocaleLowerCase("tr-TR"));
417
- if (distance > 0 && (!best || distance < best.distance)) {
418
- best = { candidate, distance };
419
- }
420
- }
421
- if (best && best.distance <= 2) {
422
- return { isCorrect: false, word, suggestion: best.candidate };
408
+ // 3. No exact match in TDK's curated lists: fall back to the closest
409
+ // headword (by edit distance) across TDK's full ~81k-word list (the same
410
+ // data `getSuggestions()` uses). Restricted to single-token, lowercase
411
+ // headwords so it doesn't suggest compounds/phrases or proper nouns.
412
+ if (this.autocompleteCache.length === 0) {
413
+ this.autocompleteCache = await this.fetchAutocompleteData();
414
+ }
415
+ const cleanWord = word.trim().toLocaleLowerCase("tr-TR");
416
+ let best: { candidate: string; distance: number } | null = null;
417
+ for (const candidate of this.autocompleteCache) {
418
+ if (candidate.includes(" ") || candidate !== candidate.toLocaleLowerCase("tr-TR")) continue;
419
+ const distance = this.levenshtein(cleanWord, candidate);
420
+ if (distance > 0 && (!best || distance < best.distance)) {
421
+ best = { candidate, distance };
422
+ if (distance === 1) break;
423
423
  }
424
424
  }
425
+ if (best && best.distance <= 2) {
426
+ return { isCorrect: false, word, suggestion: best.candidate };
427
+ }
425
428
  return { isCorrect: false, word };
426
429
  }
427
430
 
428
431
  /**
429
432
  * Fetches daily content (word of the day, proverbs, rules, etc).
433
+ * `bypassCache` skips both reading and writing `dailyContentCache` even
434
+ * when `enableCache(true)` is on — used by `getRule()`'s retry loop, which
435
+ * needs a fresh random `/icerik` draw on every attempt; without it, once
436
+ * caching is enabled the loop would just re-read the same cached response
437
+ * 25 times and could never find a rule outside that first random draw.
430
438
  */
431
- public static async getDailyContent(): Promise<DailyContent | null> {
432
- if (this.isCacheEnabled && this.dailyContentCache) return this.dailyContentCache;
433
-
439
+ public static async getDailyContent(bypassCache = false): Promise<DailyContent | null> {
440
+ if (!bypassCache && this.isCacheEnabled && this.dailyContentCache) return this.dailyContentCache;
441
+
434
442
  try {
435
443
  const response = await fetch(`${this.BASE_URL}/icerik`, {
436
444
  headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" },
437
445
  });
438
446
  if (response.ok) {
439
447
  const data = await response.json() as DailyContent;
440
- if (this.isCacheEnabled) this.dailyContentCache = data;
448
+ if (!bypassCache && this.isCacheEnabled) this.dailyContentCache = data;
441
449
  return data;
442
450
  }
443
451
  } catch {
@@ -480,10 +488,12 @@ export class TDK {
480
488
  * `/icerik` daily-content feed, e.g. `{ adi: "Kısaltmalar", url: "https://..." }`.
481
489
  * Note: like `getRandomWord()`, this is NOT a fixed catalog — `/icerik`
482
490
  * appears to hand back a single randomly-rotated rule per request, so two
483
- * calls a second apart can return entirely different rules.
491
+ * calls a second apart can return entirely different rules. `bypassCache`
492
+ * (used internally by `getRule()`'s retry loop) forces a fresh `/icerik`
493
+ * draw even when `enableCache(true)` is on.
484
494
  */
485
- public static async getKurallar(): Promise<TDKRule[]> {
486
- const daily = await this.getDailyContent();
495
+ public static async getKurallar(bypassCache = false): Promise<TDKRule[]> {
496
+ const daily = await this.getDailyContent(bypassCache);
487
497
  return daily?.kural ?? [];
488
498
  }
489
499
 
@@ -493,16 +503,19 @@ export class TDK {
493
503
  * hands back a single randomly-rotated rule per request (out of a pool of
494
504
  * roughly twenty) rather than a fixed catalog, a single `getKurallar()`
495
505
  * draw would rarely match a given name — this re-draws (bounded, with a
496
- * short delay) until it finds a match or gives up. Returns `null` if no
497
- * match turns up within the attempt budget or the matched page can't be
498
- * parsed.
506
+ * short delay) until it finds a match or gives up. Every attempt bypasses
507
+ * `dailyContentCache` without that, once `enableCache(true)` is on, all
508
+ * 25 attempts would just re-read the same cached `/icerik` response and
509
+ * could never find a rule outside whatever the first draw happened to be.
510
+ * Returns `null` if no match turns up within the attempt budget or the
511
+ * matched page can't be parsed.
499
512
  */
500
513
  public static async getRule(name: string): Promise<string | null> {
501
514
  if (!name || name.trim() === "") return null;
502
515
  const target = name.trim().toLocaleLowerCase("tr-TR");
503
516
 
504
517
  for (let attempt = 0; attempt < 25; attempt++) {
505
- const rules = await this.getKurallar();
518
+ const rules = await this.getKurallar(true);
506
519
  const match = rules.find((r) => r.adi.toLocaleLowerCase("tr-TR").includes(target));
507
520
  if (match) return this.fetchRuleText(match.url);
508
521
  await this.delay(100);