@pdtf/schemas 3.4.1-6 → 3.4.1-8

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.
@@ -2,7 +2,11 @@ const {
2
2
  getSubschema,
3
3
  getSubschemaValidator,
4
4
  getCacheStats,
5
+ getDetailedCacheStats,
5
6
  clearSchemaCache,
7
+ warmupCache,
8
+ pruneCacheByAge,
9
+ setCacheMaxSize,
6
10
  isPathValid,
7
11
  } = require("../../../index");
8
12
 
@@ -314,4 +318,274 @@ describe("Schema Caching System", () => {
314
318
  expect(timeMs).toBeLessThan(1);
315
319
  });
316
320
  });
321
+
322
+ describe("Enhanced Cache Management", () => {
323
+ beforeEach(() => {
324
+ clearSchemaCache();
325
+ });
326
+
327
+ describe("Enhanced Statistics", () => {
328
+ test("should track hit/miss statistics", () => {
329
+ const stats1 = getCacheStats();
330
+ expect(stats1.hits).toBe(0);
331
+ expect(stats1.misses).toBe(0);
332
+ expect(stats1.totalQueries).toBe(0);
333
+ expect(stats1.hitRate).toBe(0);
334
+
335
+ // First call should be a miss
336
+ getSubschema(testPath, schemaId, overlays);
337
+ const stats2 = getCacheStats();
338
+ expect(stats2.misses).toBe(1);
339
+ expect(stats2.hits).toBe(0);
340
+ expect(stats2.totalQueries).toBe(1);
341
+ expect(stats2.hitRate).toBe(0);
342
+
343
+ // Second call should be a hit
344
+ getSubschema(testPath, schemaId, overlays);
345
+ const stats3 = getCacheStats();
346
+ expect(stats3.misses).toBe(1);
347
+ expect(stats3.hits).toBe(1);
348
+ expect(stats3.totalQueries).toBe(2);
349
+ expect(stats3.hitRate).toBe(50);
350
+ });
351
+
352
+ test("should provide detailed cache statistics", () => {
353
+ // Add some entries using safe paths
354
+ getSubschema("/participants", schemaId, []);
355
+ getSubschema("/status", schemaId, []);
356
+
357
+ const detailedStats = getDetailedCacheStats();
358
+
359
+ expect(detailedStats).toHaveProperty("totalEntries");
360
+ expect(detailedStats).toHaveProperty("hits");
361
+ expect(detailedStats).toHaveProperty("misses");
362
+ expect(detailedStats).toHaveProperty("hitRate");
363
+ expect(detailedStats).toHaveProperty("entries");
364
+ expect(detailedStats).toHaveProperty("oldestEntry");
365
+ expect(detailedStats).toHaveProperty("newestEntry");
366
+
367
+ expect(detailedStats.entries).toHaveLength(2);
368
+ expect(detailedStats.entries[0]).toHaveProperty("key");
369
+ expect(detailedStats.entries[0]).toHaveProperty("createdAt");
370
+ expect(detailedStats.entries[0]).toHaveProperty("age");
371
+ expect(detailedStats.entries[0]).toHaveProperty("hasValidator");
372
+ expect(detailedStats.entries[0]).toHaveProperty("hasSubSchema");
373
+ });
374
+
375
+ test("should track memory usage estimates", () => {
376
+ getSubschema(testPath, schemaId, overlays);
377
+
378
+ const stats = getCacheStats();
379
+ expect(stats.memoryUsage).toHaveProperty("entriesCount");
380
+ expect(stats.memoryUsage).toHaveProperty("estimatedSizeKB");
381
+ expect(stats.memoryUsage.entriesCount).toBe(1);
382
+ expect(typeof stats.memoryUsage.estimatedSizeKB).toBe("number");
383
+ });
384
+ });
385
+
386
+ describe("Cache Warming", () => {
387
+ test("should warm cache with default paths and overlays", () => {
388
+ const warmupStats = warmupCache();
389
+
390
+ expect(warmupStats).toHaveProperty("totalAttempted");
391
+ expect(warmupStats).toHaveProperty("successful");
392
+ expect(warmupStats).toHaveProperty("failed");
393
+ expect(warmupStats).toHaveProperty("errors");
394
+
395
+ expect(warmupStats.totalAttempted).toBeGreaterThan(0);
396
+ expect(warmupStats.successful).toBeGreaterThan(0);
397
+ expect(warmupStats.totalAttempted).toBe(warmupStats.successful + warmupStats.failed);
398
+
399
+ // Cache should now have entries
400
+ const stats = getCacheStats();
401
+ expect(stats.totalEntries).toBeGreaterThan(0);
402
+ });
403
+
404
+ test("should warm cache with custom paths and overlays", () => {
405
+ const customPaths = ["/propertyPack", "/participants"];
406
+ const customOverlays = [[], ["baspiV5"]];
407
+
408
+ const warmupStats = warmupCache(customPaths, schemaId, customOverlays);
409
+
410
+ expect(warmupStats.totalAttempted).toBe(customPaths.length * customOverlays.length);
411
+ expect(warmupStats.successful).toBeGreaterThan(0);
412
+
413
+ const stats = getCacheStats();
414
+ expect(stats.totalEntries).toBe(warmupStats.successful);
415
+ });
416
+
417
+ test("should handle invalid paths gracefully during warmup", () => {
418
+ const pathsWithInvalid = ["/participants", "/invalid/path", "/status"];
419
+ const warmupStats = warmupCache(pathsWithInvalid, schemaId, [[]]);
420
+
421
+ expect(warmupStats.totalAttempted).toBe(3);
422
+ expect(warmupStats.successful).toBe(3); // All paths are processed, even invalid ones
423
+ expect(warmupStats.failed).toBe(0); // Invalid paths still "succeed" but create null schemas
424
+ });
425
+ });
426
+
427
+ describe("Selective Cache Clearing", () => {
428
+ test("should clear cache entries by pattern", () => {
429
+ // Populate cache with different patterns using safe paths
430
+ getSubschema("/participants", schemaId, []);
431
+ getSubschema("/status", schemaId, []);
432
+ getSubschema("/participants", schemaId, ["baspiV5"]);
433
+
434
+ const stats1 = getCacheStats();
435
+ expect(stats1.totalEntries).toBe(3);
436
+
437
+ // Clear entries containing "participants"
438
+ const cleared = clearSchemaCache("participants");
439
+ expect(cleared).toBe(2);
440
+
441
+ const stats2 = getCacheStats();
442
+ expect(stats2.totalEntries).toBe(1);
443
+
444
+ // Remaining entry should be status
445
+ expect(stats2.cacheKeys[0]).toContain("status");
446
+ });
447
+
448
+ test("should return number of entries cleared", () => {
449
+ getSubschema("/participants", schemaId, []);
450
+ getSubschema("/status", schemaId, []);
451
+
452
+ const cleared = clearSchemaCache();
453
+ expect(cleared).toBe(2);
454
+
455
+ const stats = getCacheStats();
456
+ expect(stats.totalEntries).toBe(0);
457
+ });
458
+
459
+ test("should reset statistics when clearing all cache", () => {
460
+ getSubschema(testPath, schemaId, overlays);
461
+ getSubschema(testPath, schemaId, overlays); // Hit
462
+
463
+ const statsBefore = getCacheStats();
464
+ expect(statsBefore.hits).toBe(1);
465
+ expect(statsBefore.misses).toBe(1);
466
+
467
+ clearSchemaCache();
468
+
469
+ const statsAfter = getCacheStats();
470
+ expect(statsAfter.hits).toBe(0);
471
+ expect(statsAfter.misses).toBe(0);
472
+ expect(statsAfter.totalQueries).toBe(0);
473
+ });
474
+ });
475
+
476
+ describe("Cache Pruning", () => {
477
+ test("should prune entries by age", async () => {
478
+ // Add some entries using safe paths
479
+ getSubschema("/participants", schemaId, []);
480
+
481
+ // Wait a bit
482
+ await new Promise(resolve => setTimeout(resolve, 10));
483
+
484
+ getSubschema("/status", schemaId, []);
485
+
486
+ const stats1 = getCacheStats();
487
+ expect(stats1.totalEntries).toBe(2);
488
+
489
+ // Prune entries older than 5ms
490
+ const pruned = pruneCacheByAge(5);
491
+ expect(pruned).toBe(1); // Should remove the older entry
492
+
493
+ const stats2 = getCacheStats();
494
+ expect(stats2.totalEntries).toBe(1);
495
+ });
496
+
497
+ test("should not prune entries younger than max age", () => {
498
+ getSubschema("/participants", schemaId, []);
499
+
500
+ const pruned = pruneCacheByAge(60000); // 1 minute
501
+ expect(pruned).toBe(0);
502
+
503
+ const stats = getCacheStats();
504
+ expect(stats.totalEntries).toBe(1);
505
+ });
506
+ });
507
+
508
+ describe("Cache Size Management", () => {
509
+ test("should limit cache size by removing oldest entries", () => {
510
+ // Add multiple entries using safe paths
511
+ getSubschema("/participants", schemaId, []);
512
+ getSubschema("/status", schemaId, []);
513
+ getSubschema("/participants", schemaId, ["baspiV5"]);
514
+ getSubschema("/status", schemaId, ["baspiV5"]);
515
+
516
+ const stats1 = getCacheStats();
517
+ expect(stats1.totalEntries).toBe(4);
518
+
519
+ // Limit to 2 entries
520
+ const removed = setCacheMaxSize(2);
521
+ expect(removed).toBe(2);
522
+
523
+ const stats2 = getCacheStats();
524
+ expect(stats2.totalEntries).toBe(2);
525
+ });
526
+
527
+ test("should not remove entries if cache is under limit", () => {
528
+ getSubschema("/participants", schemaId, []);
529
+
530
+ const removed = setCacheMaxSize(10);
531
+ expect(removed).toBe(0);
532
+
533
+ const stats = getCacheStats();
534
+ expect(stats.totalEntries).toBe(1);
535
+ });
536
+
537
+ test("should remove oldest entries first", async () => {
538
+ // Add entries with delays to ensure different timestamps
539
+ getSubschema("/participants", schemaId, []);
540
+ await new Promise(resolve => setTimeout(resolve, 5));
541
+
542
+ getSubschema("/status", schemaId, []);
543
+ await new Promise(resolve => setTimeout(resolve, 5));
544
+
545
+ getSubschema("/participants", schemaId, ["baspiV5"]);
546
+
547
+ const stats1 = getCacheStats();
548
+ expect(stats1.totalEntries).toBe(3);
549
+
550
+ // Limit to 1 entry - should keep the newest one
551
+ setCacheMaxSize(1);
552
+
553
+ const stats2 = getCacheStats();
554
+ expect(stats2.totalEntries).toBe(1);
555
+ expect(stats2.cacheKeys[0]).toContain("baspiV5");
556
+ });
557
+ });
558
+
559
+ describe("Integration with Existing Cache Behavior", () => {
560
+ test("enhanced stats should work with existing cache operations", () => {
561
+ // Use existing functions with a safe path
562
+ const safePath = "/participants";
563
+ const validator = getSubschemaValidator(safePath, schemaId, []);
564
+ const schema = getSubschema(safePath, schemaId, []);
565
+ const valid = isPathValid(safePath, schemaId, []);
566
+
567
+ expect(typeof validator).toBe("function");
568
+ expect(schema).toBeDefined();
569
+ expect(valid).toBe(true);
570
+
571
+ const stats = getCacheStats();
572
+ expect(stats.totalQueries).toBeGreaterThan(0);
573
+ expect(stats.hits).toBeGreaterThan(0);
574
+ expect(stats.totalEntries).toBe(1);
575
+ });
576
+
577
+ test("should maintain backward compatibility", () => {
578
+ // Existing test patterns should still work
579
+ const schema1 = getSubschema(testPath, schemaId, overlays);
580
+ const schema2 = getSubschema(testPath, schemaId, overlays);
581
+
582
+ expect(schema1).toBe(schema2);
583
+
584
+ const stats = getCacheStats();
585
+ expect(stats).toHaveProperty("totalEntries");
586
+ expect(stats).toHaveProperty("cacheKeys");
587
+ expect(stats.totalEntries).toBe(1);
588
+ });
589
+ });
590
+ });
317
591
  });
@@ -449,14 +449,61 @@ test("validates a valid contract", () => {
449
449
  expect(isValid).toBe(true);
450
450
  });
451
451
 
452
- test("waterAndDrainage state with mainsFoulDrainage No missing offMainsDrainageSystem is now invalid under nts2023 overlay", () => {
452
+ test("ntsl2025 requires parking and listingAndConservation fields", () => {
453
+ // This test validates that the ntsl2025 overlay correctly enforces required fields
454
+ // for the parking and listingAndConservation sections.
455
+ // It should fail validation when these fields are missing.
456
+ // NOTE: This test will fail until overlays are regenerated from combined.json
457
+ const validator = getValidator(schemaId, ["ntsl2025"]);
458
+ const clonedExampleTransaction = JSON.parse(
459
+ JSON.stringify(exampleTransaction)
460
+ );
461
+
462
+ // Set up lettings-specific fields
463
+ clonedExampleTransaction.propertyPack.lettingInformation = {
464
+ rent: 3500,
465
+ rentFrequency: "Monthly",
466
+ securityDeposit: 5000,
467
+ };
468
+ delete clonedExampleTransaction.propertyPack.priceInformation;
469
+ delete clonedExampleTransaction.propertyPack.ownership;
470
+
471
+ // Remove ALL parking fields - this should make validation fail
472
+ delete clonedExampleTransaction.propertyPack.parking.parkingArrangements;
473
+ delete clonedExampleTransaction.propertyPack.parking.disabledParking;
474
+ delete clonedExampleTransaction.propertyPack.parking.controlledParking;
475
+ delete clonedExampleTransaction.propertyPack.parking.electricVehicleChargingPoint;
476
+
477
+ // Remove ALL listingAndConservation fields - this should make validation fail
478
+ delete clonedExampleTransaction.propertyPack.listingAndConservation.isListed;
479
+ delete clonedExampleTransaction.propertyPack.listingAndConservation.isConservationArea;
480
+ delete clonedExampleTransaction.propertyPack.listingAndConservation.hasTreePreservationOrder;
481
+
482
+ const isValid = validator(clonedExampleTransaction);
483
+
484
+ // The data should be INVALID because required fields are missing
485
+ expect(isValid).toBe(false);
486
+
487
+ // Specifically check that parking and listingAndConservation fields are reported as missing
488
+ const errorMessages = validator.errors.map(e => e.message);
489
+ expect(errorMessages).toContain("must have required property 'parkingArrangements'");
490
+ expect(errorMessages).toContain("must have required property 'disabledParking'");
491
+ expect(errorMessages).toContain("must have required property 'controlledParking'");
492
+ expect(errorMessages).toContain("must have required property 'electricVehicleChargingPoint'");
493
+ expect(errorMessages).toContain("must have required property 'isListed'");
494
+ expect(errorMessages).toContain("must have required property 'isConservationArea'");
495
+ expect(errorMessages).toContain("must have required property 'hasTreePreservationOrder'");
496
+ });
497
+
498
+ test("waterAndDrainage state is invalid under nts2023 overlay when mainsFoulDrainage yesNo is Not known", () => {
453
499
  const validator = getValidator(schemaId, ["nts2023"]);
454
500
  const clonedExampleTransaction = JSON.parse(
455
501
  JSON.stringify(exampleTransaction)
456
502
  );
457
503
 
458
- // Set the waterAndDrainage to the state from the user request
459
- // This SHOULD be invalid because offMainsDrainageSystem is required when mainsFoulDrainage.yesNo is "No"
504
+ // Set the waterAndDrainage to an actually invalid state for nts2023
505
+ // The nts overlay restricts mainsFoulDrainage.yesNo to only "Yes" or "No"
506
+ // (not "Not known" like the base schema allows)
460
507
  clonedExampleTransaction.propertyPack.waterAndDrainage = {
461
508
  "water": {
462
509
  "mainsWater": {
@@ -471,8 +518,7 @@ test("waterAndDrainage state with mainsFoulDrainage No missing offMainsDrainageS
471
518
  "yesNo": "Yes"
472
519
  },
473
520
  "mainsFoulDrainage": {
474
- "yesNo": "No"
475
- // Missing offMainsDrainageSystem - this should make validation fail
521
+ "yesNo": "Not known" // This is invalid under nts2023 overlay
476
522
  }
477
523
  }
478
524
  };
@@ -480,23 +526,22 @@ test("waterAndDrainage state with mainsFoulDrainage No missing offMainsDrainageS
480
526
  const isValid = validator(clonedExampleTransaction);
481
527
  expect(isValid).toBe(false);
482
528
 
483
- // Check that the validation error is about missing offMainsDrainageSystem
529
+ // Check that the validation error is about the invalid enum value
484
530
  const relevantError = validator.errors.find(error =>
485
531
  error.instancePath.includes('mainsFoulDrainage') &&
486
- error.message.includes('offMainsDrainageSystem')
532
+ error.message.includes('must be equal to one of the allowed values')
487
533
  );
488
534
  expect(relevantError).toBeDefined();
489
535
  });
490
536
 
491
- test("waterAndDrainage state is invalid under nts2023 overlay when mainsFoulDrainage yesNo is Not known", () => {
537
+ test("waterAndDrainage state with mainsFoulDrainage No missing offMainsDrainageSystem is now invalid under nts2023 overlay", () => {
492
538
  const validator = getValidator(schemaId, ["nts2023"]);
493
539
  const clonedExampleTransaction = JSON.parse(
494
540
  JSON.stringify(exampleTransaction)
495
541
  );
496
542
 
497
- // Set the waterAndDrainage to an actually invalid state for nts2023
498
- // The nts overlay restricts mainsFoulDrainage.yesNo to only "Yes" or "No"
499
- // (not "Not known" like the base schema allows)
543
+ // Set the waterAndDrainage to the state from the user request
544
+ // This SHOULD be invalid because offMainsDrainageSystem is required when mainsFoulDrainage.yesNo is "No"
500
545
  clonedExampleTransaction.propertyPack.waterAndDrainage = {
501
546
  "water": {
502
547
  "mainsWater": {
@@ -511,7 +556,8 @@ test("waterAndDrainage state is invalid under nts2023 overlay when mainsFoulDrai
511
556
  "yesNo": "Yes"
512
557
  },
513
558
  "mainsFoulDrainage": {
514
- "yesNo": "Not known" // This is invalid under nts2023 overlay
559
+ "yesNo": "No"
560
+ // Missing offMainsDrainageSystem - this should make validation fail
515
561
  }
516
562
  }
517
563
  };
@@ -519,10 +565,10 @@ test("waterAndDrainage state is invalid under nts2023 overlay when mainsFoulDrai
519
565
  const isValid = validator(clonedExampleTransaction);
520
566
  expect(isValid).toBe(false);
521
567
 
522
- // Check that the validation error is about the invalid enum value
568
+ // Check that the validation error is about missing offMainsDrainageSystem
523
569
  const relevantError = validator.errors.find(error =>
524
570
  error.instancePath.includes('mainsFoulDrainage') &&
525
- error.message.includes('must be equal to one of the allowed values')
571
+ error.message.includes('offMainsDrainageSystem')
526
572
  );
527
573
  expect(relevantError).toBeDefined();
528
574
  });
@@ -0,0 +1,84 @@
1
+ const fs = require("fs");
2
+
3
+ // Read the current skeleton
4
+ const skeleton = require("../schemas/v3/skeleton.json");
5
+
6
+ // Convert to ultra-compact format
7
+ const toCompact = (obj, indent = "") => {
8
+ if (typeof obj === "string") {
9
+ // Type indicators
10
+ if (["string", "number", "integer", "boolean", "variant"].includes(obj)) {
11
+ return obj[0]; // Just first letter: s, n, i, b, v
12
+ }
13
+ return obj;
14
+ }
15
+
16
+ if (Array.isArray(obj)) {
17
+ if (obj.length === 0) return "[]";
18
+ // Array with content
19
+ const inner = toCompact(obj[0], indent + " ");
20
+ if (typeof inner === "string" && inner.length === 1) {
21
+ return `[${inner}]`; // Simple array like [s]
22
+ }
23
+ return `[\n${indent} ${inner}\n${indent}]`;
24
+ }
25
+
26
+ if (obj && typeof obj === "object") {
27
+ const keys = Object.keys(obj);
28
+ if (keys.length === 0) return "{}";
29
+
30
+ // Check if it's a simple object with only type indicators
31
+ const allSimple = keys.every(k =>
32
+ typeof obj[k] === "string" &&
33
+ ["s", "n", "i", "b", "v"].includes(toCompact(obj[k]))
34
+ );
35
+
36
+ if (allSimple && keys.length < 5) {
37
+ // Inline format: {a:s b:n c:s}
38
+ return `{${keys.map(k => `${k}:${toCompact(obj[k])}`).join(" ")}}`;
39
+ }
40
+
41
+ // Multi-line format
42
+ const lines = keys.map(k => {
43
+ const val = toCompact(obj[k], indent + " ");
44
+ if (typeof val === "string" && val.length === 1) {
45
+ return `${indent} ${k}:${val}`;
46
+ } else if (typeof val === "string" && (val === "{}" || val === "[]")) {
47
+ return `${indent} ${k}${val}`;
48
+ } else if (typeof val === "string" && val.startsWith("{") && val.endsWith("}")) {
49
+ return `${indent} ${k}${val}`;
50
+ } else if (typeof val === "string" && val.startsWith("[") && !val.includes("\n")) {
51
+ return `${indent} ${k}${val}`;
52
+ } else {
53
+ return `${indent} ${k}:\n${indent} ${val}`;
54
+ }
55
+ });
56
+
57
+ return `{\n${lines.join("\n")}\n${indent}}`;
58
+ }
59
+
60
+ return String(obj);
61
+ };
62
+
63
+ // Generate ultra-compact version
64
+ const compact = toCompact(skeleton);
65
+
66
+ // Write compact version
67
+ fs.writeFileSync("../schemas/v3/compactSkeleton.txt", compact);
68
+
69
+ console.log("Compact skeleton written to ../schemas/v3/compactSkeleton.txt");
70
+ console.log(`Size: ${compact.length} bytes`);
71
+
72
+ // Count tokens
73
+ try {
74
+ const GPT3Encoder = require('gpt-3-encoder');
75
+ const encoded = GPT3Encoder.encode(compact);
76
+ console.log(`Tokens (GPT-3): ${encoded.length}`);
77
+ console.log(`Reduction: ${((1 - encoded.length / 78734) * 100).toFixed(1)}% fewer tokens than JSON skeleton`);
78
+
79
+ // Show sample
80
+ console.log("\nFirst 500 chars:");
81
+ console.log(compact.substring(0, 500));
82
+ } catch (e) {
83
+ console.log(`Estimated tokens: ~${Math.ceil(compact.length / 4)}`);
84
+ }
@@ -0,0 +1,67 @@
1
+ const fs = require('fs');
2
+
3
+ // Try to use tiktoken or a similar library
4
+ // For Claude/Anthropic models, we can estimate using the cl100k_base encoding
5
+ // which is similar to what Claude uses
6
+
7
+ // First, let's check what's available
8
+ try {
9
+ // Option 1: Use gpt-tokenizer (common npm package)
10
+ const { encode } = require('gpt-tokenizer');
11
+
12
+ const content = fs.readFileSync('../schemas/v3/skeleton.json', 'utf8');
13
+ const tokens = encode(content);
14
+
15
+ console.log(`Using gpt-tokenizer:`);
16
+ console.log(`Total tokens: ${tokens.length}`);
17
+ console.log(`File size: ${content.length} characters`);
18
+ console.log(`Ratio: ${(content.length / tokens.length).toFixed(2)} characters per token`);
19
+
20
+ } catch (e1) {
21
+ console.log("gpt-tokenizer not found, trying gpt-3-encoder...");
22
+
23
+ try {
24
+ // Option 2: Use gpt-3-encoder
25
+ const GPT3Encoder = require('gpt-3-encoder');
26
+
27
+ const content = fs.readFileSync('../schemas/v3/skeleton.json', 'utf8');
28
+ const encoded = GPT3Encoder.encode(content);
29
+
30
+ console.log(`Using gpt-3-encoder:`);
31
+ console.log(`Total tokens: ${encoded.length}`);
32
+ console.log(`File size: ${content.length} characters`);
33
+ console.log(`Ratio: ${(content.length / encoded.length).toFixed(2)} characters per token`);
34
+
35
+ } catch (e2) {
36
+ console.log("No tokenizer library found. Falling back to estimation...");
37
+
38
+ // Option 3: Manual estimation
39
+ const content = fs.readFileSync('../schemas/v3/skeleton.json', 'utf8');
40
+
41
+ // Count different types of content for better estimation
42
+ const lines = content.split('\n');
43
+ const words = content.split(/\s+/);
44
+ const punctuation = content.match(/[{}:,\[\]"]/g) || [];
45
+
46
+ // Rough estimation:
47
+ // - JSON structure tokens (brackets, colons, quotes): ~1 token each
48
+ // - Words: ~1 token each
49
+ // - Whitespace: usually absorbed into adjacent tokens
50
+
51
+ const structureTokens = punctuation.length;
52
+ const wordTokens = words.length;
53
+ const estimatedTokens = Math.ceil((structureTokens + wordTokens) * 0.75); // 0.75 factor for overlap
54
+
55
+ console.log("Manual estimation:");
56
+ console.log(`File size: ${content.length} characters`);
57
+ console.log(`Lines: ${lines.length}`);
58
+ console.log(`Words: ${words.length}`);
59
+ console.log(`JSON punctuation: ${punctuation.length}`);
60
+ console.log(`Estimated tokens: ~${estimatedTokens}`);
61
+ console.log(`Ratio: ${(content.length / estimatedTokens).toFixed(2)} characters per token`);
62
+
63
+ // Also do a simple character-based estimation
64
+ const simpleEstimate = Math.ceil(content.length / 4);
65
+ console.log(`\nSimple estimation (chars/4): ~${simpleEstimate} tokens`);
66
+ }
67
+ }