@pdtf/schemas 3.6.0-dev.21 → 3.6.0-dev.23

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.
@@ -139,8 +139,11 @@ position that could diverge from another producer's.
139
139
 
140
140
  1. **Overlay metadata** (`*Ref`, `*Required` keys) is stripped from V4. It is
141
141
  schema-level only and never appears in instance data.
142
- 2. A `Title` carrying only keys shared by both source arrays recomposes into
143
- `titlesToBeSold` only, not into `ownershipsToBeTransferred`.
142
+ 2. `Transaction.titlesToBeSold` lists the titles V3 listed under
143
+ `propertyPack.titlesToBeSold`. A title known only from
144
+ `ownershipsToBeTransferred` is a `Title` entity but is not listed there, which
145
+ is how recomposition tells the two apart — and why a title whose only listed
146
+ key is `titleNumber` still gets its entry back.
144
147
  3. **`ownershipsToBeTransferred` order is canonicalised** to `titlesToBeSold`
145
148
  order. The array is correlated by `titleNumber` and its order carries no
146
149
  meaning in V3, so this is a deliberate normalisation that removes the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pdtf/schemas",
3
- "version": "3.6.0-dev.21",
3
+ "version": "3.6.0-dev.23",
4
4
  "description": "Property Data Trust Framework Schemas and Utilities",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -49618,11 +49618,15 @@
49618
49618
  "intendedUse": {
49619
49619
  "type": "string",
49620
49620
  "title": "Intended use of property",
49621
+ "description": "The primary purpose of the purchase: who will occupy the property, and on what basis. Secondary activities (a home office, a lodger, pets, alterations) are recorded in the separate fields below and are never values of this field, so a home-worker is \"Owner occupied\" with intendsBusiness true.",
49621
49622
  "enum": [
49622
49623
  "Owner occupied",
49623
49624
  "Buy-to-let",
49624
49625
  "Holiday let",
49625
- "Second home"
49626
+ "Second home",
49627
+ "Family member occupied",
49628
+ "Development or renovation",
49629
+ "Business or mixed-use premises"
49626
49630
  ]
49627
49631
  },
49628
49632
  "firstTimeBuyer": {
@@ -49671,6 +49675,11 @@
49671
49675
  "title": "Buyer has pets",
49672
49676
  "description": "Relevant to lease and covenant pet restriction assessment"
49673
49677
  },
49678
+ "petDetails": {
49679
+ "type": "string",
49680
+ "title": "Pets or other animals",
49681
+ "description": "The pets, horses or other animals the buyer intends to keep at the property, and how many (e.g. \"2 dogs, 1 cat\")"
49682
+ },
49674
49683
  "intendsSublet": {
49675
49684
  "type": "boolean",
49676
49685
  "title": "Buyer intends to sublet",
@@ -49695,6 +49704,16 @@
49695
49704
  "title": "Buyer intends significant alterations",
49696
49705
  "description": "Relevant to covenant, planning and building regulations assessment"
49697
49706
  },
49707
+ "alterationDescription": {
49708
+ "type": "string",
49709
+ "title": "Planned alterations",
49710
+ "description": "What the buyer intends to alter, extend or develop"
49711
+ },
49712
+ "vehicleRequirements": {
49713
+ "type": "string",
49714
+ "title": "Vehicles to keep at the property",
49715
+ "description": "Any caravan, motorhome, commercial vehicle or boat the buyer intends to keep at the property. Relevant to parking and use covenants"
49716
+ },
49698
49717
  "financingMethod": {
49699
49718
  "type": "string",
49700
49719
  "title": "Primary financing method",
@@ -4518,11 +4518,14 @@
4518
4518
  requiresMortgage
4519
4519
  isAdditionalProperty
4520
4520
  hasPets
4521
+ petDetails
4521
4522
  intendsSublet
4522
4523
  subletType
4523
4524
  intendsBusiness
4524
4525
  businessType
4525
4526
  intendsAlterations
4527
+ alterationDescription
4528
+ vehicleRequirements
4526
4529
  financingMethod
4527
4530
  helpToBuyEquityLoan
4528
4531
  armedForcesHelpToBuy
@@ -41645,11 +41645,15 @@
41645
41645
  "intendedUse": {
41646
41646
  "type": "string",
41647
41647
  "title": "Intended use of property",
41648
+ "description": "The primary purpose of the purchase: who will occupy the property, and on what basis. Secondary activities (a home office, a lodger, pets, alterations) are recorded in the separate fields below and are never values of this field, so a home-worker is \"Owner occupied\" with intendsBusiness true.",
41648
41649
  "enum": [
41649
41650
  "Owner occupied",
41650
41651
  "Buy-to-let",
41651
41652
  "Holiday let",
41652
- "Second home"
41653
+ "Second home",
41654
+ "Family member occupied",
41655
+ "Development or renovation",
41656
+ "Business or mixed-use premises"
41653
41657
  ]
41654
41658
  },
41655
41659
  "firstTimeBuyer": {
@@ -41701,6 +41705,11 @@
41701
41705
  "title": "Buyer has pets",
41702
41706
  "description": "Relevant to lease and covenant pet restriction assessment"
41703
41707
  },
41708
+ "petDetails": {
41709
+ "type": "string",
41710
+ "title": "Pets or other animals",
41711
+ "description": "The pets, horses or other animals the buyer intends to keep at the property, and how many (e.g. \"2 dogs, 1 cat\")"
41712
+ },
41704
41713
  "intendsSublet": {
41705
41714
  "type": "boolean",
41706
41715
  "title": "Buyer intends to sublet",
@@ -41732,6 +41741,16 @@
41732
41741
  "title": "Buyer intends significant alterations",
41733
41742
  "description": "Relevant to covenant, planning and building regulations assessment"
41734
41743
  },
41744
+ "alterationDescription": {
41745
+ "type": "string",
41746
+ "title": "Planned alterations",
41747
+ "description": "What the buyer intends to alter, extend or develop"
41748
+ },
41749
+ "vehicleRequirements": {
41750
+ "type": "string",
41751
+ "title": "Vehicles to keep at the property",
41752
+ "description": "Any caravan, motorhome, commercial vehicle or boat the buyer intends to keep at the property. Relevant to parking and use covenants"
41753
+ },
41735
41754
  "financingMethod": {
41736
41755
  "type": "string",
41737
41756
  "title": "Primary financing method",
@@ -5780,11 +5780,14 @@
5780
5780
  "requiresMortgage": "variant",
5781
5781
  "isAdditionalProperty": "boolean",
5782
5782
  "hasPets": "boolean",
5783
+ "petDetails": "string",
5783
5784
  "intendsSublet": "boolean",
5784
5785
  "subletType": "string",
5785
5786
  "intendsBusiness": "boolean",
5786
5787
  "businessType": "string",
5787
5788
  "intendsAlterations": "boolean",
5789
+ "alterationDescription": "string",
5790
+ "vehicleRequirements": "string",
5788
5791
  "financingMethod": "string",
5789
5792
  "helpToBuyEquityLoan": "boolean",
5790
5793
  "armedForcesHelpToBuy": "boolean",
@@ -6,10 +6,10 @@
6
6
  "description": "A gift of funds towards a purchase, made by a gift donor. Implies the Gift Donor role.",
7
7
  "x-pdtf-source": {
8
8
  "packageName": "@pdtf/schemas",
9
- "packageVersion": "3.6.0-dev.21",
9
+ "packageVersion": "3.6.0-dev.23",
10
10
  "v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
11
11
  "combinedPath": "src/schemas/v3/combined.json",
12
- "combinedSha256": "1c6c5812f73a36d3674d5fb7b5eb1f6ce524d09c4195e281e078b9e41ea94f6d"
12
+ "combinedSha256": "21df4c6fb2117e9913eb9b16e48c9e6fe5f9ae9988af99e22bfcd4ef4bc6ceab"
13
13
  },
14
14
  "properties": {
15
15
  "id": {
@@ -6,10 +6,10 @@
6
6
  "description": "An offer made by a prospective buyer in a property transaction.",
7
7
  "x-pdtf-source": {
8
8
  "packageName": "@pdtf/schemas",
9
- "packageVersion": "3.6.0-dev.21",
9
+ "packageVersion": "3.6.0-dev.23",
10
10
  "v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
11
11
  "combinedPath": "src/schemas/v3/combined.json",
12
- "combinedSha256": "1c6c5812f73a36d3674d5fb7b5eb1f6ce524d09c4195e281e078b9e41ea94f6d"
12
+ "combinedSha256": "21df4c6fb2117e9913eb9b16e48c9e6fe5f9ae9988af99e22bfcd4ef4bc6ceab"
13
13
  },
14
14
  "properties": {
15
15
  "id": {
@@ -130,11 +130,15 @@
130
130
  "intendedUse": {
131
131
  "type": "string",
132
132
  "title": "Intended use of property",
133
+ "description": "The primary purpose of the purchase: who will occupy the property, and on what basis. Secondary activities (a home office, a lodger, pets, alterations) are recorded in the separate fields below and are never values of this field, so a home-worker is \"Owner occupied\" with intendsBusiness true.",
133
134
  "enum": [
134
135
  "Owner occupied",
135
136
  "Buy-to-let",
136
137
  "Holiday let",
137
- "Second home"
138
+ "Second home",
139
+ "Family member occupied",
140
+ "Development or renovation",
141
+ "Business or mixed-use premises"
138
142
  ]
139
143
  },
140
144
  "firstTimeBuyer": {
@@ -186,6 +190,11 @@
186
190
  "title": "Buyer has pets",
187
191
  "description": "Relevant to lease and covenant pet restriction assessment"
188
192
  },
193
+ "petDetails": {
194
+ "type": "string",
195
+ "title": "Pets or other animals",
196
+ "description": "The pets, horses or other animals the buyer intends to keep at the property, and how many (e.g. \"2 dogs, 1 cat\")"
197
+ },
189
198
  "intendsSublet": {
190
199
  "type": "boolean",
191
200
  "title": "Buyer intends to sublet",
@@ -217,6 +226,16 @@
217
226
  "title": "Buyer intends significant alterations",
218
227
  "description": "Relevant to covenant, planning and building regulations assessment"
219
228
  },
229
+ "alterationDescription": {
230
+ "type": "string",
231
+ "title": "Planned alterations",
232
+ "description": "What the buyer intends to alter, extend or develop"
233
+ },
234
+ "vehicleRequirements": {
235
+ "type": "string",
236
+ "title": "Vehicles to keep at the property",
237
+ "description": "Any caravan, motorhome, commercial vehicle or boat the buyer intends to keep at the property. Relevant to parking and use covenants"
238
+ },
220
239
  "financingMethod": {
221
240
  "type": "string",
222
241
  "title": "Primary financing method",
@@ -6,10 +6,10 @@
6
6
  "description": "A legal entity (company, firm, partnership) participating in a property transaction.",
7
7
  "x-pdtf-source": {
8
8
  "packageName": "@pdtf/schemas",
9
- "packageVersion": "3.6.0-dev.21",
9
+ "packageVersion": "3.6.0-dev.23",
10
10
  "v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
11
11
  "combinedPath": "src/schemas/v3/combined.json",
12
- "combinedSha256": "1c6c5812f73a36d3674d5fb7b5eb1f6ce524d09c4195e281e078b9e41ea94f6d"
12
+ "combinedSha256": "21df4c6fb2117e9913eb9b16e48c9e6fe5f9ae9988af99e22bfcd4ef4bc6ceab"
13
13
  },
14
14
  "properties": {
15
15
  "id": {
@@ -6,10 +6,10 @@
6
6
  "description": "A natural person participating in a property transaction — name, contact details, address, verification status.",
7
7
  "x-pdtf-source": {
8
8
  "packageName": "@pdtf/schemas",
9
- "packageVersion": "3.6.0-dev.21",
9
+ "packageVersion": "3.6.0-dev.23",
10
10
  "v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
11
11
  "combinedPath": "src/schemas/v3/combined.json",
12
- "combinedSha256": "1c6c5812f73a36d3674d5fb7b5eb1f6ce524d09c4195e281e078b9e41ea94f6d"
12
+ "combinedSha256": "21df4c6fb2117e9913eb9b16e48c9e6fe5f9ae9988af99e22bfcd4ef4bc6ceab"
13
13
  },
14
14
  "properties": {
15
15
  "name": {
@@ -6,10 +6,10 @@
6
6
  "description": "Physical and environmental facts about a property — address, construction, fixtures, services, searches, documents. Excludes ownership, titles, and participants.",
7
7
  "x-pdtf-source": {
8
8
  "packageName": "@pdtf/schemas",
9
- "packageVersion": "3.6.0-dev.21",
9
+ "packageVersion": "3.6.0-dev.23",
10
10
  "v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
11
11
  "combinedPath": "src/schemas/v3/combined.json",
12
- "combinedSha256": "1c6c5812f73a36d3674d5fb7b5eb1f6ce524d09c4195e281e078b9e41ea94f6d"
12
+ "combinedSha256": "21df4c6fb2117e9913eb9b16e48c9e6fe5f9ae9988af99e22bfcd4ef4bc6ceab"
13
13
  },
14
14
  "properties": {
15
15
  "address": {
@@ -6,10 +6,10 @@
6
6
  "description": "A professional representation relationship — one party instructed by another, e.g. a conveyancer acting for a seller. One entity per (representative, represented party) pair: a conveyancer instructed jointly by two sellers yields two, and sellers who instruct separate conveyancers yield one each.",
7
7
  "x-pdtf-source": {
8
8
  "packageName": "@pdtf/schemas",
9
- "packageVersion": "3.6.0-dev.21",
9
+ "packageVersion": "3.6.0-dev.23",
10
10
  "v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
11
11
  "combinedPath": "src/schemas/v3/combined.json",
12
- "combinedSha256": "1c6c5812f73a36d3674d5fb7b5eb1f6ce524d09c4195e281e078b9e41ea94f6d"
12
+ "combinedSha256": "21df4c6fb2117e9913eb9b16e48c9e6fe5f9ae9988af99e22bfcd4ef4bc6ceab"
13
13
  },
14
14
  "properties": {
15
15
  "id": {
@@ -6,10 +6,10 @@
6
6
  "description": "The legal capacity in which a seller is selling a given title. Derived from the V3 participants 'Seller' branch (sellersCapacity, dateBecameOwnerOrAuthority).",
7
7
  "x-pdtf-source": {
8
8
  "packageName": "@pdtf/schemas",
9
- "packageVersion": "3.6.0-dev.21",
9
+ "packageVersion": "3.6.0-dev.23",
10
10
  "v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
11
11
  "combinedPath": "src/schemas/v3/combined.json",
12
- "combinedSha256": "1c6c5812f73a36d3674d5fb7b5eb1f6ce524d09c4195e281e078b9e41ea94f6d"
12
+ "combinedSha256": "21df4c6fb2117e9913eb9b16e48c9e6fe5f9ae9988af99e22bfcd4ef4bc6ceab"
13
13
  },
14
14
  "properties": {
15
15
  "id": {
@@ -6,10 +6,10 @@
6
6
  "description": "HM Land Registry title — register extract, tenure type, and legal interest details for a single title number.",
7
7
  "x-pdtf-source": {
8
8
  "packageName": "@pdtf/schemas",
9
- "packageVersion": "3.6.0-dev.21",
9
+ "packageVersion": "3.6.0-dev.23",
10
10
  "v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
11
11
  "combinedPath": "src/schemas/v3/combined.json",
12
- "combinedSha256": "1c6c5812f73a36d3674d5fb7b5eb1f6ce524d09c4195e281e078b9e41ea94f6d"
12
+ "combinedSha256": "21df4c6fb2117e9913eb9b16e48c9e6fe5f9ae9988af99e22bfcd4ef4bc6ceab"
13
13
  },
14
14
  "properties": {
15
15
  "titleNumber": {
@@ -6,10 +6,10 @@
6
6
  "description": "Transaction-level metadata — status, milestones, chain, contracts, offers, enquiries, and sale context.",
7
7
  "x-pdtf-source": {
8
8
  "packageName": "@pdtf/schemas",
9
- "packageVersion": "3.6.0-dev.21",
9
+ "packageVersion": "3.6.0-dev.23",
10
10
  "v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
11
11
  "combinedPath": "src/schemas/v3/combined.json",
12
- "combinedSha256": "1c6c5812f73a36d3674d5fb7b5eb1f6ce524d09c4195e281e078b9e41ea94f6d"
12
+ "combinedSha256": "21df4c6fb2117e9913eb9b16e48c9e6fe5f9ae9988af99e22bfcd4ef4bc6ceab"
13
13
  },
14
14
  "properties": {
15
15
  "transactionId": {
@@ -984,11 +984,15 @@
984
984
  "intendedUse": {
985
985
  "type": "string",
986
986
  "title": "Intended use of property",
987
+ "description": "The primary purpose of the purchase: who will occupy the property, and on what basis. Secondary activities (a home office, a lodger, pets, alterations) are recorded in the separate fields below and are never values of this field, so a home-worker is \"Owner occupied\" with intendsBusiness true.",
987
988
  "enum": [
988
989
  "Owner occupied",
989
990
  "Buy-to-let",
990
991
  "Holiday let",
991
- "Second home"
992
+ "Second home",
993
+ "Family member occupied",
994
+ "Development or renovation",
995
+ "Business or mixed-use premises"
992
996
  ]
993
997
  },
994
998
  "firstTimeBuyer": {
@@ -1040,6 +1044,11 @@
1040
1044
  "title": "Buyer has pets",
1041
1045
  "description": "Relevant to lease and covenant pet restriction assessment"
1042
1046
  },
1047
+ "petDetails": {
1048
+ "type": "string",
1049
+ "title": "Pets or other animals",
1050
+ "description": "The pets, horses or other animals the buyer intends to keep at the property, and how many (e.g. \"2 dogs, 1 cat\")"
1051
+ },
1043
1052
  "intendsSublet": {
1044
1053
  "type": "boolean",
1045
1054
  "title": "Buyer intends to sublet",
@@ -1071,6 +1080,16 @@
1071
1080
  "title": "Buyer intends significant alterations",
1072
1081
  "description": "Relevant to covenant, planning and building regulations assessment"
1073
1082
  },
1083
+ "alterationDescription": {
1084
+ "type": "string",
1085
+ "title": "Planned alterations",
1086
+ "description": "What the buyer intends to alter, extend or develop"
1087
+ },
1088
+ "vehicleRequirements": {
1089
+ "type": "string",
1090
+ "title": "Vehicles to keep at the property",
1091
+ "description": "Any caravan, motorhome, commercial vehicle or boat the buyer intends to keep at the property. Relevant to parking and use covenants"
1092
+ },
1074
1093
  "financingMethod": {
1075
1094
  "type": "string",
1076
1095
  "title": "Primary financing method",
@@ -1396,7 +1415,7 @@
1396
1415
  },
1397
1416
  "titlesToBeSold": {
1398
1417
  "title": "Titles to be Sold",
1399
- "description": "Array of URNs referencing Title entities to be sold in this transaction",
1418
+ "description": "Ordered URNs of the Title entities that V3 listed in propertyPack.titlesToBeSold. A title known only from propertyPack.ownership.ownershipsToBeTransferred is a Title entity but is NOT listed here, so recomposition can tell the two apart and rebuild each source array faithfully.",
1400
1419
  "type": "array",
1401
1420
  "minItems": 1,
1402
1421
  "items": {
@@ -6,10 +6,10 @@
6
6
  "description": "A party's role in a transaction, where no more specific relationship credential applies. Asserts participation in a role, not a relationship to another named party.",
7
7
  "x-pdtf-source": {
8
8
  "packageName": "@pdtf/schemas",
9
- "packageVersion": "3.6.0-dev.21",
9
+ "packageVersion": "3.6.0-dev.23",
10
10
  "v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
11
11
  "combinedPath": "src/schemas/v3/combined.json",
12
- "combinedSha256": "1c6c5812f73a36d3674d5fb7b5eb1f6ce524d09c4195e281e078b9e41ea94f6d"
12
+ "combinedSha256": "21df4c6fb2117e9913eb9b16e48c9e6fe5f9ae9988af99e22bfcd4ef4bc6ceab"
13
13
  },
14
14
  "properties": {
15
15
  "id": {
@@ -5,10 +5,10 @@
5
5
  "generator": "src/utils/generateV4Schemas.js",
6
6
  "source": {
7
7
  "packageName": "@pdtf/schemas",
8
- "packageVersion": "3.6.0-dev.21",
8
+ "packageVersion": "3.6.0-dev.23",
9
9
  "v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
10
10
  "combinedPath": "src/schemas/v3/combined.json",
11
- "combinedSha256": "1c6c5812f73a36d3674d5fb7b5eb1f6ce524d09c4195e281e078b9e41ea94f6d"
11
+ "combinedSha256": "21df4c6fb2117e9913eb9b16e48c9e6fe5f9ae9988af99e22bfcd4ef4bc6ceab"
12
12
  },
13
13
  "entities": {
14
14
  "Property": {
@@ -93,6 +93,13 @@
93
93
  "The round trip restores deep equality, NOT byte equality: object key order is not preserved, because keys are regrouped by entity and reassembled. JSON attaches no meaning to key order, but do not compare a recomposed instance to the original by hashing or string equality — compare structurally, or canonicalise first (e.g. JCS) if you need a stable digest."
94
94
  ]
95
95
  },
96
+ "extensionPolicy": {
97
+ "description": "What each rule does with instance keys the V3 schema does not define — platform extensions such as an externalIds namespace or a vendor's own block. `passthrough` carries them into the entity and back out again on recomposition; `drop` discards them. Exactly one rule per V3 node is the sink, so an unknown key has a single, predictable home. verifyCoverage() guarantees every V3 SCHEMA key has a V4 home; this is the answer for everything outside the schema.",
98
+ "values": [
99
+ "passthrough",
100
+ "drop"
101
+ ]
102
+ },
96
103
  "arrayKeys": [
97
104
  {
98
105
  "pointer": "/participants",
@@ -883,6 +890,7 @@
883
890
  "covenantAnalysis"
884
891
  ]
885
892
  },
893
+ "extensions": "passthrough",
886
894
  "instance": {
887
895
  "cardinality": "many",
888
896
  "orderedBy": {
@@ -892,7 +900,7 @@
892
900
  "correlateBy": "titleNumber",
893
901
  "primarySource": true
894
902
  },
895
- "notes": "Primary source: a Title carrying only shared keys recomposes to this array."
903
+ "notes": "Primary source: it takes any key the V3 schema does not define, and recomposition emits one entry per id in Transaction.titlesToBeSold."
896
904
  },
897
905
  {
898
906
  "id": "title.ownershipsToBeTransferred",
@@ -913,6 +921,7 @@
913
921
  "otherOwnershipDetails"
914
922
  ]
915
923
  },
924
+ "extensions": "drop",
916
925
  "instance": {
917
926
  "cardinality": "many",
918
927
  "orderedBy": {
@@ -944,6 +953,7 @@
944
953
  "ownershipsToBeTransferred"
945
954
  ]
946
955
  },
956
+ "extensions": "passthrough",
947
957
  "instance": {
948
958
  "cardinality": "single"
949
959
  }
@@ -961,6 +971,7 @@
961
971
  "legalOwners"
962
972
  ]
963
973
  },
974
+ "extensions": "passthrough",
964
975
  "instance": {
965
976
  "cardinality": "single"
966
977
  }
@@ -979,6 +990,7 @@
979
990
  "organisationReference"
980
991
  ]
981
992
  },
993
+ "extensions": "drop",
982
994
  "instance": {
983
995
  "cardinality": "many",
984
996
  "orderedBy": {
@@ -990,6 +1002,7 @@
990
1002
  },
991
1003
  {
992
1004
  "id": "credential.sellerCapacity",
1005
+ "extensions": "drop",
993
1006
  "v3Pointer": "/participants/{index}",
994
1007
  "entity": "SellerCapacity",
995
1008
  "entityPointer": "",
@@ -1008,6 +1021,7 @@
1008
1021
  },
1009
1022
  {
1010
1023
  "id": "credential.offer",
1024
+ "extensions": "drop",
1011
1025
  "v3Pointer": "/participants/{index}",
1012
1026
  "entity": "Offer",
1013
1027
  "entityPointer": "",
@@ -1025,6 +1039,7 @@
1025
1039
  },
1026
1040
  {
1027
1041
  "id": "credential.gift",
1042
+ "extensions": "drop",
1028
1043
  "v3Pointer": "/participants/{index}",
1029
1044
  "entity": "Gift",
1030
1045
  "entityPointer": "",
@@ -1043,6 +1058,7 @@
1043
1058
  },
1044
1059
  {
1045
1060
  "id": "credential.representation",
1061
+ "extensions": "drop",
1046
1062
  "v3Pointer": "/participants/{index}",
1047
1063
  "entity": "Representation",
1048
1064
  "entityPointer": "",
@@ -1065,6 +1081,7 @@
1065
1081
  },
1066
1082
  {
1067
1083
  "id": "credential.transactionRole",
1084
+ "extensions": "drop",
1068
1085
  "v3Pointer": "/participants/{index}",
1069
1086
  "entity": "TransactionRole",
1070
1087
  "entityPointer": "",
@@ -1099,6 +1116,7 @@
1099
1116
  "verification"
1100
1117
  ]
1101
1118
  },
1119
+ "extensions": "passthrough",
1102
1120
  "instance": {
1103
1121
  "cardinality": "many",
1104
1122
  "orderedBy": {
@@ -1121,6 +1139,7 @@
1121
1139
  "$schema"
1122
1140
  ]
1123
1141
  },
1142
+ "extensions": "passthrough",
1124
1143
  "instance": {
1125
1144
  "cardinality": "single"
1126
1145
  }
@@ -398,7 +398,7 @@ const generateTransaction = () => {
398
398
  topLevel.titlesToBeSold = {
399
399
  title: "Titles to be Sold",
400
400
  description:
401
- "Array of URNs referencing Title entities to be sold in this transaction",
401
+ "Ordered URNs of the Title entities that V3 listed in propertyPack.titlesToBeSold. A title known only from propertyPack.ownership.ownershipsToBeTransferred is a Title entity but is NOT listed here, so recomposition can tell the two apart and rebuild each source array faithfully.",
402
402
  type: "array",
403
403
  minItems: 1,
404
404
  items: urnRef("Title URN", "URN reference to a Title entity"),
@@ -973,6 +973,7 @@ const buildRules = () => [
973
973
  entity: "Title",
974
974
  entityPointer: "",
975
975
  keys: { mode: "include", values: titlesKeys },
976
+ extensions: "passthrough",
976
977
  instance: {
977
978
  cardinality: "many",
978
979
  orderedBy: { entity: "Transaction", pointer: "/titlesToBeSold" },
@@ -980,7 +981,7 @@ const buildRules = () => [
980
981
  primarySource: true,
981
982
  },
982
983
  notes:
983
- "Primary source: a Title carrying only shared keys recomposes to this array.",
984
+ "Primary source: it takes any key the V3 schema does not define, and recomposition emits one entry per id in Transaction.titlesToBeSold.",
984
985
  },
985
986
  {
986
987
  id: "title.ownershipsToBeTransferred",
@@ -988,6 +989,7 @@ const buildRules = () => [
988
989
  entity: "Title",
989
990
  entityPointer: "",
990
991
  keys: { mode: "include", values: ownershipItemKeys },
992
+ extensions: "drop",
991
993
  instance: {
992
994
  cardinality: "many",
993
995
  orderedBy: { entity: "Transaction", pointer: "/titlesToBeSold" },
@@ -1012,6 +1014,7 @@ const buildRules = () => [
1012
1014
  entity: "Transaction",
1013
1015
  entityPointer: "/saleContext",
1014
1016
  keys: { mode: "exclude", values: ["ownershipsToBeTransferred"] },
1017
+ extensions: "passthrough",
1015
1018
  instance: { cardinality: "single" },
1016
1019
  },
1017
1020
 
@@ -1022,6 +1025,7 @@ const buildRules = () => [
1022
1025
  entity: "Property",
1023
1026
  entityPointer: "",
1024
1027
  keys: { mode: "exclude", values: PROPERTY_EXCLUDE },
1028
+ extensions: "passthrough",
1025
1029
  instance: { cardinality: "single" },
1026
1030
  },
1027
1031
 
@@ -1032,6 +1036,7 @@ const buildRules = () => [
1032
1036
  entity: "Transaction",
1033
1037
  entityPointer: "/participants/{index}",
1034
1038
  keys: { mode: "include", values: PARTICIPANT_ROSTER_FIELDS },
1039
+ extensions: "drop",
1035
1040
  instance: {
1036
1041
  cardinality: "many",
1037
1042
  orderedBy: { entity: "Transaction", pointer: "/participants" },
@@ -1046,6 +1051,7 @@ const buildRules = () => [
1046
1051
  // returns the candidates; pass the decomposed entities to narrow it.
1047
1052
  {
1048
1053
  id: "credential.sellerCapacity",
1054
+ extensions: "drop",
1049
1055
  v3Pointer: "/participants/{index}",
1050
1056
  entity: "SellerCapacity",
1051
1057
  entityPointer: "",
@@ -1055,6 +1061,7 @@ const buildRules = () => [
1055
1061
  },
1056
1062
  {
1057
1063
  id: "credential.offer",
1064
+ extensions: "drop",
1058
1065
  v3Pointer: "/participants/{index}",
1059
1066
  entity: "Offer",
1060
1067
  entityPointer: "",
@@ -1064,6 +1071,7 @@ const buildRules = () => [
1064
1071
  },
1065
1072
  {
1066
1073
  id: "credential.gift",
1074
+ extensions: "drop",
1067
1075
  v3Pointer: "/participants/{index}",
1068
1076
  entity: "Gift",
1069
1077
  entityPointer: "",
@@ -1073,6 +1081,7 @@ const buildRules = () => [
1073
1081
  },
1074
1082
  {
1075
1083
  id: "credential.representation",
1084
+ extensions: "drop",
1076
1085
  v3Pointer: "/participants/{index}",
1077
1086
  entity: "Representation",
1078
1087
  entityPointer: "",
@@ -1084,6 +1093,7 @@ const buildRules = () => [
1084
1093
  },
1085
1094
  {
1086
1095
  id: "credential.transactionRole",
1096
+ extensions: "drop",
1087
1097
  v3Pointer: "/participants/{index}",
1088
1098
  entity: "TransactionRole",
1089
1099
  entityPointer: "",
@@ -1098,6 +1108,7 @@ const buildRules = () => [
1098
1108
  entity: "Person",
1099
1109
  entityPointer: "",
1100
1110
  keys: { mode: "include", values: personKeys },
1111
+ extensions: "passthrough",
1101
1112
  instance: {
1102
1113
  cardinality: "many",
1103
1114
  orderedBy: {
@@ -1115,6 +1126,7 @@ const buildRules = () => [
1115
1126
  entity: "Transaction",
1116
1127
  entityPointer: "",
1117
1128
  keys: { mode: "exclude", values: [...TRANSACTION_EXCLUDE, "$schema"] },
1129
+ extensions: "passthrough",
1118
1130
  instance: { cardinality: "single" },
1119
1131
  },
1120
1132
  ];
@@ -1236,6 +1248,11 @@ const mapping = {
1236
1248
  "The round trip restores deep equality, NOT byte equality: object key order is not preserved, because keys are regrouped by entity and reassembled. JSON attaches no meaning to key order, but do not compare a recomposed instance to the original by hashing or string equality — compare structurally, or canonicalise first (e.g. JCS) if you need a stable digest.",
1237
1249
  ],
1238
1250
  },
1251
+ extensionPolicy: {
1252
+ description:
1253
+ "What each rule does with instance keys the V3 schema does not define — platform extensions such as an externalIds namespace or a vendor's own block. `passthrough` carries them into the entity and back out again on recomposition; `drop` discards them. Exactly one rule per V3 node is the sink, so an unknown key has a single, predictable home. verifyCoverage() guarantees every V3 SCHEMA key has a V4 home; this is the answer for everything outside the schema.",
1254
+ values: ["passthrough", "drop"],
1255
+ },
1239
1256
  arrayKeys: arrayKeys.keyed,
1240
1257
  arrayKeyScopes: {
1241
1258
  canonical:
package/src/utils/v4.js CHANGED
@@ -46,12 +46,69 @@ const omit = (obj, keys) =>
46
46
  Object.entries(obj || {}).filter(([k]) => !keys.includes(k))
47
47
  );
48
48
 
49
- /** Apply a rule's `keys` selector to a V3 node. */
49
+ /**
50
+ * Every key any rule at this V3 pointer claims by name. Anything outside it is
51
+ * an extension: a platform's own block, or an externalIds namespace the schema
52
+ * does not define.
53
+ */
54
+ const claimedKeysAt = (v3Pointer) => {
55
+ const claimed = new Set();
56
+ for (const rule of mapping.rules) {
57
+ if (rule.v3Pointer !== v3Pointer || rule.keys?.mode !== "include") continue;
58
+ for (const key of rule.keys.values) claimed.add(key);
59
+ }
60
+ return claimed;
61
+ };
62
+
63
+ const CLAIMED_BY_POINTER = new Map();
64
+ const claimedFor = (v3Pointer) => {
65
+ if (!CLAIMED_BY_POINTER.has(v3Pointer)) {
66
+ CLAIMED_BY_POINTER.set(v3Pointer, claimedKeysAt(v3Pointer));
67
+ }
68
+ return CLAIMED_BY_POINTER.get(v3Pointer);
69
+ };
70
+
71
+ /**
72
+ * Apply a rule's `keys` selector to a V3 node.
73
+ *
74
+ * An exclude-mode rule takes everything it does not exclude, extensions
75
+ * included. An include-mode rule names its keys, so extensions would fall
76
+ * through the gap — which is why each V3 node nominates exactly one rule as its
77
+ * extension sink (`extensions: "passthrough"` in the manifest). Without that,
78
+ * whether a platform's own keys survived depended on which mode happened to
79
+ * claim their parent.
80
+ */
50
81
  const selectKeys = (node, rule) => {
51
82
  if (!rule.keys) return { ...(node || {}) };
52
- return rule.keys.mode === "include"
53
- ? pick(node, rule.keys.values)
54
- : omit(node, rule.keys.values);
83
+ if (rule.keys.mode === "exclude") return omit(node, rule.keys.values);
84
+
85
+ const selected = pick(node, rule.keys.values);
86
+ if (rule.extensions === "passthrough") {
87
+ const claimed = claimedFor(rule.v3Pointer);
88
+ for (const [key, value] of Object.entries(node || {})) {
89
+ if (!claimed.has(key)) selected[key] = value;
90
+ }
91
+ }
92
+ return selected;
93
+ };
94
+
95
+ /**
96
+ * A hole in a V3 array — an index written before an earlier one exists — is
97
+ * skipped by forEach, which would silently shift every later item and lose its
98
+ * position. Refuse instead: the position is data here, and a quiet shift is
99
+ * worse than a stop.
100
+ */
101
+ const assertDense = (array, label) => {
102
+ if (!Array.isArray(array)) return array;
103
+ for (let i = 0; i < array.length; i += 1) {
104
+ if (!(i in array)) {
105
+ throw new Error(
106
+ `v4 decompose: ${label} has a hole at index ${i} (length ${array.length}). ` +
107
+ "Array position carries meaning here, so the gap must be filled or the entry removed before decomposing."
108
+ );
109
+ }
110
+ }
111
+ return array;
55
112
  };
56
113
 
57
114
  const isEmpty = (v) =>
@@ -153,17 +210,19 @@ const decompose = (v3, { idFactory } = {}) => {
153
210
  * malformed data. Failing loudly beats losing an entry.
154
211
  */
155
212
  const mergeSource = (items, rule, sourceName) => {
156
- const seen = new Set();
213
+ const seen = new Map();
214
+ assertDense(items, sourceName);
157
215
  items.forEach((item, index) => {
158
216
  const correlationValue = item[correlateBy];
159
217
  if (correlationValue !== undefined) {
160
218
  if (seen.has(correlationValue)) {
161
219
  throw new Error(
162
- `v4 decompose: ${sourceName}[${index}] repeats ${correlateBy} "${correlationValue}". ` +
220
+ `v4 decompose: ${sourceName}[${index}] repeats ${correlateBy} "${correlationValue}", ` +
221
+ `already used by ${sourceName}[${seen.get(correlationValue)}]. ` +
163
222
  `${correlateBy} must be unique within an array — it is what correlates the two Title sources.`
164
223
  );
165
224
  }
166
- seen.add(correlationValue);
225
+ seen.set(correlationValue, index);
167
226
  }
168
227
  Object.assign(upsert(correlationValue), selectKeys(item, rule));
169
228
  });
@@ -171,6 +230,10 @@ const decompose = (v3, { idFactory } = {}) => {
171
230
 
172
231
  // titlesToBeSold order leads; ownership-only titles are appended after.
173
232
  mergeSource(titleItems, titlesRule, "propertyPack.titlesToBeSold");
233
+ // Which titles V3 actually listed under titlesToBeSold, as opposed to knowing
234
+ // only from ownership. Without this, a title whose sole titlesToBeSold key is
235
+ // titleNumber is indistinguishable from one that was never listed there.
236
+ const listedUnderTitles = new Set(titles);
174
237
  mergeSource(
175
238
  ownershipItems,
176
239
  ownershipsRule,
@@ -182,7 +245,7 @@ const decompose = (v3, { idFactory } = {}) => {
182
245
  });
183
246
 
184
247
  // --- Person + participant context ---------------------------------------
185
- const participants = v3.participants || [];
248
+ const participants = assertDense(v3.participants || [], "participants");
186
249
  const persons = [];
187
250
  const personIndexById = new Map();
188
251
  const participantContext = [];
@@ -225,7 +288,7 @@ const decompose = (v3, { idFactory } = {}) => {
225
288
  assignIfPresent(
226
289
  transaction,
227
290
  "titlesToBeSold",
228
- titles.map((t) => t.id)
291
+ titles.filter((t) => listedUnderTitles.has(t)).map((t) => t.id)
229
292
  );
230
293
  assignIfPresent(transaction, "participants", participantContext);
231
294
 
@@ -392,31 +455,41 @@ const recompose = ({
392
455
  const propertyPack = omit(Property || {}, ["id"]);
393
456
 
394
457
  // Split each Title back into the two source arrays it was merged from.
395
- // A key shared by both sources is written to every array that gets an entry;
396
- // a Title carrying *only* shared keys goes to the primarySource array alone.
458
+ // Transaction.titlesToBeSold says which titles V3 listed under titlesToBeSold,
459
+ // so an entry is emitted for every one of them — including a title whose only
460
+ // key there was titleNumber, which is every transaction between the seller
461
+ // naming the title and the deeds arriving.
397
462
  const titlesOwned = titlesRule.keys.values;
398
463
  const ownershipsOwned = ownershipsRule.keys.values;
464
+ const schemaKeys = new Set([...titlesOwned, ...ownershipsOwned, "id"]);
399
465
 
400
466
  const titlesToBeSold = [];
401
467
  const ownershipsToBeTransferred = [];
402
468
 
403
- // Transaction.titlesToBeSold is the authoritative order for Title entities.
404
469
  const titleById = new Map(titles.map((t) => [t.id, t]));
405
- const orderedTitles = (transaction.titlesToBeSold || []).map(
406
- (id, index) => titleById.get(id) || titles[index]
407
- );
408
- const titleList = orderedTitles.length ? orderedTitles.filter(Boolean) : titles;
470
+ const listedIds = transaction.titlesToBeSold || [];
471
+ const listed = listedIds.map((id) => titleById.get(id)).filter(Boolean);
472
+ // Ordered by the listed titles first, then any known only from ownership.
473
+ const titleList = [...listed, ...titles.filter((t) => !listedIds.includes(t.id))];
409
474
 
410
475
  for (const title of titleList) {
411
- const titlesPart = pick(title, titlesOwned);
412
476
  const ownershipPart = pick(title, ownershipsOwned);
413
-
414
- const hasTitlesOnly = Object.keys(omit(titlesPart, SHARED_TITLE_KEYS)).length > 0;
415
- const hasOwnershipOnly =
477
+ const hasOwnership =
416
478
  Object.keys(omit(ownershipPart, SHARED_TITLE_KEYS)).length > 0;
417
479
 
418
- if (hasTitlesOnly || !hasOwnershipOnly) titlesToBeSold.push(titlesPart);
419
- if (hasOwnershipOnly) ownershipsToBeTransferred.push(ownershipPart);
480
+ if (listed.includes(title)) {
481
+ const titlesPart = pick(title, titlesOwned);
482
+ // Extension keys go back to the rule that took them (titlesToBeSold is
483
+ // the declared sink); see mapping.extensionPolicy.
484
+ if (titlesRule.extensions === "passthrough") {
485
+ for (const [key, value] of Object.entries(title)) {
486
+ if (!schemaKeys.has(key)) titlesPart[key] = value;
487
+ }
488
+ }
489
+ titlesToBeSold.push(titlesPart);
490
+ }
491
+
492
+ if (hasOwnership) ownershipsToBeTransferred.push(ownershipPart);
420
493
  }
421
494
 
422
495
  assignIfPresent(propertyPack, "titlesToBeSold", titlesToBeSold);