@pdtf/schemas 3.6.0-4 → 3.6.0-41

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 (64) hide show
  1. package/.claude/plans/not-seeing-associated-costs.md +99 -0
  2. package/.claude/settings.local.json +22 -3
  3. package/README.md +52 -33
  4. package/docs/sef25-extensions-ui-spec.md +355 -0
  5. package/index.js +130 -66
  6. package/package.json +4 -2
  7. package/src/examples/v3/exampleTransaction.json +3 -3
  8. package/src/schemas/v3/combined.json +7651 -1672
  9. package/src/schemas/v3/compactSkeleton.txt +365 -14
  10. package/src/schemas/v3/overlays/baspi4.json +163 -18
  11. package/src/schemas/v3/overlays/baspi5.json +168 -20
  12. package/src/schemas/v3/overlays/extensions/as.json +4 -2
  13. package/src/schemas/v3/overlays/extensions/dk.json +32 -0
  14. package/src/schemas/v3/overlays/extensions/dr.json +9 -9
  15. package/src/schemas/v3/overlays/extensions/er.json +4 -2
  16. package/src/schemas/v3/overlays/extensions/fd.json +6 -3
  17. package/src/schemas/v3/overlays/extensions/hi.json +6 -3
  18. package/src/schemas/v3/overlays/extensions/hs.json +4 -2
  19. package/src/schemas/v3/overlays/extensions/ic.json +59 -0
  20. package/src/schemas/v3/overlays/extensions/jk.json +28 -8
  21. package/src/schemas/v3/overlays/extensions/la.json +4 -2
  22. package/src/schemas/v3/overlays/extensions/lc.json +75 -0
  23. package/src/schemas/v3/overlays/extensions/ma.json +6 -3
  24. package/src/schemas/v3/overlays/extensions/mc.json +4 -2
  25. package/src/schemas/v3/overlays/extensions/mi.json +69 -0
  26. package/src/schemas/v3/overlays/extensions/nd.json +43 -0
  27. package/src/schemas/v3/overlays/extensions/oa.json +4 -2
  28. package/src/schemas/v3/overlays/extensions/oc.json +4 -2
  29. package/src/schemas/v3/overlays/extensions/pc.json +37 -0
  30. package/src/schemas/v3/overlays/extensions/ph.json +194 -0
  31. package/src/schemas/v3/overlays/extensions/rw.json +68 -0
  32. package/src/schemas/v3/overlays/extensions/sb.json +4 -2
  33. package/src/schemas/v3/overlays/extensions/sc.json +129 -0
  34. package/src/schemas/v3/overlays/extensions/sd.json +63 -0
  35. package/src/schemas/v3/overlays/extensions/sf.json +4 -2
  36. package/src/schemas/v3/overlays/extensions/sl.json +4 -2
  37. package/src/schemas/v3/overlays/extensions/tf.json +6 -3
  38. package/src/schemas/v3/overlays/extensions/tr.json +73 -0
  39. package/src/schemas/v3/overlays/extensions/wg.json +350 -0
  40. package/src/schemas/v3/overlays/fme1.json +16 -14
  41. package/src/schemas/v3/overlays/lpe1.json +60 -30
  42. package/src/schemas/v3/overlays/nts.json +68 -1
  43. package/src/schemas/v3/overlays/nts2.json +68 -4
  44. package/src/schemas/v3/overlays/ntsl.json +68 -1
  45. package/src/schemas/v3/overlays/ntsl2.json +68 -4
  46. package/src/schemas/v3/overlays/piq.json +38 -1
  47. package/src/schemas/v3/overlays/rds.json +118 -6
  48. package/src/schemas/v3/overlays/sef25.json +1067 -0
  49. package/src/schemas/v3/overlays/ta6.json +46 -16
  50. package/src/schemas/v3/overlays/ta6ed6.json +6498 -0
  51. package/src/schemas/v3/overlays/ta7.json +70 -6
  52. package/src/schemas/v3/overlays/ta7ed5.json +1539 -0
  53. package/src/schemas/v3/pdtf-transaction.json +4361 -676
  54. package/src/schemas/v3/skeleton.json +505 -36
  55. package/src/tests/v3/buyerCircumstances.test.js +543 -0
  56. package/src/tests/v3/extensionOverlays.test.js +911 -0
  57. package/src/tests/v3/offers.test.js +37 -40
  58. package/src/tests/v3/patternProperties.test.js +149 -30
  59. package/src/tests/v3/ta6ed6.test.js +2317 -0
  60. package/src/tests/v3/ta7ed5.test.js +567 -0
  61. package/src/tests/v3/transactionSchema.test.js +31 -0
  62. package/src/utils/extractExtensionOverlays.js +216 -31
  63. package/src/utils/extractOverlay.js +88 -23
  64. package/src/utils/pathSkeleton.js +1 -1
@@ -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,140 @@ 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
+ },
153
310
  };
154
311
 
155
- // Function to extract properties at specific paths with nts2Ref
156
- function extractPathsWithNts2Ref(schema, paths) {
312
+ // Function to extract properties at specific paths with given ref type
313
+ function extractPathsWithRef(schema, paths, refConfig) {
157
314
  const result = {};
158
315
 
159
316
  paths.forEach((path) => {
160
317
  try {
161
318
  const value = jp.get(schema, path);
162
- if (value && hasNts2Ref(value)) {
319
+ if (value && hasRef(value, refConfig)) {
163
320
  // Set the value at the same path in result
164
- jp.set(result, path, extractNts2Properties(value));
321
+ jp.set(result, path, extractRefProperties(value, refConfig));
165
322
  }
166
323
  } catch (err) {
167
324
  console.warn(`Path not found: ${path}`);
@@ -171,11 +328,11 @@ function extractPathsWithNts2Ref(schema, paths) {
171
328
  return result;
172
329
  }
173
330
 
174
- // Check if an object or its descendants have nts2Ref
175
- function hasNts2Ref(obj) {
331
+ // Check if an object or its descendants have the specified ref
332
+ function hasRef(obj, refConfig) {
176
333
  let found = false;
177
334
  traverse(obj).forEach(function (element) {
178
- if (element && element.nts2Ref) {
335
+ if (element && element[refConfig.ref]) {
179
336
  found = true;
180
337
  this.stop();
181
338
  }
@@ -183,16 +340,16 @@ function hasNts2Ref(obj) {
183
340
  return found;
184
341
  }
185
342
 
186
- // Extract only NTS2-specific properties (with nts2Ref)
187
- function extractNts2Properties(obj, parentKey) {
343
+ // Extract only ref-specific properties
344
+ function extractRefProperties(obj, refConfig, parentKey) {
188
345
  const result = {};
189
346
 
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;
347
+ // Copy ref-specific metadata at current level
348
+ if (obj[refConfig.ref]) result[refConfig.outputRef] = obj[refConfig.ref];
349
+ if (obj[refConfig.required]) result.required = obj[refConfig.required];
350
+ if (obj[refConfig.title]) result.title = obj[refConfig.title];
351
+ if (obj[refConfig.description]) result.description = obj[refConfig.description];
352
+ if (obj[refConfig.enumKey]) result.enum = obj[refConfig.enumKey];
196
353
 
197
354
  // Handle discriminator
198
355
  if (obj.discriminator) {
@@ -204,11 +361,11 @@ function extractNts2Properties(obj, parentKey) {
204
361
  result.properties = {};
205
362
  Object.keys(obj.properties).forEach((key) => {
206
363
  const prop = obj.properties[key];
207
- if (hasNts2Ref(prop)) {
208
- result.properties[key] = extractNts2Properties(prop, key);
364
+ if (hasRef(prop, refConfig)) {
365
+ result.properties[key] = extractRefProperties(prop, refConfig, key);
209
366
  }
210
367
  });
211
- // Only keep properties if we found some with nts2Ref
368
+ // Only keep properties if we found some with ref
212
369
  if (Object.keys(result.properties).length === 0) {
213
370
  delete result.properties;
214
371
  }
@@ -216,8 +373,8 @@ function extractNts2Properties(obj, parentKey) {
216
373
 
217
374
  // Handle arrays
218
375
  if (obj.items) {
219
- if (hasNts2Ref(obj.items)) {
220
- result.items = extractNts2Properties(obj.items);
376
+ if (hasRef(obj.items, refConfig)) {
377
+ result.items = extractRefProperties(obj.items, refConfig);
221
378
  }
222
379
  }
223
380
 
@@ -225,8 +382,8 @@ function extractNts2Properties(obj, parentKey) {
225
382
  if (obj.oneOf) {
226
383
  // Always preserve the full array structure and order
227
384
  const processedOneOf = obj.oneOf.map((schema, index) => {
228
- if (hasNts2Ref(schema)) {
229
- const extracted = extractNts2Properties(schema);
385
+ if (hasRef(schema, refConfig)) {
386
+ const extracted = extractRefProperties(schema, refConfig);
230
387
  // For oneOf schemas, we need to preserve discriminator enum values
231
388
  if (obj.discriminator && schema.properties) {
232
389
  const propName = obj.discriminator.propertyName;
@@ -234,7 +391,7 @@ function extractNts2Properties(obj, parentKey) {
234
391
  if (!extracted.properties) extracted.properties = {};
235
392
  extracted.properties[propName] = {
236
393
  enum:
237
- schema.properties[propName].nts2Enum ||
394
+ schema.properties[propName][refConfig.enumKey] ||
238
395
  schema.properties[propName].enum,
239
396
  };
240
397
  }
@@ -258,7 +415,7 @@ function extractNts2Properties(obj, parentKey) {
258
415
  }
259
416
  return extracted;
260
417
  } else {
261
- // If this branch doesn't have nts2Ref, preserve the base discriminator enum
418
+ // If this branch doesn't have ref, preserve the base discriminator enum
262
419
  if (obj.discriminator && schema.properties) {
263
420
  const propName = obj.discriminator.propertyName;
264
421
  if (schema.properties[propName]) {
@@ -281,7 +438,7 @@ function extractNts2Properties(obj, parentKey) {
281
438
  // If parentKey is set, and this object has extension metadata, return a stub for the parent
282
439
  if (
283
440
  parentKey &&
284
- (result.ntsRef || result.required || result.discriminator)
441
+ (result[refConfig.outputRef] || result.required || result.discriminator)
285
442
  ) {
286
443
  // This is an extension property inside a oneOf branch
287
444
  // Return a stub for the parent property with metadata and oneOf structure
@@ -311,16 +468,16 @@ function extractNts2Properties(obj, parentKey) {
311
468
  }
312
469
 
313
470
  // Add required array at parent levels if needed
314
- function addRequiredArrays(overlay, schema) {
471
+ function addRequiredArrays(overlay, schema, refConfig) {
315
472
  traverse(overlay).forEach(function (node) {
316
473
  if (node && node.properties) {
317
474
  const schemaPath = "/" + this.path.join("/");
318
475
  try {
319
476
  const schemaNode = jp.get(schema, schemaPath);
320
- if (schemaNode && schemaNode.nts2Required) {
477
+ if (schemaNode && schemaNode[refConfig.required]) {
321
478
  // Filter to only include properties that exist in this overlay
322
479
  const overlayProps = Object.keys(node.properties);
323
- const requiredProps = schemaNode.nts2Required.filter((prop) =>
480
+ const requiredProps = schemaNode[refConfig.required].filter((prop) =>
324
481
  overlayProps.includes(prop)
325
482
  );
326
483
  if (requiredProps.length > 0) {
@@ -335,15 +492,43 @@ function addRequiredArrays(overlay, schema) {
335
492
  return overlay;
336
493
  }
337
494
 
495
+ // Annotate intermediate nodes with refs from the source schema
496
+ // This ensures every level of the overlay tree carries an ntsRef
497
+ function addIntermediateRefs(overlay, schema, refConfig) {
498
+ traverse(overlay).forEach(function (node) {
499
+ if (node && typeof node === "object" && !Array.isArray(node) && node.properties) {
500
+ // Skip if this node already has a ref
501
+ if (node[refConfig.outputRef]) return;
502
+
503
+ const schemaPath = "/" + this.path.join("/");
504
+ try {
505
+ const schemaNode = jp.get(schema, schemaPath);
506
+ if (schemaNode && schemaNode[refConfig.ref]) {
507
+ node[refConfig.outputRef] = schemaNode[refConfig.ref];
508
+ }
509
+ } catch (err) {
510
+ // Path doesn't exist in schema, skip
511
+ }
512
+ }
513
+ });
514
+ return overlay;
515
+ }
516
+
338
517
  // Generate extension overlays
339
518
  Object.entries(extensionMappings).forEach(([code, config]) => {
340
- console.log(`\nGenerating extension overlay: ${code} (${config.name})`);
519
+ const refType = config.refType || "nts2Ref";
520
+ const refConfig = getRefConfig(refType);
521
+
522
+ console.log(`\nGenerating extension overlay: ${code} (${config.name}) [${refType}]`);
341
523
 
342
524
  // Extract the specific paths
343
- let overlay = extractPathsWithNts2Ref(combinedSchema, config.paths);
525
+ let overlay = extractPathsWithRef(combinedSchema, config.paths, refConfig);
344
526
 
345
527
  // Add required arrays where needed
346
- overlay = addRequiredArrays(overlay, combinedSchema);
528
+ overlay = addRequiredArrays(overlay, combinedSchema, refConfig);
529
+
530
+ // Add refs to intermediate nodes in the path hierarchy
531
+ overlay = addIntermediateRefs(overlay, combinedSchema, refConfig);
347
532
 
348
533
  // Add schema metadata
349
534
  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,6 +42,7 @@ const extractFields = [
25
42
  "rds",
26
43
  "oc1",
27
44
  "sr24",
45
+ "sef25",
28
46
  ];
29
47
 
30
48
  const flattenSkeleton = (schema) => {
@@ -68,23 +86,21 @@ const flattenSkeleton = (schema) => {
68
86
  });
69
87
  }
70
88
 
71
- // Handle oneOf - merge all possible properties
89
+ // Handle oneOf - merge all possible properties (recursively handles nested oneOf)
72
90
  if (schema.oneOf) {
73
91
  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") {
92
+ const variantResult = flattenSkeleton(aOneOf);
93
+ if (variantResult && typeof variantResult === "object" && !Array.isArray(variantResult)) {
94
+ Object.entries(variantResult).forEach(([key, value]) => {
95
+ if (key === "_variants") return;
96
+ if (returnStructure[key] && JSON.stringify(returnStructure[key]) !== JSON.stringify(value)) {
97
+ if (typeof returnStructure[key] === "string" || typeof value === "string") {
81
98
  returnStructure[key] = "variant";
82
99
  } else {
83
- // For complex types, merge properties
84
- returnStructure[key] = { ...returnStructure[key], ...newValue, _variants: true };
100
+ returnStructure[key] = { ...returnStructure[key], ...value, _variants: true };
85
101
  }
86
102
  } else {
87
- returnStructure[key] = newValue;
103
+ returnStructure[key] = value;
88
104
  }
89
105
  });
90
106
  }
@@ -97,6 +113,23 @@ const flattenSkeleton = (schema) => {
97
113
 
98
114
  const extractOverlay = (sourceSchema, ref) => {
99
115
  const refName = `${ref}Ref`;
116
+ const extendedTagMap = {
117
+ Type: "type",
118
+ MinLength: "minLength",
119
+ MaxLength: "maxLength",
120
+ Pattern: "pattern",
121
+ Minimum: "minimum",
122
+ Maximum: "maximum",
123
+ MinItems: "minItems",
124
+ MaxItems: "maxItems",
125
+ UniqueItems: "uniqueItems",
126
+ AdditionalProperties: "additionalProperties",
127
+ Format: "format",
128
+ MinProperties: "minProperties",
129
+ MaxProperties: "maxProperties",
130
+ MultipleOf: "multipleOf",
131
+ Const: "const",
132
+ };
100
133
  const returnSchema = {
101
134
  $schema: "http://json-schema.org/draft-07/schema#",
102
135
  $id: `https://trust.propdata.org.uk/schemas/v3/overlays/${ref}.json`,
@@ -107,7 +140,7 @@ const extractOverlay = (sourceSchema, ref) => {
107
140
  if (element[refName]) {
108
141
  jp.set(returnSchema, `${path}/${refName}`, element[refName]);
109
142
 
110
- if (element.discriminator) {
143
+ if (element.discriminator && Array.isArray(element.oneOf)) {
111
144
  jp.set(returnSchema, `${path}/discriminator`, element.discriminator);
112
145
  const { propertyName } = element.discriminator;
113
146
  element.oneOf.forEach((oneOf, index) => {
@@ -134,7 +167,7 @@ const extractOverlay = (sourceSchema, ref) => {
134
167
  }
135
168
 
136
169
  // also handle discriminator properties nested in items
137
- if (element.items?.discriminator) {
170
+ if (element.items?.discriminator && Array.isArray(element.items.oneOf)) {
138
171
  jp.set(
139
172
  returnSchema,
140
173
  `${path}/items/discriminator`,
@@ -184,6 +217,18 @@ const extractOverlay = (sourceSchema, ref) => {
184
217
  if (element[refEnum]) {
185
218
  jp.set(returnSchema, `${path}/enum`, element[refEnum]);
186
219
  }
220
+
221
+ const refConst = `${ref}Const`;
222
+ if (Object.prototype.hasOwnProperty.call(element, refConst)) {
223
+ jp.set(returnSchema, `${path}/const`, element[refConst]);
224
+ }
225
+
226
+ Object.entries(extendedTagMap).forEach(([suffix, schemaKey]) => {
227
+ const tagKey = `${ref}${suffix}`;
228
+ if (Object.prototype.hasOwnProperty.call(element, tagKey)) {
229
+ jp.set(returnSchema, `${path}/${schemaKey}`, element[tagKey]);
230
+ }
231
+ });
187
232
  });
188
233
  return returnSchema;
189
234
  };
@@ -198,11 +243,15 @@ const deleteProperties = (sourceSchema, propertyNames) => {
198
243
  };
199
244
 
200
245
  const overlays = {};
246
+ const overlaysDir = path.join(outputDir, "overlays");
247
+ if (!fs.existsSync(overlaysDir)) {
248
+ fs.mkdirSync(overlaysDir, { recursive: true });
249
+ }
201
250
 
202
251
  extractFields.forEach((key) => {
203
252
  let overlay = extractOverlay(combinedSchema, key);
204
253
  overlays[key] = overlay;
205
- const fileName = `../schemas/v3/overlays/${key}.json`;
254
+ const fileName = path.join(overlaysDir, `${key}.json`);
206
255
  fs.writeFileSync(fileName, JSON.stringify(overlay, null, 2));
207
256
  console.log(`Overlay ${key} written to ${fileName}`);
208
257
  });
@@ -212,14 +261,30 @@ const coreSchema = deleteProperties(combinedSchema, [
212
261
  ...extractFields.map((item) => `${item}Ref`),
213
262
  ...extractFields.map((item) => `${item}Required`),
214
263
  ...extractFields.map((item) => `${item}Title`),
264
+ ...extractFields.map((item) => `${item}Description`),
215
265
  ...extractFields.map((item) => `${item}Enum`),
266
+ ...extractFields.map((item) => `${item}Type`),
267
+ ...extractFields.map((item) => `${item}MinLength`),
268
+ ...extractFields.map((item) => `${item}MaxLength`),
269
+ ...extractFields.map((item) => `${item}Pattern`),
270
+ ...extractFields.map((item) => `${item}Minimum`),
271
+ ...extractFields.map((item) => `${item}Maximum`),
272
+ ...extractFields.map((item) => `${item}MinItems`),
273
+ ...extractFields.map((item) => `${item}MaxItems`),
274
+ ...extractFields.map((item) => `${item}UniqueItems`),
275
+ ...extractFields.map((item) => `${item}AdditionalProperties`),
276
+ ...extractFields.map((item) => `${item}Format`),
277
+ ...extractFields.map((item) => `${item}MinProperties`),
278
+ ...extractFields.map((item) => `${item}MaxProperties`),
279
+ ...extractFields.map((item) => `${item}MultipleOf`),
280
+ ...extractFields.map((item) => `${item}Const`),
216
281
  ]);
217
282
 
218
283
  fs.writeFileSync(
219
- "../schemas/v3/pdtf-transaction.json",
284
+ path.join(outputDir, "pdtf-transaction.json"),
220
285
  JSON.stringify(coreSchema, null, 2)
221
286
  );
222
- console.log("Core schema written to ../schemas/v3/pdtf-transaction.json");
287
+ console.log(`Core schema written to ${path.join(outputDir, "pdtf-transaction.json")}`);
223
288
 
224
289
  const skeletonSchema = deleteProperties(coreSchema, [
225
290
  "$schema",
@@ -238,10 +303,10 @@ const skeletonSchema = deleteProperties(coreSchema, [
238
303
  const skeletonSchemaFlattened = flattenSkeleton(skeletonSchema);
239
304
 
240
305
  fs.writeFileSync(
241
- "../schemas/v3/skeleton.json",
306
+ path.join(outputDir, "skeleton.json"),
242
307
  JSON.stringify(skeletonSchemaFlattened, null, 2)
243
308
  );
244
- console.log("Flat Skeleton schema written to ../schemas/v3/skeleton.json");
309
+ console.log(`Flat Skeleton schema written to ${path.join(outputDir, "skeleton.json")}`);
245
310
 
246
311
  // Generate compact skeleton format for better token efficiency
247
312
  const toCompact = (obj, indent = "") => {
@@ -267,8 +332,8 @@ const toCompact = (obj, indent = "") => {
267
332
  // Check if this is a keyed object (has "*" key)
268
333
  if (keys.length === 1 && keys[0] === "*") {
269
334
  const inner = toCompact(obj["*"], indent);
270
- if (inner === "") {
271
- return "{*}"; // Keyed object of primitives
335
+ if (inner === "" || inner === "{*}") {
336
+ return "{*}"; // Keyed object of primitives (or nested keyed objects with no structure)
272
337
  }
273
338
  return `{*}\n${inner}`;
274
339
  }
@@ -308,7 +373,7 @@ const innerContent = toCompact(skeletonSchemaFlattened, "");
308
373
  const compactSkeleton = innerContent ? innerContent : "";
309
374
 
310
375
  fs.writeFileSync(
311
- "../schemas/v3/compactSkeleton.txt",
376
+ path.join(outputDir, "compactSkeleton.txt"),
312
377
  compactSkeleton
313
378
  );
314
- console.log("Compact Skeleton written to ../schemas/v3/compactSkeleton.txt");
379
+ 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