@pdtf/schemas 3.6.0-dev.6 → 3.6.0-dev.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.
Files changed (131) hide show
  1. package/busy-spence-79c6bf/CLAUDE.md +101 -0
  2. package/busy-spence-79c6bf/LICENSE +21 -0
  3. package/busy-spence-79c6bf/README.md +435 -0
  4. package/busy-spence-79c6bf/docs/branch-release-workflow.md +138 -0
  5. package/busy-spence-79c6bf/docs/sef25-extensions-ui-spec.md +355 -0
  6. package/busy-spence-79c6bf/index.js +654 -0
  7. package/busy-spence-79c6bf/package-lock.json +7299 -0
  8. package/busy-spence-79c6bf/package.json +44 -0
  9. package/busy-spence-79c6bf/scripts/ta6ed6-titles.js +403 -0
  10. package/busy-spence-79c6bf/scripts/ta7ed5-titles.js +183 -0
  11. package/busy-spence-79c6bf/src/examples/v1/exampleAddParticipantVouch.json +32 -0
  12. package/busy-spence-79c6bf/src/examples/v1/exampleDocumentedVouch.json +40 -0
  13. package/busy-spence-79c6bf/src/examples/v1/exampleElectronicRecord.json +30 -0
  14. package/busy-spence-79c6bf/src/examples/v1/exampleTransaction.json +878 -0
  15. package/busy-spence-79c6bf/src/examples/v1/exampleVouch.json +25 -0
  16. package/busy-spence-79c6bf/src/examples/v2/exampleTransaction.json +901 -0
  17. package/busy-spence-79c6bf/src/examples/v3/exampleAddParticipantVouch.json +38 -0
  18. package/busy-spence-79c6bf/src/examples/v3/exampleCustomOverlay.json +55 -0
  19. package/busy-spence-79c6bf/src/examples/v3/exampleCustomOverlay2.json +93 -0
  20. package/busy-spence-79c6bf/src/examples/v3/exampleDocumentedVouch.json +47 -0
  21. package/busy-spence-79c6bf/src/examples/v3/exampleElectronicRecord.json +36 -0
  22. package/busy-spence-79c6bf/src/examples/v3/exampleTransaction.json +1038 -0
  23. package/busy-spence-79c6bf/src/examples/v3/exampleVouch.json +32 -0
  24. package/busy-spence-79c6bf/src/mappings/SendPropertyDetailsRequestSchema.json +1588 -0
  25. package/busy-spence-79c6bf/src/mappings/adf.js +198 -0
  26. package/busy-spence-79c6bf/src/schemas/v1/GeoJSON.json +1279 -0
  27. package/busy-spence-79c6bf/src/schemas/v1/baspiNotes.html +102 -0
  28. package/busy-spence-79c6bf/src/schemas/v1/energy-performance-certificate.json +124 -0
  29. package/busy-spence-79c6bf/src/schemas/v1/freehold-information.json +2051 -0
  30. package/busy-spence-79c6bf/src/schemas/v1/leasehold-information.json +2064 -0
  31. package/busy-spence-79c6bf/src/schemas/v1/legal-information.json +1045 -0
  32. package/busy-spence-79c6bf/src/schemas/v1/material-facts.json +2109 -0
  33. package/busy-spence-79c6bf/src/schemas/v1/pdtf-transaction-search-results.json +40 -0
  34. package/busy-spence-79c6bf/src/schemas/v1/pdtf-transaction.json +191 -0
  35. package/busy-spence-79c6bf/src/schemas/v1/searches/drainage-and-water.json +261 -0
  36. package/busy-spence-79c6bf/src/schemas/v1/searches/local-land-charges.json +69 -0
  37. package/busy-spence-79c6bf/src/schemas/v1/searches/local-searches-required.json +857 -0
  38. package/busy-spence-79c6bf/src/schemas/v1/searches.json +237 -0
  39. package/busy-spence-79c6bf/src/schemas/v1/title-deed.json +841 -0
  40. package/busy-spence-79c6bf/src/schemas/v2/baspiNotes.html +102 -0
  41. package/busy-spence-79c6bf/src/schemas/v2/combined.json +37731 -0
  42. package/busy-spence-79c6bf/src/schemas/v2/overlays/baspi.json +8132 -0
  43. package/busy-spence-79c6bf/src/schemas/v2/overlays/con29DW.json +705 -0
  44. package/busy-spence-79c6bf/src/schemas/v2/overlays/con29R.json +2956 -0
  45. package/busy-spence-79c6bf/src/schemas/v2/overlays/fme1.json +1259 -0
  46. package/busy-spence-79c6bf/src/schemas/v2/overlays/llc1.json +25 -0
  47. package/busy-spence-79c6bf/src/schemas/v2/overlays/lpe1.json +2171 -0
  48. package/busy-spence-79c6bf/src/schemas/v2/overlays/nts.json +326 -0
  49. package/busy-spence-79c6bf/src/schemas/v2/overlays/oc1.json +2799 -0
  50. package/busy-spence-79c6bf/src/schemas/v2/overlays/piq.json +3419 -0
  51. package/busy-spence-79c6bf/src/schemas/v2/overlays/rds.json +3736 -0
  52. package/busy-spence-79c6bf/src/schemas/v2/overlays/ta10.json +5306 -0
  53. package/busy-spence-79c6bf/src/schemas/v2/overlays/ta6.json +4533 -0
  54. package/busy-spence-79c6bf/src/schemas/v2/overlays/ta7.json +1045 -0
  55. package/busy-spence-79c6bf/src/schemas/v2/pdtf-transaction.json +34351 -0
  56. package/busy-spence-79c6bf/src/schemas/v2/skeleton.json +3973 -0
  57. package/busy-spence-79c6bf/src/schemas/v3/combined.json +48643 -0
  58. package/busy-spence-79c6bf/src/schemas/v3/compactSkeleton.txt +4519 -0
  59. package/busy-spence-79c6bf/src/schemas/v3/overlays/README.md +299 -0
  60. package/busy-spence-79c6bf/src/schemas/v3/overlays/baspi4.json +8030 -0
  61. package/busy-spence-79c6bf/src/schemas/v3/overlays/baspi5.json +8180 -0
  62. package/busy-spence-79c6bf/src/schemas/v3/overlays/con29DW.json +716 -0
  63. package/busy-spence-79c6bf/src/schemas/v3/overlays/con29R.json +2981 -0
  64. package/busy-spence-79c6bf/src/schemas/v3/overlays/extensions/ac.json +334 -0
  65. package/busy-spence-79c6bf/src/schemas/v3/overlays/extensions/as.json +74 -0
  66. package/busy-spence-79c6bf/src/schemas/v3/overlays/extensions/dk.json +33 -0
  67. package/busy-spence-79c6bf/src/schemas/v3/overlays/extensions/dr.json +73 -0
  68. package/busy-spence-79c6bf/src/schemas/v3/overlays/extensions/er.json +85 -0
  69. package/busy-spence-79c6bf/src/schemas/v3/overlays/extensions/fd.json +75 -0
  70. package/busy-spence-79c6bf/src/schemas/v3/overlays/extensions/hi.json +51 -0
  71. package/busy-spence-79c6bf/src/schemas/v3/overlays/extensions/hs.json +73 -0
  72. package/busy-spence-79c6bf/src/schemas/v3/overlays/extensions/ic.json +94 -0
  73. package/busy-spence-79c6bf/src/schemas/v3/overlays/extensions/jk.json +96 -0
  74. package/busy-spence-79c6bf/src/schemas/v3/overlays/extensions/la.json +85 -0
  75. package/busy-spence-79c6bf/src/schemas/v3/overlays/extensions/lc.json +76 -0
  76. package/busy-spence-79c6bf/src/schemas/v3/overlays/extensions/ma.json +90 -0
  77. package/busy-spence-79c6bf/src/schemas/v3/overlays/extensions/mc.json +38 -0
  78. package/busy-spence-79c6bf/src/schemas/v3/overlays/extensions/mi.json +70 -0
  79. package/busy-spence-79c6bf/src/schemas/v3/overlays/extensions/nd.json +44 -0
  80. package/busy-spence-79c6bf/src/schemas/v3/overlays/extensions/oa.json +24 -0
  81. package/busy-spence-79c6bf/src/schemas/v3/overlays/extensions/oc.json +57 -0
  82. package/busy-spence-79c6bf/src/schemas/v3/overlays/extensions/pc.json +38 -0
  83. package/busy-spence-79c6bf/src/schemas/v3/overlays/extensions/ph.json +195 -0
  84. package/busy-spence-79c6bf/src/schemas/v3/overlays/extensions/rw.json +69 -0
  85. package/busy-spence-79c6bf/src/schemas/v3/overlays/extensions/sb.json +73 -0
  86. package/busy-spence-79c6bf/src/schemas/v3/overlays/extensions/sc.json +133 -0
  87. package/busy-spence-79c6bf/src/schemas/v3/overlays/extensions/sd.json +64 -0
  88. package/busy-spence-79c6bf/src/schemas/v3/overlays/extensions/sf.json +74 -0
  89. package/busy-spence-79c6bf/src/schemas/v3/overlays/extensions/sl.json +68 -0
  90. package/busy-spence-79c6bf/src/schemas/v3/overlays/extensions/ta.json +204 -0
  91. package/busy-spence-79c6bf/src/schemas/v3/overlays/extensions/tf.json +103 -0
  92. package/busy-spence-79c6bf/src/schemas/v3/overlays/extensions/tr.json +74 -0
  93. package/busy-spence-79c6bf/src/schemas/v3/overlays/extensions/wg.json +351 -0
  94. package/busy-spence-79c6bf/src/schemas/v3/overlays/fme1.json +1266 -0
  95. package/busy-spence-79c6bf/src/schemas/v3/overlays/llc1.json +25 -0
  96. package/busy-spence-79c6bf/src/schemas/v3/overlays/lpe1.json +2199 -0
  97. package/busy-spence-79c6bf/src/schemas/v3/overlays/nts.json +2159 -0
  98. package/busy-spence-79c6bf/src/schemas/v3/overlays/nts2.json +2744 -0
  99. package/busy-spence-79c6bf/src/schemas/v3/overlays/ntsl.json +1815 -0
  100. package/busy-spence-79c6bf/src/schemas/v3/overlays/ntsl2.json +2219 -0
  101. package/busy-spence-79c6bf/src/schemas/v3/overlays/oc1.json +2817 -0
  102. package/busy-spence-79c6bf/src/schemas/v3/overlays/piq.json +3324 -0
  103. package/busy-spence-79c6bf/src/schemas/v3/overlays/rds.json +3668 -0
  104. package/busy-spence-79c6bf/src/schemas/v3/overlays/sef25.json +1653 -0
  105. package/busy-spence-79c6bf/src/schemas/v3/overlays/sr24.json +38 -0
  106. package/busy-spence-79c6bf/src/schemas/v3/overlays/ta10.json +4645 -0
  107. package/busy-spence-79c6bf/src/schemas/v3/overlays/ta6.json +4572 -0
  108. package/busy-spence-79c6bf/src/schemas/v3/overlays/ta6ed6.json +6665 -0
  109. package/busy-spence-79c6bf/src/schemas/v3/overlays/ta7.json +1537 -0
  110. package/busy-spence-79c6bf/src/schemas/v3/overlays/ta7ed5.json +1585 -0
  111. package/busy-spence-79c6bf/src/schemas/v3/pdtf-transaction.json +41570 -0
  112. package/busy-spence-79c6bf/src/schemas/v3/skeleton.json +5767 -0
  113. package/busy-spence-79c6bf/src/schemas/verifiedClaims/README.md +21 -0
  114. package/busy-spence-79c6bf/src/schemas/verifiedClaims/pdtf-verified-claims.json +447 -0
  115. package/busy-spence-79c6bf/src/utils/compactSkeleton.js +84 -0
  116. package/busy-spence-79c6bf/src/utils/countTokens.js +67 -0
  117. package/busy-spence-79c6bf/src/utils/extractExtensionOverlays.js +633 -0
  118. package/busy-spence-79c6bf/src/utils/extractOverlay.js +381 -0
  119. package/busy-spence-79c6bf/src/utils/extractProperties.js +44 -0
  120. package/busy-spence-79c6bf/src/utils/listPathTitles.js +36 -0
  121. package/busy-spence-79c6bf/src/utils/minimalSkeleton.js +85 -0
  122. package/busy-spence-79c6bf/src/utils/pathList.csv +903 -0
  123. package/busy-spence-79c6bf/src/utils/pathSkeleton.js +181 -0
  124. package/busy-spence-79c6bf/ta6ed6-titles-plan.md +118 -0
  125. package/busy-spence-79c6bf/ta7ed5-titles-spec.md +89 -0
  126. package/package.json +1 -1
  127. package/src/schemas/v3/combined.json +228 -50
  128. package/src/schemas/v3/overlays/llc1.json +1 -2
  129. package/src/schemas/v3/overlays/ta6ed6.json +204 -75
  130. package/src/schemas/v3/overlays/ta7ed5.json +54 -20
  131. package/src/schemas/v3/pdtf-transaction.json +7 -7
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@pdtf/schemas",
3
+ "version": "3.6.0-dev.8",
4
+ "description": "Property Data Trust Framework Schemas and Utilities",
5
+ "main": "index.js",
6
+ "directories": {
7
+ "doc": "docs"
8
+ },
9
+ "scripts": {
10
+ "test": "jest",
11
+ "test:watch": "jest --watch",
12
+ "extract-overlays": "cd src/utils && node extractOverlay.js",
13
+ "extract-extension-overlays": "cd src/utils && node extractExtensionOverlays.js",
14
+ "publish:dev": "npm publish --tag dev",
15
+ "publish:next": "npm publish --tag next",
16
+ "publish:latest": "npm publish --tag latest"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/Property-Data-Trust-Framework/schemas.git"
21
+ },
22
+ "keywords": [
23
+ "PDTF",
24
+ "Schemas"
25
+ ],
26
+ "author": "",
27
+ "license": "MIT",
28
+ "bugs": {
29
+ "url": "https://github.com/Property-Data-Trust-Framework/schemas/issues"
30
+ },
31
+ "homepage": "https://github.com/Property-Data-Trust-Framework/schemas#readme",
32
+ "dependencies": {
33
+ "@jdw/jst": "^2.0.0-beta.15",
34
+ "ajv": "^8.12.0",
35
+ "ajv-formats": "^2.1.1",
36
+ "deepmerge": "^4.3.1",
37
+ "gpt-3-encoder": "^1.1.4",
38
+ "jsonpointer": "^5.0.0",
39
+ "traverse": "^0.6.7"
40
+ },
41
+ "devDependencies": {
42
+ "jest": "^27.5.1"
43
+ }
44
+ }
@@ -0,0 +1,403 @@
1
+ // One-off tooling: apply exact TA6 6th-edition field titles (ta6ed6Title) to
2
+ // src/schemas/v3/combined.json.
3
+ //
4
+ // Source of truth: the Law Society "TA6 Property Information Form (6th edition)
5
+ // (2025)" — text extracted with pdftotext (see tmp/TA6 Form 6th edition.pdf).
6
+ //
7
+ // LABELS is keyed by "<ta6ed6Ref>::<last path segment>" and holds the VERBATIM
8
+ // form label. normalize() applies the house style (see mapping-report §C.5):
9
+ // - strip leading sub-question markers "(a) ", "(i) " ...
10
+ // - strip leading conditional prefixes "If yes, " / "If no, "
11
+ // - collapse whitespace, capitalise first letter.
12
+ // A ta6ed6Title is written only where the normalised form label DIFFERS from the
13
+ // property's existing base `title` (per the agreed scope). Collisions on a key
14
+ // (same field repeated across oneOf branches) intentionally share one label.
15
+
16
+ const LABELS = {
17
+ // ---- Section 1: Property and seller details ----
18
+ "1.4::sellersCapacity": "What is your role in the sale?",
19
+ "1.5::dateBecameOwnerOrAuthority":
20
+ "When did you become the owner of, or get authority to sell, the property? (DD/MM/YYYY)",
21
+ "1.1::address": "Address of the property",
22
+ "1.2::uprn": "UPRN (if known)",
23
+
24
+ // ---- Section 2: Boundaries ----
25
+ "2::legalBoundaries": "Boundaries",
26
+ "2.1::uniformBoundaries":
27
+ "Looking towards the property from the road, who owns or accepts responsibility to maintain or repair the boundary features:",
28
+ "2.1a::left": "On the left?",
29
+ "2.1b::right": "On the right?",
30
+ "2.1c::rear": "At the rear?",
31
+ "2.1d::front": "At the front?",
32
+ "2.2.1::details":
33
+ "If the boundaries of the property are irregular, indicate ownership by written description or by reference to a plan:",
34
+ "2.3.2::details": "If yes, please give details:",
35
+ "2.4.2::details": "If yes, please give details:",
36
+ "2.5::partyWallAct":
37
+ "Are you aware of any notice being served or received under the Party Wall etc. Act 1996 in respect of any shared/party walls or boundaries?",
38
+ "2.5.2::details": "If yes, supply a copy and give details of any works carried out or agreed:",
39
+
40
+ // ---- Section 3: Disputes ----
41
+ "3::disputesAndComplaints": "Disputes",
42
+ "3.1.2::details": "If yes, give details such as when this took place and who was involved:",
43
+ "3.2.2::details": "If yes, please give details:",
44
+
45
+ // ---- Section 4: Notices ----
46
+ "4.1::otherNotices":
47
+ "Are you aware of any notices, communications, negotiations or discussions which affect the property or a property nearby? For example, from or to a neighbour, council or government department.",
48
+ "4.1.2::details": "If yes, please give details:",
49
+ "4.2::neighbourDevelopment":
50
+ "Are you aware of any plans or proposals to develop property or land nearby?",
51
+ "4.2.2::details": "If yes, please give details:",
52
+ "4.3::planningApplication":
53
+ "Are you aware of any proposals to make alterations to or change the use of buildings nearby?",
54
+ "4.3.2::details": "If yes, please give details:",
55
+
56
+ // ---- Section 5: Alterations ----
57
+ "5::alterationsAndChanges": "Alterations",
58
+ // 5.1 alteration type checkboxes (a)-(i)
59
+ "5.1a::windowReplacementsSince2002":
60
+ "Replacement windows, roof windows, roof lights, glazed doors since 1 April 2002",
61
+ "5.1c::hasAddedConservatory": "Conservatory",
62
+ "5.1g::removalOfChimneyBreast": "Removal of chimney breast",
63
+ // 5.3 non-residential use
64
+ "5.3::nonResidentialPurposes":
65
+ "Is any part of the property used exclusively for non-residential purposes?",
66
+ "5.3.2::details": "If yes, give details and supply a copy of any relevant planning permission:",
67
+ // 5.4 breaches
68
+ "5.4::planningPermissionBreaches":
69
+ "Are you aware of any breaches of planning permission conditions or building regulations consent conditions, unfinished work or work that does not have all necessary consents?",
70
+ "5.4.2::details": "If yes, please give details:",
71
+ // 5.5 unresolved issues
72
+ "5.5::unresolvedPlanningIssues":
73
+ "Are you aware of any planning or building control issues that need to be resolved?",
74
+ "5.5.2::details": "If yes, please give details:",
75
+ // 5.6 solar power systems
76
+ "5.6::solarPanels":
77
+ "Has a solar power system for generating electricity, hot water or heating been installed at the property?",
78
+ "5.6a::hotWaterOnly":
79
+ "Is the system used only to provide hot water or heating and not to generate electricity?",
80
+ "5.6b::yearInstalled": "Which year was the system installed? (YYYY)",
81
+ "5.6c::panelsOwnedOutright": "Do you own the system outright?",
82
+ "5.6d::panelProviderLease":
83
+ "Has a long lease of the roof or air space been granted to a solar power system provider? A typical long lease may last 20 to 25 years.",
84
+ "5.6e::maintenanceAgreement": "Do you have a maintenance agreement in place for the system?",
85
+ "5.6f::photovoltaicBatteries": "Is there a battery for storing solar power?",
86
+ "5.6f2::details":
87
+ "If yes, provide the make, model and storage capacity in kWh of the battery:",
88
+ "5.6g::nationalGrid": "Does the system feed into the National Grid?",
89
+ "5.6g1::fitOrSegInPlace":
90
+ "Is there a Feed-in Tariff (FIT) or Smart Export Guarantee (SEG) in place?",
91
+ "5.6g4::fitSegAssignmentProcedure":
92
+ "Provide details of the procedure for assigning the benefit of the FIT or SEG agreement on completion of the purchase to the buyer.",
93
+ "5.6h::installationCertificates":
94
+ "Provide a copy of the building regulations completion certificate or compliance certificate (e.g. MCS) for the installation of the system.",
95
+
96
+ // ---- Section 6: Guarantees and warranties ----
97
+ "6::guaranteesWarrantiesAndIndemnityInsurances": "Guarantees and warranties",
98
+ "6.1::hasValidGuaranteesOrWarranties":
99
+ "Does the property have any of the following unexpired guarantees or warranties?",
100
+ "6.1a::newHomeWarranty": "New home warranty (e.g. NHBC or similar)",
101
+ "6.1b::dampProofingTreatment": "Damp proofing",
102
+ "6.1c::timberRotOrInfestationTreatment": "Timber treatment",
103
+ "6.1d::doubleGlazing": "Windows, roof lights, roof windows or glazed doors",
104
+ "6.1e::roofingWork": "Roofing",
105
+ "6.1g::subsidenceWork": "Underpinning",
106
+ "6.1i::otherGuarantees": "Other (please state):",
107
+ "6.1i2::details": "If yes, please give details:",
108
+ "6.2::outstandingClaimsOrApplications":
109
+ "Are you aware of any claims under any of these guarantees or warranties?",
110
+ "6.2.2::details": "If yes, please give details:",
111
+ "6.3::breachOfTermsOrConditions":
112
+ "Are you aware of anything that may breach the terms and conditions of any of these guarantees or warranties?",
113
+ "6.3.2::details": "If yes, please give details:",
114
+
115
+ // ---- Section 7: Insurance ----
116
+ "7::insurance": "Insurance",
117
+ "7.1.1::whoInsures": "If no, who insures the property?",
118
+
119
+ // ---- Section 8: Environmental matters ----
120
+ "8::environmentalIssues": "Environmental matters",
121
+ "8::flooding": "Flooding",
122
+ "8.1.1::hasBeenFlooded": "Are you aware of the property or any part of it ever being flooded?",
123
+ "8.2.1::hasFloodDefences":
124
+ "Are you aware of any defences installed at the property to prevent flooding?",
125
+ "8.3::radon": "Radon",
126
+ "8.3::radonTest": "Are you aware of any radon tests that have been carried out on the property?",
127
+ "8.3.1a::details": "Was the test result below the 'recommended action level'?",
128
+ "8.3.1b::attachments": "Supply a copy of the report",
129
+ "8.4::remedialMeasuresOnConstruction":
130
+ "Were any remedial measures undertaken to reduce radon gas levels in the property? Measures could have been undertaken during construction or while adding an extension.",
131
+ "8.5::greenDealLoan": "Green Deal",
132
+ "8.5.1::hasGreenDealLoan":
133
+ "Have any installations in the property been financed under the Green Deal scheme?",
134
+ "8.5.1b::details":
135
+ "If yes, give details of all installations and supply a copy of your last electricity bill:",
136
+ "8.6::japaneseKnotweed": "Japanese knotweed",
137
+ "8.6.1::yesNo": "Is the property affected by Japanese knotweed?",
138
+ "8.6.2::managementPlanInPlace":
139
+ "Is there a Japanese knotweed management and treatment plan in place?",
140
+ "8.6.3::attachments": "Provide a copy of the plan with any insurance cover linked to the plan.",
141
+ "8.7::knotweedSurveyCarriedOut":
142
+ "Has a Japanese knotweed survey been carried out in relation to the property?",
143
+ "8.7.2::attachments": "Provide a copy of the survey.",
144
+
145
+ // ---- Section 9: Rights and informal arrangements ----
146
+ "9::rightsAndInformalArrangements": "Rights and informal arrangements",
147
+ "9.4::rightsOrArrangements":
148
+ "Do the owners of any other properties exercise any rights or arrangements over the property?",
149
+ "9.5::askedOthersToContribute":
150
+ "Have you asked the owner of any other properties to contribute towards the cost of the jointly used facilities?",
151
+ "9.5.2::details":
152
+ "If yes, specify whether you receive this payment or if it is made to a third party. Include details about how much is paid and how often payments are made:",
153
+ "9.6::disagreementOrComplaint":
154
+ "Are you aware of any disagreement or complaint about any such right or arrangement?",
155
+ "9.6.2::details": "If yes, please give details:",
156
+ "9::servicesCrossing": "Facilities crossing the property or any other property",
157
+ "9.7.2::details": "If yes, please give details:",
158
+ "9.8.2::details": "If yes, please give details:",
159
+ "9.9.2::details": "If yes, supply a copy or give details:",
160
+
161
+ // ---- Section 10: Parking ----
162
+ "10::parking": "Parking",
163
+ "10.1::parkingArrangements": "What are the parking arrangements at the property?",
164
+ "10.3::electricVehicleChargingPoint":
165
+ "Does the property have an electric vehicle (EV) charging point?",
166
+ "10.3a::chargerDetails":
167
+ "Specify the type of EV charger, connector and its location and provide building regulation approvals:",
168
+ "10.3b::cableCrossesPavement": "Does an EV charging cable have to cross the public pavement?",
169
+ "10.3b2::details": "If yes, specify any relevant local authority licence or terms and conditions:",
170
+
171
+ // ---- Section 11: Services ----
172
+ "11::electricalWorks": "Electrical systems",
173
+ "11.1::electricalWorkSince2005":
174
+ "Are you aware of any electrical installation works carried out at the property?",
175
+ "11.1::details": "If yes, give details including when this work took place:",
176
+ // (11.2 "Does the property have any certificates..." is the suppliedCertificate
177
+ // object, already titled among the pre-existing set. yearWorkCarriedOut is an
178
+ // overlay-only year field with no distinct form label — left as base.)
179
+ "11.3::testedByQualifiedElectrician":
180
+ "Does the property have an Electrical Installation Condition Report (EICR)?",
181
+ "11.4::heating": "Heating systems",
182
+ "11.4.1::heatingType": "How is the property heated? Tick all that apply:",
183
+ "11.4a::centralHeatingInstalled":
184
+ "When was the heating system installed? (DD/MM/YYYY)",
185
+ "11.4c::replacementOtherThanBoiler":
186
+ "Has there been any replacement to the heating system (other than replacement of a boiler)?",
187
+ "11.4b2::boilerInstallationDate":
188
+ "If there is a boiler (of any kind) when was it installed? (DD/MM/YYYY)",
189
+ "11.4g1::heatingLastServicedDate":
190
+ "In what year was the boiler and heating system last serviced or maintained?",
191
+ "11.4g2::heatingLastServicedReport": "Please supply a copy of any service report.",
192
+ "11.5a::mainsFoulDrainage": "Foul water drainage?",
193
+ "11.5b::mainsSurfaceWaterDrainage": "Surface water drainage?",
194
+ "11.7a::dateDischargeCommenced": "When did the discharge commence? (MM/YYYY)",
195
+ "11.7b::dateInstalled": "When was the system installed? (MM/YYYY)",
196
+ "11.7c::dateReplaced": "When was the sewerage system last replaced or upgraded? (MM/YYYY)",
197
+ "11.7d::dateLastEmptied": "If a cesspool, when was the container last emptied? (MM/YYYY)",
198
+ "11.7e::dateLastServiced":
199
+ "If the property is served by a sewage treatment plant, when was the treatment plant last serviced? (MM/YYYY)",
200
+ "11.7g::hasInfiltrationSystem":
201
+ "If the sewerage system discharges to the ground, does it have an infiltration system?",
202
+ "11.7h::otherConnectedProperties":
203
+ "Is the use of the sewerage system shared with other properties?",
204
+ "11.7i::plantOnOtherLand":
205
+ "Is any part of the sewerage system, or the access to it, outside the boundary of the property?",
206
+ "11.7i::attachments":
207
+ "If yes, supply a plan showing the location of the system and how access is obtained.",
208
+ // waterAndDrainage parent container spans form 11.5-11.7 + 12.3 — no single form
209
+ // label; the `drainage` child already carries "Drains and sewers".
210
+
211
+ // ---- Section 12: Connection to services ----
212
+ "12::connectivity": "Connection to services",
213
+ // electricity / water are broad containers (electricity also holds solar etc.);
214
+ // the form labels the service line, which is the mainsElectricity / mainsWater child.
215
+ "12.1.2::supplier": "Provider's name",
216
+ "12.1.3::location": "Location of meter",
217
+ "12.1.4::mpan": "MPAN number",
218
+ "12.2.3::location": "Location of meter",
219
+ "12.2.4::mprn": "MPRN number",
220
+ "12.3.2::supplier": "Provider's name",
221
+ "12.3.3::stopcock": "Location of stopcock",
222
+ "12.3.3::location": "Location of stopcock",
223
+ "12.3.4::location": "Location of meter (if any)",
224
+ "12.5.2::supplier": "Provider's name",
225
+ "12.5.3::makeModel": "Make/model",
226
+ "12.5.4::serviceProviderName": "Service provider's name",
227
+ "12.6.3::makeModel": "Make/model",
228
+ "12.6.4::serviceProviderName": "Service provider's name",
229
+ "12.7::telephone": "Telephone",
230
+ "12.7.2::supplier": "Provider's name",
231
+ "12.8::broadband": "Broadband",
232
+ "12.8.2::providerName": "Provider's name",
233
+ "12.9.1::otherServices": "If there are any other services, please give details:",
234
+
235
+ // ---- Section 13: Transaction information ----
236
+ "13::occupiers": "Occupiers",
237
+ "13.1::otherPropertyInChain":
238
+ "Does this sale depend on you completing the purchase of another property on the same day?",
239
+ "13.2::moveRestrictionDates": "Do you have any special requirements about a moving date?",
240
+ "13.2.2::details": "If yes, please give details:",
241
+ "13.4::hasOthersAged17OrOver": "Does anyone else, aged 17 or over, live at the property?",
242
+ "13.4b::aged17OrOverTenantsOrLodgers":
243
+ "Are any of those occupiers who are aged 17 or over your tenants or lodgers?",
244
+ "13.5.1::soldWithVacantPossession":
245
+ "Is the property being sold with vacant possession (empty of all occupiers, rubbish and any contents or fittings not included in the sale)?",
246
+ // Form 13.6 is one question; the schema splits it into two booleans. Bind the form
247
+ // text to the "sign to confirm will vacate" property; leave aged17OrOverAgreedToLeave.
248
+ "13.6b::aged17OrOverWillSignToConfirmWillVacate":
249
+ "Have all occupiers aged 17 or over agreed to sign the sale contract and to vacate the property on or before completion?",
250
+ "13.7b::attachments":
251
+ "If the property is not being sold with vacant possession, provide details of all occupiers and copies of their tenancy agreements.",
252
+
253
+ // ---- Section 14: Completion ----
254
+ "14::completionAndMoving": "Completion",
255
+
256
+ // ---- Section 15: Additional information ----
257
+ "15::additionalInformation": "Additional information about your answers",
258
+ "15.1::consents":
259
+ "Please supply copies of all consents that have been given under any covenants, estate management schemes or other restrictions affecting the title to the property for all actions you have disclosed in your answers to questions 5 (Alterations), 8.5 (Green Deal), 10.3 (EV charging points) and 11 (Services).",
260
+ "15.1.2::consentsAttached": "Consents attached:",
261
+ "15.1.3::consentsToFollow": "Consents to follow:",
262
+ "15.1.4::consentsNotAvailable": "List any consents that are not available:",
263
+ "15.2::furtherInformation":
264
+ "If there is any further information about any of your answers on this form, provide details below and/or supply additional documents.",
265
+ };
266
+
267
+ function normalize(s) {
268
+ let t = String(s).replace(/\s+/g, " ").trim();
269
+ t = t.replace(/^\([a-z0-9]+\)\s*/i, ""); // (a) (i) ...
270
+ t = t.replace(/^if\s+(yes|no),?\s*/i, ""); // If yes, / If no,
271
+ t = t.replace(/\s*:\s*$/, ""); // omit trailing colon (house style)
272
+ if (t.length) t = t[0].toUpperCase() + t.slice(1);
273
+ return t;
274
+ }
275
+
276
+ // Compare key for "differs from base?" — trailing colon/space ignored so a
277
+ // colon-only difference does not trigger an override.
278
+ function cmpKey(s) {
279
+ return String(s).replace(/\s*:\s*$/, "").trim();
280
+ }
281
+
282
+ function toJsoncPath(dotted) {
283
+ return dotted.split(".").map((seg) => {
284
+ const m = seg.match(/^\[(\d+)\]$/);
285
+ return m ? Number(m[1]) : seg;
286
+ });
287
+ }
288
+
289
+ // Pre-existing ta6ed6Title values that violate the house style (un-stripped
290
+ // "(a)" / "If yes," prefixes). Exact string replacements applied after inserts.
291
+ const FIXUPS = [
292
+ [
293
+ "(a) state the full names of any occupiers (other than the sellers) aged 17 or over:",
294
+ "State the full names of any occupiers (other than the sellers) aged 17 or over:",
295
+ ],
296
+ ["If yes, what type of flooding took place?", "What type of flooding took place?"],
297
+ ];
298
+
299
+ // Apply ta6ed6Title insertions to a combined.json text, returning {text, applied}.
300
+ function applyTitles(text) {
301
+ const jsonc = require("jsonc-parser");
302
+ const rows = require("/tmp/ta6ed6_worksheet.json");
303
+ const opts = { formattingOptions: { insertSpaces: true, tabSize: 2 } };
304
+ let applied = 0;
305
+ for (const r of rows) {
306
+ if (r.ed6) continue; // don't touch the pre-existing 51 here
307
+ const seg = r.path.split(".").pop().replace(/^\[\d+\]$/, "IDX");
308
+ const key = r.ref + "::" + seg;
309
+ if (!(key in LABELS)) continue;
310
+ const want = normalize(LABELS[key]);
311
+ if (want === cmpKey(r.title)) continue; // identical to base (colon ignored) — skip
312
+ const objPath = toJsoncPath(r.path);
313
+ const insertOpts = {
314
+ ...opts,
315
+ getInsertionIndex: (props) => {
316
+ const i = props.indexOf("title");
317
+ if (i >= 0) return i + 1;
318
+ const j = props.indexOf("ta6ed6Ref");
319
+ return j >= 0 ? j + 1 : props.length;
320
+ },
321
+ };
322
+ const edits = jsonc.modify(text, [...objPath, "ta6ed6Title"], want, insertOpts);
323
+ text = jsonc.applyEdits(text, edits);
324
+ applied++;
325
+ }
326
+ let fixed = 0;
327
+ for (const [from, to] of FIXUPS) {
328
+ const needle = `"ta6ed6Title": ${JSON.stringify(from)}`;
329
+ const repl = `"ta6ed6Title": ${JSON.stringify(to)}`;
330
+ if (text.includes(needle)) {
331
+ text = text.split(needle).join(repl);
332
+ fixed++;
333
+ }
334
+ }
335
+ // House style: ta6ed6Title sits immediately after title, so overrides are
336
+ // obvious. New inserts already do; reposition any pre-existing that don't.
337
+ const moved = reorderTitles();
338
+ function reorderTitles() {
339
+ const tree = JSON.parse(text);
340
+ const targets = [];
341
+ (function walk(node, path) {
342
+ if (!node || typeof node !== "object") return;
343
+ if (!Array.isArray(node) && "ta6ed6Title" in node && "title" in node) {
344
+ const keys = Object.keys(node);
345
+ if (keys.indexOf("ta6ed6Title") !== keys.indexOf("title") + 1) {
346
+ targets.push({ path, value: node.ta6ed6Title });
347
+ }
348
+ }
349
+ for (const k of Object.keys(node)) walk(node[k], path.concat(Array.isArray(node) ? Number(k) : k));
350
+ })(tree, []);
351
+ for (const { path, value } of targets) {
352
+ text = jsonc.applyEdits(text, jsonc.modify(text, [...path, "ta6ed6Title"], undefined, opts));
353
+ text = jsonc.applyEdits(
354
+ text,
355
+ jsonc.modify(text, [...path, "ta6ed6Title"], value, {
356
+ ...opts,
357
+ getInsertionIndex: (props) => props.indexOf("title") + 1,
358
+ })
359
+ );
360
+ }
361
+ return targets.length;
362
+ }
363
+
364
+ // House style: strip a trailing colon from every ta6ed6Title (new + pre-existing).
365
+ let colons = 0;
366
+ text = text.replace(/("ta6ed6Title": "(?:[^"\\]|\\.)*?)\s*:(")/g, (_, a, b) => {
367
+ colons++;
368
+ return a + b;
369
+ });
370
+ return { text, applied, fixed, colons, moved };
371
+ }
372
+
373
+ module.exports = { LABELS, normalize, applyTitles, toJsoncPath };
374
+
375
+ if (require.main === module) {
376
+ // Dry-run report: show what WOULD change.
377
+ const rows = require("/tmp/ta6ed6_worksheet.json");
378
+ let add = 0,
379
+ same = 0,
380
+ missing = [];
381
+ for (const r of rows) {
382
+ if (r.ed6) continue;
383
+ const seg = r.path.split(".").pop().replace(/^\[\d+\]$/, "IDX");
384
+ const key = r.ref + "::" + seg;
385
+ if (!(key in LABELS)) continue;
386
+ const want = normalize(LABELS[key]);
387
+ if (want === cmpKey(r.title)) {
388
+ same++;
389
+ } else {
390
+ add++;
391
+ console.log(`ADD ${r.ref.padEnd(10)} ${seg.padEnd(28)} :: ${want}`);
392
+ }
393
+ }
394
+ // labels defined but not matched to any candidate path
395
+ const matchedKeys = new Set();
396
+ for (const r of rows) {
397
+ const seg = r.path.split(".").pop().replace(/^\[\d+\]$/, "IDX");
398
+ matchedKeys.add(r.ref + "::" + seg);
399
+ }
400
+ for (const k of Object.keys(LABELS)) if (!matchedKeys.has(k)) missing.push(k);
401
+ console.log(`\n${add} to add, ${same} already match base title.`);
402
+ if (missing.length) console.log(`\nLABELS keys with NO matching property (check!):\n ` + missing.join("\n "));
403
+ }
@@ -0,0 +1,183 @@
1
+ // One-off tooling: apply exact TA7 5th-edition field titles (ta7ed5Title) to
2
+ // src/schemas/v3/combined.json.
3
+ //
4
+ // Source of truth: the Law Society "TA7 Leasehold Information Form (5th edition)
5
+ // (2025)" — text extracted with pdftotext (TA7-5th-edition.pdf, 9 pages).
6
+ //
7
+ // LABELS is keyed by "<ta7ed5Ref>::<last path segment>" and holds the VERBATIM
8
+ // form label. normalize() applies the house style: strip leading "(a)"/"(i)" and
9
+ // "If yes,"/"If no," prefixes, omit trailing colon, collapse whitespace,
10
+ // capitalise first letter. A ta7ed5Title is written only where the normalised
11
+ // form label DIFFERS from the base `title` (colon ignored). Every ta7ed5Title is
12
+ // placed immediately after `title`; a global pass strips trailing colons from the
13
+ // pre-existing titles too. Curly apostrophes match the form.
14
+
15
+ const LABELS = {
16
+ // ---- Section 1: Property and seller details ----
17
+ "1.1::address": "Address of the property",
18
+ "1.1.3::uprn": "UPRN (if known)",
19
+
20
+ // ---- Section 2: The lease (2.2 payment demands — sub-item names) ----
21
+ "2.2a::mostRecentGroundRentDemandStatus": "Ground rent",
22
+ "2.2b::mostRecentServiceChargeDemandStatus": "Service charge",
23
+ "2.2c::mostRecentBuildingsInsuranceDemandStatus": "Buildings insurance",
24
+
25
+ // ---- Section 3: Contact details ----
26
+ "3::contactDetails": "Contact details",
27
+ "3.1.2::managementCompany": "Management company",
28
+ // per-contact field labels: Name / Email
29
+ "3.1.1.1::nameOrOrganisation": "Name",
30
+ "3.1.2.1::nameOrOrganisation": "Name",
31
+ "3.1.3.1::nameOrOrganisation": "Name",
32
+ "3.1.4.1::nameOrOrganisation": "Name",
33
+ "3.1.5.1::nameOrOrganisation": "Name",
34
+ "3.1.1.3::emailAddress": "Email",
35
+ "3.1.2.3::emailAddress": "Email",
36
+ "3.1.3.3::emailAddress": "Email",
37
+ "3.1.4.3::emailAddress": "Email",
38
+ "3.1.5.3::emailAddress": "Email",
39
+
40
+ // ---- Section 4: Management ----
41
+ "4.1.2.1::nameOrOrganisation": "Name",
42
+ "4.1.2.3::emailAddress": "Email",
43
+ "4.2::collectsbuildingInsurancePremiums":
44
+ "Does the managing agent arrange or collect charges for the insurance of the building?",
45
+ "4.3::collectsGroundRent": "If ground rent is payable, is it collected by the managing agent?",
46
+ "4.4.1::yesNo":
47
+ "Does the seller alone, or jointly with others, own the freehold, any headlease or a management company named in the lease?",
48
+ "4.4.2::attachments":
49
+ "If yes, supply evidence of your ownership or share certificate for membership of the organisation that is responsible for the management of the building.",
50
+
51
+ // ---- Section 5: Maintenance and service charges ----
52
+ "5.1::externally": "In what year was the outside of the building last decorated?",
53
+ "5.2::internally": "In what year were any internal communal parts last decorated?",
54
+ "5.3::sellerContributesToServiceCharge": "Do you contribute to the cost of maintaining the building?",
55
+ "5.4::largeAdditionalExpense":
56
+ "Do you know of any expense (e.g. the cost of redecoration of outside or communal areas that is not usually incurred annually) that is likely to be shown in the service charge accounts within the next three years?",
57
+ "5.4.2::details": "If yes, please give details:",
58
+ "5.5::serviceChargeChallenged":
59
+ "Have you challenged the service charge or any expense in the last three years?",
60
+ "5.6::problemInServiceChargeLevel":
61
+ "Do you know of any issues in the last three years regarding the level of service charges or with the management?",
62
+
63
+ // ---- Section 7: Enfranchisement ----
64
+ "7.3::sellerAwareOfResponse":
65
+ "Are you aware of any response to a notice disclosed in replies to 7.1 and 7.2 above?",
66
+
67
+ // ---- Section 8: Consents ----
68
+ "8.1::changesInTermOfLease": "Are you aware of any changes to the terms of the lease?",
69
+ "8.2::landlordConsents":
70
+ "Are you aware of the landlord giving any formal or informal consents required by the lease?",
71
+ "8.2.2::details":
72
+ "If yes, please supply the consent or if the consent was not in writing, please give details:",
73
+
74
+ // ---- Section 9: Alterations ----
75
+ "9.1::sellerAwareOfAlterations":
76
+ "Are you aware of any alterations having been made to the property since the lease was originally granted?",
77
+
78
+ // ---- Section 10: Complaints ----
79
+ "10::disputes": "Complaints",
80
+
81
+ // ---- Section 11: Building safety for blocks of flats ----
82
+ "11::buildingSafetyAct": "Building safety for blocks of flats",
83
+ };
84
+
85
+ function normalize(s) {
86
+ let t = String(s).replace(/\s+/g, " ").trim();
87
+ t = t.replace(/^\([a-z0-9]+\)\s*/i, ""); // (a) (i) ...
88
+ t = t.replace(/^if\s+(yes|no),?\s*/i, ""); // If yes, / If no,
89
+ t = t.replace(/\s*:\s*$/, ""); // omit trailing colon (house style)
90
+ if (t.length) t = t[0].toUpperCase() + t.slice(1);
91
+ return t;
92
+ }
93
+
94
+ function cmpKey(s) {
95
+ return String(s).replace(/\s*:\s*$/, "").trim();
96
+ }
97
+
98
+ function toJsoncPath(dotted) {
99
+ return dotted.split(".").map((seg) => {
100
+ const m = seg.match(/^\[(\d+)\]$/);
101
+ return m ? Number(m[1]) : seg;
102
+ });
103
+ }
104
+
105
+ function applyTitles(text) {
106
+ const jsonc = require("jsonc-parser");
107
+ const rows = require("/tmp/ta7ed5_worksheet.json");
108
+ const opts = { formattingOptions: { insertSpaces: true, tabSize: 2 } };
109
+ let applied = 0;
110
+ for (const r of rows) {
111
+ if (r.ed5) continue; // don't re-insert pre-existing
112
+ const seg = r.path.split(".").pop().replace(/^\[\d+\]$/, "IDX");
113
+ const key = r.ref + "::" + seg;
114
+ if (!(key in LABELS)) continue;
115
+ const want = normalize(LABELS[key]);
116
+ if (want === cmpKey(r.title)) continue; // colon-only / identical — skip
117
+ const objPath = toJsoncPath(r.path);
118
+ const edits = jsonc.modify(text, [...objPath, "ta7ed5Title"], want, {
119
+ ...opts,
120
+ getInsertionIndex: (props) => {
121
+ const i = props.indexOf("title");
122
+ if (i >= 0) return i + 1;
123
+ const j = props.indexOf("ta7ed5Ref");
124
+ return j >= 0 ? j + 1 : props.length;
125
+ },
126
+ });
127
+ text = jsonc.applyEdits(text, edits);
128
+ applied++;
129
+ }
130
+ // House style: ta7ed5Title immediately after title — reposition pre-existing.
131
+ const tree = JSON.parse(text);
132
+ const targets = [];
133
+ (function walk(node, path) {
134
+ if (!node || typeof node !== "object") return;
135
+ if (!Array.isArray(node) && "ta7ed5Title" in node && "title" in node) {
136
+ const keys = Object.keys(node);
137
+ if (keys.indexOf("ta7ed5Title") !== keys.indexOf("title") + 1) targets.push({ path, value: node.ta7ed5Title });
138
+ }
139
+ for (const k of Object.keys(node)) walk(node[k], path.concat(Array.isArray(node) ? Number(k) : k));
140
+ })(tree, []);
141
+ for (const { path, value } of targets) {
142
+ text = jsonc.applyEdits(text, jsonc.modify(text, [...path, "ta7ed5Title"], undefined, opts));
143
+ text = jsonc.applyEdits(
144
+ text,
145
+ jsonc.modify(text, [...path, "ta7ed5Title"], value, {
146
+ ...opts,
147
+ getInsertionIndex: (props) => props.indexOf("title") + 1,
148
+ })
149
+ );
150
+ }
151
+ const moved = targets.length;
152
+ // House style: strip a trailing colon from every ta7ed5Title (new + pre-existing).
153
+ let colons = 0;
154
+ text = text.replace(/("ta7ed5Title": "(?:[^"\\]|\\.)*?)\s*:(")/g, (_, a, b) => {
155
+ colons++;
156
+ return a + b;
157
+ });
158
+ return { text, applied, moved, colons };
159
+ }
160
+
161
+ module.exports = { LABELS, normalize, cmpKey, applyTitles, toJsoncPath };
162
+
163
+ if (require.main === module) {
164
+ const rows = require("/tmp/ta7ed5_worksheet.json");
165
+ let add = 0,
166
+ same = 0;
167
+ const matched = new Set();
168
+ for (const r of rows) matched.add(r.ref + "::" + r.path.split(".").pop().replace(/^\[\d+\]$/, "IDX"));
169
+ for (const r of rows) {
170
+ if (r.ed5) continue;
171
+ const key = r.ref + "::" + r.path.split(".").pop().replace(/^\[\d+\]$/, "IDX");
172
+ if (!(key in LABELS)) continue;
173
+ const want = normalize(LABELS[key]);
174
+ if (want === cmpKey(r.title)) same++;
175
+ else {
176
+ add++;
177
+ console.log(`ADD ${r.ref.padEnd(9)} ${key.split("::")[1].padEnd(34)} :: ${want}`);
178
+ }
179
+ }
180
+ const orphans = Object.keys(LABELS).filter((k) => !matched.has(k));
181
+ console.log(`\n${add} to add, ${same} already match base title.`);
182
+ if (orphans.length) console.log(`\nLABELS with NO matching property (check!):\n ` + orphans.join("\n "));
183
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "verification": {
3
+ "trust_framework": "uk_pdtf",
4
+ "time": "2022-01-25T13:05:03.108Z",
5
+ "evidence": [
6
+ {
7
+ "type": "vouch",
8
+ "attestation": {
9
+ "type": "digital_attestation",
10
+ "voucher": {
11
+ "name": "Martin Abode"
12
+ }
13
+ },
14
+ "verification_method": {
15
+ "type": "auth"
16
+ }
17
+ }
18
+ ]
19
+ },
20
+ "claims": {
21
+ "/participants/-": {
22
+ "name": {
23
+ "firstName": "Martin",
24
+ "lastName": "Abode"
25
+ },
26
+ "email": "martin@abode.co.uk",
27
+ "organisation": "Abode and Floggit Estate Agents",
28
+ "organisationReference": "MAR/12243/22",
29
+ "role": "Estate Agent"
30
+ }
31
+ }
32
+ }