@pdtf/schemas 3.6.0-dev.21 → 3.6.0-dev.22
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.
- package/docs/v4-mapping-and-recomposition.md +5 -2
- package/package.json +1 -1
- package/src/schemas/v4/Gift.json +1 -1
- package/src/schemas/v4/Offer.json +1 -1
- package/src/schemas/v4/Organisation.json +1 -1
- package/src/schemas/v4/Person.json +1 -1
- package/src/schemas/v4/Property.json +1 -1
- package/src/schemas/v4/Representation.json +1 -1
- package/src/schemas/v4/SellerCapacity.json +1 -1
- package/src/schemas/v4/Title.json +1 -1
- package/src/schemas/v4/Transaction.json +2 -2
- package/src/schemas/v4/TransactionRole.json +1 -1
- package/src/schemas/v4/mapping.json +21 -2
- package/src/utils/generateV4Schemas.js +19 -2
- package/src/utils/v4.js +95 -22
|
@@ -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.
|
|
143
|
-
`titlesToBeSold
|
|
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
package/src/schemas/v4/Gift.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
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.
|
|
9
|
+
"packageVersion": "3.6.0-dev.22",
|
|
10
10
|
"v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
|
|
11
11
|
"combinedPath": "src/schemas/v3/combined.json",
|
|
12
12
|
"combinedSha256": "1c6c5812f73a36d3674d5fb7b5eb1f6ce524d09c4195e281e078b9e41ea94f6d"
|
|
@@ -6,7 +6,7 @@
|
|
|
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.
|
|
9
|
+
"packageVersion": "3.6.0-dev.22",
|
|
10
10
|
"v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
|
|
11
11
|
"combinedPath": "src/schemas/v3/combined.json",
|
|
12
12
|
"combinedSha256": "1c6c5812f73a36d3674d5fb7b5eb1f6ce524d09c4195e281e078b9e41ea94f6d"
|
|
@@ -6,7 +6,7 @@
|
|
|
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.
|
|
9
|
+
"packageVersion": "3.6.0-dev.22",
|
|
10
10
|
"v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
|
|
11
11
|
"combinedPath": "src/schemas/v3/combined.json",
|
|
12
12
|
"combinedSha256": "1c6c5812f73a36d3674d5fb7b5eb1f6ce524d09c4195e281e078b9e41ea94f6d"
|
|
@@ -6,7 +6,7 @@
|
|
|
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.
|
|
9
|
+
"packageVersion": "3.6.0-dev.22",
|
|
10
10
|
"v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
|
|
11
11
|
"combinedPath": "src/schemas/v3/combined.json",
|
|
12
12
|
"combinedSha256": "1c6c5812f73a36d3674d5fb7b5eb1f6ce524d09c4195e281e078b9e41ea94f6d"
|
|
@@ -6,7 +6,7 @@
|
|
|
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.
|
|
9
|
+
"packageVersion": "3.6.0-dev.22",
|
|
10
10
|
"v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
|
|
11
11
|
"combinedPath": "src/schemas/v3/combined.json",
|
|
12
12
|
"combinedSha256": "1c6c5812f73a36d3674d5fb7b5eb1f6ce524d09c4195e281e078b9e41ea94f6d"
|
|
@@ -6,7 +6,7 @@
|
|
|
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.
|
|
9
|
+
"packageVersion": "3.6.0-dev.22",
|
|
10
10
|
"v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
|
|
11
11
|
"combinedPath": "src/schemas/v3/combined.json",
|
|
12
12
|
"combinedSha256": "1c6c5812f73a36d3674d5fb7b5eb1f6ce524d09c4195e281e078b9e41ea94f6d"
|
|
@@ -6,7 +6,7 @@
|
|
|
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.
|
|
9
|
+
"packageVersion": "3.6.0-dev.22",
|
|
10
10
|
"v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
|
|
11
11
|
"combinedPath": "src/schemas/v3/combined.json",
|
|
12
12
|
"combinedSha256": "1c6c5812f73a36d3674d5fb7b5eb1f6ce524d09c4195e281e078b9e41ea94f6d"
|
|
@@ -6,7 +6,7 @@
|
|
|
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.
|
|
9
|
+
"packageVersion": "3.6.0-dev.22",
|
|
10
10
|
"v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
|
|
11
11
|
"combinedPath": "src/schemas/v3/combined.json",
|
|
12
12
|
"combinedSha256": "1c6c5812f73a36d3674d5fb7b5eb1f6ce524d09c4195e281e078b9e41ea94f6d"
|
|
@@ -6,7 +6,7 @@
|
|
|
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.
|
|
9
|
+
"packageVersion": "3.6.0-dev.22",
|
|
10
10
|
"v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
|
|
11
11
|
"combinedPath": "src/schemas/v3/combined.json",
|
|
12
12
|
"combinedSha256": "1c6c5812f73a36d3674d5fb7b5eb1f6ce524d09c4195e281e078b9e41ea94f6d"
|
|
@@ -1396,7 +1396,7 @@
|
|
|
1396
1396
|
},
|
|
1397
1397
|
"titlesToBeSold": {
|
|
1398
1398
|
"title": "Titles to be Sold",
|
|
1399
|
-
"description": "
|
|
1399
|
+
"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
1400
|
"type": "array",
|
|
1401
1401
|
"minItems": 1,
|
|
1402
1402
|
"items": {
|
|
@@ -6,7 +6,7 @@
|
|
|
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.
|
|
9
|
+
"packageVersion": "3.6.0-dev.22",
|
|
10
10
|
"v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
|
|
11
11
|
"combinedPath": "src/schemas/v3/combined.json",
|
|
12
12
|
"combinedSha256": "1c6c5812f73a36d3674d5fb7b5eb1f6ce524d09c4195e281e078b9e41ea94f6d"
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"generator": "src/utils/generateV4Schemas.js",
|
|
6
6
|
"source": {
|
|
7
7
|
"packageName": "@pdtf/schemas",
|
|
8
|
-
"packageVersion": "3.6.0-dev.
|
|
8
|
+
"packageVersion": "3.6.0-dev.22",
|
|
9
9
|
"v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
|
|
10
10
|
"combinedPath": "src/schemas/v3/combined.json",
|
|
11
11
|
"combinedSha256": "1c6c5812f73a36d3674d5fb7b5eb1f6ce524d09c4195e281e078b9e41ea94f6d"
|
|
@@ -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:
|
|
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
|
-
"
|
|
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:
|
|
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
|
-
/**
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
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.
|
|
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
|
-
//
|
|
396
|
-
//
|
|
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
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const titleList =
|
|
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 (
|
|
419
|
-
|
|
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);
|