@pdtf/schemas 3.4.0-9 → 3.4.1-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.
@@ -0,0 +1,253 @@
1
+ const {
2
+ getTransactionSchema,
3
+ extensionOverlays,
4
+ getValidator,
5
+ } = require("../../../index.js");
6
+
7
+ const schemaId = "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json";
8
+
9
+ describe("Extension Overlays", () => {
10
+
11
+ describe("String key support", () => {
12
+ test("should load extension overlays using string keys", () => {
13
+ const schema = getTransactionSchema(schemaId, ["nts2023", "jk"]);
14
+ const jkProp = schema.properties?.propertyPack?.properties?.specialistIssues?.properties?.japaneseKnotweed;
15
+
16
+ expect(jkProp).toBeDefined();
17
+ expect(jkProp.ntsRef).toBe("A5.3");
18
+ });
19
+
20
+ test("should work with multiple extension string keys", () => {
21
+ const schema = getTransactionSchema(schemaId, ["nts2023", "jk", "tf", "ma"]);
22
+
23
+ const jkProp = schema.properties?.propertyPack?.properties?.specialistIssues?.properties?.japaneseKnotweed;
24
+ const tfProp = schema.properties?.propertyPack?.properties?.ownership?.properties?.ownershipsToBeTransferred?.items?.oneOf?.[2]?.properties?.leaseholdInformation?.properties?.serviceCharge?.oneOf?.[1]?.properties?.transferFees;
25
+ const maProp = schema.properties?.propertyPack?.properties?.ownership?.properties?.ownershipsToBeTransferred?.items?.oneOf?.[2]?.properties?.leaseholdInformation?.properties?.contactDetails;
26
+
27
+ expect(jkProp?.ntsRef).toBe("A5.3");
28
+ expect(tfProp?.ntsRef).toBe("A3.5.1.1");
29
+ expect(maProp?.ntsRef).toBe("A1.5.4");
30
+ });
31
+
32
+ test("should be equivalent to direct object access", () => {
33
+ const schemaString = getTransactionSchema(schemaId, ["nts2023", "jk"]);
34
+ const schemaDirect = getTransactionSchema(schemaId, ["nts2023", extensionOverlays.jk]);
35
+
36
+ expect(schemaString).toEqual(schemaDirect);
37
+ });
38
+ });
39
+
40
+ describe("Individual extension functionality", () => {
41
+ test("should add Japanese Knotweed extension correctly", () => {
42
+ const schema = getTransactionSchema(schemaId, ["nts2023", "jk"]);
43
+ const jkProp = schema.properties?.propertyPack?.properties?.specialistIssues?.properties?.japaneseKnotweed;
44
+
45
+ expect(jkProp).toBeDefined();
46
+ expect(jkProp.ntsRef).toBe("A5.3");
47
+ expect(jkProp.required).toContain("yesNo");
48
+ expect(jkProp.discriminator?.propertyName).toBe("yesNo");
49
+ });
50
+
51
+ test("should add Transfer Fees extension correctly", () => {
52
+ const schema = getTransactionSchema(schemaId, ["nts2023", "tf"]);
53
+ const tfProp = schema.properties?.propertyPack?.properties?.ownership?.properties?.ownershipsToBeTransferred?.items?.oneOf?.[2]?.properties?.leaseholdInformation?.properties?.serviceCharge?.oneOf?.[1]?.properties?.transferFees;
54
+
55
+ expect(tfProp).toBeDefined();
56
+ expect(tfProp.ntsRef).toBe("A3.5.1.1");
57
+ expect(tfProp.title).toBe("Are there any additional fees payable on sale or letting?");
58
+ });
59
+
60
+ test("should add Managing Agent extension correctly", () => {
61
+ const schema = getTransactionSchema(schemaId, ["nts2023", "ma"]);
62
+ const maProp = schema.properties?.propertyPack?.properties?.ownership?.properties?.ownershipsToBeTransferred?.items?.oneOf?.[2]?.properties?.leaseholdInformation?.properties?.contactDetails;
63
+
64
+ expect(maProp).toBeDefined();
65
+ expect(maProp.ntsRef).toBe("A1.5.4");
66
+ expect(maProp.required).toContain("contacts");
67
+ });
68
+
69
+ test("should add Solar Panels extension correctly", () => {
70
+ const schema = getTransactionSchema(schemaId, ["nts2023", "sl"]);
71
+ const slProp = schema.properties?.propertyPack?.properties?.electricity?.properties?.solarPanels?.oneOf?.[1]?.properties?.panelsOwnedOutright;
72
+
73
+ expect(slProp).toBeDefined();
74
+ expect(slProp.ntsRef).toBe("B3.7.1");
75
+ });
76
+
77
+ test("should add Heating Installation extension correctly", () => {
78
+ const schema = getTransactionSchema(schemaId, ["nts2023", "hi"]);
79
+ const hiProp = schema.properties?.propertyPack?.properties?.heating?.properties?.heatingSystem?.oneOf?.[1]?.properties?.centralHeatingDetails?.properties?.centralHeatingInstalled;
80
+
81
+ expect(hiProp).toBeDefined();
82
+ expect(hiProp.ntsRef).toBe("B3.4.3.2");
83
+ });
84
+ });
85
+
86
+ describe("Extension overlay availability", () => {
87
+ test("should export all extension overlays", () => {
88
+ const expectedKeys = [
89
+ "as", "dr", "jk", "sb", "hs", // Specialist Issues
90
+ "oa", "la", "sf", "mc", // Property Features
91
+ "er", "ma", "tf", // Ownership & Financial
92
+ "sl", "hi", "fd", // Utilities & Services
93
+ "oc" // Transaction
94
+ ];
95
+
96
+ expect(Object.keys(extensionOverlays).sort()).toEqual(expectedKeys.sort());
97
+ });
98
+
99
+ test("should have valid JSON structure for all extensions", () => {
100
+ Object.entries(extensionOverlays).forEach(([key, overlay]) => {
101
+ expect(overlay).toBeDefined();
102
+ expect(overlay.$schema).toBe("http://json-schema.org/draft-07/schema#");
103
+ expect(overlay.$id).toContain(`extensions/${key}.json`);
104
+ expect(overlay.properties).toBeDefined();
105
+ });
106
+ });
107
+ });
108
+
109
+ describe("NTS2 equivalence test", () => {
110
+ test("should produce equivalent result to nts2 when all relevant extensions are combined", () => {
111
+ // Get the full NTS2 schema
112
+ const nts2Schema = getTransactionSchema(schemaId, ["nts2025"]);
113
+
114
+ // Get NTS base with all relevant extensions
115
+ const allExtensions = [
116
+ "nts2023",
117
+ // Specialist issues extensions
118
+ "as", "dr", "jk", "sb", "hs",
119
+ // Property features extensions
120
+ "oa", "la", "sf", "mc",
121
+ // Ownership & financial extensions
122
+ "er", "ma", "tf",
123
+ // Utilities & services extensions
124
+ "sl", "hi", "fd",
125
+ // Transaction extensions
126
+ "oc"
127
+ ];
128
+
129
+ const ntsWithAllExtensions = getTransactionSchema(schemaId, allExtensions);
130
+
131
+ // Compare key structural elements
132
+ const nts2PropPack = nts2Schema.properties?.propertyPack;
133
+ const extPropPack = ntsWithAllExtensions.properties?.propertyPack;
134
+
135
+ // Check that specialist issues section exists in both
136
+ expect(nts2PropPack?.properties?.specialistIssues).toBeDefined();
137
+ expect(extPropPack?.properties?.specialistIssues).toBeDefined();
138
+
139
+ // Check key specialist issues properties
140
+ const nts2SI = nts2PropPack.properties.specialistIssues.properties;
141
+ const extSI = extPropPack.properties.specialistIssues.properties;
142
+
143
+ expect(extSI?.japaneseKnotweed).toBeDefined();
144
+ expect(extSI?.dryRotEtcTreatment).toBeDefined();
145
+ expect(extSI?.containsAsbestos).toBeDefined();
146
+ expect(extSI?.subsidenceOrStructuralFault).toBeDefined();
147
+ expect(extSI?.ongoingHealthOrSafetyIssue).toBeDefined();
148
+
149
+ // Check that references match (extensions use ntsRef, NTS2 uses nts2Ref)
150
+ if (nts2SI?.japaneseKnotweed && extSI?.japaneseKnotweed) {
151
+ expect(extSI.japaneseKnotweed.ntsRef).toBe("A5.3");
152
+ // NTS2 might have nts2Ref instead - this is expected difference
153
+ }
154
+
155
+ // Check outside areas
156
+ expect(extPropPack?.properties?.residentialPropertyFeatures?.properties?.outsideAreas).toBeDefined();
157
+
158
+ // Check leasehold extensions
159
+ const leaseholdInfo = extPropPack?.properties?.ownership?.properties?.ownershipsToBeTransferred?.items?.oneOf?.[2]?.properties?.leaseholdInformation;
160
+ expect(leaseholdInfo?.properties?.contactDetails).toBeDefined(); // Managing agent
161
+ expect(leaseholdInfo?.properties?.serviceCharge?.oneOf?.[1]?.properties?.transferFees).toBeDefined(); // Transfer fees
162
+
163
+ // Check utility extensions
164
+ expect(extPropPack?.properties?.electricity?.properties?.solarPanels?.oneOf?.[1]?.properties?.panelsOwnedOutright).toBeDefined();
165
+ expect(extPropPack?.properties?.heating?.properties?.heatingSystem?.oneOf?.[1]?.properties?.centralHeatingDetails?.properties?.centralHeatingInstalled).toBeDefined();
166
+
167
+ // Check construction extensions
168
+ expect(extPropPack?.properties?.typeOfConstruction?.properties?.loft).toBeDefined();
169
+ expect(extPropPack?.properties?.typeOfConstruction?.properties?.sprayFoamInsulation).toBeDefined();
170
+
171
+ // Check completion and moving
172
+ expect(extPropPack?.properties?.completionAndMoving?.properties?.otherPropertyInChain).toBeDefined();
173
+ });
174
+
175
+ test("should validate data successfully with all extensions", () => {
176
+ const allExtensions = [
177
+ "nts2023", "as", "dr", "jk", "sb", "hs", "oa", "la", "sf", "mc",
178
+ "er", "ma", "tf", "sl", "hi", "fd", "oc"
179
+ ];
180
+
181
+ const validator = getValidator(schemaId, allExtensions);
182
+ expect(validator).toBeDefined();
183
+ expect(typeof validator).toBe("function");
184
+
185
+ // Test with minimal valid data structure
186
+ const testData = {
187
+ propertyPack: {
188
+ priceInformation: { price: 100000, priceQualifier: "Freehold" },
189
+ ownership: { ownershipsToBeTransferred: [] },
190
+ councilTax: { band: "A" },
191
+ energyEfficiency: { epcRating: "C" },
192
+ buildInformation: { building: { buildDate: 2000 } },
193
+ typeOfConstruction: { isStandardForm: { yesNo: "Yes" } },
194
+ electricity: { mainsElectricity: { yesNo: "Yes" } },
195
+ connectivity: { mobilePhoneCoverage: { signal: "Good" } },
196
+ waterAndDrainage: { water: { supplier: "Thames Water" } },
197
+ heating: { heatingSystem: { yesNo: "Yes" } },
198
+ parking: { parkingArrangements: { yesNo: "No" } },
199
+ listingAndConservation: { listedBuilding: { yesNo: "No" } },
200
+ rightsAndInformalArrangements: { rightsGranted: { yesNo: "No" } },
201
+ environmentalIssues: { flooding: { yesNo: "No" } },
202
+ notices: { noticesAndProposals: { yesNo: "No" } },
203
+ specialistIssues: {
204
+ japaneseKnotweed: { yesNo: "No" },
205
+ dryRotEtcTreatment: { yesNo: "No" },
206
+ containsAsbestos: { yesNo: "No" },
207
+ subsidenceOrStructuralFault: { yesNo: "No" },
208
+ ongoingHealthOrSafetyIssue: { yesNo: "No" }
209
+ }
210
+ }
211
+ };
212
+
213
+ // This might not validate due to missing required fields, but validator should not throw
214
+ expect(() => validator(testData)).not.toThrow();
215
+ });
216
+ });
217
+
218
+ describe("Extension merging behavior", () => {
219
+ test("should preserve required arrays when merging extensions", () => {
220
+ const schema = getTransactionSchema(schemaId, ["nts2023", "jk", "as"]);
221
+ const siSection = schema.properties?.propertyPack?.properties?.specialistIssues;
222
+
223
+ expect(siSection?.required).toBeDefined();
224
+ expect(siSection.required).toContain("japaneseKnotweed");
225
+ expect(siSection.required).toContain("containsAsbestos");
226
+ });
227
+
228
+ test("should handle discriminator patterns correctly", () => {
229
+ const schema = getTransactionSchema(schemaId, ["nts2023", "jk"]);
230
+ const jkProp = schema.properties?.propertyPack?.properties?.specialistIssues?.properties?.japaneseKnotweed;
231
+
232
+ expect(jkProp?.discriminator).toBeDefined();
233
+ expect(jkProp.discriminator.propertyName).toBe("yesNo");
234
+ expect(jkProp.oneOf).toBeDefined();
235
+ expect(Array.isArray(jkProp.oneOf)).toBe(true);
236
+ });
237
+
238
+ test("should merge oneOf schemas correctly", () => {
239
+ const schema = getTransactionSchema(schemaId, ["nts2023", "tf"]);
240
+ const tfProp = schema.properties?.propertyPack?.properties?.ownership?.properties?.ownershipsToBeTransferred?.items?.oneOf?.[2]?.properties?.leaseholdInformation?.properties?.serviceCharge?.oneOf?.[1]?.properties?.transferFees;
241
+
242
+ expect(tfProp?.oneOf).toBeDefined();
243
+ expect(tfProp.oneOf.length).toBeGreaterThan(0);
244
+
245
+ // Check the structure - details should be in the conditional oneOf branch
246
+ const conditionalBranch = tfProp.oneOf.find(branch =>
247
+ branch.properties?.details || branch.required?.includes("details")
248
+ );
249
+ expect(conditionalBranch).toBeDefined();
250
+ expect(conditionalBranch.properties?.details || conditionalBranch.required?.includes("details")).toBeTruthy();
251
+ });
252
+ });
253
+ });
@@ -0,0 +1,271 @@
1
+ const traverse = require("traverse");
2
+ const jp = require("jsonpointer");
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ const combinedSchema = require("../schemas/v3/combined.json");
7
+
8
+ // Define the extension overlay mappings
9
+ const extensionMappings = {
10
+ // Outside areas
11
+ oa: {
12
+ name: "outsideAreas",
13
+ description: "Outside areas extension for NTS",
14
+ paths: ["/properties/propertyPack/properties/residentialPropertyFeatures/properties/outsideAreas"]
15
+ },
16
+
17
+ // Estate rentcharges for freehold
18
+ er: {
19
+ name: "estateRentcharges",
20
+ description: "Estate rentcharges for freehold properties",
21
+ paths: ["/properties/propertyPack/properties/ownership/properties/ownershipsToBeTransferred/items/oneOf/0/properties/estateRentcharges"]
22
+ },
23
+
24
+ // Managing agent contact details for leasehold
25
+ ma: {
26
+ name: "managingAgent",
27
+ description: "Managing agent contact details for leasehold",
28
+ paths: ["/properties/propertyPack/properties/ownership/properties/ownershipsToBeTransferred/items/oneOf/2/properties/leaseholdInformation/properties/contactDetails"]
29
+ },
30
+
31
+ // Transfer fees for leasehold
32
+ tf: {
33
+ name: "transferFees",
34
+ description: "Transfer fees for leasehold",
35
+ paths: ["/properties/propertyPack/properties/ownership/properties/ownershipsToBeTransferred/items/oneOf/2/properties/leaseholdInformation/properties/serviceCharge/oneOf/1/properties/transferFees"]
36
+ },
37
+
38
+ // Main construction type if standard
39
+ mc: {
40
+ name: "mainConstruction",
41
+ description: "Main construction type if standard form",
42
+ paths: ["/properties/propertyPack/properties/typeOfConstruction/properties/isStandardForm/oneOf/0/properties/constructionType"]
43
+ },
44
+
45
+ // Loft access and details
46
+ la: {
47
+ name: "loftAccess",
48
+ description: "Loft access and details",
49
+ paths: ["/properties/propertyPack/properties/typeOfConstruction/properties/loft"]
50
+ },
51
+
52
+ // Spray foam insulation
53
+ sf: {
54
+ name: "sprayFoam",
55
+ description: "Spray foam insulation",
56
+ paths: ["/properties/propertyPack/properties/typeOfConstruction/properties/sprayFoamInsulation"]
57
+ },
58
+
59
+ // Specialist issues - dry rot
60
+ dr: {
61
+ name: "dryRot",
62
+ description: "Dry rot treatment specialist issue",
63
+ paths: ["/properties/propertyPack/properties/specialistIssues/properties/dryRotEtcTreatment"]
64
+ },
65
+
66
+ // Specialist issues - asbestos
67
+ as: {
68
+ name: "asbestos",
69
+ description: "Asbestos specialist issue",
70
+ paths: ["/properties/propertyPack/properties/specialistIssues/properties/containsAsbestos"]
71
+ },
72
+
73
+ // Specialist issues - Japanese knotweed
74
+ jk: {
75
+ name: "japaneseKnotweed",
76
+ description: "Japanese knotweed specialist issue",
77
+ paths: ["/properties/propertyPack/properties/specialistIssues/properties/japaneseKnotweed"]
78
+ },
79
+
80
+ // Specialist issues - subsidence
81
+ sb: {
82
+ name: "subsidence",
83
+ description: "Subsidence or structural fault specialist issue",
84
+ paths: ["/properties/propertyPack/properties/specialistIssues/properties/subsidenceOrStructuralFault"]
85
+ },
86
+
87
+ // Specialist issues - health and safety
88
+ hs: {
89
+ name: "healthSafety",
90
+ description: "Health and safety specialist issue",
91
+ paths: ["/properties/propertyPack/properties/specialistIssues/properties/ongoingHealthOrSafetyIssue"]
92
+ },
93
+
94
+ // Solar panels leased
95
+ sl: {
96
+ name: "solarPanelsLeased",
97
+ description: "Solar panels ownership details",
98
+ paths: ["/properties/propertyPack/properties/electricity/properties/solarPanels/oneOf/1/properties/panelsOwnedOutright"]
99
+ },
100
+
101
+ // Heating installation date
102
+ hi: {
103
+ name: "heatingInstalled",
104
+ description: "Central heating installation date",
105
+ paths: ["/properties/propertyPack/properties/heating/properties/heatingSystem/oneOf/1/properties/centralHeatingDetails/properties/centralHeatingInstalled"]
106
+ },
107
+
108
+ // Flood defences
109
+ fd: {
110
+ name: "floodDefences",
111
+ description: "Flood defence information",
112
+ paths: ["/properties/propertyPack/properties/environmentalIssues/properties/flooding/properties/floodDefences"]
113
+ },
114
+
115
+ // Other property in chain
116
+ oc: {
117
+ name: "otherPropertyChain",
118
+ description: "Other property in chain dependency",
119
+ paths: ["/properties/propertyPack/properties/completionAndMoving/properties/otherPropertyInChain"]
120
+ }
121
+ };
122
+
123
+ // Function to extract properties at specific paths with nts2Ref
124
+ function extractPathsWithNts2Ref(schema, paths) {
125
+ const result = {};
126
+
127
+ paths.forEach(path => {
128
+ try {
129
+ const value = jp.get(schema, path);
130
+ if (value && hasNts2Ref(value)) {
131
+ // Set the value at the same path in result
132
+ jp.set(result, path, extractNts2Properties(value));
133
+ }
134
+ } catch (err) {
135
+ console.warn(`Path not found: ${path}`);
136
+ }
137
+ });
138
+
139
+ return result;
140
+ }
141
+
142
+ // Check if an object or its descendants have nts2Ref
143
+ function hasNts2Ref(obj) {
144
+ let found = false;
145
+ traverse(obj).forEach(function(element) {
146
+ if (element && element.nts2Ref) {
147
+ found = true;
148
+ this.stop();
149
+ }
150
+ });
151
+ return found;
152
+ }
153
+
154
+ // Extract only NTS2-specific properties (with nts2Ref)
155
+ function extractNts2Properties(obj) {
156
+ const result = {};
157
+
158
+ // Copy nts2-specific metadata at current level
159
+ if (obj.nts2Ref) result.ntsRef = obj.nts2Ref;
160
+ if (obj.nts2Required) result.required = obj.nts2Required;
161
+ if (obj.nts2Title) result.title = obj.nts2Title;
162
+ if (obj.nts2Description) result.description = obj.nts2Description;
163
+ if (obj.nts2Enum) result.enum = obj.nts2Enum;
164
+
165
+ // Handle discriminator
166
+ if (obj.discriminator) {
167
+ result.discriminator = obj.discriminator;
168
+ }
169
+
170
+ // Recursively process properties
171
+ if (obj.properties) {
172
+ result.properties = {};
173
+ Object.keys(obj.properties).forEach(key => {
174
+ const prop = obj.properties[key];
175
+ if (hasNts2Ref(prop)) {
176
+ result.properties[key] = extractNts2Properties(prop);
177
+ }
178
+ });
179
+ // Only keep properties if we found some with nts2Ref
180
+ if (Object.keys(result.properties).length === 0) {
181
+ delete result.properties;
182
+ }
183
+ }
184
+
185
+ // Handle arrays
186
+ if (obj.items) {
187
+ if (hasNts2Ref(obj.items)) {
188
+ result.items = extractNts2Properties(obj.items);
189
+ }
190
+ }
191
+
192
+ // Handle oneOf
193
+ if (obj.oneOf) {
194
+ const processedOneOf = [];
195
+ obj.oneOf.forEach((schema, index) => {
196
+ if (hasNts2Ref(schema)) {
197
+ const extracted = extractNts2Properties(schema);
198
+ // For oneOf schemas, we need to preserve discriminator enum values
199
+ if (obj.discriminator && schema.properties) {
200
+ const propName = obj.discriminator.propertyName;
201
+ if (schema.properties[propName]) {
202
+ if (!extracted.properties) extracted.properties = {};
203
+ extracted.properties[propName] = {
204
+ enum: schema.properties[propName].nts2Enum || schema.properties[propName].enum
205
+ };
206
+ }
207
+ }
208
+ processedOneOf.push(extracted);
209
+ }
210
+ });
211
+ if (processedOneOf.length > 0) {
212
+ result.oneOf = processedOneOf;
213
+ }
214
+ }
215
+
216
+ // Copy other JSON Schema keywords that might be present
217
+ ['type', 'format', 'minimum', 'maximum', 'minLength', 'maxLength', 'minItems', 'maxItems'].forEach(keyword => {
218
+ if (obj[keyword] !== undefined) {
219
+ result[keyword] = obj[keyword];
220
+ }
221
+ });
222
+
223
+ return result;
224
+ }
225
+
226
+ // Add required array at parent levels if needed
227
+ function addRequiredArrays(overlay, schema) {
228
+ traverse(overlay).forEach(function(node) {
229
+ if (node && node.properties) {
230
+ const schemaPath = "/" + this.path.join("/");
231
+ try {
232
+ const schemaNode = jp.get(schema, schemaPath);
233
+ if (schemaNode && schemaNode.nts2Required) {
234
+ // Filter to only include properties that exist in this overlay
235
+ const overlayProps = Object.keys(node.properties);
236
+ const requiredProps = schemaNode.nts2Required.filter(prop => overlayProps.includes(prop));
237
+ if (requiredProps.length > 0) {
238
+ node.required = requiredProps;
239
+ }
240
+ }
241
+ } catch (err) {
242
+ // Path doesn't exist in schema, skip
243
+ }
244
+ }
245
+ });
246
+ return overlay;
247
+ }
248
+
249
+ // Generate extension overlays
250
+ Object.entries(extensionMappings).forEach(([code, config]) => {
251
+ console.log(`\nGenerating extension overlay: ${code} (${config.name})`);
252
+
253
+ // Extract the specific paths
254
+ let overlay = extractPathsWithNts2Ref(combinedSchema, config.paths);
255
+
256
+ // Add required arrays where needed
257
+ overlay = addRequiredArrays(overlay, combinedSchema);
258
+
259
+ // Add schema metadata
260
+ overlay.$schema = "http://json-schema.org/draft-07/schema#";
261
+ overlay.$id = `https://trust.propdata.org.uk/schemas/v3/overlays/extensions/${code}.json`;
262
+ overlay.$comment = config.description;
263
+
264
+ // Write the overlay file
265
+ const fileName = path.join(__dirname, `../schemas/v3/overlays/extensions/${code}.json`);
266
+ fs.writeFileSync(fileName, JSON.stringify(overlay, null, 2));
267
+ console.log(`Extension overlay ${code} written to ${fileName}`);
268
+ });
269
+
270
+
271
+ console.log("\nExtension overlay extraction complete!");