@pdtf/schemas 3.6.0-dev.30 → 3.6.0-dev.33

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.
@@ -343,6 +343,63 @@ transaction already carries the titles to be sold. It is emitted for **every**
343
343
  seller, whether or not a capacity has been declared, so the credential embodying
344
344
  the Seller role is never missing while a form is still being filled in.
345
345
 
346
+ ## Property–Title links
347
+
348
+ `PropertyTitleLink` is the one relationship credential that references no
349
+ Transaction (spec 01 D34). It says a UPRN is related to a title, as at a date,
350
+ on a stated basis:
351
+
352
+ ```jsonc
353
+ { "id": "urn:pdtf:propertyTitleLink:ptl-1",
354
+ "property": "urn:pdtf:uprn:100023456789",
355
+ "title": "urn:pdtf:titleNumber:AB10000",
356
+ "basis": { "type": "Register Dataset",
357
+ "dataset": "HMLR Title Number and UPRN Look Up",
358
+ "asOf": "2026-10-01" } }
359
+ ```
360
+
361
+ The relationship is many-to-many. A flat's UPRN can sit on its lease and on the
362
+ block freehold, and the block freehold covers every flat in the block. The link
363
+ is standing, so it is issued once for the UPRN and title and reused by every
364
+ sale, not reissued for each one.
365
+
366
+ **HMLR asserts, the seller selects.** The links say which titles a UPRN sits
367
+ on. `Transaction.titlesToBeSold` says which of those are being conveyed. A
368
+ linked title the transaction does not select is related but not sold, so no
369
+ extra field is needed for it. `linkedTitles()` (exported as `v4LinkedTitles`)
370
+ computes this:
371
+
372
+ ```js
373
+ const { v4LinkedTitles } = require("@pdtf/schemas");
374
+
375
+ v4LinkedTitles({ property, links, transaction, titles });
376
+ // { linked, sold, relatedNotSold, unlinkedSold, superseded, awaitingRegisterLink }
377
+ ```
378
+
379
+ `unlinkedSold` lists titles being sold with no link behind them. That is how a
380
+ missing link shows up, and the conveyancer reports it to HMLR. A wrong link
381
+ needs no handling: the seller does not select it.
382
+
383
+ **One issuer per fact.** Registration status decides who issues a link.
384
+ HM Land Registry issues it for a registered title (`urn:pdtf:titleNumber:*`, on
385
+ `Register Dataset`). The seller's conveyancer issues it for unregistered land
386
+ (`urn:pdtf:unregisteredTitle:*`, on `Epitome of Title`), which HMLR holds
387
+ nothing about. The schema ties `basis` to the title's URN scheme. The trust
388
+ framework checks who the issuer is, because a credential subject cannot vouch
389
+ for its own signer.
390
+
391
+ **First registration.** The Title issued on first registration lists the
392
+ unregistered URN in `alsoKnownAs`. Links and selections that name the old URN
393
+ resolve to the new title number. Once HMLR issues a link from the same UPRN to
394
+ that number, it supersedes the conveyancer's link. Until then the conveyancer's
395
+ link stays in force and the title is listed in `awaitingRegisterLink`.
396
+
397
+ Neither direction of the round trip touches these links. V3 records no
398
+ UPRN-to-title link, so `decompose` does not invent one and `recompose` ignores
399
+ any it is given. The manifest lists the link as `kind: "credential"`,
400
+ `scope: "standing"`. The five transaction-scoped credentials are listed with
401
+ `scope: "transaction"`.
402
+
346
403
  ### If you want an identifier everyone can mint
347
404
 
348
405
  Three patterns, of which V3 already uses two:
package/index.js CHANGED
@@ -829,6 +829,7 @@ const {
829
829
  identifyV3Pointer,
830
830
  resolveV3Pointer,
831
831
  resolveV3PointerAll,
832
+ linkedTitles: v4LinkedTitles,
832
833
  } = require("./src/utils/v4");
833
834
 
834
835
  module.exports = {
@@ -861,4 +862,5 @@ module.exports = {
861
862
  identifyV3Pointer,
862
863
  resolveV3Pointer,
863
864
  resolveV3PointerAll,
865
+ v4LinkedTitles,
864
866
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pdtf/schemas",
3
- "version": "3.6.0-dev.30",
3
+ "version": "3.6.0-dev.33",
4
4
  "description": "Property Data Trust Framework Schemas and Utilities",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -6770,10 +6770,15 @@
6770
6770
  "Not applicable"
6771
6771
  ],
6772
6772
  "ta7ed5Enum": [
6773
+ "Managing Agent",
6773
6774
  "Landlord",
6774
6775
  "Management Company",
6776
+ "Not applicable",
6775
6777
  "Other"
6776
- ]
6778
+ ],
6779
+ "ta7ed5EnumNames": {
6780
+ "Managing Agent": "Yes"
6781
+ }
6777
6782
  }
6778
6783
  },
6779
6784
  "discriminator": {
@@ -26476,6 +26481,30 @@
26476
26481
  },
26477
26482
  "voDataIndoorNo4g": {
26478
26483
  "type": "integer"
26484
+ },
26485
+ "eeCoverage": {
26486
+ "description": "EE predicted mobile coverage score from Ofcom. 4: good in-home and outdoors; 3: variable in-home, good outdoors; 2: good outdoors only; 1: variable outdoors only; 0: poor to none.",
26487
+ "type": "integer",
26488
+ "minimum": 0,
26489
+ "maximum": 4
26490
+ },
26491
+ "h3Coverage": {
26492
+ "description": "Three predicted mobile coverage score from Ofcom. 4: good in-home and outdoors; 3: variable in-home, good outdoors; 2: good outdoors only; 1: variable outdoors only; 0: poor to none.",
26493
+ "type": "integer",
26494
+ "minimum": 0,
26495
+ "maximum": 4
26496
+ },
26497
+ "tfCoverage": {
26498
+ "description": "O2 predicted mobile coverage score from Ofcom. 4: good in-home and outdoors; 3: variable in-home, good outdoors; 2: good outdoors only; 1: variable outdoors only; 0: poor to none.",
26499
+ "type": "integer",
26500
+ "minimum": 0,
26501
+ "maximum": 4
26502
+ },
26503
+ "voCoverage": {
26504
+ "description": "Vodafone predicted mobile coverage score from Ofcom. 4: good in-home and outdoors; 3: variable in-home, good outdoors; 2: good outdoors only; 1: variable outdoors only; 0: poor to none.",
26505
+ "type": "integer",
26506
+ "minimum": 0,
26507
+ "maximum": 4
26479
26508
  }
26480
26509
  }
26481
26510
  }
@@ -2198,6 +2198,10 @@
2198
2198
  voDataOutdoorNo4g
2199
2199
  voDataIndoor
2200
2200
  voDataIndoorNo4g
2201
+ eeCoverage
2202
+ h3Coverage
2203
+ tfCoverage
2204
+ voCoverage
2201
2205
  otherServices
2202
2206
  insurance
2203
2207
  difficultiesObtainingInsurance
@@ -380,8 +380,17 @@
380
380
  "ta7ed5Ref": "4.3.2",
381
381
  "title": "Who collects the ground rent?",
382
382
  "enum": [
383
+ "Managing Agent",
383
384
  "Landlord",
384
385
  "Management Company",
386
+ "Not applicable",
387
+ "Other"
388
+ ],
389
+ "enumNames": [
390
+ "Yes",
391
+ "Landlord",
392
+ "Management Company",
393
+ "Not applicable",
385
394
  "Other"
386
395
  ]
387
396
  }
@@ -21222,6 +21222,30 @@
21222
21222
  },
21223
21223
  "voDataIndoorNo4g": {
21224
21224
  "type": "integer"
21225
+ },
21226
+ "eeCoverage": {
21227
+ "description": "EE predicted mobile coverage score from Ofcom. 4: good in-home and outdoors; 3: variable in-home, good outdoors; 2: good outdoors only; 1: variable outdoors only; 0: poor to none.",
21228
+ "type": "integer",
21229
+ "minimum": 0,
21230
+ "maximum": 4
21231
+ },
21232
+ "h3Coverage": {
21233
+ "description": "Three predicted mobile coverage score from Ofcom. 4: good in-home and outdoors; 3: variable in-home, good outdoors; 2: good outdoors only; 1: variable outdoors only; 0: poor to none.",
21234
+ "type": "integer",
21235
+ "minimum": 0,
21236
+ "maximum": 4
21237
+ },
21238
+ "tfCoverage": {
21239
+ "description": "O2 predicted mobile coverage score from Ofcom. 4: good in-home and outdoors; 3: variable in-home, good outdoors; 2: good outdoors only; 1: variable outdoors only; 0: poor to none.",
21240
+ "type": "integer",
21241
+ "minimum": 0,
21242
+ "maximum": 4
21243
+ },
21244
+ "voCoverage": {
21245
+ "description": "Vodafone predicted mobile coverage score from Ofcom. 4: good in-home and outdoors; 3: variable in-home, good outdoors; 2: good outdoors only; 1: variable outdoors only; 0: poor to none.",
21246
+ "type": "integer",
21247
+ "minimum": 0,
21248
+ "maximum": 4
21225
21249
  }
21226
21250
  }
21227
21251
  }
@@ -2793,7 +2793,11 @@
2793
2793
  "voDataOutdoor": "integer",
2794
2794
  "voDataOutdoorNo4g": "integer",
2795
2795
  "voDataIndoor": "integer",
2796
- "voDataIndoorNo4g": "integer"
2796
+ "voDataIndoorNo4g": "integer",
2797
+ "eeCoverage": "integer",
2798
+ "h3Coverage": "integer",
2799
+ "tfCoverage": "integer",
2800
+ "voCoverage": "integer"
2797
2801
  }
2798
2802
  },
2799
2803
  "otherServices": "string"
@@ -6,10 +6,10 @@
6
6
  "description": "A gift of funds towards a purchase, made by a giftor. Implies the Giftor role.",
7
7
  "x-pdtf-source": {
8
8
  "packageName": "@pdtf/schemas",
9
- "packageVersion": "3.6.0-dev.30",
9
+ "packageVersion": "3.6.0-dev.33",
10
10
  "v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
11
11
  "combinedPath": "src/schemas/v3/combined.json",
12
- "combinedSha256": "4e95946a946b37088b153052034fd0584cc27a6eb53f78ede52cd0ab1fde669f"
12
+ "combinedSha256": "52ce2e5cb67b32ad9e80d6aae2bb7f5baf842d2ea7190ded5e23df72a0dae91b"
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.30",
9
+ "packageVersion": "3.6.0-dev.33",
10
10
  "v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
11
11
  "combinedPath": "src/schemas/v3/combined.json",
12
- "combinedSha256": "4e95946a946b37088b153052034fd0584cc27a6eb53f78ede52cd0ab1fde669f"
12
+ "combinedSha256": "52ce2e5cb67b32ad9e80d6aae2bb7f5baf842d2ea7190ded5e23df72a0dae91b"
13
13
  },
14
14
  "properties": {
15
15
  "id": {
@@ -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.30",
9
+ "packageVersion": "3.6.0-dev.33",
10
10
  "v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
11
11
  "combinedPath": "src/schemas/v3/combined.json",
12
- "combinedSha256": "4e95946a946b37088b153052034fd0584cc27a6eb53f78ede52cd0ab1fde669f"
12
+ "combinedSha256": "52ce2e5cb67b32ad9e80d6aae2bb7f5baf842d2ea7190ded5e23df72a0dae91b"
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.30",
9
+ "packageVersion": "3.6.0-dev.33",
10
10
  "v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
11
11
  "combinedPath": "src/schemas/v3/combined.json",
12
- "combinedSha256": "4e95946a946b37088b153052034fd0584cc27a6eb53f78ede52cd0ab1fde669f"
12
+ "combinedSha256": "52ce2e5cb67b32ad9e80d6aae2bb7f5baf842d2ea7190ded5e23df72a0dae91b"
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.30",
9
+ "packageVersion": "3.6.0-dev.33",
10
10
  "v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
11
11
  "combinedPath": "src/schemas/v3/combined.json",
12
- "combinedSha256": "4e95946a946b37088b153052034fd0584cc27a6eb53f78ede52cd0ab1fde669f"
12
+ "combinedSha256": "52ce2e5cb67b32ad9e80d6aae2bb7f5baf842d2ea7190ded5e23df72a0dae91b"
13
13
  },
14
14
  "properties": {
15
15
  "address": {
@@ -15164,6 +15164,30 @@
15164
15164
  },
15165
15165
  "voDataIndoorNo4g": {
15166
15166
  "type": "integer"
15167
+ },
15168
+ "eeCoverage": {
15169
+ "description": "EE predicted mobile coverage score from Ofcom. 4: good in-home and outdoors; 3: variable in-home, good outdoors; 2: good outdoors only; 1: variable outdoors only; 0: poor to none.",
15170
+ "type": "integer",
15171
+ "minimum": 0,
15172
+ "maximum": 4
15173
+ },
15174
+ "h3Coverage": {
15175
+ "description": "Three predicted mobile coverage score from Ofcom. 4: good in-home and outdoors; 3: variable in-home, good outdoors; 2: good outdoors only; 1: variable outdoors only; 0: poor to none.",
15176
+ "type": "integer",
15177
+ "minimum": 0,
15178
+ "maximum": 4
15179
+ },
15180
+ "tfCoverage": {
15181
+ "description": "O2 predicted mobile coverage score from Ofcom. 4: good in-home and outdoors; 3: variable in-home, good outdoors; 2: good outdoors only; 1: variable outdoors only; 0: poor to none.",
15182
+ "type": "integer",
15183
+ "minimum": 0,
15184
+ "maximum": 4
15185
+ },
15186
+ "voCoverage": {
15187
+ "description": "Vodafone predicted mobile coverage score from Ofcom. 4: good in-home and outdoors; 3: variable in-home, good outdoors; 2: good outdoors only; 1: variable outdoors only; 0: poor to none.",
15188
+ "type": "integer",
15189
+ "minimum": 0,
15190
+ "maximum": 4
15167
15191
  }
15168
15192
  }
15169
15193
  }
@@ -0,0 +1,183 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://trust.propdata.org.uk/schemas/v4/entities/propertyTitleLink.json",
4
+ "type": "object",
5
+ "title": "Property–Title Link Entity",
6
+ "description": "A standing assertion that a property (UPRN) is related to a title. Many-to-many: one UPRN can sit on several titles, and one title (a block freehold, say) can cover many UPRNs. Issued by HM Land Registry for a registered title, and by the seller's conveyancer, on the epitome of title, for unregistered land. References no Transaction: a Transaction selects the titles being sold from the linked titles through titlesToBeSold, and a linked title it does not select is related but not sold.",
7
+ "x-pdtf-source": {
8
+ "packageName": "@pdtf/schemas",
9
+ "packageVersion": "3.6.0-dev.33",
10
+ "v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
11
+ "combinedPath": "src/schemas/v3/combined.json",
12
+ "combinedSha256": "52ce2e5cb67b32ad9e80d6aae2bb7f5baf842d2ea7190ded5e23df72a0dae91b"
13
+ },
14
+ "properties": {
15
+ "id": {
16
+ "title": "Property–Title Link URN",
17
+ "description": "Uniform Resource Name identifier for the link, urn:pdtf:propertyTitleLink:{id}",
18
+ "type": "string",
19
+ "pattern": "^urn:[a-z0-9][a-z0-9-]{0,31}:[a-zA-Z0-9._~:/?#\\[\\]@!$&'()*+,;=-]+$"
20
+ },
21
+ "property": {
22
+ "title": "Property Reference",
23
+ "description": "URN referencing the Property entity, urn:pdtf:uprn:{uprn}",
24
+ "type": "string",
25
+ "pattern": "^urn:[a-z0-9][a-z0-9-]{0,31}:[a-zA-Z0-9._~:/?#\\[\\]@!$&'()*+,;=-]+$"
26
+ },
27
+ "title": {
28
+ "title": "Title Reference",
29
+ "description": "URN referencing the Title entity: urn:pdtf:titleNumber:{number} for a registered title, urn:pdtf:unregisteredTitle:{id} for unregistered land",
30
+ "type": "string",
31
+ "pattern": "^urn:[a-z0-9][a-z0-9-]{0,31}:[a-zA-Z0-9._~:/?#\\[\\]@!$&'()*+,;=-]+$"
32
+ },
33
+ "basis": {
34
+ "title": "Basis of the link",
35
+ "description": "What the link rests on, and the date it speaks as at. A registered title is linked on the register's own UPRN-to-title data; unregistered land on the epitome of title.",
36
+ "type": "object",
37
+ "properties": {
38
+ "type": {
39
+ "title": "Basis type",
40
+ "type": "string",
41
+ "enum": [
42
+ "Register Dataset",
43
+ "Epitome of Title"
44
+ ]
45
+ },
46
+ "asOf": {
47
+ "title": "As at",
48
+ "description": "The date the link speaks as at: the dataset's effective date, or the date the epitome was examined",
49
+ "type": "string",
50
+ "format": "date"
51
+ }
52
+ },
53
+ "required": [
54
+ "type",
55
+ "asOf"
56
+ ],
57
+ "discriminator": {
58
+ "propertyName": "type"
59
+ },
60
+ "oneOf": [
61
+ {
62
+ "properties": {
63
+ "type": {
64
+ "type": "string",
65
+ "enum": [
66
+ "Register Dataset"
67
+ ]
68
+ },
69
+ "dataset": {
70
+ "title": "Dataset",
71
+ "description": "The register dataset the link is taken from, e.g. \"HMLR Title Number and UPRN Look Up\"",
72
+ "type": "string"
73
+ },
74
+ "datasetRelease": {
75
+ "title": "Dataset release",
76
+ "description": "The release the link is taken from, e.g. the full-file name of HMLR's monthly release (\"LR_UPRN_FULL_APR.csv\"). The dataset records no date per row, so asOf is the release date.",
77
+ "type": "string"
78
+ }
79
+ },
80
+ "required": [
81
+ "dataset"
82
+ ]
83
+ },
84
+ {
85
+ "properties": {
86
+ "type": {
87
+ "type": "string",
88
+ "enum": [
89
+ "Epitome of Title"
90
+ ]
91
+ },
92
+ "rootOfTitle": {
93
+ "title": "Root of title",
94
+ "description": "The good root of title the epitome starts from",
95
+ "type": "object",
96
+ "properties": {
97
+ "description": {
98
+ "title": "Description",
99
+ "description": "e.g. \"Conveyance on sale\"",
100
+ "type": "string"
101
+ },
102
+ "date": {
103
+ "title": "Date",
104
+ "description": "Date of the root document",
105
+ "type": "string",
106
+ "format": "date"
107
+ }
108
+ }
109
+ },
110
+ "epitomeDigest": {
111
+ "title": "Epitome digest",
112
+ "description": "SHA-256 digest of the epitome of title as examined, in the form sha256:{hex} (spec 02 §6.6)",
113
+ "type": "string",
114
+ "pattern": "^sha256:[0-9a-f]{64}$"
115
+ }
116
+ },
117
+ "required": [
118
+ "rootOfTitle"
119
+ ]
120
+ }
121
+ ]
122
+ }
123
+ },
124
+ "required": [
125
+ "property",
126
+ "title",
127
+ "basis"
128
+ ],
129
+ "allOf": [
130
+ {
131
+ "if": {
132
+ "properties": {
133
+ "title": {
134
+ "type": "string",
135
+ "pattern": "^urn:pdtf:titleNumber:"
136
+ }
137
+ },
138
+ "required": [
139
+ "title"
140
+ ]
141
+ },
142
+ "then": {
143
+ "properties": {
144
+ "basis": {
145
+ "type": "object",
146
+ "properties": {
147
+ "type": {
148
+ "type": "string",
149
+ "const": "Register Dataset"
150
+ }
151
+ }
152
+ }
153
+ }
154
+ }
155
+ },
156
+ {
157
+ "if": {
158
+ "properties": {
159
+ "title": {
160
+ "type": "string",
161
+ "pattern": "^urn:pdtf:unregisteredTitle:"
162
+ }
163
+ },
164
+ "required": [
165
+ "title"
166
+ ]
167
+ },
168
+ "then": {
169
+ "properties": {
170
+ "basis": {
171
+ "type": "object",
172
+ "properties": {
173
+ "type": {
174
+ "type": "string",
175
+ "const": "Epitome of Title"
176
+ }
177
+ }
178
+ }
179
+ }
180
+ }
181
+ }
182
+ ]
183
+ }
@@ -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.30",
9
+ "packageVersion": "3.6.0-dev.33",
10
10
  "v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
11
11
  "combinedPath": "src/schemas/v3/combined.json",
12
- "combinedSha256": "4e95946a946b37088b153052034fd0584cc27a6eb53f78ede52cd0ab1fde669f"
12
+ "combinedSha256": "52ce2e5cb67b32ad9e80d6aae2bb7f5baf842d2ea7190ded5e23df72a0dae91b"
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.30",
9
+ "packageVersion": "3.6.0-dev.33",
10
10
  "v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
11
11
  "combinedPath": "src/schemas/v3/combined.json",
12
- "combinedSha256": "4e95946a946b37088b153052034fd0584cc27a6eb53f78ede52cd0ab1fde669f"
12
+ "combinedSha256": "52ce2e5cb67b32ad9e80d6aae2bb7f5baf842d2ea7190ded5e23df72a0dae91b"
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.30",
9
+ "packageVersion": "3.6.0-dev.33",
10
10
  "v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
11
11
  "combinedPath": "src/schemas/v3/combined.json",
12
- "combinedSha256": "4e95946a946b37088b153052034fd0584cc27a6eb53f78ede52cd0ab1fde669f"
12
+ "combinedSha256": "52ce2e5cb67b32ad9e80d6aae2bb7f5baf842d2ea7190ded5e23df72a0dae91b"
13
13
  },
14
14
  "properties": {
15
15
  "titleNumber": {
@@ -11184,10 +11184,15 @@
11184
11184
  "Not applicable"
11185
11185
  ],
11186
11186
  "ta7ed5Enum": [
11187
+ "Managing Agent",
11187
11188
  "Landlord",
11188
11189
  "Management Company",
11190
+ "Not applicable",
11189
11191
  "Other"
11190
- ]
11192
+ ],
11193
+ "ta7ed5EnumNames": {
11194
+ "Managing Agent": "Yes"
11195
+ }
11191
11196
  }
11192
11197
  },
11193
11198
  "discriminator": {
@@ -14655,6 +14660,16 @@
14655
14660
  "description": "Uniform Resource Name identifier for the title",
14656
14661
  "type": "string",
14657
14662
  "pattern": "^urn:[a-z0-9][a-z0-9-]{0,31}:[a-zA-Z0-9._~:/?#\\[\\]@!$&'()*+,;=-]+$"
14663
+ },
14664
+ "alsoKnownAs": {
14665
+ "title": "Also Known As",
14666
+ "description": "Earlier identifiers for this title — on first registration, the urn:pdtf:unregisteredTitle:{id} it was known by before a title number was issued.",
14667
+ "type": "array",
14668
+ "items": {
14669
+ "type": "string",
14670
+ "pattern": "^urn:[a-z0-9][a-z0-9-]{0,31}:[a-zA-Z0-9._~:/?#\\[\\]@!$&'()*+,;=-]+$"
14671
+ },
14672
+ "uniqueItems": true
14658
14673
  }
14659
14674
  }
14660
14675
  }
@@ -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.30",
9
+ "packageVersion": "3.6.0-dev.33",
10
10
  "v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
11
11
  "combinedPath": "src/schemas/v3/combined.json",
12
- "combinedSha256": "4e95946a946b37088b153052034fd0584cc27a6eb53f78ede52cd0ab1fde669f"
12
+ "combinedSha256": "52ce2e5cb67b32ad9e80d6aae2bb7f5baf842d2ea7190ded5e23df72a0dae91b"
13
13
  },
14
14
  "properties": {
15
15
  "transactionId": {
@@ -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.30",
9
+ "packageVersion": "3.6.0-dev.33",
10
10
  "v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
11
11
  "combinedPath": "src/schemas/v3/combined.json",
12
- "combinedSha256": "4e95946a946b37088b153052034fd0584cc27a6eb53f78ede52cd0ab1fde669f"
12
+ "combinedSha256": "52ce2e5cb67b32ad9e80d6aae2bb7f5baf842d2ea7190ded5e23df72a0dae91b"
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.30",
8
+ "packageVersion": "3.6.0-dev.33",
9
9
  "v3SchemaId": "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json",
10
10
  "combinedPath": "src/schemas/v3/combined.json",
11
- "combinedSha256": "4e95946a946b37088b153052034fd0584cc27a6eb53f78ede52cd0ab1fde669f"
11
+ "combinedSha256": "52ce2e5cb67b32ad9e80d6aae2bb7f5baf842d2ea7190ded5e23df72a0dae91b"
12
12
  },
13
13
  "entities": {
14
14
  "Property": {
@@ -39,27 +39,38 @@
39
39
  "Representation": {
40
40
  "$id": "https://trust.propdata.org.uk/schemas/v4/entities/representation.json",
41
41
  "kind": "credential",
42
- "idScheme": "did"
42
+ "scope": "transaction",
43
+ "idScheme": "urn"
43
44
  },
44
45
  "TransactionRole": {
45
46
  "$id": "https://trust.propdata.org.uk/schemas/v4/entities/transactionRole.json",
46
47
  "kind": "credential",
47
- "idScheme": "did"
48
+ "scope": "transaction",
49
+ "idScheme": "urn"
48
50
  },
49
51
  "Gift": {
50
52
  "$id": "https://trust.propdata.org.uk/schemas/v4/entities/gift.json",
51
53
  "kind": "credential",
52
- "idScheme": "did"
54
+ "scope": "transaction",
55
+ "idScheme": "urn"
53
56
  },
54
57
  "SellerCapacity": {
55
58
  "$id": "https://trust.propdata.org.uk/schemas/v4/entities/sellerCapacity.json",
56
59
  "kind": "credential",
57
- "idScheme": "did"
60
+ "scope": "transaction",
61
+ "idScheme": "urn"
58
62
  },
59
63
  "Offer": {
60
64
  "$id": "https://trust.propdata.org.uk/schemas/v4/entities/offer.json",
61
65
  "kind": "credential",
62
- "idScheme": "did"
66
+ "scope": "transaction",
67
+ "idScheme": "urn"
68
+ },
69
+ "PropertyTitleLink": {
70
+ "$id": "https://trust.propdata.org.uk/schemas/v4/entities/propertyTitleLink.json",
71
+ "kind": "credential",
72
+ "scope": "standing",
73
+ "idScheme": "urn"
63
74
  }
64
75
  },
65
76
  "roundTrip": {
@@ -74,7 +85,7 @@
74
85
  "Gift",
75
86
  "TransactionRole"
76
87
  ],
77
- "note": "All of these are required to reconstruct a V3 instance. The credentials are not redundant copies: role and every relationship live only on them, so that revoking a credential removes what it asserts rather than leaving a second copy on the transaction. Organisation is the one entity outside the round trip.",
88
+ "note": "All of these are required to reconstruct a V3 instance. The credentials are not redundant copies: role and every relationship live only on them, so that revoking a credential removes what it asserts rather than leaving a second copy on the transaction. Organisation and PropertyTitleLink are outside the round trip: V3 records a participant's firm only as free text, and records no UPRN-to-title link at all, so decompose does not invent either and recompose does not need them.",
78
89
  "roleImpliedByCredential": {
79
90
  "SellerCapacity": "Seller",
80
91
  "Offer": "Buyer",
@@ -874,6 +885,22 @@
874
885
  "notes": "An offer in Transaction.offers that no participant references stays transaction data and yields no credential, since Offer requires a buyer."
875
886
  }
876
887
  },
888
+ "standingCredentials": {
889
+ "PropertyTitleLink": {
890
+ "description": "A standing UPRN-to-title link. Not projected from V3. A Transaction's titlesToBeSold selects the titles being sold from the titles linked to its property; see linkedTitles() in src/utils/v4.js.",
891
+ "issuerByTitleScheme": {
892
+ "urn:pdtf:titleNumber:": {
893
+ "issuer": "HM Land Registry",
894
+ "basis": "Register Dataset"
895
+ },
896
+ "urn:pdtf:unregisteredTitle:": {
897
+ "issuer": "Seller's conveyancer",
898
+ "basis": "Epitome of Title"
899
+ }
900
+ },
901
+ "supersession": "On first registration the Title for the new title number lists the unregistered URN in alsoKnownAs. An HMLR link from the same UPRN to that title supersedes the conveyancer's link to the unregistered URN; the conveyancer's link stays in force until one exists."
902
+ }
903
+ },
877
904
  "collisions": [
878
905
  {
879
906
  "key": "titleNumber",
@@ -20,6 +20,9 @@
20
20
  * SellerCapacity — legal capacity of a seller against a title (derived from
21
21
  * the V3 participants "Seller" oneOf branch)
22
22
  * Offer — buyer offer against a transaction
23
+ * PropertyTitleLink — standing UPRN↔title relationship (HMLR for registered
24
+ * titles, the seller's conveyancer for unregistered land);
25
+ * the one credential that references no Transaction
23
26
  *
24
27
  * The run also emits src/schemas/v4/mapping.json — the machine-readable V3⇄V4
25
28
  * pointer mapping consumed by src/utils/v4.js. See
@@ -203,6 +206,26 @@ const CREDENTIAL_ENTITIES = [
203
206
  * role as their own discriminator: "what kind of representation is this" is a
204
207
  * property of the representation, not a duplicated participant attribute.
205
208
  */
209
+ /**
210
+ * Standing credentials: assertions that are true independently of any sale, so
211
+ * they reference no Transaction (D32 as narrowed by D34 in spec 01).
212
+ *
213
+ * PropertyTitleLink says "this UPRN is related to this title". For a registered
214
+ * title HM Land Registry is the only issuer; for unregistered land, which HMLR
215
+ * holds nothing about, the seller's conveyancer issues it on the strength of the
216
+ * epitome of title. The two cases never overlap, so there is one issuer per
217
+ * fact. The seller *selects* from these links via Transaction.titlesToBeSold; a
218
+ * linked title that is not selected is related but not sold.
219
+ *
220
+ * V3 records no such link — it has one propertyPack and the titles being sold,
221
+ * nothing about which titles HMLR relates to the UPRN — so these are outside the
222
+ * round trip: decompose does not invent them and recompose does not need them.
223
+ */
224
+ const STANDING_CREDENTIAL_ENTITIES = ["PropertyTitleLink"];
225
+
226
+ /** Entities whose identifier is a DID; every other entity is a URN. */
227
+ const DID_ENTITIES = ["Transaction", "Person", "Organisation"];
228
+
206
229
  const ROLE_IMPLIED_BY_CREDENTIAL = {
207
230
  SellerCapacity: "Seller",
208
231
  Offer: "Buyer",
@@ -359,6 +382,19 @@ const generateTitle = () => {
359
382
  "Uniform Resource Name identifier for the title"
360
383
  );
361
384
 
385
+ // First registration (spec 01 §9.1 Q5): the Title issued on registration is
386
+ // keyed by its new title number and lists the unregistered identifier it
387
+ // replaces, so links and transactions that name the old URN still resolve.
388
+ // Not in V3, so never emitted by decompose.
389
+ titleItemProps.alsoKnownAs = {
390
+ title: "Also Known As",
391
+ description:
392
+ "Earlier identifiers for this title — on first registration, the urn:pdtf:unregisteredTitle:{id} it was known by before a title number was issued.",
393
+ type: "array",
394
+ items: { type: "string", pattern: URN_PATTERN },
395
+ uniqueItems: true,
396
+ };
397
+
362
398
  const required = [
363
399
  ...new Set([
364
400
  ...(pp.titlesToBeSold.items.required || []),
@@ -839,6 +875,149 @@ const generateOffer = () => {
839
875
  );
840
876
  };
841
877
 
878
+ const generatePropertyTitleLink = () => {
879
+ // Hand-authored: V3 has nothing to derive it from. The first relationship
880
+ // credential with no `transaction` — it is standing, true of the UPRN and
881
+ // the title whether or not either is being sold (spec 01 D34).
882
+ //
883
+ // `basis` is part of what is asserted, not just how the issuer came by it:
884
+ // HMLR's UPRN-to-title data is neither complete nor wholly accurate, so a
885
+ // link is only ever "related as at this date, according to this source".
886
+ // How the issuer obtained it goes in the credential's own `evidence`
887
+ // (spec 02 §6) as usual.
888
+ const REGISTER = "Register Dataset";
889
+ const EPITOME = "Epitome of Title";
890
+ const dateField = (title, description) => ({
891
+ title,
892
+ description,
893
+ type: "string",
894
+ format: "date",
895
+ });
896
+
897
+ const schema = makeSchema(
898
+ "propertyTitleLink",
899
+ "Property–Title Link Entity",
900
+ "A standing assertion that a property (UPRN) is related to a title. Many-to-many: one UPRN can sit on several titles, and one title (a block freehold, say) can cover many UPRNs. Issued by HM Land Registry for a registered title, and by the seller's conveyancer, on the epitome of title, for unregistered land. References no Transaction: a Transaction selects the titles being sold from the linked titles through titlesToBeSold, and a linked title it does not select is related but not sold.",
901
+ {
902
+ id: urnRef(
903
+ "Property–Title Link URN",
904
+ "Uniform Resource Name identifier for the link, urn:pdtf:propertyTitleLink:{id}"
905
+ ),
906
+ property: urnRef(
907
+ "Property Reference",
908
+ "URN referencing the Property entity, urn:pdtf:uprn:{uprn}"
909
+ ),
910
+ title: urnRef(
911
+ "Title Reference",
912
+ "URN referencing the Title entity: urn:pdtf:titleNumber:{number} for a registered title, urn:pdtf:unregisteredTitle:{id} for unregistered land"
913
+ ),
914
+ basis: {
915
+ title: "Basis of the link",
916
+ description:
917
+ "What the link rests on, and the date it speaks as at. A registered title is linked on the register's own UPRN-to-title data; unregistered land on the epitome of title.",
918
+ type: "object",
919
+ properties: {
920
+ type: {
921
+ title: "Basis type",
922
+ type: "string",
923
+ enum: [REGISTER, EPITOME],
924
+ },
925
+ asOf: dateField(
926
+ "As at",
927
+ "The date the link speaks as at: the dataset's effective date, or the date the epitome was examined"
928
+ ),
929
+ },
930
+ required: ["type", "asOf"],
931
+ discriminator: { propertyName: "type" },
932
+ oneOf: [
933
+ {
934
+ properties: {
935
+ type: { type: "string", enum: [REGISTER] },
936
+ dataset: {
937
+ title: "Dataset",
938
+ description:
939
+ "The register dataset the link is taken from, e.g. \"HMLR Title Number and UPRN Look Up\"",
940
+ type: "string",
941
+ },
942
+ datasetRelease: {
943
+ title: "Dataset release",
944
+ description:
945
+ "The release the link is taken from, e.g. the full-file name of HMLR's monthly release (\"LR_UPRN_FULL_APR.csv\"). The dataset records no date per row, so asOf is the release date.",
946
+ type: "string",
947
+ },
948
+ },
949
+ required: ["dataset"],
950
+ },
951
+ {
952
+ properties: {
953
+ type: { type: "string", enum: [EPITOME] },
954
+ rootOfTitle: {
955
+ title: "Root of title",
956
+ description:
957
+ "The good root of title the epitome starts from",
958
+ type: "object",
959
+ properties: {
960
+ description: {
961
+ title: "Description",
962
+ description: "e.g. \"Conveyance on sale\"",
963
+ type: "string",
964
+ },
965
+ date: dateField("Date", "Date of the root document"),
966
+ },
967
+ },
968
+ epitomeDigest: {
969
+ title: "Epitome digest",
970
+ description:
971
+ "SHA-256 digest of the epitome of title as examined, in the form sha256:{hex} (spec 02 §6.6)",
972
+ type: "string",
973
+ pattern: "^sha256:[0-9a-f]{64}$",
974
+ },
975
+ },
976
+ required: ["rootOfTitle"],
977
+ },
978
+ ],
979
+ },
980
+ },
981
+ ["property", "title", "basis"]
982
+ );
983
+
984
+ // One issuer per fact, decided by registration status: a registered title is
985
+ // linked on the register, unregistered land on the epitome. The schema
986
+ // enforces the basis; the issuer (HMLR, or a conveyancer) is checked through
987
+ // the trust framework, since a credential subject cannot name its own signer.
988
+ schema.allOf = [
989
+ {
990
+ if: {
991
+ properties: { title: { type: "string", pattern: "^urn:pdtf:titleNumber:" } },
992
+ required: ["title"],
993
+ },
994
+ then: {
995
+ properties: {
996
+ basis: {
997
+ type: "object",
998
+ properties: { type: { type: "string", const: REGISTER } },
999
+ },
1000
+ },
1001
+ },
1002
+ },
1003
+ {
1004
+ if: {
1005
+ properties: { title: { type: "string", pattern: "^urn:pdtf:unregisteredTitle:" } },
1006
+ required: ["title"],
1007
+ },
1008
+ then: {
1009
+ properties: {
1010
+ basis: {
1011
+ type: "object",
1012
+ properties: { type: { type: "string", const: EPITOME } },
1013
+ },
1014
+ },
1015
+ },
1016
+ },
1017
+ ];
1018
+ return schema;
1019
+ };
1020
+
842
1021
  // ---------------------------------------------------------------------------
843
1022
  // Main — generate all schemas and write to src/schemas/v4/
844
1023
  // ---------------------------------------------------------------------------
@@ -853,6 +1032,7 @@ const generators = {
853
1032
  Gift: generateGift,
854
1033
  SellerCapacity: generateSellerCapacity,
855
1034
  Offer: generateOffer,
1035
+ PropertyTitleLink: generatePropertyTitleLink,
856
1036
  };
857
1037
 
858
1038
  if (!fs.existsSync(V4_DIR)) {
@@ -1226,17 +1406,22 @@ const mapping = {
1226
1406
  $id: schema.$id,
1227
1407
  kind: DERIVED_ENTITIES.includes(name)
1228
1408
  ? "derived"
1229
- : CREDENTIAL_ENTITIES.includes(name)
1409
+ : CREDENTIAL_ENTITIES.includes(name) ||
1410
+ STANDING_CREDENTIAL_ENTITIES.includes(name)
1230
1411
  ? "credential"
1231
1412
  : "standalone",
1232
- idScheme: name === "Property" || name === "Title" ? "urn" : "did",
1413
+ // Relationship credentials are either scoped to one Transaction (and
1414
+ // carry `transaction`) or standing (and carry none) — D32 / D34.
1415
+ ...(CREDENTIAL_ENTITIES.includes(name) && { scope: "transaction" }),
1416
+ ...(STANDING_CREDENTIAL_ENTITIES.includes(name) && { scope: "standing" }),
1417
+ idScheme: DID_ENTITIES.includes(name) ? "did" : "urn",
1233
1418
  },
1234
1419
  ])
1235
1420
  ),
1236
1421
  roundTrip: {
1237
1422
  entities: [...DERIVED_ENTITIES, ...CREDENTIAL_ENTITIES],
1238
1423
  note:
1239
- "All of these are required to reconstruct a V3 instance. The credentials are not redundant copies: role and every relationship live only on them, so that revoking a credential removes what it asserts rather than leaving a second copy on the transaction. Organisation is the one entity outside the round trip.",
1424
+ "All of these are required to reconstruct a V3 instance. The credentials are not redundant copies: role and every relationship live only on them, so that revoking a credential removes what it asserts rather than leaving a second copy on the transaction. Organisation and PropertyTitleLink are outside the round trip: V3 records a participant's firm only as free text, and records no UPRN-to-title link at all, so decompose does not invent either and recompose does not need them.",
1240
1425
  roleImpliedByCredential: ROLE_IMPLIED_BY_CREDENTIAL,
1241
1426
  strippedKeySuffixes: ["Ref", "Required"],
1242
1427
  knownExceptions: [
@@ -1327,6 +1512,24 @@ const mapping = {
1327
1512
  "An offer in Transaction.offers that no participant references stays transaction data and yields no credential, since Offer requires a buyer.",
1328
1513
  },
1329
1514
  },
1515
+ standingCredentials: {
1516
+ PropertyTitleLink: {
1517
+ description:
1518
+ "A standing UPRN-to-title link. Not projected from V3. A Transaction's titlesToBeSold selects the titles being sold from the titles linked to its property; see linkedTitles() in src/utils/v4.js.",
1519
+ issuerByTitleScheme: {
1520
+ "urn:pdtf:titleNumber:": {
1521
+ issuer: "HM Land Registry",
1522
+ basis: "Register Dataset",
1523
+ },
1524
+ "urn:pdtf:unregisteredTitle:": {
1525
+ issuer: "Seller's conveyancer",
1526
+ basis: "Epitome of Title",
1527
+ },
1528
+ },
1529
+ supersession:
1530
+ "On first registration the Title for the new title number lists the unregistered URN in alsoKnownAs. An HMLR link from the same UPRN to that title supersedes the conveyancer's link to the unregistered URN; the conveyancer's link stays in force until one exists.",
1531
+ },
1532
+ },
1330
1533
  collisions: titleCollisionKeys.map((key) => ({
1331
1534
  key,
1332
1535
  entity: "Title",
package/src/utils/v4.js CHANGED
@@ -826,8 +826,126 @@ const resolveV3PointerAll = (pointer) => {
826
826
 
827
827
  const resolveV3Pointer = (pointer) => resolveV3PointerAll(pointer)[0] || null;
828
828
 
829
+ // ---------------------------------------------------------------------------
830
+ // Standing Property–Title links (spec 01 D34)
831
+ // ---------------------------------------------------------------------------
832
+ const REGISTERED_TITLE = "urn:pdtf:titleNumber:";
833
+ const UNREGISTERED_TITLE = "urn:pdtf:unregisteredTitle:";
834
+ const REGISTER_BASIS = "Register Dataset";
835
+ const EPITOME_BASIS = "Epitome of Title";
836
+
837
+ /**
838
+ * Every title related to a property, and which of them a transaction sells.
839
+ *
840
+ * HMLR asserts, the seller selects: PropertyTitleLink credentials say which
841
+ * titles a UPRN sits on, and the Transaction's titlesToBeSold says which of
842
+ * those are being conveyed. Nothing else is needed to show a related title that
843
+ * is not being sold — a superior freehold in a leasehold flat sale, say — since
844
+ * it is simply a linked title the transaction does not select.
845
+ *
846
+ * Pass only links that have already been verified: signature, issuer and
847
+ * status (spec 14). This works on credential subjects and decides nothing about
848
+ * trust beyond the rule the schema also enforces, that a registered title is
849
+ * linked on the register and an unregistered one on the epitome.
850
+ *
851
+ * First registration: a Title whose alsoKnownAs lists an unregistered URN is
852
+ * the same title under its new number. Links and selections naming the old URN
853
+ * resolve to it, and an HMLR link from the same UPRN to the new number
854
+ * supersedes the conveyancer's link to the old one. Until HMLR's link exists the
855
+ * conveyancer's still stands, and the title is reported in `awaitingRegisterLink`.
856
+ *
857
+ * @param {object} args
858
+ * @param {string} args.property Property URN (urn:pdtf:uprn:{uprn})
859
+ * @param {object[]} args.links PropertyTitleLink credential subjects
860
+ * @param {object} [args.transaction] Transaction, for titlesToBeSold
861
+ * @param {object[]} [args.titles] Title subjects, for alsoKnownAs
862
+ * @returns {{
863
+ * property: string,
864
+ * linked: string[], every title currently linked to the property
865
+ * sold: string[], titlesToBeSold that are linked
866
+ * relatedNotSold: string[], linked titles the transaction does not sell
867
+ * unlinkedSold: string[], titlesToBeSold with no link behind them
868
+ * superseded: string[], ids of links superseded by first registration
869
+ * awaitingRegisterLink: string[], registered titles linked only on an epitome
870
+ * }}
871
+ */
872
+ const linkedTitles = ({ property, links = [], transaction, titles = [] }) => {
873
+ if (!property) throw new Error("linkedTitles: property is required");
874
+
875
+ // Unregistered URN -> the registered title that now carries it.
876
+ const registeredAs = new Map();
877
+ for (const title of titles) {
878
+ for (const alias of title?.alsoKnownAs || []) {
879
+ const existing = registeredAs.get(alias);
880
+ if (existing && existing !== title.id) {
881
+ throw new Error(
882
+ `linkedTitles: "${alias}" is listed in alsoKnownAs by both "${existing}" and "${title.id}"`
883
+ );
884
+ }
885
+ registeredAs.set(alias, title.id);
886
+ }
887
+ }
888
+ const canonical = (urn) => registeredAs.get(urn) || urn;
889
+
890
+ const forProperty = links.filter((link) => link.property === property);
891
+ for (const link of forProperty) {
892
+ const basis = link.basis?.type;
893
+ const expected = link.title?.startsWith(REGISTERED_TITLE)
894
+ ? REGISTER_BASIS
895
+ : link.title?.startsWith(UNREGISTERED_TITLE)
896
+ ? EPITOME_BASIS
897
+ : null;
898
+ if (expected && basis !== expected) {
899
+ throw new Error(
900
+ `linkedTitles: link "${link.id || link.title}" to ${link.title} rests on "${basis}", not "${expected}"`
901
+ );
902
+ }
903
+ }
904
+
905
+ const fromRegister = new Set(
906
+ forProperty
907
+ .filter((link) => link.basis?.type === REGISTER_BASIS)
908
+ .map((link) => link.title)
909
+ );
910
+
911
+ const superseded = [];
912
+ const awaiting = new Set();
913
+ const linked = [];
914
+ const seen = new Set();
915
+ for (const link of forProperty) {
916
+ const title = canonical(link.title);
917
+ const isRegisteredAlias = title !== link.title;
918
+ if (isRegisteredAlias && fromRegister.has(title)) {
919
+ superseded.push(link.id || link.title);
920
+ continue;
921
+ }
922
+ if (isRegisteredAlias) awaiting.add(title);
923
+ if (!seen.has(title)) {
924
+ seen.add(title);
925
+ linked.push(title);
926
+ }
927
+ }
928
+
929
+ const selected = [
930
+ ...new Set((transaction?.titlesToBeSold || []).map(canonical)),
931
+ ];
932
+ const linkedSet = new Set(linked);
933
+ const selectedSet = new Set(selected);
934
+
935
+ return {
936
+ property,
937
+ linked,
938
+ sold: selected.filter((t) => linkedSet.has(t)),
939
+ relatedNotSold: linked.filter((t) => !selectedSet.has(t)),
940
+ unlinkedSold: selected.filter((t) => !linkedSet.has(t)),
941
+ superseded,
942
+ awaitingRegisterLink: linked.filter((t) => awaiting.has(t)),
943
+ };
944
+ };
945
+
829
946
  module.exports = {
830
947
  mapping,
948
+ linkedTitles,
831
949
  decompose,
832
950
  recompose,
833
951
  projectRelationships,