cesr-ts 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +31 -3
  2. package/esm/src/adapters/async-iterable.js +9 -9
  3. package/esm/src/annotate/annotator.js +18 -10
  4. package/esm/src/annotate/comments.js +3 -6
  5. package/esm/src/annotate/render.js +123 -24
  6. package/esm/src/bench/parser-benchmark.js +134 -0
  7. package/esm/src/core/bytes.js +6 -0
  8. package/esm/src/core/errors.js +24 -0
  9. package/esm/src/core/parser-attachment-collector.js +154 -0
  10. package/esm/src/core/parser-constants.js +74 -0
  11. package/esm/src/core/parser-deferred-frames.js +73 -0
  12. package/esm/src/core/parser-engine.js +128 -505
  13. package/esm/src/core/parser-frame-parser.js +643 -0
  14. package/esm/src/core/parser-policy.js +137 -0
  15. package/esm/src/core/parser-stream-state.js +62 -0
  16. package/esm/src/core/recovery-diagnostics.js +25 -0
  17. package/esm/src/index.js +4 -0
  18. package/esm/src/parser/attachment-fallback-policy.js +142 -0
  19. package/esm/src/parser/group-dispatch.js +547 -233
  20. package/esm/src/primitives/counter.js +4 -5
  21. package/esm/src/primitives/mapper.js +126 -45
  22. package/esm/src/primitives/matter.js +1 -1
  23. package/esm/src/router/router-stub.js +6 -6
  24. package/esm/src/serder/serder.js +44 -7
  25. package/esm/src/serder/smell.js +2 -1
  26. package/esm/src/tables/counter-version-registry.js +201 -0
  27. package/esm/src/version.js +2 -2
  28. package/package.json +3 -1
  29. package/types/_dnt.polyfills.d.ts +5 -0
  30. package/types/_dnt.polyfills.d.ts.map +1 -1
  31. package/types/src/adapters/async-iterable.d.ts +2 -2
  32. package/types/src/adapters/async-iterable.d.ts.map +1 -1
  33. package/types/src/adapters/effection.d.ts +2 -2
  34. package/types/src/adapters/effection.d.ts.map +1 -1
  35. package/types/src/annotate/annotator.d.ts.map +1 -1
  36. package/types/src/annotate/comments.d.ts.map +1 -1
  37. package/types/src/annotate/render.d.ts +8 -2
  38. package/types/src/annotate/render.d.ts.map +1 -1
  39. package/types/src/annotate/types.d.ts +2 -2
  40. package/types/src/annotate/types.d.ts.map +1 -1
  41. package/types/src/bench/parser-benchmark.d.ts +70 -0
  42. package/types/src/bench/parser-benchmark.d.ts.map +1 -0
  43. package/types/src/core/bytes.d.ts +6 -0
  44. package/types/src/core/bytes.d.ts.map +1 -1
  45. package/types/src/core/errors.d.ts +10 -0
  46. package/types/src/core/errors.d.ts.map +1 -1
  47. package/types/src/core/parser-attachment-collector.d.ts +51 -0
  48. package/types/src/core/parser-attachment-collector.d.ts.map +1 -0
  49. package/types/src/core/parser-constants.d.ts +30 -0
  50. package/types/src/core/parser-constants.d.ts.map +1 -0
  51. package/types/src/core/parser-deferred-frames.d.ts +38 -0
  52. package/types/src/core/parser-deferred-frames.d.ts.map +1 -0
  53. package/types/src/core/parser-engine.d.ts +53 -44
  54. package/types/src/core/parser-engine.d.ts.map +1 -1
  55. package/types/src/core/parser-frame-parser.d.ts +89 -0
  56. package/types/src/core/parser-frame-parser.d.ts.map +1 -0
  57. package/types/src/core/parser-policy.d.ts +27 -0
  58. package/types/src/core/parser-policy.d.ts.map +1 -0
  59. package/types/src/core/parser-stream-state.d.ts +30 -0
  60. package/types/src/core/parser-stream-state.d.ts.map +1 -0
  61. package/types/src/core/recovery-diagnostics.d.ts +59 -0
  62. package/types/src/core/recovery-diagnostics.d.ts.map +1 -0
  63. package/types/src/core/types.d.ts +61 -7
  64. package/types/src/core/types.d.ts.map +1 -1
  65. package/types/src/index.d.ts +4 -0
  66. package/types/src/index.d.ts.map +1 -1
  67. package/types/src/parser/attachment-fallback-policy.d.ts +78 -0
  68. package/types/src/parser/attachment-fallback-policy.d.ts.map +1 -0
  69. package/types/src/parser/group-dispatch.d.ts +85 -15
  70. package/types/src/parser/group-dispatch.d.ts.map +1 -1
  71. package/types/src/primitives/aggor.d.ts +2 -2
  72. package/types/src/primitives/aggor.d.ts.map +1 -1
  73. package/types/src/primitives/blinder.d.ts +2 -2
  74. package/types/src/primitives/blinder.d.ts.map +1 -1
  75. package/types/src/primitives/counter.d.ts.map +1 -1
  76. package/types/src/primitives/mapper.d.ts +44 -1
  77. package/types/src/primitives/mapper.d.ts.map +1 -1
  78. package/types/src/primitives/mediar.d.ts +2 -2
  79. package/types/src/primitives/mediar.d.ts.map +1 -1
  80. package/types/src/primitives/sealer.d.ts +2 -2
  81. package/types/src/primitives/sealer.d.ts.map +1 -1
  82. package/types/src/router/router-stub.d.ts +5 -5
  83. package/types/src/router/router-stub.d.ts.map +1 -1
  84. package/types/src/serder/serder.d.ts +2 -2
  85. package/types/src/serder/serder.d.ts.map +1 -1
  86. package/types/src/serder/serdery.d.ts +2 -2
  87. package/types/src/serder/serdery.d.ts.map +1 -1
  88. package/types/src/serder/smell.d.ts.map +1 -1
  89. package/types/src/tables/counter-version-registry.d.ts +90 -0
  90. package/types/src/tables/counter-version-registry.d.ts.map +1 -0
  91. package/types/src/version.d.ts +2 -2
  92. package/types/src/version.d.ts.map +1 -1
@@ -1,41 +1,350 @@
1
1
  import { parseCounter } from "../primitives/counter.js";
2
2
  import { parseMatter } from "../primitives/matter.js";
3
3
  import { parseIndexer } from "../primitives/indexer.js";
4
- import { DeserializeError, GroupSizeError, ShortageError, UnknownCodeError, } from "../core/errors.js";
4
+ import { GroupSizeError, ShortageError, UnknownCodeError, } from "../core/errors.js";
5
5
  import { CtrDexV1, CtrDexV2 } from "../tables/counter-codex.js";
6
- const SIGER_LIST_CODES_BY_VERSION = {
7
- 1: new Set([
8
- CtrDexV1.ControllerIdxSigs,
9
- CtrDexV1.WitnessIdxSigs,
10
- ]),
11
- 2: new Set([
12
- CtrDexV2.ControllerIdxSigs,
13
- CtrDexV2.BigControllerIdxSigs,
14
- CtrDexV2.WitnessIdxSigs,
15
- CtrDexV2.BigWitnessIdxSigs,
16
- ]),
17
- };
18
- const WRAPPER_GROUP_CODES_V1 = new Set([
19
- CtrDexV1.AttachmentGroup,
20
- CtrDexV1.BigAttachmentGroup,
21
- CtrDexV1.BodyWithAttachmentGroup,
22
- CtrDexV1.BigBodyWithAttachmentGroup,
23
- ]);
24
- const WRAPPER_GROUP_CODES_V2 = new Set([
25
- CtrDexV2.AttachmentGroup,
26
- CtrDexV2.BigAttachmentGroup,
27
- CtrDexV2.BodyWithAttachmentGroup,
28
- CtrDexV2.BigBodyWithAttachmentGroup,
29
- ]);
6
+ import { resolveVersionedRegistryValue, } from "../tables/counter-version-registry.js";
7
+ import { b64ToInt, intToB64 } from "../core/bytes.js";
8
+ import { composeRecoveryDiagnosticObserver, } from "../core/recovery-diagnostics.js";
9
+ import { createAttachmentVersionFallbackPolicy, } from "./attachment-fallback-policy.js";
10
+ export { createAttachmentVersionFallbackPolicy, } from "./attachment-fallback-policy.js";
11
+ function qb64Item(qb64, opaque = false) {
12
+ return { kind: "qb64", qb64, opaque };
13
+ }
14
+ function qb2Item(qb2, opaque = false) {
15
+ return { kind: "qb2", qb2, opaque };
16
+ }
17
+ function tupleItem(items) {
18
+ return { kind: "tuple", items };
19
+ }
20
+ function nestedGroupItem(code, name, count) {
21
+ return { kind: "group", code, name, count };
22
+ }
23
+ /** Normalize unknown throwables to an `Error` for diagnostic emission. */
24
+ function asError(error) {
25
+ return error instanceof Error ? error : new Error(String(error));
26
+ }
27
+ /**
28
+ * Canonical attachment dispatch specification (authoring form).
29
+ *
30
+ * This is the single source of truth for:
31
+ * - code -> parser routing
32
+ * - semantic shape classification
33
+ * - wrapper-group recursion eligibility
34
+ * - nested siger-list allowance
35
+ *
36
+ * Maintainer workflow:
37
+ * when adding/removing a counter route, edit this constant only, then rely on
38
+ * descriptor expansion + generated dispatch maps below.
39
+ */
40
+ export const ATTACHMENT_DISPATCH_SPEC = [
41
+ {
42
+ parserKind: "genusVersion",
43
+ semanticShape: "genusVersionMarker",
44
+ codesByVersion: {
45
+ 1: [CtrDexV1.KERIACDCGenusVersion],
46
+ 2: [CtrDexV2.KERIACDCGenusVersion],
47
+ },
48
+ },
49
+ {
50
+ parserKind: "quadlet",
51
+ semanticShape: "countedGroupPayload",
52
+ codesByVersion: {
53
+ 1: [
54
+ CtrDexV1.GenericGroup,
55
+ CtrDexV1.BigGenericGroup,
56
+ CtrDexV1.NonNativeBodyGroup,
57
+ CtrDexV1.BigNonNativeBodyGroup,
58
+ CtrDexV1.ESSRPayloadGroup,
59
+ CtrDexV1.BigESSRPayloadGroup,
60
+ CtrDexV1.PathedMaterialCouples,
61
+ CtrDexV1.BigPathedMaterialCouples,
62
+ ],
63
+ 2: [
64
+ CtrDexV2.GenericGroup,
65
+ CtrDexV2.BigGenericGroup,
66
+ CtrDexV2.NonNativeBodyGroup,
67
+ CtrDexV2.BigNonNativeBodyGroup,
68
+ CtrDexV2.ESSRPayloadGroup,
69
+ CtrDexV2.BigESSRPayloadGroup,
70
+ CtrDexV2.PathedMaterialCouples,
71
+ CtrDexV2.BigPathedMaterialCouples,
72
+ CtrDexV2.DatagramSegmentGroup,
73
+ CtrDexV2.BigDatagramSegmentGroup,
74
+ CtrDexV2.ESSRWrapperGroup,
75
+ CtrDexV2.BigESSRWrapperGroup,
76
+ CtrDexV2.FixBodyGroup,
77
+ CtrDexV2.BigFixBodyGroup,
78
+ CtrDexV2.MapBodyGroup,
79
+ CtrDexV2.BigMapBodyGroup,
80
+ CtrDexV2.GenericMapGroup,
81
+ CtrDexV2.BigGenericMapGroup,
82
+ CtrDexV2.GenericListGroup,
83
+ CtrDexV2.BigGenericListGroup,
84
+ ],
85
+ },
86
+ },
87
+ {
88
+ parserKind: "quadlet",
89
+ semanticShape: "wrapperGroupPayload",
90
+ wrapperGroup: true,
91
+ codesByVersion: {
92
+ 1: [
93
+ CtrDexV1.AttachmentGroup,
94
+ CtrDexV1.BigAttachmentGroup,
95
+ CtrDexV1.BodyWithAttachmentGroup,
96
+ CtrDexV1.BigBodyWithAttachmentGroup,
97
+ ],
98
+ 2: [
99
+ CtrDexV2.AttachmentGroup,
100
+ CtrDexV2.BigAttachmentGroup,
101
+ CtrDexV2.BodyWithAttachmentGroup,
102
+ CtrDexV2.BigBodyWithAttachmentGroup,
103
+ ],
104
+ },
105
+ },
106
+ {
107
+ parserKind: "repeatTuple",
108
+ semanticShape: "primitiveTuples",
109
+ tupleKinds: ["indexer"],
110
+ allowsSigerList: true,
111
+ codesByVersion: {
112
+ 1: [
113
+ CtrDexV1.ControllerIdxSigs,
114
+ CtrDexV1.WitnessIdxSigs,
115
+ ],
116
+ 2: [
117
+ CtrDexV2.ControllerIdxSigs,
118
+ CtrDexV2.BigControllerIdxSigs,
119
+ CtrDexV2.WitnessIdxSigs,
120
+ CtrDexV2.BigWitnessIdxSigs,
121
+ ],
122
+ },
123
+ },
124
+ {
125
+ parserKind: "repeatTuple",
126
+ semanticShape: "primitiveTuples",
127
+ tupleKinds: ["matter", "matter"],
128
+ codesByVersion: {
129
+ 1: [
130
+ CtrDexV1.NonTransReceiptCouples,
131
+ CtrDexV1.FirstSeenReplayCouples,
132
+ CtrDexV1.SealSourceCouples,
133
+ ],
134
+ 2: [
135
+ CtrDexV2.NonTransReceiptCouples,
136
+ CtrDexV2.BigNonTransReceiptCouples,
137
+ CtrDexV2.FirstSeenReplayCouples,
138
+ CtrDexV2.BigFirstSeenReplayCouples,
139
+ CtrDexV2.SealSourceCouples,
140
+ CtrDexV2.BigSealSourceCouples,
141
+ CtrDexV2.BackerRegistrarSealCouples,
142
+ CtrDexV2.BigBackerRegistrarSealCouples,
143
+ CtrDexV2.TypedDigestSealCouples,
144
+ CtrDexV2.BigTypedDigestSealCouples,
145
+ ],
146
+ },
147
+ },
148
+ {
149
+ parserKind: "repeatTuple",
150
+ semanticShape: "primitiveTuples",
151
+ tupleKinds: ["matter", "matter", "matter", "indexer"],
152
+ codesByVersion: {
153
+ 1: [
154
+ CtrDexV1.TransReceiptQuadruples,
155
+ ],
156
+ 2: [
157
+ CtrDexV2.TransReceiptQuadruples,
158
+ CtrDexV2.BigTransReceiptQuadruples,
159
+ ],
160
+ },
161
+ },
162
+ {
163
+ parserKind: "repeatTuple",
164
+ semanticShape: "primitiveTuples",
165
+ tupleKinds: ["matter", "matter", "matter"],
166
+ codesByVersion: {
167
+ 1: [
168
+ CtrDexV1.SealSourceTriples,
169
+ ],
170
+ 2: [
171
+ CtrDexV2.SealSourceTriples,
172
+ CtrDexV2.BigSealSourceTriples,
173
+ ],
174
+ },
175
+ },
176
+ {
177
+ parserKind: "repeatTuple",
178
+ semanticShape: "primitiveTuples",
179
+ tupleKinds: ["matter"],
180
+ codesByVersion: {
181
+ 2: [
182
+ CtrDexV2.SealSourceLastSingles,
183
+ CtrDexV2.BigSealSourceLastSingles,
184
+ CtrDexV2.DigestSealSingles,
185
+ CtrDexV2.BigDigestSealSingles,
186
+ CtrDexV2.MerkleRootSealSingles,
187
+ CtrDexV2.BigMerkleRootSealSingles,
188
+ ],
189
+ },
190
+ },
191
+ {
192
+ parserKind: "transIdxSigGroups",
193
+ semanticShape: "signatureGroupTuples",
194
+ codesByVersion: {
195
+ 1: [
196
+ CtrDexV1.TransIdxSigGroups,
197
+ ],
198
+ 2: [
199
+ CtrDexV2.TransIdxSigGroups,
200
+ CtrDexV2.BigTransIdxSigGroups,
201
+ ],
202
+ },
203
+ },
204
+ {
205
+ parserKind: "transLastIdxSigGroups",
206
+ semanticShape: "lastSignatureGroupTuples",
207
+ codesByVersion: {
208
+ 1: [
209
+ CtrDexV1.TransLastIdxSigGroups,
210
+ ],
211
+ 2: [
212
+ CtrDexV2.TransLastIdxSigGroups,
213
+ CtrDexV2.BigTransLastIdxSigGroups,
214
+ ],
215
+ },
216
+ },
217
+ {
218
+ parserKind: "sadPathSig",
219
+ semanticShape: "sadPathSignatures",
220
+ codesByVersion: {
221
+ 1: [CtrDexV1.SadPathSig],
222
+ },
223
+ },
224
+ {
225
+ parserKind: "sadPathSigGroup",
226
+ semanticShape: "sadPathSignatureGroup",
227
+ codesByVersion: {
228
+ 1: [CtrDexV1.SadPathSigGroup],
229
+ },
230
+ },
231
+ {
232
+ parserKind: "repeatTuple",
233
+ semanticShape: "primitiveTuples",
234
+ tupleKinds: ["matter", "matter", "matter", "matter"],
235
+ codesByVersion: {
236
+ 2: [
237
+ CtrDexV2.BlindedStateQuadruples,
238
+ CtrDexV2.BigBlindedStateQuadruples,
239
+ CtrDexV2.TypedMediaQuadruples,
240
+ CtrDexV2.BigTypedMediaQuadruples,
241
+ ],
242
+ },
243
+ },
244
+ {
245
+ parserKind: "repeatTuple",
246
+ semanticShape: "primitiveTuples",
247
+ tupleKinds: [
248
+ "matter",
249
+ "matter",
250
+ "matter",
251
+ "matter",
252
+ "matter",
253
+ "matter",
254
+ ],
255
+ codesByVersion: {
256
+ 2: [
257
+ CtrDexV2.BoundStateSextuples,
258
+ CtrDexV2.BigBoundStateSextuples,
259
+ ],
260
+ },
261
+ },
262
+ ];
263
+ /**
264
+ * Expand grouped spec rows into per-version descriptors.
265
+ *
266
+ * Why:
267
+ * downstream builders operate on `(version, code)` units, while spec authoring
268
+ * stays concise by grouping shared semantics across major versions.
269
+ */
270
+ function expandDispatchSpec(spec) {
271
+ const descriptors = [];
272
+ for (const family of spec) {
273
+ for (const version of [1, 2]) {
274
+ for (const code of family.codesByVersion[version] ?? []) {
275
+ descriptors.push({
276
+ version,
277
+ code,
278
+ parserKind: family.parserKind,
279
+ semanticShape: family.semanticShape,
280
+ tupleKinds: family.tupleKinds,
281
+ wrapperGroup: family.wrapperGroup === true,
282
+ allowsSigerList: family.allowsSigerList === true,
283
+ });
284
+ }
285
+ }
286
+ }
287
+ return descriptors;
288
+ }
289
+ /**
290
+ * Build a version-indexed code-set projection from descriptors.
291
+ *
292
+ * Used for secondary routing metadata (wrapper recursion/siger-list allowance)
293
+ * so those sets cannot drift from the canonical dispatch spec.
294
+ */
295
+ function buildCodeSetByVersion(descriptors, predicate) {
296
+ const byVersion = {
297
+ 1: new Set(),
298
+ 2: new Set(),
299
+ };
300
+ for (const descriptor of descriptors) {
301
+ if (predicate(descriptor)) {
302
+ byVersion[descriptor.version].add(descriptor.code);
303
+ }
304
+ }
305
+ return byVersion;
306
+ }
307
+ /**
308
+ * Normalized per-code dispatch descriptors used by all derived routing tables.
309
+ *
310
+ * Invariant:
311
+ * this array is generated once at module load from `ATTACHMENT_DISPATCH_SPEC`
312
+ * and should not be hand-edited.
313
+ */
314
+ const ATTACHMENT_DISPATCH_DESCRIPTORS = expandDispatchSpec(ATTACHMENT_DISPATCH_SPEC);
315
+ /** Siger-list counter codes by major version (for nested trans sig group parsing). */
316
+ const SIGER_LIST_CODES_BY_MAJOR = buildCodeSetByVersion(ATTACHMENT_DISPATCH_DESCRIPTORS, (descriptor) => descriptor.allowsSigerList);
317
+ /** Wrapper counters by major version whose payloads recurse as nested groups. */
318
+ const WRAPPER_GROUP_CODES_BY_MAJOR = buildCodeSetByVersion(ATTACHMENT_DISPATCH_DESCRIPTORS, (descriptor) => descriptor.wrapperGroup);
319
+ const WRAPPER_GROUP_CODES_V1 = WRAPPER_GROUP_CODES_BY_MAJOR[1];
320
+ const WRAPPER_GROUP_CODES_V2 = WRAPPER_GROUP_CODES_BY_MAJOR[2];
321
+ /** Universal genus-version counter code (v1/v2 compatible encoding semantics). */
322
+ const GENUS_VERSION_CODE = CtrDexV2.KERIACDCGenusVersion;
323
+ /** Domain-sensitive primitive size (qb64 vs qb2). */
30
324
  function primitiveSize(primitive, domain) {
31
325
  return domain === "bny" ? primitive.fullSizeB2 : primitive.fullSize;
32
326
  }
327
+ /** Alias for readability at counter header consumption callsites. */
33
328
  function counterHeaderSize(counter, domain) {
34
329
  return primitiveSize(counter, domain);
35
330
  }
331
+ /** Counter count unit size by domain: quadlets in text, triplets in binary. */
36
332
  function quadletUnitSize(domain) {
37
333
  return domain === "bny" ? 3 : 4;
38
334
  }
335
+ /** Decode a genus-version counter payload into major/minor `Versionage`. */
336
+ function decodeVersionCounter(counter) {
337
+ const triplet = counter.qb64.length >= 3
338
+ ? counter.qb64.slice(-3)
339
+ : intToB64(counter.count, 3);
340
+ const majorRaw = b64ToInt(triplet[0] ?? "A");
341
+ const minorRaw = b64ToInt(triplet[1] ?? "A");
342
+ return {
343
+ major: majorRaw === 1 ? 1 : 2,
344
+ minor: minorRaw,
345
+ };
346
+ }
347
+ /** Parse a fixed primitive tuple shape and return qb64 strings in source order. */
39
348
  function parseTuple(input, kinds, domain) {
40
349
  const items = [];
41
350
  let offset = 0;
@@ -43,24 +352,38 @@ function parseTuple(input, kinds, domain) {
43
352
  const part = kind === "indexer"
44
353
  ? parseIndexer(input.slice(offset), domain)
45
354
  : parseMatter(input.slice(offset), domain);
46
- items.push(part.qb64);
355
+ items.push(qb64Item(part.qb64));
47
356
  offset += primitiveSize(part, domain);
48
357
  }
49
358
  return { items, consumed: offset };
50
359
  }
360
+ /** Parse `count` tuple repetitions with stable ordering and exact byte accounting. */
51
361
  function parseRepeated(input, count, kinds, domain) {
52
362
  const items = [];
53
363
  let offset = 0;
54
364
  for (let i = 0; i < count; i++) {
55
365
  const tuple = parseTuple(input.slice(offset), kinds, domain);
56
- items.push(tuple.items);
366
+ items.push(tupleItem(tuple.items));
57
367
  offset += tuple.consumed;
58
368
  }
59
369
  return { items, consumed: offset };
60
370
  }
371
+ /** Split opaque counted payload into domain units (quadlets for text, triplets for binary). */
372
+ function splitOpaqueUnits(payload, domain, expectedCount, opaque = false) {
373
+ if (domain === "bny") {
374
+ const count = expectedCount ?? Math.floor(payload.length / 3);
375
+ return Array.from({ length: count }, (_v, i) => qb2Item(payload.slice(i * 3, i * 3 + 3), opaque));
376
+ }
377
+ const text = String.fromCharCode(...payload);
378
+ if (expectedCount !== undefined) {
379
+ return Array.from({ length: expectedCount }, (_v, i) => qb64Item(text.slice(i * 4, i * 4 + 4), opaque));
380
+ }
381
+ return (text.match(/.{1,4}/g) ?? []).map((token) => qb64Item(token, opaque));
382
+ }
383
+ /** Parse nested siger-list group headed by a version-appropriate siger counter. */
61
384
  function parseSigerList(input, version, domain) {
62
385
  const counter = parseCounter(input, version, domain);
63
- const allowed = SIGER_LIST_CODES_BY_VERSION[version.major];
386
+ const allowed = resolveVersionedRegistryValue(SIGER_LIST_CODES_BY_VERSION, version, "siger-list code set").value;
64
387
  if (!allowed.has(counter.code)) {
65
388
  throw new UnknownCodeError(`Expected siger-list counter but got ${counter.code}`);
66
389
  }
@@ -68,11 +391,20 @@ function parseSigerList(input, version, domain) {
68
391
  let offset = counterHeaderSize(counter, domain);
69
392
  for (let i = 0; i < counter.count; i++) {
70
393
  const part = parseIndexer(input.slice(offset), domain);
71
- items.push(part.qb64);
394
+ items.push(qb64Item(part.qb64));
72
395
  offset += primitiveSize(part, domain);
73
396
  }
74
397
  return { items, consumed: offset };
75
398
  }
399
+ /**
400
+ * Parse quadlet/triplet-counted groups.
401
+ *
402
+ * Behavior:
403
+ * - non-wrapper groups return raw unit chunks (text quadlets or binary triplets).
404
+ * - wrapper groups recurse into nested attachment-group dispatch.
405
+ * - compat mode preserves unread wrapper remainder as opaque units on non-boundary
406
+ * nested parse errors.
407
+ */
76
408
  function parseQuadletGroup(input, counter, version, wrapperCodes, domain, context) {
77
409
  const unitSize = quadletUnitSize(domain);
78
410
  const payloadSize = counter.count * unitSize;
@@ -86,32 +418,47 @@ function parseQuadletGroup(input, counter, version, wrapperCodes, domain, contex
86
418
  // Wrapper payload is a packed stream of nested groups (best effort).
87
419
  const items = [];
88
420
  let offset = 0;
421
+ let nestedVersion = version;
89
422
  while (offset < payload.length) {
90
423
  try {
424
+ const nestedCounter = parseCounter(payload.slice(offset), nestedVersion, domain);
425
+ if (nestedCounter.code === GENUS_VERSION_CODE) {
426
+ // Wrapper-scoped version override for subsequent nested groups.
427
+ offset += counterHeaderSize(nestedCounter, domain);
428
+ nestedVersion = decodeVersionCounter(nestedCounter);
429
+ continue;
430
+ }
91
431
  const nested = parseAttachmentDispatchCompat(
92
432
  // Allow mixed-version nested groups inside wrapper payloads.
93
433
  // Some real-world streams wrap v2 groups inside v1 wrappers.
94
- payload.slice(offset), version, domain, context);
95
- items.push({
96
- code: nested.group.code,
97
- name: nested.group.name,
98
- count: nested.group.count,
99
- });
434
+ payload.slice(offset), nestedVersion, domain, context);
435
+ items.push(nestedGroupItem(nested.group.code, nested.group.name, nested.group.count));
100
436
  if (nested.consumed === 0) {
101
437
  throw new GroupSizeError("Nested attachment parser consumed zero bytes");
102
438
  }
103
439
  offset += nested.consumed;
104
440
  }
105
441
  catch (error) {
106
- if (error instanceof ShortageError ||
107
- error instanceof GroupSizeError) {
108
- throw error;
442
+ const normalized = asError(error);
443
+ if (normalized instanceof ShortageError ||
444
+ normalized instanceof GroupSizeError) {
445
+ throw normalized;
446
+ }
447
+ if (!context.versionFallbackPolicy.shouldPreserveWrapperRemainder(normalized)) {
448
+ throw normalized;
109
449
  }
110
450
  // Intentional recovery point: keep unread wrapper tail as opaque units.
111
451
  const remainder = payload.slice(offset);
112
- const opaque = domain === "bny"
113
- ? Array.from({ length: Math.floor(remainder.length / 3) }, (_v, i) => remainder.slice(i * 3, i * 3 + 3))
114
- : (String.fromCharCode(...remainder).match(/.{1,4}/g) ?? []);
452
+ const opaque = splitOpaqueUnits(remainder, domain, undefined, true);
453
+ context.recoveryDiagnosticObserver?.({
454
+ type: "wrapper-opaque-tail-preserved",
455
+ version: nestedVersion,
456
+ domain,
457
+ wrapperCode: counter.code,
458
+ opaqueItemCount: opaque.length,
459
+ errorName: normalized.name,
460
+ reason: normalized.message,
461
+ });
115
462
  items.push(...opaque);
116
463
  offset = payload.length;
117
464
  }
@@ -121,11 +468,10 @@ function parseQuadletGroup(input, counter, version, wrapperCodes, domain, contex
121
468
  }
122
469
  return { items, consumed: total };
123
470
  }
124
- const items = domain === "bny"
125
- ? Array.from({ length: counter.count }, (_v, i) => payload.slice(i * 3, i * 3 + 3))
126
- : (String.fromCharCode(...payload).match(/.{1,4}/g) ?? []);
471
+ const items = splitOpaqueUnits(payload, domain, counter.count);
127
472
  return { items, consumed: total };
128
473
  }
474
+ /** Build parser for repeated tuple-based group families. */
129
475
  function repeatTupleParser(kinds) {
130
476
  return (input, counter, _version, domain, _context) => {
131
477
  const headerSize = counterHeaderSize(counter, domain);
@@ -133,6 +479,7 @@ function repeatTupleParser(kinds) {
133
479
  return { items: parsed.items, consumed: parsed.consumed + headerSize };
134
480
  };
135
481
  }
482
+ /** Parse transferable indexed-signature groups with a 3-field header + siger list. */
136
483
  function transIdxSigGroupsParser(input, counter, version, domain, _context) {
137
484
  const items = [];
138
485
  let offset = counterHeaderSize(counter, domain);
@@ -145,10 +492,11 @@ function transIdxSigGroupsParser(input, counter, version, domain, _context) {
145
492
  offset += header.consumed;
146
493
  const sigers = parseSigerList(input.slice(offset), version, domain);
147
494
  offset += sigers.consumed;
148
- items.push([...header.items, sigers.items]);
495
+ items.push(tupleItem([...header.items, tupleItem(sigers.items)]));
149
496
  }
150
497
  return { items, consumed: offset };
151
498
  }
499
+ /** Parse transferable last-establishment sig groups with header + siger list. */
152
500
  function transLastIdxSigGroupsParser(input, counter, version, domain, _context) {
153
501
  const items = [];
154
502
  let offset = counterHeaderSize(counter, domain);
@@ -157,10 +505,11 @@ function transLastIdxSigGroupsParser(input, counter, version, domain, _context)
157
505
  offset += header.consumed;
158
506
  const sigers = parseSigerList(input.slice(offset), version, domain);
159
507
  offset += sigers.consumed;
160
- items.push([...header.items, sigers.items]);
508
+ items.push(tupleItem([...header.items, tupleItem(sigers.items)]));
161
509
  }
162
510
  return { items, consumed: offset };
163
511
  }
512
+ /** Parse SadPathSig groups: each path is followed by one nested signature group. */
164
513
  function sadPathSigParser(input, counter, version, domain, context) {
165
514
  const items = [];
166
515
  let offset = counterHeaderSize(counter, domain);
@@ -169,187 +518,125 @@ function sadPathSigParser(input, counter, version, domain, context) {
169
518
  offset += primitiveSize(path, domain);
170
519
  const sigGroup = parseAttachmentDispatchCompat(input.slice(offset), version, domain, context);
171
520
  offset += sigGroup.consumed;
172
- items.push([path.qb64, {
173
- code: sigGroup.group.code,
174
- name: sigGroup.group.name,
175
- count: sigGroup.group.count,
176
- }]);
521
+ items.push(tupleItem([
522
+ qb64Item(path.qb64),
523
+ nestedGroupItem(sigGroup.group.code, sigGroup.group.name, sigGroup.group.count),
524
+ ]));
177
525
  }
178
526
  return { items, consumed: offset };
179
527
  }
528
+ /** Parse SadPathSigGroup: root path + repeated (path, nested signature group). */
180
529
  function sadPathSigGroupParser(input, counter, version, domain, context) {
181
530
  const items = [];
182
531
  let offset = counterHeaderSize(counter, domain);
183
532
  const root = parseMatter(input.slice(offset), domain);
184
533
  offset += primitiveSize(root, domain);
185
- items.push(root.qb64);
534
+ items.push(qb64Item(root.qb64));
186
535
  for (let i = 0; i < counter.count; i++) {
187
536
  const path = parseMatter(input.slice(offset), domain);
188
537
  offset += primitiveSize(path, domain);
189
538
  const sigGroup = parseAttachmentDispatchCompat(input.slice(offset), version, domain, context);
190
539
  offset += sigGroup.consumed;
191
- items.push([path.qb64, {
192
- code: sigGroup.group.code,
193
- name: sigGroup.group.name,
194
- count: sigGroup.group.count,
195
- }]);
540
+ items.push(tupleItem([
541
+ qb64Item(path.qb64),
542
+ nestedGroupItem(sigGroup.group.code, sigGroup.group.name, sigGroup.group.count),
543
+ ]));
196
544
  }
197
545
  return { items, consumed: offset };
198
546
  }
547
+ /** Genus-version groups carry only the counter token itself as semantic payload. */
199
548
  function genusVersionParser(_input, counter, _version, domain, _context) {
200
549
  return {
201
- items: [counter.qb64],
550
+ items: [qb64Item(counter.qb64)],
202
551
  consumed: counterHeaderSize(counter, domain),
203
552
  };
204
553
  }
554
+ /** v1 quadlet/triplet parser variant with v1 wrapper-code semantics. */
205
555
  const V1_QUADLET_PARSER = (input, counter, version, domain, context) => parseQuadletGroup(input, counter, version, WRAPPER_GROUP_CODES_V1, domain, context);
556
+ /** v2 quadlet/triplet parser variant with v2 wrapper-code semantics. */
206
557
  const V2_QUADLET_PARSER = (input, counter, version, domain, context) => parseQuadletGroup(input, counter, version, WRAPPER_GROUP_CODES_V2, domain, context);
207
- const V1_DISPATCH = new Map([
208
- [CtrDexV1.GenericGroup, V1_QUADLET_PARSER],
209
- [CtrDexV1.BigGenericGroup, V1_QUADLET_PARSER],
210
- [CtrDexV1.BodyWithAttachmentGroup, V1_QUADLET_PARSER],
211
- [CtrDexV1.BigBodyWithAttachmentGroup, V1_QUADLET_PARSER],
212
- [CtrDexV1.AttachmentGroup, V1_QUADLET_PARSER],
213
- [CtrDexV1.BigAttachmentGroup, V1_QUADLET_PARSER],
214
- [CtrDexV1.NonNativeBodyGroup, V1_QUADLET_PARSER],
215
- [CtrDexV1.BigNonNativeBodyGroup, V1_QUADLET_PARSER],
216
- [CtrDexV1.ESSRPayloadGroup, V1_QUADLET_PARSER],
217
- [CtrDexV1.BigESSRPayloadGroup, V1_QUADLET_PARSER],
218
- [CtrDexV1.PathedMaterialCouples, V1_QUADLET_PARSER],
219
- [CtrDexV1.BigPathedMaterialCouples, V1_QUADLET_PARSER],
220
- [CtrDexV1.ControllerIdxSigs, repeatTupleParser(["indexer"])],
221
- [CtrDexV1.WitnessIdxSigs, repeatTupleParser(["indexer"])],
222
- [CtrDexV1.NonTransReceiptCouples, repeatTupleParser(["matter", "matter"])],
223
- [
224
- CtrDexV1.TransReceiptQuadruples,
225
- repeatTupleParser(["matter", "matter", "matter", "indexer"]),
226
- ],
227
- [CtrDexV1.FirstSeenReplayCouples, repeatTupleParser(["matter", "matter"])],
228
- [CtrDexV1.SealSourceCouples, repeatTupleParser(["matter", "matter"])],
229
- [
230
- CtrDexV1.SealSourceTriples,
231
- repeatTupleParser(["matter", "matter", "matter"]),
232
- ],
233
- [CtrDexV1.TransIdxSigGroups, transIdxSigGroupsParser],
234
- [CtrDexV1.TransLastIdxSigGroups, transLastIdxSigGroupsParser],
235
- [CtrDexV1.SadPathSig, sadPathSigParser],
236
- [CtrDexV1.SadPathSigGroup, sadPathSigGroupParser],
237
- [CtrDexV1.KERIACDCGenusVersion, genusVersionParser],
238
- ]);
239
- const V2_DISPATCH = new Map([
240
- [CtrDexV2.GenericGroup, V2_QUADLET_PARSER],
241
- [CtrDexV2.BigGenericGroup, V2_QUADLET_PARSER],
242
- [CtrDexV2.BodyWithAttachmentGroup, V2_QUADLET_PARSER],
243
- [CtrDexV2.BigBodyWithAttachmentGroup, V2_QUADLET_PARSER],
244
- [CtrDexV2.AttachmentGroup, V2_QUADLET_PARSER],
245
- [CtrDexV2.BigAttachmentGroup, V2_QUADLET_PARSER],
246
- [CtrDexV2.NonNativeBodyGroup, V2_QUADLET_PARSER],
247
- [CtrDexV2.BigNonNativeBodyGroup, V2_QUADLET_PARSER],
248
- [CtrDexV2.ESSRPayloadGroup, V2_QUADLET_PARSER],
249
- [CtrDexV2.BigESSRPayloadGroup, V2_QUADLET_PARSER],
250
- [CtrDexV2.PathedMaterialCouples, V2_QUADLET_PARSER],
251
- [CtrDexV2.BigPathedMaterialCouples, V2_QUADLET_PARSER],
252
- [CtrDexV2.DatagramSegmentGroup, V2_QUADLET_PARSER],
253
- [CtrDexV2.BigDatagramSegmentGroup, V2_QUADLET_PARSER],
254
- [CtrDexV2.ESSRWrapperGroup, V2_QUADLET_PARSER],
255
- [CtrDexV2.BigESSRWrapperGroup, V2_QUADLET_PARSER],
256
- [CtrDexV2.FixBodyGroup, V2_QUADLET_PARSER],
257
- [CtrDexV2.BigFixBodyGroup, V2_QUADLET_PARSER],
258
- [CtrDexV2.MapBodyGroup, V2_QUADLET_PARSER],
259
- [CtrDexV2.BigMapBodyGroup, V2_QUADLET_PARSER],
260
- [CtrDexV2.GenericMapGroup, V2_QUADLET_PARSER],
261
- [CtrDexV2.BigGenericMapGroup, V2_QUADLET_PARSER],
262
- [CtrDexV2.GenericListGroup, V2_QUADLET_PARSER],
263
- [CtrDexV2.BigGenericListGroup, V2_QUADLET_PARSER],
264
- [CtrDexV2.ControllerIdxSigs, repeatTupleParser(["indexer"])],
265
- [CtrDexV2.BigControllerIdxSigs, repeatTupleParser(["indexer"])],
266
- [CtrDexV2.WitnessIdxSigs, repeatTupleParser(["indexer"])],
267
- [CtrDexV2.BigWitnessIdxSigs, repeatTupleParser(["indexer"])],
268
- [CtrDexV2.NonTransReceiptCouples, repeatTupleParser(["matter", "matter"])],
269
- [CtrDexV2.BigNonTransReceiptCouples, repeatTupleParser(["matter", "matter"])],
270
- [
271
- CtrDexV2.TransReceiptQuadruples,
272
- repeatTupleParser(["matter", "matter", "matter", "indexer"]),
273
- ],
274
- [
275
- CtrDexV2.BigTransReceiptQuadruples,
276
- repeatTupleParser(["matter", "matter", "matter", "indexer"]),
277
- ],
278
- [CtrDexV2.FirstSeenReplayCouples, repeatTupleParser(["matter", "matter"])],
279
- [CtrDexV2.BigFirstSeenReplayCouples, repeatTupleParser(["matter", "matter"])],
280
- [CtrDexV2.SealSourceCouples, repeatTupleParser(["matter", "matter"])],
281
- [CtrDexV2.BigSealSourceCouples, repeatTupleParser(["matter", "matter"])],
282
- [
283
- CtrDexV2.SealSourceTriples,
284
- repeatTupleParser(["matter", "matter", "matter"]),
285
- ],
286
- [
287
- CtrDexV2.BigSealSourceTriples,
288
- repeatTupleParser(["matter", "matter", "matter"]),
289
- ],
290
- [CtrDexV2.SealSourceLastSingles, repeatTupleParser(["matter"])],
291
- [CtrDexV2.BigSealSourceLastSingles, repeatTupleParser(["matter"])],
292
- [CtrDexV2.DigestSealSingles, repeatTupleParser(["matter"])],
293
- [CtrDexV2.BigDigestSealSingles, repeatTupleParser(["matter"])],
294
- [CtrDexV2.MerkleRootSealSingles, repeatTupleParser(["matter"])],
295
- [CtrDexV2.BigMerkleRootSealSingles, repeatTupleParser(["matter"])],
296
- [
297
- CtrDexV2.BackerRegistrarSealCouples,
298
- repeatTupleParser(["matter", "matter"]),
299
- ],
300
- [
301
- CtrDexV2.BigBackerRegistrarSealCouples,
302
- repeatTupleParser(["matter", "matter"]),
303
- ],
304
- [CtrDexV2.TypedDigestSealCouples, repeatTupleParser(["matter", "matter"])],
305
- [CtrDexV2.BigTypedDigestSealCouples, repeatTupleParser(["matter", "matter"])],
306
- [CtrDexV2.TransIdxSigGroups, transIdxSigGroupsParser],
307
- [CtrDexV2.BigTransIdxSigGroups, transIdxSigGroupsParser],
308
- [CtrDexV2.TransLastIdxSigGroups, transLastIdxSigGroupsParser],
309
- [CtrDexV2.BigTransLastIdxSigGroups, transLastIdxSigGroupsParser],
310
- [
311
- CtrDexV2.BlindedStateQuadruples,
312
- repeatTupleParser(["matter", "matter", "matter", "matter"]),
313
- ],
314
- [
315
- CtrDexV2.BigBlindedStateQuadruples,
316
- repeatTupleParser(["matter", "matter", "matter", "matter"]),
317
- ],
318
- [
319
- CtrDexV2.BoundStateSextuples,
320
- repeatTupleParser([
321
- "matter",
322
- "matter",
323
- "matter",
324
- "matter",
325
- "matter",
326
- "matter",
327
- ]),
328
- ],
329
- [
330
- CtrDexV2.BigBoundStateSextuples,
331
- repeatTupleParser([
332
- "matter",
333
- "matter",
334
- "matter",
335
- "matter",
336
- "matter",
337
- "matter",
338
- ]),
339
- ],
340
- [
341
- CtrDexV2.TypedMediaQuadruples,
342
- repeatTupleParser(["matter", "matter", "matter", "matter"]),
343
- ],
344
- [
345
- CtrDexV2.BigTypedMediaQuadruples,
346
- repeatTupleParser(["matter", "matter", "matter", "matter"]),
347
- ],
348
- [CtrDexV2.KERIACDCGenusVersion, genusVersionParser],
349
- ]);
558
+ /**
559
+ * Parser instance cache keyed by tuple shape (`matter|indexer|...`).
560
+ *
561
+ * Why:
562
+ * many codes share identical repeated-tuple semantics; caching preserves one
563
+ * parser closure per tuple shape and keeps dispatch-map construction stable.
564
+ */
565
+ const REPEAT_TUPLE_PARSER_CACHE = new Map();
566
+ /**
567
+ * Resolve one expanded descriptor into its executable parser function.
568
+ *
569
+ * Invariant:
570
+ * this switch must remain exhaustive over `DispatchParserKind`.
571
+ */
572
+ function parserForDescriptor(descriptor) {
573
+ switch (descriptor.parserKind) {
574
+ case "quadlet":
575
+ return descriptor.version === 2 ? V2_QUADLET_PARSER : V1_QUADLET_PARSER;
576
+ case "repeatTuple": {
577
+ if (!descriptor.tupleKinds || descriptor.tupleKinds.length === 0) {
578
+ throw new Error(`Dispatch descriptor ${descriptor.code} is missing tupleKinds`);
579
+ }
580
+ const key = descriptor.tupleKinds.join("|");
581
+ let parser = REPEAT_TUPLE_PARSER_CACHE.get(key);
582
+ if (!parser) {
583
+ parser = repeatTupleParser(descriptor.tupleKinds);
584
+ REPEAT_TUPLE_PARSER_CACHE.set(key, parser);
585
+ }
586
+ return parser;
587
+ }
588
+ case "transIdxSigGroups":
589
+ return transIdxSigGroupsParser;
590
+ case "transLastIdxSigGroups":
591
+ return transLastIdxSigGroupsParser;
592
+ case "sadPathSig":
593
+ return sadPathSigParser;
594
+ case "sadPathSigGroup":
595
+ return sadPathSigGroupParser;
596
+ case "genusVersion":
597
+ return genusVersionParser;
598
+ }
599
+ }
600
+ /**
601
+ * Build major-version dispatch maps from normalized descriptors.
602
+ *
603
+ * Invariant:
604
+ * each code appears at most once per major version in the final maps; if a code
605
+ * were duplicated in descriptors, later entries would overwrite earlier ones.
606
+ */
607
+ function buildDispatchByVersion(descriptors) {
608
+ const dispatch = {
609
+ 1: new Map(),
610
+ 2: new Map(),
611
+ };
612
+ for (const descriptor of descriptors) {
613
+ dispatch[descriptor.version].set(descriptor.code, parserForDescriptor(descriptor));
614
+ }
615
+ return dispatch;
616
+ }
617
+ /** Generated dispatch maps keyed by major version. */
618
+ const DISPATCH_BY_MAJOR = buildDispatchByVersion(ATTACHMENT_DISPATCH_DESCRIPTORS);
619
+ /**
620
+ * Lift major-indexed artifacts into a major+minor registry.
621
+ *
622
+ * Current parser supports one minor (`0`) per major, but this keeps lookup
623
+ * contracts explicit and ready for minor-version progression.
624
+ */
625
+ function asMinorZeroRegistry(byMajor) {
626
+ return Object.freeze({
627
+ 1: Object.freeze({ 0: byMajor[1] }),
628
+ 2: Object.freeze({ 0: byMajor[2] }),
629
+ });
630
+ }
631
+ /** Siger-list counter code sets resolved by major+minor stream version. */
632
+ const SIGER_LIST_CODES_BY_VERSION = asMinorZeroRegistry(SIGER_LIST_CODES_BY_MAJOR);
633
+ /** Attachment-group dispatch registry resolved by major+minor stream version. */
634
+ const DISPATCH_BY_VERSION = asMinorZeroRegistry(DISPATCH_BY_MAJOR);
635
+ /** Resolve attachment dispatch table for the requested stream version. */
350
636
  function getDispatch(version) {
351
- return version.major >= 2 ? V2_DISPATCH : V1_DISPATCH;
637
+ return resolveVersionedRegistryValue(DISPATCH_BY_VERSION, version, "attachment dispatch registry").value;
352
638
  }
639
+ /** Parse one attachment group with an explicit version (no version fallback). */
353
640
  function parseAttachmentDispatchWithVersion(input, version, domain, context) {
354
641
  const counter = parseCounter(input, version, domain);
355
642
  const parser = getDispatch(version).get(counter.code);
@@ -371,50 +658,77 @@ function parseAttachmentDispatchWithVersion(input, version, domain, context) {
371
658
  consumed: parsed.consumed,
372
659
  };
373
660
  }
374
- function parseAttachmentDispatchByMode(input, version, domain, context) {
375
- if (context.mode === "strict") {
376
- return parseAttachmentDispatchWithVersion(input, version, domain, context);
377
- }
661
+ /**
662
+ * Parse one attachment group using an injected fallback strategy.
663
+ *
664
+ * Flow:
665
+ * 1) attempt parse with provided version
666
+ * 2) ask policy whether failure is terminal or retryable
667
+ * 3) on retry, emit fallback observation and parse with policy-selected version
668
+ */
669
+ function parseAttachmentDispatchWithPolicy(input, version, domain, context) {
378
670
  try {
379
671
  return parseAttachmentDispatchWithVersion(input, version, domain, context);
380
672
  }
381
673
  catch (error) {
382
- if (!(error instanceof UnknownCodeError) &&
383
- !(error instanceof DeserializeError)) {
384
- throw error;
674
+ const normalized = asError(error);
675
+ const decision = context.versionFallbackPolicy.onVersionDispatchFailure(normalized, version, domain);
676
+ if (decision.action === "throw") {
677
+ context.recoveryDiagnosticObserver?.({
678
+ type: "version-fallback-rejected",
679
+ version,
680
+ domain,
681
+ errorName: normalized.name,
682
+ reason: normalized.message,
683
+ });
684
+ throw normalized;
385
685
  }
386
- const alternate = version.major >= 2
387
- ? { major: 1, minor: 0 }
388
- : { major: 2, minor: 0 };
389
- const info = {
390
- from: version,
391
- to: alternate,
392
- domain,
393
- reason: error.message,
394
- };
395
- if (context.onVersionFallback) {
396
- context.onVersionFallback(info);
397
- }
398
- else {
399
- console.warn(`CESR attachment dispatch fallback ${version.major}.${version.minor} -> ${alternate.major}.${alternate.minor} (${domain}): ${error.message}`);
400
- }
401
- return parseAttachmentDispatchWithVersion(input, alternate, domain, context);
686
+ context.recoveryDiagnosticObserver?.({
687
+ type: "version-fallback-accepted",
688
+ from: decision.info.from,
689
+ to: decision.info.to,
690
+ domain: decision.info.domain,
691
+ reason: decision.info.reason,
692
+ });
693
+ context.versionFallbackPolicy.onVersionFallback(decision.info);
694
+ return parseAttachmentDispatchWithVersion(input, decision.retryVersion, domain, context);
402
695
  }
403
696
  }
404
697
  /**
405
- * Parse with primary version, then retry with the alternate major on unknown
406
- * code/deserialize failures. This mirrors real-world mixed-stream tolerance.
698
+ * Parse one attachment group with policy-aware fallback behavior.
699
+ *
700
+ * By default this uses compat policy semantics to mirror historical
701
+ * `parseAttachmentDispatchCompat` behavior unless overridden via
702
+ * `options.versionFallbackPolicy`.
407
703
  */
408
704
  export function parseAttachmentDispatchCompat(input, version, domain, options = {}) {
705
+ const versionFallbackPolicy = options.versionFallbackPolicy ??
706
+ createAttachmentVersionFallbackPolicy({
707
+ mode: options.mode,
708
+ });
709
+ const recoveryDiagnosticObserver = composeRecoveryDiagnosticObserver({
710
+ onRecoveryDiagnostic: options.onRecoveryDiagnostic,
711
+ onAttachmentVersionFallback: options.versionFallbackPolicy
712
+ ? undefined
713
+ : options.onVersionFallback,
714
+ });
409
715
  const context = {
410
- mode: options.mode ?? "compat",
411
- onVersionFallback: options.onVersionFallback,
716
+ versionFallbackPolicy,
717
+ recoveryDiagnosticObserver,
412
718
  };
413
- return parseAttachmentDispatchByMode(input, version, domain, context);
719
+ return parseAttachmentDispatchWithPolicy(input, version, domain, context);
414
720
  }
415
- /** Strict versioned attachment-group dispatch (no major-version fallback). */
721
+ /**
722
+ * Parse one attachment group with strict fail-fast semantics.
723
+ *
724
+ * Why:
725
+ * this API guarantees no major-version fallback and no wrapper opaque-tail
726
+ * preservation, matching parity/validation use cases.
727
+ */
416
728
  export function parseAttachmentDispatch(input, version, domain) {
417
- return parseAttachmentDispatchByMode(input, version, domain, {
418
- mode: "strict",
729
+ return parseAttachmentDispatchWithPolicy(input, version, domain, {
730
+ versionFallbackPolicy: createAttachmentVersionFallbackPolicy({
731
+ mode: "strict",
732
+ }),
419
733
  });
420
734
  }