@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.
- package/.claude/settings.local.json +6 -1
- package/index.js +190 -12
- package/package.json +2 -1
- package/src/schemas/v3/combined.json +11 -0
- package/src/schemas/v3/compactSkeleton.txt +3340 -0
- package/src/schemas/v3/overlays/ntsl2.json +11 -0
- package/src/schemas/v3/skeleton.json +3208 -3036
- package/src/tests/v3/caching.test.js +274 -0
- package/src/tests/v3/transactionSchema.test.js +60 -14
- package/src/utils/compactSkeleton.js +84 -0
- package/src/utils/countTokens.js +67 -0
- package/src/utils/extractOverlay.js +96 -5
- package/src/utils/minimalSkeleton.js +85 -0
- package/src/utils/pathSkeleton.js +181 -0
|
@@ -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("
|
|
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
|
|
459
|
-
//
|
|
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": "
|
|
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
|
|
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('
|
|
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
|
|
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
|
|
498
|
-
//
|
|
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": "
|
|
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
|
|
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('
|
|
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
|
+
}
|