1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
//! JSON-LD context envelope: build / frame / strip a DPP passport payload.
use OnceLock;
use ;
use ;
/// The `gs1:` prefix IRI, read from the register rather than written here.
///
/// # Why this is not a literal
///
/// It was one, and the doc comment below carried its provenance — the date GS1's
/// definition was read, what it said, the licence. That is the right *content*
/// filed in the wrong *place*: a doc comment is not a record, nothing checks it,
/// and `dpp-vocab` exists precisely so this class of claim has one home with a
/// source and a `checkedOn` date. The register was created to unify two
/// mechanisms held to different standards of rigour; this was the one still on
/// the looser standard.
///
/// # Panics
///
/// If the `gs1` record is absent, carries no `namespaceIri`, or no longer
/// permits emission. All three are build-time facts about files embedded in
/// `dpp-vocab`, and every one of them means this context must stop claiming
/// GS1's vocabulary. Emitting a prefix the register refuses is the failure this
/// arrangement exists to prevent, so it is not papered over with a fallback.
/// Remote contexts this passport context references.
///
/// **A string entry in an `@context` array is fetched by the consumer at
/// expansion time.** One that does not resolve is not cosmetic: a conforming
/// processor fails the whole document with a remote-context load error, and a
/// lenient one drops every term it cannot define. Since our payload uses bare
/// keys, that means the `ld+json` door would convey no linked data at all —
/// worse than serving plain JSON, because the `@context` is itself a claim that
/// the document is semantically resolvable.
///
/// So this list is deliberately short and deliberately explicit: adding to it
/// means editing this constant *and* the test that pins it, which is the point
/// at which someone checks the URL. Two entries were removed on 2026-07-30 for
/// returning 404 — `https://ref.gs1.org/standards/digital-link/context/`, which
/// this crate referenced, and `https://odal-node.io/schemas/dpp/v1`, which the
/// resolver hand-rolled.
///
/// **It is now empty, and a passport expands the same either way.** The last
/// entry was `https://www.w3.org/ns/did/v1`, which did exactly one job here:
/// aliasing `id` to `@id`. That alias is now defined inline, so the entry
/// defined nothing a passport uses — while still carrying the whole-document
/// failure mode above. Measured with a JSON-LD processor rather than reasoned
/// about: a full passport expands to the same entry count with the context and
/// without it, `@id` intact, and no term of ours collides with anything that
/// document protects.
///
/// The constant stays rather than disappearing, because an empty list is a
/// statement — *this document fetches nothing* — and it is the thing the test
/// pins. Adding an entry remains a deliberate act with a check attached.
///
/// Term-to-IRI mappings are a different matter and are inlined below: a prefix
/// IRI names a vocabulary and is never dereferenced during expansion, so it
/// carries no such obligation.
pub const REMOTE_CONTEXTS: & = &;
/// Build the JSON-LD context for an Odal Node passport.
///
/// The vocabulary is **inlined** rather than hosted. Hosting a context document
/// is a commitment to keep a URL resolving for as long as any passport
/// referencing it exists — years, under ESPR retention — and that is an
/// operational obligation, not a library decision. An inline term map cannot
/// 404, and it can be adopted later without invalidating passports issued now.
///
/// Every term maps to our own `dpp:` prefix, with one exception.
///
/// `gtin`, `createdAt` and `updatedAt` all used to borrow GS1's and
/// Schema.org's prefixes (`gs1:gtin`, `schema:dateCreated`,
/// `schema:dateModified`) with no provenance record, which is exactly the
/// unsupported claim `dpp-vocab`'s rule exists to catch. All three were
/// withdrawn to `dpp:` on 2026-08-10.
///
/// **`gtin` is back, and only `gtin`.** Its record is `vocabularies/gs1.json`
/// in `dpp-vocab`, carrying what was read, when, and under what licence —
/// `gs1_namespace` reads the prefix IRI from there rather than repeating it,
/// so the claim and its evidence cannot drift apart. GS1's `gtin` is
/// [`Gtin`](dpp_domain::Gtin)'s shape exactly, which is why the term says
/// something true.
///
/// 🚨 It is now defined **inside `productIdentifier`**, not at the top level,
/// because that is where the key is. A term is only reachable at the position
/// the key occupies: when the identifier moved under `productIdentifier` and
/// that node had no definition of its own, expansion dropped the node and
/// carried `gtin` down with it. The scheme 2 and 3 terms are scoped the same
/// way, and deliberately are **not** global — `scheme` is also a facility
/// snapshot field, and one global term would give a GLN scheme the product
/// identifier's meaning.
///
/// The other two stay `dpp:`. Schema.org is `tracked` in `dpp-vocab`: evaluated,
/// not adopted, and its record does not permit emission. And note that what was
/// read was **one term**, not the GS1 vocabulary — declaring the `gs1:` prefix
/// is what makes the compact form expand, not a claim that anything else under
/// it has been checked.
///
/// Every key a `Passport` emits, mapped to the IRI that gives it meaning.
///
/// # Why this is a table and not a literal
///
/// A JSON-LD term is reachable only at the position its key occupies, and a key
/// with no term is dropped on expansion together with everything inside it.
/// That makes this list's *completeness* the property that matters, and a list
/// whose completeness matters should be something a test can read. It was a
/// `json!` literal covering six of the envelope's keys, and nothing anywhere
/// compared it against the struct it claims to describe.
/// `every_passport_wire_key_has_a_term` is now that comparison.
///
/// 🚨 Two of the terms it did carry pointed at keys that do not exist.
/// `passportId` was never emitted — the key is `id` — and `jws` was never
/// emitted either, because the fields serialise as `jwsSignature` and
/// `publicJwsSignature`. Both are gone; the signature keys are below under the
/// names they actually have.
///
/// 🚨 `productGroup` expanded to `dpp:product_group` — the only IRI here
/// that disagreed with its own key, alone in snake_case among camelCase
/// neighbours. It is corrected rather than preserved. An IRI *is* a term's
/// identity, so this does change what the term means, and that is affordable
/// exactly once: there are no passports in the field to mean anything
/// different to. Keeping it would have bought compatibility with nobody at the
/// cost of a permanent inconsistency in a published vocabulary.
///
/// Every IRI is `dpp:`. The one foreign prefix in this context is `gs1:gtin`,
/// scoped under `productIdentifier`, and it is foreign only because `dpp-vocab`
/// holds a provenance record for it. `id` is absent here because it aliases the
/// `@id` keyword rather than naming a vocabulary term.
///
/// # These IRIs are names, not addresses
///
/// `dpp:` expands into [`OWN_JSONLD_NAMESPACE`], which does not resolve and is
/// under no obligation to — the reasoning is on the constant itself in
/// `dpp-vocab`, and it is the standing position rather than an oversight. A
/// prefix IRI is concatenated with the term and never fetched, so minting
/// names under it costs nothing and promises nothing. The
/// obligation that *is* real belongs to a **string entry** in the `@context`
/// array, which a consumer does fetch; that is [`REMOTE_CONTEXTS`], and it is
/// deliberately short for exactly this reason. Do not read the one rule onto
/// the other.
const PASSPORT_TERMS: & = &;
/// The literal is built once and cloned per call — callers extend the
/// returned value (e.g. [`frame_passport`] merges passport fields into it),
/// so it must stay an owned, independently-mutable `Value` per call site.
/// The `@context` value alone, for a caller that already has a passport object
/// and needs to stamp the context onto it.
///
/// Exists so the resolver stops constructing its own: two definitions of one
/// context is how the served one came to reference a URL that 404s while this
/// one referenced a different URL that also 404s.
/// Wrap a passport JSON value in a JSON-LD envelope.
///
/// A non-object payload cannot be merged into the `@context` object; it is
/// returned **unchanged** rather than silently discarded into a bare, empty
/// envelope.
/// Extract the plain data from a JSON-LD framed passport (strip `@context`).