@pdtf/schemas 3.6.0-8 → 3.6.0-dev.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.
Files changed (71) hide show
  1. package/CLAUDE.md +101 -0
  2. package/README.md +57 -33
  3. package/docs/backend-lms-profile-filtering-spec.md +180 -0
  4. package/docs/branch-release-workflow.md +138 -0
  5. package/docs/sef25-extensions-ui-spec.md +355 -0
  6. package/index.js +134 -66
  7. package/package.json +6 -2
  8. package/src/examples/v3/exampleTransaction.json +3 -3
  9. package/src/schemas/v3/combined.json +6981 -1234
  10. package/src/schemas/v3/compactSkeleton.txt +316 -5
  11. package/src/schemas/v3/overlays/baspi4.json +162 -20
  12. package/src/schemas/v3/overlays/baspi5.json +167 -22
  13. package/src/schemas/v3/overlays/extensions/ac.json +333 -0
  14. package/src/schemas/v3/overlays/extensions/as.json +4 -2
  15. package/src/schemas/v3/overlays/extensions/dk.json +32 -0
  16. package/src/schemas/v3/overlays/extensions/dr.json +9 -9
  17. package/src/schemas/v3/overlays/extensions/er.json +4 -2
  18. package/src/schemas/v3/overlays/extensions/fd.json +6 -3
  19. package/src/schemas/v3/overlays/extensions/hi.json +6 -3
  20. package/src/schemas/v3/overlays/extensions/hs.json +4 -2
  21. package/src/schemas/v3/overlays/extensions/ic.json +59 -0
  22. package/src/schemas/v3/overlays/extensions/jk.json +28 -8
  23. package/src/schemas/v3/overlays/extensions/la.json +4 -2
  24. package/src/schemas/v3/overlays/extensions/lc.json +75 -0
  25. package/src/schemas/v3/overlays/extensions/ma.json +6 -3
  26. package/src/schemas/v3/overlays/extensions/mc.json +4 -2
  27. package/src/schemas/v3/overlays/extensions/mi.json +69 -0
  28. package/src/schemas/v3/overlays/extensions/nd.json +43 -0
  29. package/src/schemas/v3/overlays/extensions/oa.json +4 -2
  30. package/src/schemas/v3/overlays/extensions/oc.json +4 -2
  31. package/src/schemas/v3/overlays/extensions/pc.json +37 -0
  32. package/src/schemas/v3/overlays/extensions/ph.json +194 -0
  33. package/src/schemas/v3/overlays/extensions/rw.json +68 -0
  34. package/src/schemas/v3/overlays/extensions/sb.json +4 -2
  35. package/src/schemas/v3/overlays/extensions/sc.json +129 -0
  36. package/src/schemas/v3/overlays/extensions/sd.json +63 -0
  37. package/src/schemas/v3/overlays/extensions/sf.json +4 -2
  38. package/src/schemas/v3/overlays/extensions/sl.json +4 -2
  39. package/src/schemas/v3/overlays/extensions/ta.json +252 -0
  40. package/src/schemas/v3/overlays/extensions/tf.json +6 -3
  41. package/src/schemas/v3/overlays/extensions/tr.json +73 -0
  42. package/src/schemas/v3/overlays/extensions/wg.json +350 -0
  43. package/src/schemas/v3/overlays/fme1.json +16 -14
  44. package/src/schemas/v3/overlays/lpe1.json +60 -30
  45. package/src/schemas/v3/overlays/nts.json +68 -1
  46. package/src/schemas/v3/overlays/nts2.json +68 -4
  47. package/src/schemas/v3/overlays/ntsl.json +68 -1
  48. package/src/schemas/v3/overlays/ntsl2.json +68 -4
  49. package/src/schemas/v3/overlays/piq.json +37 -3
  50. package/src/schemas/v3/overlays/rds.json +117 -8
  51. package/src/schemas/v3/overlays/sef25.json +1639 -0
  52. package/src/schemas/v3/overlays/ta6.json +46 -16
  53. package/src/schemas/v3/overlays/ta6ed6.json +6533 -0
  54. package/src/schemas/v3/overlays/ta7.json +70 -6
  55. package/src/schemas/v3/overlays/ta7ed5.json +1551 -0
  56. package/src/schemas/v3/pdtf-transaction.json +3896 -686
  57. package/src/schemas/v3/skeleton.json +442 -35
  58. package/src/utils/extractExtensionOverlays.js +293 -31
  59. package/src/utils/extractOverlay.js +94 -27
  60. package/src/utils/pathSkeleton.js +1 -1
  61. package/.claude/settings.local.json +0 -42
  62. package/.github/workflows/pages.yml +0 -50
  63. package/CNAME +0 -1
  64. package/_config.yml +0 -1
  65. package/src/tests/v3/caching.test.js +0 -591
  66. package/src/tests/v3/enquiries.test.js +0 -1096
  67. package/src/tests/v3/extensionOverlays.test.js +0 -525
  68. package/src/tests/v3/offers.test.js +0 -823
  69. package/src/tests/v3/patternProperties.test.js +0 -565
  70. package/src/tests/v3/transactionSchema.test.js +0 -615
  71. package/src/tests/v3/verifiedClaimsValidator.test.js +0 -195
@@ -5,6 +5,41 @@ const path = require("path");
5
5
 
6
6
  const combinedSchema = require("../schemas/v3/combined.json");
7
7
 
8
+ // Derive ref-related field names from a refType
9
+ function getRefConfig(refType) {
10
+ if (refType === "nts2Ref") {
11
+ // Legacy mapping: nts2Ref -> ntsRef in output
12
+ return {
13
+ ref: "nts2Ref",
14
+ outputRef: "ntsRef",
15
+ required: "nts2Required",
16
+ title: "nts2Title",
17
+ description: "nts2Description",
18
+ enumKey: "nts2Enum",
19
+ };
20
+ }
21
+ if (refType === "sef25Ref") {
22
+ // SEF25 extensions also appear as ntsRef in output
23
+ return {
24
+ ref: "sef25Ref",
25
+ outputRef: "ntsRef",
26
+ required: "sef25Required",
27
+ title: "sef25Title",
28
+ description: "sef25Description",
29
+ enumKey: "sef25Enum",
30
+ };
31
+ }
32
+ // For other ref types, derive keys by replacing "Ref" suffix
33
+ return {
34
+ ref: refType,
35
+ outputRef: refType,
36
+ required: refType.replace("Ref", "Required"),
37
+ title: refType.replace("Ref", "Title"),
38
+ description: refType.replace("Ref", "Description"),
39
+ enumKey: refType.replace("Ref", "Enum"),
40
+ };
41
+ }
42
+
8
43
  // Define the extension overlay mappings
9
44
  const extensionMappings = {
10
45
  // Outside areas
@@ -150,18 +185,192 @@ const extensionMappings = {
150
185
  "/properties/propertyPack/properties/completionAndMoving/properties/otherPropertyInChain",
151
186
  ],
152
187
  },
188
+
189
+ // Private supply costs (water & sewerage)
190
+ sc: {
191
+ name: "supplyCosts",
192
+ description: "Associated costs for private water and sewerage supply",
193
+ refType: "sef25Ref",
194
+ paths: [
195
+ "/properties/propertyPack/properties/waterAndDrainage/properties/water/properties/mainsWater/oneOf/0/properties/associatedCost",
196
+ "/properties/propertyPack/properties/waterAndDrainage/properties/drainage/properties/mainsFoulDrainage/oneOf/2/properties/associatedCost",
197
+ ],
198
+ },
199
+
200
+ // Parking permit costs
201
+ pc: {
202
+ name: "parkingPermitCost",
203
+ description: "Parking permit cost and frequency",
204
+ refType: "sef25Ref",
205
+ paths: [
206
+ "/properties/propertyPack/properties/parking/properties/controlledParking/oneOf/1/properties/costOfPermit",
207
+ "/properties/propertyPack/properties/parking/properties/controlledParking/oneOf/1/properties/costOfPermitFrequency",
208
+ ],
209
+ },
210
+
211
+ // Property hazards
212
+ ph: {
213
+ name: "propertyHazards",
214
+ description: "Property hazards and known issues",
215
+ refType: "sef25Ref",
216
+ paths: [
217
+ "/properties/propertyPack/properties/specialistIssues",
218
+ ],
219
+ },
220
+
221
+ // Dropped kerb access to parking
222
+ dk: {
223
+ name: "droppedKerbAccess",
224
+ description: "Dropped kerb access to parking",
225
+ refType: "sef25Ref",
226
+ paths: [
227
+ "/properties/propertyPack/properties/parking/properties/droppedKerbAccess",
228
+ ],
229
+ },
230
+
231
+ // Private right of way
232
+ rw: {
233
+ name: "privateRightOfWay",
234
+ description: "Private right of way",
235
+ refType: "sef25Ref",
236
+ paths: [
237
+ "/properties/propertyPack/properties/rightsAndInformalArrangements/properties/rightsOrArrangements/properties/privateRightOfWay",
238
+ ],
239
+ },
240
+
241
+ // Storm, fire or flood damage
242
+ sd: {
243
+ name: "stormFireFloodDamage",
244
+ description: "Storm, fire or flood damage",
245
+ refType: "sef25Ref",
246
+ paths: [
247
+ "/properties/propertyPack/properties/environmentalIssues/properties/stormFireFloodDamage",
248
+ ],
249
+ },
250
+
251
+ // Solar panel lease costs
252
+ lc: {
253
+ name: "solarLeaseCosts",
254
+ description: "Solar panel lease costs",
255
+ refType: "sef25Ref",
256
+ paths: [
257
+ "/properties/propertyPack/properties/electricity/properties/solarPanels/oneOf/1/properties/panelsOwnedOutright/oneOf/1/properties/associatedCost",
258
+ ],
259
+ },
260
+
261
+ // Warranties and guarantees upfront
262
+ wg: {
263
+ name: "warrantiesGuarantees",
264
+ description: "Warranties and guarantees upfront Y/N for SEF25",
265
+ refType: "sef25Ref",
266
+ paths: [
267
+ "/properties/propertyPack/properties/guaranteesWarrantiesAndIndemnityInsurances",
268
+ ],
269
+ },
270
+
271
+ // Insurance claims upfront
272
+ ic: {
273
+ name: "insuranceClaims",
274
+ description: "Insurance claims upfront for SEF25",
275
+ refType: "sef25Ref",
276
+ paths: [
277
+ "/properties/propertyPack/properties/insurance/oneOf/1/properties/insuranceClaims",
278
+ ],
279
+ },
280
+
281
+ // Neighbour development upfront
282
+ nd: {
283
+ name: "neighbourDevelopment",
284
+ description: "Neighbour development upfront for SEF25",
285
+ refType: "sef25Ref",
286
+ paths: [
287
+ "/properties/propertyPack/properties/notices/properties/neighbourDevelopment",
288
+ ],
289
+ },
290
+
291
+ // Other material issue upfront
292
+ mi: {
293
+ name: "otherMaterialIssue",
294
+ description: "Other material issue upfront for SEF25",
295
+ refType: "sef25Ref",
296
+ paths: [
297
+ "/properties/propertyPack/properties/additionalInformation/properties/otherMaterialIssue",
298
+ ],
299
+ },
300
+
301
+ // Title restrictions for freehold
302
+ tr: {
303
+ name: "titleRestrictions",
304
+ description: "Title restrictions for freehold properties",
305
+ refType: "sef25Ref",
306
+ paths: [
307
+ "/properties/propertyPack/properties/ownership/properties/ownershipsToBeTransferred/items/oneOf/0/properties/titleRestrictions",
308
+ ],
309
+ },
310
+
311
+ // Alterations and changes
312
+ ac: {
313
+ name: "alterationsAndChanges",
314
+ description: "Selected alterations and changes fields for SEF25",
315
+ refType: "sef25Ref",
316
+ preserveDiscriminators: true,
317
+ paths: [
318
+ "/properties/propertyPack/properties/alterationsAndChanges/properties/extension/properties/yesNo",
319
+ "/properties/propertyPack/properties/alterationsAndChanges/properties/extension/oneOf/1/properties/details",
320
+ "/properties/propertyPack/properties/alterationsAndChanges/properties/extension/oneOf/1/properties/planningPermission/properties/yesNo",
321
+ "/properties/propertyPack/properties/alterationsAndChanges/properties/extension/oneOf/1/properties/buildingRegApproval/properties/yesNo",
322
+ "/properties/propertyPack/properties/alterationsAndChanges/properties/loftConversion/properties/yesNo",
323
+ "/properties/propertyPack/properties/alterationsAndChanges/properties/loftConversion/oneOf/1/properties/details",
324
+ "/properties/propertyPack/properties/alterationsAndChanges/properties/loftConversion/oneOf/1/properties/planningPermission/properties/yesNo",
325
+ "/properties/propertyPack/properties/alterationsAndChanges/properties/loftConversion/oneOf/1/properties/buildingRegApproval/properties/yesNo",
326
+ "/properties/propertyPack/properties/alterationsAndChanges/properties/garageConversion/properties/yesNo",
327
+ "/properties/propertyPack/properties/alterationsAndChanges/properties/garageConversion/oneOf/1/properties/details",
328
+ "/properties/propertyPack/properties/alterationsAndChanges/properties/garageConversion/oneOf/1/properties/planningPermission/properties/yesNo",
329
+ "/properties/propertyPack/properties/alterationsAndChanges/properties/garageConversion/oneOf/1/properties/buildingRegApproval/properties/yesNo",
330
+ "/properties/propertyPack/properties/alterationsAndChanges/properties/removalOfInternalWalls/properties/yesNo",
331
+ "/properties/propertyPack/properties/alterationsAndChanges/properties/removalOfInternalWalls/oneOf/1/properties/details",
332
+ "/properties/propertyPack/properties/alterationsAndChanges/properties/removalOfInternalWalls/oneOf/1/properties/planningPermission/properties/yesNo",
333
+ "/properties/propertyPack/properties/alterationsAndChanges/properties/removalOfInternalWalls/oneOf/1/properties/buildingRegApproval/properties/yesNo",
334
+ "/properties/propertyPack/properties/alterationsAndChanges/properties/changeOfUse/properties/yesNo",
335
+ "/properties/propertyPack/properties/alterationsAndChanges/properties/changeOfUse/oneOf/1/properties/details",
336
+ "/properties/propertyPack/properties/alterationsAndChanges/properties/changeOfUse/oneOf/1/properties/planningPermission/properties/yesNo",
337
+ "/properties/propertyPack/properties/alterationsAndChanges/properties/changeOfUse/oneOf/1/properties/buildingRegApproval/properties/yesNo",
338
+ ],
339
+ },
340
+
341
+ // TA6 edition 6 compatibility for shared MI/NTS validation
342
+ ta: {
343
+ name: "ta6Ed6Compatibility",
344
+ description:
345
+ "Compatibility enum ranges for validating shared PDTF state containing TA6 edition 6 answers",
346
+ refType: "ta6ed6CompatRef",
347
+ paths: [
348
+ "/properties/propertyPack/properties/parking",
349
+ "/properties/propertyPack/properties/heating",
350
+ "/properties/propertyPack/properties/rightsAndInformalArrangements",
351
+ "/properties/propertyPack/properties/legalBoundaries",
352
+ ],
353
+ },
153
354
  };
154
355
 
155
- // Function to extract properties at specific paths with nts2Ref
156
- function extractPathsWithNts2Ref(schema, paths) {
356
+ // Function to extract properties at specific paths with given ref type
357
+ function extractPathsWithRef(
358
+ schema,
359
+ paths,
360
+ refConfig,
361
+ preserveDiscriminators = false
362
+ ) {
157
363
  const result = {};
158
364
 
159
365
  paths.forEach((path) => {
160
366
  try {
161
367
  const value = jp.get(schema, path);
162
- if (value && hasNts2Ref(value)) {
368
+ if (value && hasRef(value, refConfig)) {
163
369
  // Set the value at the same path in result
164
- jp.set(result, path, extractNts2Properties(value));
370
+ jp.set(result, path, extractRefProperties(value, refConfig));
371
+ if (preserveDiscriminators) {
372
+ addAncestorDiscriminators(result, schema, path);
373
+ }
165
374
  }
166
375
  } catch (err) {
167
376
  console.warn(`Path not found: ${path}`);
@@ -171,11 +380,31 @@ function extractPathsWithNts2Ref(schema, paths) {
171
380
  return result;
172
381
  }
173
382
 
174
- // Check if an object or its descendants have nts2Ref
175
- function hasNts2Ref(obj) {
383
+ function addAncestorDiscriminators(result, schema, path) {
384
+ const parts = path.split("/").filter(Boolean);
385
+
386
+ for (let index = 1; index <= parts.length; index += 1) {
387
+ const ancestorPath = `/${parts.slice(0, index).join("/")}`;
388
+
389
+ try {
390
+ const schemaNode = jp.get(schema, ancestorPath);
391
+ if (!schemaNode?.discriminator) continue;
392
+
393
+ const resultNode = jp.get(result, ancestorPath);
394
+ if (resultNode && typeof resultNode === "object") {
395
+ resultNode.discriminator = schemaNode.discriminator;
396
+ }
397
+ } catch (err) {
398
+ // Ancestor was not materialised by this selected path.
399
+ }
400
+ }
401
+ }
402
+
403
+ // Check if an object or its descendants have the specified ref
404
+ function hasRef(obj, refConfig) {
176
405
  let found = false;
177
406
  traverse(obj).forEach(function (element) {
178
- if (element && element.nts2Ref) {
407
+ if (element && element[refConfig.ref]) {
179
408
  found = true;
180
409
  this.stop();
181
410
  }
@@ -183,16 +412,16 @@ function hasNts2Ref(obj) {
183
412
  return found;
184
413
  }
185
414
 
186
- // Extract only NTS2-specific properties (with nts2Ref)
187
- function extractNts2Properties(obj, parentKey) {
415
+ // Extract only ref-specific properties
416
+ function extractRefProperties(obj, refConfig, parentKey) {
188
417
  const result = {};
189
418
 
190
- // Copy nts2-specific metadata at current level
191
- if (obj.nts2Ref) result.ntsRef = obj.nts2Ref;
192
- if (obj.nts2Required) result.required = obj.nts2Required;
193
- if (obj.nts2Title) result.title = obj.nts2Title;
194
- if (obj.nts2Description) result.description = obj.nts2Description;
195
- if (obj.nts2Enum) result.enum = obj.nts2Enum;
419
+ // Copy ref-specific metadata at current level
420
+ if (obj[refConfig.ref]) result[refConfig.outputRef] = obj[refConfig.ref];
421
+ if (obj[refConfig.required]) result.required = obj[refConfig.required];
422
+ if (obj[refConfig.title]) result.title = obj[refConfig.title];
423
+ if (obj[refConfig.description]) result.description = obj[refConfig.description];
424
+ if (obj[refConfig.enumKey]) result.enum = obj[refConfig.enumKey];
196
425
 
197
426
  // Handle discriminator
198
427
  if (obj.discriminator) {
@@ -204,11 +433,11 @@ function extractNts2Properties(obj, parentKey) {
204
433
  result.properties = {};
205
434
  Object.keys(obj.properties).forEach((key) => {
206
435
  const prop = obj.properties[key];
207
- if (hasNts2Ref(prop)) {
208
- result.properties[key] = extractNts2Properties(prop, key);
436
+ if (hasRef(prop, refConfig)) {
437
+ result.properties[key] = extractRefProperties(prop, refConfig, key);
209
438
  }
210
439
  });
211
- // Only keep properties if we found some with nts2Ref
440
+ // Only keep properties if we found some with ref
212
441
  if (Object.keys(result.properties).length === 0) {
213
442
  delete result.properties;
214
443
  }
@@ -216,8 +445,8 @@ function extractNts2Properties(obj, parentKey) {
216
445
 
217
446
  // Handle arrays
218
447
  if (obj.items) {
219
- if (hasNts2Ref(obj.items)) {
220
- result.items = extractNts2Properties(obj.items);
448
+ if (hasRef(obj.items, refConfig)) {
449
+ result.items = extractRefProperties(obj.items, refConfig);
221
450
  }
222
451
  }
223
452
 
@@ -225,8 +454,8 @@ function extractNts2Properties(obj, parentKey) {
225
454
  if (obj.oneOf) {
226
455
  // Always preserve the full array structure and order
227
456
  const processedOneOf = obj.oneOf.map((schema, index) => {
228
- if (hasNts2Ref(schema)) {
229
- const extracted = extractNts2Properties(schema);
457
+ if (hasRef(schema, refConfig)) {
458
+ const extracted = extractRefProperties(schema, refConfig);
230
459
  // For oneOf schemas, we need to preserve discriminator enum values
231
460
  if (obj.discriminator && schema.properties) {
232
461
  const propName = obj.discriminator.propertyName;
@@ -234,7 +463,7 @@ function extractNts2Properties(obj, parentKey) {
234
463
  if (!extracted.properties) extracted.properties = {};
235
464
  extracted.properties[propName] = {
236
465
  enum:
237
- schema.properties[propName].nts2Enum ||
466
+ schema.properties[propName][refConfig.enumKey] ||
238
467
  schema.properties[propName].enum,
239
468
  };
240
469
  }
@@ -258,7 +487,7 @@ function extractNts2Properties(obj, parentKey) {
258
487
  }
259
488
  return extracted;
260
489
  } else {
261
- // If this branch doesn't have nts2Ref, preserve the base discriminator enum
490
+ // If this branch doesn't have ref, preserve the base discriminator enum
262
491
  if (obj.discriminator && schema.properties) {
263
492
  const propName = obj.discriminator.propertyName;
264
493
  if (schema.properties[propName]) {
@@ -281,7 +510,7 @@ function extractNts2Properties(obj, parentKey) {
281
510
  // If parentKey is set, and this object has extension metadata, return a stub for the parent
282
511
  if (
283
512
  parentKey &&
284
- (result.ntsRef || result.required || result.discriminator)
513
+ (result[refConfig.outputRef] || result.required || result.discriminator)
285
514
  ) {
286
515
  // This is an extension property inside a oneOf branch
287
516
  // Return a stub for the parent property with metadata and oneOf structure
@@ -311,16 +540,16 @@ function extractNts2Properties(obj, parentKey) {
311
540
  }
312
541
 
313
542
  // Add required array at parent levels if needed
314
- function addRequiredArrays(overlay, schema) {
543
+ function addRequiredArrays(overlay, schema, refConfig) {
315
544
  traverse(overlay).forEach(function (node) {
316
545
  if (node && node.properties) {
317
546
  const schemaPath = "/" + this.path.join("/");
318
547
  try {
319
548
  const schemaNode = jp.get(schema, schemaPath);
320
- if (schemaNode && schemaNode.nts2Required) {
549
+ if (schemaNode && schemaNode[refConfig.required]) {
321
550
  // Filter to only include properties that exist in this overlay
322
551
  const overlayProps = Object.keys(node.properties);
323
- const requiredProps = schemaNode.nts2Required.filter((prop) =>
552
+ const requiredProps = schemaNode[refConfig.required].filter((prop) =>
324
553
  overlayProps.includes(prop)
325
554
  );
326
555
  if (requiredProps.length > 0) {
@@ -335,15 +564,48 @@ function addRequiredArrays(overlay, schema) {
335
564
  return overlay;
336
565
  }
337
566
 
567
+ // Annotate intermediate nodes with refs from the source schema
568
+ // This ensures every level of the overlay tree carries an ntsRef
569
+ function addIntermediateRefs(overlay, schema, refConfig) {
570
+ traverse(overlay).forEach(function (node) {
571
+ if (node && typeof node === "object" && !Array.isArray(node) && node.properties) {
572
+ // Skip if this node already has a ref
573
+ if (node[refConfig.outputRef]) return;
574
+
575
+ const schemaPath = "/" + this.path.join("/");
576
+ try {
577
+ const schemaNode = jp.get(schema, schemaPath);
578
+ if (schemaNode && schemaNode[refConfig.ref]) {
579
+ node[refConfig.outputRef] = schemaNode[refConfig.ref];
580
+ }
581
+ } catch (err) {
582
+ // Path doesn't exist in schema, skip
583
+ }
584
+ }
585
+ });
586
+ return overlay;
587
+ }
588
+
338
589
  // Generate extension overlays
339
590
  Object.entries(extensionMappings).forEach(([code, config]) => {
340
- console.log(`\nGenerating extension overlay: ${code} (${config.name})`);
591
+ const refType = config.refType || "nts2Ref";
592
+ const refConfig = getRefConfig(refType);
593
+
594
+ console.log(`\nGenerating extension overlay: ${code} (${config.name}) [${refType}]`);
341
595
 
342
596
  // Extract the specific paths
343
- let overlay = extractPathsWithNts2Ref(combinedSchema, config.paths);
597
+ let overlay = extractPathsWithRef(
598
+ combinedSchema,
599
+ config.paths,
600
+ refConfig,
601
+ config.preserveDiscriminators
602
+ );
344
603
 
345
604
  // Add required arrays where needed
346
- overlay = addRequiredArrays(overlay, combinedSchema);
605
+ overlay = addRequiredArrays(overlay, combinedSchema, refConfig);
606
+
607
+ // Add refs to intermediate nodes in the path hierarchy
608
+ overlay = addIntermediateRefs(overlay, combinedSchema, refConfig);
347
609
 
348
610
  // Add schema metadata
349
611
  overlay.$schema = "http://json-schema.org/draft-07/schema#";
@@ -2,9 +2,24 @@ const { dereference } = require("@jdw/jst");
2
2
  const traverse = require("traverse");
3
3
  const jp = require("jsonpointer");
4
4
  const fs = require("fs");
5
+ const path = require("path");
5
6
  const merge = require("deepmerge");
6
7
 
7
- const combinedSchema = require("../schemas/v3/combined.json");
8
+ const combinedPath =
9
+ process.env.COMBINED_PATH ||
10
+ path.resolve(__dirname, "../schemas/v3/combined.json");
11
+ const outputDir =
12
+ process.env.OUTPUT_DIR ||
13
+ path.resolve(__dirname, "../schemas/v3");
14
+
15
+ if (!fs.existsSync(combinedPath)) {
16
+ throw new Error(`Combined schema not found: ${combinedPath}`);
17
+ }
18
+ if (!fs.existsSync(outputDir)) {
19
+ fs.mkdirSync(outputDir, { recursive: true });
20
+ }
21
+
22
+ const combinedSchema = JSON.parse(fs.readFileSync(combinedPath, "utf8"));
8
23
 
9
24
  const extractFields = [
10
25
  "baspi4",
@@ -14,7 +29,9 @@ const extractFields = [
14
29
  "ntsl",
15
30
  "ntsl2",
16
31
  "ta6",
32
+ "ta6ed6",
17
33
  "ta7",
34
+ "ta7ed5",
18
35
  "ta10",
19
36
  "lpe1",
20
37
  "fme1",
@@ -25,8 +42,11 @@ const extractFields = [
25
42
  "rds",
26
43
  "oc1",
27
44
  "sr24",
45
+ "sef25",
28
46
  ];
29
47
 
48
+ const metadataFields = [...extractFields, "ta6ed6Compat"];
49
+
30
50
  const flattenSkeleton = (schema) => {
31
51
  if (!schema) return undefined;
32
52
 
@@ -68,23 +88,21 @@ const flattenSkeleton = (schema) => {
68
88
  });
69
89
  }
70
90
 
71
- // Handle oneOf - merge all possible properties
91
+ // Handle oneOf - merge all possible properties (recursively handles nested oneOf)
72
92
  if (schema.oneOf) {
73
93
  schema.oneOf.forEach((aOneOf) => {
74
- if (aOneOf.properties) {
75
- Object.entries(aOneOf.properties).forEach(([key, value]) => {
76
- // If property already exists and differs, mark as variant
77
- const newValue = flattenSkeleton(value);
78
- if (returnStructure[key] && JSON.stringify(returnStructure[key]) !== JSON.stringify(newValue)) {
79
- // For primitive types, just mark as variant type
80
- if (typeof returnStructure[key] === "string" || typeof newValue === "string") {
94
+ const variantResult = flattenSkeleton(aOneOf);
95
+ if (variantResult && typeof variantResult === "object" && !Array.isArray(variantResult)) {
96
+ Object.entries(variantResult).forEach(([key, value]) => {
97
+ if (key === "_variants") return;
98
+ if (returnStructure[key] && JSON.stringify(returnStructure[key]) !== JSON.stringify(value)) {
99
+ if (typeof returnStructure[key] === "string" || typeof value === "string") {
81
100
  returnStructure[key] = "variant";
82
101
  } else {
83
- // For complex types, merge properties
84
- returnStructure[key] = { ...returnStructure[key], ...newValue, _variants: true };
102
+ returnStructure[key] = { ...returnStructure[key], ...value, _variants: true };
85
103
  }
86
104
  } else {
87
- returnStructure[key] = newValue;
105
+ returnStructure[key] = value;
88
106
  }
89
107
  });
90
108
  }
@@ -97,6 +115,23 @@ const flattenSkeleton = (schema) => {
97
115
 
98
116
  const extractOverlay = (sourceSchema, ref) => {
99
117
  const refName = `${ref}Ref`;
118
+ const extendedTagMap = {
119
+ Type: "type",
120
+ MinLength: "minLength",
121
+ MaxLength: "maxLength",
122
+ Pattern: "pattern",
123
+ Minimum: "minimum",
124
+ Maximum: "maximum",
125
+ MinItems: "minItems",
126
+ MaxItems: "maxItems",
127
+ UniqueItems: "uniqueItems",
128
+ AdditionalProperties: "additionalProperties",
129
+ Format: "format",
130
+ MinProperties: "minProperties",
131
+ MaxProperties: "maxProperties",
132
+ MultipleOf: "multipleOf",
133
+ Const: "const",
134
+ };
100
135
  const returnSchema = {
101
136
  $schema: "http://json-schema.org/draft-07/schema#",
102
137
  $id: `https://trust.propdata.org.uk/schemas/v3/overlays/${ref}.json`,
@@ -107,7 +142,7 @@ const extractOverlay = (sourceSchema, ref) => {
107
142
  if (element[refName]) {
108
143
  jp.set(returnSchema, `${path}/${refName}`, element[refName]);
109
144
 
110
- if (element.discriminator) {
145
+ if (element.discriminator && Array.isArray(element.oneOf)) {
111
146
  jp.set(returnSchema, `${path}/discriminator`, element.discriminator);
112
147
  const { propertyName } = element.discriminator;
113
148
  element.oneOf.forEach((oneOf, index) => {
@@ -134,7 +169,7 @@ const extractOverlay = (sourceSchema, ref) => {
134
169
  }
135
170
 
136
171
  // also handle discriminator properties nested in items
137
- if (element.items?.discriminator) {
172
+ if (element.items?.discriminator && Array.isArray(element.items.oneOf)) {
138
173
  jp.set(
139
174
  returnSchema,
140
175
  `${path}/items/discriminator`,
@@ -184,6 +219,18 @@ const extractOverlay = (sourceSchema, ref) => {
184
219
  if (element[refEnum]) {
185
220
  jp.set(returnSchema, `${path}/enum`, element[refEnum]);
186
221
  }
222
+
223
+ const refConst = `${ref}Const`;
224
+ if (Object.prototype.hasOwnProperty.call(element, refConst)) {
225
+ jp.set(returnSchema, `${path}/const`, element[refConst]);
226
+ }
227
+
228
+ Object.entries(extendedTagMap).forEach(([suffix, schemaKey]) => {
229
+ const tagKey = `${ref}${suffix}`;
230
+ if (Object.prototype.hasOwnProperty.call(element, tagKey)) {
231
+ jp.set(returnSchema, `${path}/${schemaKey}`, element[tagKey]);
232
+ }
233
+ });
187
234
  });
188
235
  return returnSchema;
189
236
  };
@@ -198,28 +245,48 @@ const deleteProperties = (sourceSchema, propertyNames) => {
198
245
  };
199
246
 
200
247
  const overlays = {};
248
+ const overlaysDir = path.join(outputDir, "overlays");
249
+ if (!fs.existsSync(overlaysDir)) {
250
+ fs.mkdirSync(overlaysDir, { recursive: true });
251
+ }
201
252
 
202
253
  extractFields.forEach((key) => {
203
254
  let overlay = extractOverlay(combinedSchema, key);
204
255
  overlays[key] = overlay;
205
- const fileName = `../schemas/v3/overlays/${key}.json`;
256
+ const fileName = path.join(overlaysDir, `${key}.json`);
206
257
  fs.writeFileSync(fileName, JSON.stringify(overlay, null, 2));
207
258
  console.log(`Overlay ${key} written to ${fileName}`);
208
259
  });
209
260
 
210
261
  const coreSchema = deleteProperties(combinedSchema, [
211
262
  "discriminator",
212
- ...extractFields.map((item) => `${item}Ref`),
213
- ...extractFields.map((item) => `${item}Required`),
214
- ...extractFields.map((item) => `${item}Title`),
215
- ...extractFields.map((item) => `${item}Enum`),
263
+ ...metadataFields.map((item) => `${item}Ref`),
264
+ ...metadataFields.map((item) => `${item}Required`),
265
+ ...metadataFields.map((item) => `${item}Title`),
266
+ ...metadataFields.map((item) => `${item}Description`),
267
+ ...metadataFields.map((item) => `${item}Enum`),
268
+ ...metadataFields.map((item) => `${item}Type`),
269
+ ...metadataFields.map((item) => `${item}MinLength`),
270
+ ...metadataFields.map((item) => `${item}MaxLength`),
271
+ ...metadataFields.map((item) => `${item}Pattern`),
272
+ ...metadataFields.map((item) => `${item}Minimum`),
273
+ ...metadataFields.map((item) => `${item}Maximum`),
274
+ ...metadataFields.map((item) => `${item}MinItems`),
275
+ ...metadataFields.map((item) => `${item}MaxItems`),
276
+ ...metadataFields.map((item) => `${item}UniqueItems`),
277
+ ...metadataFields.map((item) => `${item}AdditionalProperties`),
278
+ ...metadataFields.map((item) => `${item}Format`),
279
+ ...metadataFields.map((item) => `${item}MinProperties`),
280
+ ...metadataFields.map((item) => `${item}MaxProperties`),
281
+ ...metadataFields.map((item) => `${item}MultipleOf`),
282
+ ...metadataFields.map((item) => `${item}Const`),
216
283
  ]);
217
284
 
218
285
  fs.writeFileSync(
219
- "../schemas/v3/pdtf-transaction.json",
286
+ path.join(outputDir, "pdtf-transaction.json"),
220
287
  JSON.stringify(coreSchema, null, 2)
221
288
  );
222
- console.log("Core schema written to ../schemas/v3/pdtf-transaction.json");
289
+ console.log(`Core schema written to ${path.join(outputDir, "pdtf-transaction.json")}`);
223
290
 
224
291
  const skeletonSchema = deleteProperties(coreSchema, [
225
292
  "$schema",
@@ -238,10 +305,10 @@ const skeletonSchema = deleteProperties(coreSchema, [
238
305
  const skeletonSchemaFlattened = flattenSkeleton(skeletonSchema);
239
306
 
240
307
  fs.writeFileSync(
241
- "../schemas/v3/skeleton.json",
308
+ path.join(outputDir, "skeleton.json"),
242
309
  JSON.stringify(skeletonSchemaFlattened, null, 2)
243
310
  );
244
- console.log("Flat Skeleton schema written to ../schemas/v3/skeleton.json");
311
+ console.log(`Flat Skeleton schema written to ${path.join(outputDir, "skeleton.json")}`);
245
312
 
246
313
  // Generate compact skeleton format for better token efficiency
247
314
  const toCompact = (obj, indent = "") => {
@@ -267,8 +334,8 @@ const toCompact = (obj, indent = "") => {
267
334
  // Check if this is a keyed object (has "*" key)
268
335
  if (keys.length === 1 && keys[0] === "*") {
269
336
  const inner = toCompact(obj["*"], indent);
270
- if (inner === "") {
271
- return "{*}"; // Keyed object of primitives
337
+ if (inner === "" || inner === "{*}") {
338
+ return "{*}"; // Keyed object of primitives (or nested keyed objects with no structure)
272
339
  }
273
340
  return `{*}\n${inner}`;
274
341
  }
@@ -308,7 +375,7 @@ const innerContent = toCompact(skeletonSchemaFlattened, "");
308
375
  const compactSkeleton = innerContent ? innerContent : "";
309
376
 
310
377
  fs.writeFileSync(
311
- "../schemas/v3/compactSkeleton.txt",
378
+ path.join(outputDir, "compactSkeleton.txt"),
312
379
  compactSkeleton
313
380
  );
314
- console.log("Compact Skeleton written to ../schemas/v3/compactSkeleton.txt");
381
+ console.log(`Compact Skeleton written to ${path.join(outputDir, "compactSkeleton.txt")}`);
@@ -6,7 +6,7 @@ const combinedSchema = require("../schemas/v3/combined.json");
6
6
  // Extract fields that have overlay-specific properties
7
7
  const extractFields = [
8
8
  "baspi4", "baspi5", "nts", "nts2", "ntsl", "ntsl2",
9
- "ta6", "ta7", "ta10", "lpe1", "fme1", "piq",
9
+ "ta6", "ta7", "ta7ed5", "ta10", "lpe1", "fme1", "piq",
10
10
  "con29R", "con29DW", "llc1", "rds", "oc1", "sr24",
11
11
  ];
12
12