sememe 0.0.0-stage → 0.4.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2777 -2
  3. package/bin/sememe.mjs +556 -0
  4. package/index.mjs +48 -0
  5. package/interface/ata-ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL.json +62 -0
  6. package/interface/native-11111111111111111111111111111111.json +226 -0
  7. package/interface/native-ComputeBudget111111111111111111111111111111.json +72 -0
  8. package/interface/raydium-675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8.json +352 -0
  9. package/interface/token-TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA.json +334 -0
  10. package/interface/token-TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb.json +535 -0
  11. package/interface/token-metadata.json +132 -0
  12. package/package.json +71 -4
  13. package/src/account-list.mjs +308 -0
  14. package/src/account-union.mjs +523 -0
  15. package/src/account.mjs +199 -0
  16. package/src/acquire.mjs +145 -0
  17. package/src/adopt.mjs +206 -0
  18. package/src/ata-interface.mjs +276 -0
  19. package/src/ata.mjs +223 -0
  20. package/src/base58.mjs +96 -0
  21. package/src/canonical.mjs +24 -0
  22. package/src/cli-surface.mjs +300 -0
  23. package/src/codec.mjs +1118 -0
  24. package/src/conformance.mjs +431 -0
  25. package/src/corpus/harvest-compute-budget-2026-10-31.json +19122 -0
  26. package/src/corpus/harvest-system-2026-10-31.json +26090 -0
  27. package/src/decode.mjs +286 -0
  28. package/src/dialect.mjs +180 -0
  29. package/src/discriminators.mjs +192 -0
  30. package/src/drift.mjs +172 -0
  31. package/src/gap.mjs +205 -0
  32. package/src/lighthouse.mjs +168 -0
  33. package/src/lock.mjs +440 -0
  34. package/src/native-audit.mjs +56 -0
  35. package/src/native-corpus.mjs +57 -0
  36. package/src/native-interface.mjs +252 -0
  37. package/src/native.mjs +324 -0
  38. package/src/peer.mjs +95 -0
  39. package/src/program-id.mjs +74 -0
  40. package/src/pull.mjs +117 -0
  41. package/src/rawle.mjs +454 -0
  42. package/src/raydium-interface.mjs +747 -0
  43. package/src/raydium.mjs +391 -0
  44. package/src/receipt-digest.mjs +84 -0
  45. package/src/receipt-name.mjs +128 -0
  46. package/src/receipt-order.mjs +56 -0
  47. package/src/shank.mjs +73 -0
  48. package/src/short-forms.mjs +209 -0
  49. package/src/store-read.mjs +253 -0
  50. package/src/store-write.mjs +46 -0
  51. package/src/token-interface.mjs +394 -0
  52. package/src/token-metadata-interface.mjs +371 -0
  53. package/src/token-metadata.mjs +317 -0
  54. package/src/token.mjs +443 -0
  55. package/src/union.mjs +428 -0
  56. package/vendor/anza-solana-sdk/8db406e2edfafa25939ec782b77b5b6f30de164d/LICENSE.txt +201 -0
  57. package/vendor/anza-solana-sdk/8db406e2edfafa25939ec782b77b5b6f30de164d/compute-budget-interface/src/lib.rs +93 -0
  58. package/vendor/anza-solana-sdk/8db406e2edfafa25939ec782b77b5b6f30de164d/system-interface/src/instruction.rs +1899 -0
  59. package/vendor/lighthouse/4c579479c98635e419b1b167f08be02a71604a71/LICENSE +21 -0
  60. package/vendor/lighthouse/4c579479c98635e419b1b167f08be02a71604a71/lighthouse.json +2769 -0
  61. package/vendor/raydium-amm/91739d8d9f5cf347e0ce14ebe4d2bb7069fb31b6/LICENSE +201 -0
  62. package/vendor/raydium-amm/91739d8d9f5cf347e0ce14ebe4d2bb7069fb31b6/program/src/instruction.rs +1189 -0
  63. package/vendor/raydium-amm/91739d8d9f5cf347e0ce14ebe4d2bb7069fb31b6/program/src/state.rs +1348 -0
  64. package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/LICENSE +201 -0
  65. package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/interface/src/instruction.rs +169 -0
  66. package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/interface/src/lib.rs +12 -0
  67. package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/program/src/processor.rs +308 -0
  68. package/vendor/spl-token/737788ff5bcc3f1f44cfb222f25483187650be76/LICENSE.txt +176 -0
  69. package/vendor/spl-token/737788ff5bcc3f1f44cfb222f25483187650be76/interface/src/instruction.rs +1963 -0
  70. package/vendor/spl-token-2022/5251dcc07ecbcb721d6c4f0231090defadf00311/LICENSE.txt +176 -0
  71. package/vendor/spl-token-2022/5251dcc07ecbcb721d6c4f0231090defadf00311/interface/src/instruction.rs +2868 -0
  72. package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/LICENSE +201 -0
  73. package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/error.rs +58 -0
  74. package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/instruction.rs +596 -0
  75. package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/lib.rs +21 -0
  76. package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/state.rs +229 -0
@@ -0,0 +1,523 @@
1
+ // Union arbitration for ACCOUNT bytes: two verified interface sources, one
2
+ // account, and the disagreement made explicit instead of picked by preference.
3
+ //
4
+ // Why this exists (2026-11-08): instructions and events have had union decode
5
+ // since v0.4.0 — `sememe decode --union` reads the on-chain pull and the
6
+ // adopted first-party published copy, reports which one answered, and lets
7
+ // byte evidence decide. Accounts never got that half. `sememe account` read
8
+ // exactly ONE receipt (findStoredIdl, the newest), so a store holding two
9
+ // verified copies of a program had two opinions about an account's layout and
10
+ // only one of them was ever consulted — the README named this as the open
11
+ // item ("the same arbitration for account decoding (accounts still read a
12
+ // single receipt)").
13
+ //
14
+ // The measured case that made it concrete (2026-11-08, 12 real mainnet
15
+ // pump.fun accounts, research/account-union-2026-11-08.json): the program's
16
+ // on-chain IDL copy describes `BondingCurve` with 13 fields, the published
17
+ // copy with 19. A live 166-byte BondingCurve account decodes under the
18
+ // on-chain copy as 125 consumed + 41 unexplained bytes (partial answer), and
19
+ // under the published copy as a full byte-exact round trip. The plain path
20
+ // reported the partial one; the copy that explains every byte sat in the same
21
+ // store unread.
22
+ //
23
+ // The rules are the instruction union's rules, translated to accounts:
24
+ //
25
+ // - Both sources refuse -> refusal naming BOTH reasons.
26
+ // - One refuses, the other decodes -> admitted ONLY when the survivor
27
+ // reproduces its own bytes (full round trip, or the described prefix with
28
+ // the rest reported as trailing). A source that names an account it cannot
29
+ // re-encode is refused exactly like a first source would be.
30
+ // - Both decode under DIFFERENT account names for the same 8 bytes
31
+ // -> hard refusal (`name_conflict`): two schemas for one discriminator is
32
+ // an interface conflict, and guessing which one to believe would launder it.
33
+ // - Both decode the same name to the same args -> `agree`.
34
+ // - Same name, different args -> `layout_drift`; the winner is chosen by
35
+ // byte evidence, strongest first: a source whose re-encode reproduces the
36
+ // WHOLE account body beats one that only reproduces a described prefix,
37
+ // and between two prefix-only answers the one with FEWER unexplained
38
+ // bytes explains strictly more of the account. When the evidence does not
39
+ // decide, the PRIMARY (the on-chain pull, whenever one exists) stands and
40
+ // the drift is reported — never resolved by preference.
41
+ //
42
+ // Nothing here mutates a source. Account discriminators are filled per source
43
+ // with Anchor's own rule before comparison (src/account.mjs); a stored value
44
+ // always wins and a conflict is recorded, never overwritten.
45
+
46
+ import { decodeAccountData, withDerivedAccountDiscriminators, identifyAccount } from './account.mjs';
47
+ import { toJsonSafe } from './codec.mjs';
48
+ import { UnionRefusal } from './union.mjs';
49
+ import { diffArgsByNormalisedName } from './drift.mjs';
50
+
51
+ const reasonOf = (error) => String(error?.message ?? error).slice(0, 200);
52
+
53
+ /**
54
+ * Field paths where two decoded account trees disagree, plus the fields the
55
+ * two copies name differently.
56
+ *
57
+ * The comparison is by NORMALISED spelling (src/drift.mjs): a field both
58
+ * copies declare — `lpMint` in one, `lp_mint` in the other — is the same
59
+ * field, and a difference in its NAME is not a difference in the account.
60
+ * What lands in `paths` is a difference in VALUE or SHAPE, or a field only one
61
+ * copy declares; what lands in `spellingOnly` is the naming difference itself.
62
+ *
63
+ * Measured 2026-11-09 (research/drift-naming-2026-11-09.json): before this
64
+ * rule, meteora-damm-v1's 11 drift rows filled the 20-path cap (11/11
65
+ * truncated, 220 paths), and 33 REAL differences were crowded out because the
66
+ * cap was filled with spelling differences.
67
+ */
68
+ export function diffAccountPaths(primaryArgs, secondaryArgs) {
69
+ const diff = diffArgsByNormalisedName(primaryArgs, secondaryArgs);
70
+ return {
71
+ paths: diff.paths,
72
+ truncated: diff.truncated,
73
+ differs: diff.differs,
74
+ spellingOnly: diff.spellingOnly,
75
+ };
76
+ }
77
+
78
+ /** One source's account probe: identify the type in THIS source's table, then
79
+ * decode and re-encode under THIS source's layout. Every fact stays
80
+ * per-source — a trailing count or a round-trip flag from one copy must never
81
+ * be attached to the other copy's answer. */
82
+ function probeAccount(source, bytes, forcedName) {
83
+ try {
84
+ const identified = forcedName ? { name: forcedName } : identifyAccount(source.idl, bytes);
85
+ if (!identified) {
86
+ return {
87
+ ok: false,
88
+ reason: `no account type in this source matches the first 8 bytes `
89
+ + `(${bytes.subarray(0, 8).toString('hex')})`,
90
+ };
91
+ }
92
+ const decoded = decodeAccountData(source.idl, bytes, identified.name, { trailing: 'report' });
93
+ return {
94
+ ok: true,
95
+ decoded,
96
+ fullExact: decoded.roundTripByteExact,
97
+ describedExact: decoded.describedRoundTripByteExact,
98
+ // Provenance of the discriminator that matched, kept PER SOURCE: a
99
+ // stored value is bytes the program wrote on chain, a derived one is
100
+ // Anchor's rule filling a gap. Reporting one source's derivation under
101
+ // the other source's label would be exactly the drift this module exists
102
+ // to make visible.
103
+ discriminatorSource: source.discriminatorDerivation.filledAccounts > 0
104
+ ? (source.discriminatorDerivation.conflicts > 0 ? 'stored+derived' : 'derived')
105
+ : 'stored',
106
+ discriminatorDerivation: source.discriminatorDerivation,
107
+ };
108
+ } catch (error) {
109
+ return { ok: false, reason: reasonOf(error) };
110
+ }
111
+ }
112
+
113
+ /** Fill discriminator gaps for this source only, keeping the source's identity. */
114
+ function prepareAccountSource(source) {
115
+ const { idl, filled, conflicts } = withDerivedAccountDiscriminators(source.idl);
116
+ // Which accounts carried a STORED discriminator before filling — the line
117
+ // between "this copy derives its discriminator from the name" (so a renamed
118
+ // copy can never match) and "this copy pins a value that disagrees with the
119
+ // bytes" (a drifted copy). The rename diagnosis below must only claim the
120
+ // first mechanism when it is the first mechanism.
121
+ const storedAccountNames = new Set(
122
+ (source.idl?.accounts ?? [])
123
+ .filter((a) => Array.isArray(a?.discriminator) && a.discriminator.length === 8)
124
+ .map((a) => a.name),
125
+ );
126
+ return {
127
+ ...source,
128
+ idl,
129
+ storedAccountNames,
130
+ discriminatorDerivation: { filledAccounts: filled, conflicts: conflicts.length },
131
+ };
132
+ }
133
+
134
+ /**
135
+ * The refusing copy may well KNOW this account — under a different spelling.
136
+ * Anchor derives an account discriminator from the EXACT name, so a copy that
137
+ * spells it `vault` instead of `Vault` derives a different 8 bytes and can
138
+ * never identify the deployed account, no matter how good its layout is.
139
+ * Reporting that as "no account type matches these bytes" is true of the
140
+ * discriminator but hides the mechanism from the user, who then cannot tell a
141
+ * missing account from a RENAMED one (measured on 12 real meteora-vault
142
+ * accounts, research/account-union-cross-2026-11-08.json).
143
+ *
144
+ * Called only when the OTHER source identified these bytes: a name is what
145
+ * makes the comparison possible. Two sources that both fail tell us nothing
146
+ * about each other (see the R3 control).
147
+ */
148
+ function diagnoseRename(failedProbe, failedSource, bytes, otherName) {
149
+ if (!otherName) return null;
150
+ const accounts = failedSource.idl?.accounts ?? [];
151
+ const variant = accounts.find((a) => typeof a?.name === 'string'
152
+ && a.name !== otherName
153
+ && a.name.toLowerCase() === otherName.toLowerCase());
154
+ if (!variant) return null;
155
+ const head = bytes.subarray(0, 8).toString('hex');
156
+ const declared = Buffer.from(variant.discriminator ?? []).toString('hex');
157
+ if (failedSource.storedAccountNames?.has(variant.name)) {
158
+ // A stored value that disagrees is a different phenomenon (drift), not a
159
+ // rename: say that instead of blaming the derivation.
160
+ failedProbe.reason += `; this copy DOES declare the account, as "${variant.name}", with stored `
161
+ + `discriminator ${declared} which does not match these bytes (${head}) — the other copy `
162
+ + `identifies them as "${otherName}"`;
163
+ return variant.name;
164
+ }
165
+ failedProbe.reason += `; this copy DOES declare the account, as "${variant.name}" — but Anchor `
166
+ + `derives an account discriminator from the exact name, so "${variant.name}" derives `
167
+ + `${declared} and these bytes carry ${head} (the other copy identifies them as "${otherName}")`;
168
+ return variant.name;
169
+ }
170
+
171
+ /**
172
+ * Format one field's declared type as the reader sees it in the IDL, so two
173
+ * copies' declarations can be compared line by line (`publicKey[30]`, `u64`,
174
+ * `VaultBumps`, `struct{4}`).
175
+ */
176
+ function formatFieldType(type, depth = 0) {
177
+ if (typeof type === 'string') return type;
178
+ if (depth > 6) return '…';
179
+ if (Array.isArray(type)) return type.map((t) => formatFieldType(t, depth + 1)).join(', ');
180
+ if (type && typeof type === 'object') {
181
+ if (Array.isArray(type.array)) return `${formatFieldType(type.array[0], depth + 1)}[${type.array[1]}]`;
182
+ if (type.vec !== undefined) return `${formatFieldType(type.vec, depth + 1)}[]`;
183
+ if (type.option !== undefined) return `${formatFieldType(type.option, depth + 1)}?`;
184
+ if (type.defined !== undefined) {
185
+ return typeof type.defined === 'string' ? type.defined : (type.defined?.name ?? JSON.stringify(type.defined));
186
+ }
187
+ if (Array.isArray(type.fields)) return `struct{${type.fields.length}}`;
188
+ if (type.kind) return `${type.kind}`;
189
+ }
190
+ return JSON.stringify(type);
191
+ }
192
+
193
+ /** The declared field list of one account type in one copy (inline or types). */
194
+ function declaredFields(idl, accountName) {
195
+ const account = (idl?.accounts ?? []).find((a) => a?.name === accountName);
196
+ const inline = account?.type && typeof account.type === 'object' ? account.type : null;
197
+ const typedef = (idl?.types ?? []).find((t) => t?.name === accountName);
198
+ const type = inline ?? (typedef?.type ?? null);
199
+ if (!type || !Array.isArray(type.fields)) return null;
200
+ return type.fields;
201
+ }
202
+
203
+ /**
204
+ * Compare two copies' DECLARED layout for the same account — nothing is
205
+ * decoded, no byte decides anything here.
206
+ *
207
+ * This is the fact the rename diagnosis could see but not state: a copy that
208
+ * can never identify the deployed bytes may still declare a different layout
209
+ * for them. Reporting it keeps the verdict `primary_only` (the report is not
210
+ * evidence) while telling the reader that the answer they got is contested
211
+ * field by field. Measured on 12 meteora-vault accounts: 30 keys vs 29 keys
212
+ * plus `version`, `totalLpSupply`, `padding0` — 960 bytes either way.
213
+ *
214
+ * Fields are paired by NORMALISED spelling so that a pure rename
215
+ * (`totalAmount` vs `total_amount`) is not reported as a layout difference;
216
+ * a field only one copy declares, or one both declare with different types,
217
+ * is.
218
+ */
219
+ function diffDeclaredLayout(primaryIdl, primaryName, secondaryIdl, secondaryName) {
220
+ const primaryFields = declaredFields(primaryIdl, primaryName);
221
+ const secondaryFields = declaredFields(secondaryIdl, secondaryName);
222
+ if (!primaryFields || !secondaryFields) return null;
223
+
224
+ const normalise = (key) => String(key).replace(/^_+/, '')
225
+ .replace(/_+([a-z0-9])/g, (m, c) => c.toUpperCase());
226
+ const present = (field) => `${formatFieldType(field.type)}`;
227
+
228
+ const secondaryByNorm = new Map(secondaryFields.map((f) => [normalise(f.name), f]));
229
+ const matchedNorm = new Set();
230
+ const diffs = [];
231
+
232
+ for (const field of primaryFields) {
233
+ const key = normalise(field.name);
234
+ const other = secondaryByNorm.get(key);
235
+ if (!other) {
236
+ diffs.push({ path: field.name, primary: present(field), secondary: null });
237
+ continue;
238
+ }
239
+ matchedNorm.add(key);
240
+ if (present(field) !== present(other)) {
241
+ diffs.push({ path: field.name, primary: present(field), secondary: present(other) });
242
+ }
243
+ }
244
+ for (const field of secondaryFields) {
245
+ if (matchedNorm.has(normalise(field.name))) continue; // paired above
246
+ diffs.push({ path: field.name, primary: null, secondary: present(field) });
247
+ }
248
+
249
+ return {
250
+ primary: { account: primaryName, fields: primaryFields.length },
251
+ secondary: { account: secondaryName, fields: secondaryFields.length },
252
+ differs: diffs.length > 0,
253
+ diffs,
254
+ };
255
+ }
256
+
257
+ function accountView(source, probeResult) {
258
+ if (probeResult === null) return { label: source?.label ?? null, present: false };
259
+ return {
260
+ label: source.label,
261
+ present: true,
262
+ fingerprint: source.idlMeta?.fingerprint ?? null,
263
+ receipt: source.idlMeta?.receipt ?? null,
264
+ decoded: probeResult.ok,
265
+ refusedReason: probeResult.ok ? undefined : probeResult.reason,
266
+ name: probeResult.ok ? probeResult.decoded.name : undefined,
267
+ // Which byte evidence THIS source produced: every byte, the described
268
+ // prefix only, or neither. The arbitration below reads these three.
269
+ evidence: probeResult.ok
270
+ ? (probeResult.fullExact ? 'full_roundtrip'
271
+ : (probeResult.describedExact ? 'described_roundtrip' : 'none'))
272
+ : undefined,
273
+ roundTripByteExact: probeResult.ok ? probeResult.fullExact : undefined,
274
+ describedRoundTripByteExact: probeResult.ok ? probeResult.describedExact : undefined,
275
+ consumed: probeResult.ok ? probeResult.decoded.consumed : undefined,
276
+ trailingBytes: probeResult.ok ? probeResult.decoded.trailing.count : undefined,
277
+ };
278
+ }
279
+
280
+ function accountEnvelope(program, fields, winnerSource, view) {
281
+ return {
282
+ program,
283
+ ...fields,
284
+ idl: winnerSource.idlMeta,
285
+ object: winnerSource.objectPath,
286
+ view,
287
+ };
288
+ }
289
+
290
+ function accountFields(probe, bytes) {
291
+ const decoded = probe.decoded;
292
+ return {
293
+ kind: 'account',
294
+ account: decoded.name,
295
+ name: decoded.name,
296
+ args: toJsonSafe(decoded.args),
297
+ discriminator: decoded.discriminator,
298
+ discriminatorMatches: decoded.hasDiscriminator,
299
+ bytes: decoded.total,
300
+ consumed: decoded.consumed,
301
+ trailing: decoded.trailing,
302
+ roundTripByteExact: decoded.roundTripByteExact,
303
+ describedRoundTripByteExact: decoded.describedRoundTripByteExact,
304
+ describedBytes: decoded.consumed,
305
+ discriminatorSource: probe.discriminatorSource,
306
+ discriminatorDerivation: probe.discriminatorDerivation,
307
+ };
308
+ }
309
+
310
+ /**
311
+ * Decode one account's bytes against a primary and an optional secondary
312
+ * verified interface source.
313
+ *
314
+ * Sources are `{ label, idl, idlMeta, objectPath }` and must already be
315
+ * store-verified (receipt re-hash etc.) — this module arbitrates, it does not
316
+ * re-verify. `forcedName` skips identification for callers that already know
317
+ * the account type; when given, both sources decode THAT name, so a name
318
+ * conflict is impossible by construction and the verdict can never be a
319
+ * coin flip over identification.
320
+ *
321
+ * @throws {UnionRefusal} for 'name_conflict', a fallback without byte
322
+ * evidence, and total refusal — carrying `details.view`, so the caller
323
+ * gets BOTH sources' verdicts in the failure.
324
+ */
325
+ export function unionAccountDecode({ program, primary, secondary, bytes, name: forcedName, trailing, additional = [] }) {
326
+ const soleSource = !primary?.idl && secondary?.idl ? secondary : null;
327
+ if (!primary?.idl && !soleSource) {
328
+ throw new UnionRefusal('invalid_input', 'unionAccountDecode needs a primary source');
329
+ }
330
+ if (!(bytes instanceof Uint8Array)) {
331
+ throw new UnionRefusal('invalid_input', 'unionAccountDecode needs the account bytes as a Buffer');
332
+ }
333
+ bytes = Buffer.isBuffer(bytes)
334
+ ? bytes
335
+ : Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
336
+ if (trailing !== undefined && trailing !== 'report' && trailing !== 'error') {
337
+ throw new UnionRefusal('invalid_input', `unknown trailing policy ${JSON.stringify(trailing)}`);
338
+ }
339
+
340
+ const prim = prepareAccountSource(soleSource ?? primary);
341
+ const sec = secondary?.idl && secondary !== soleSource ? prepareAccountSource(secondary) : null;
342
+ const primAdopted = prim.idlMeta?.slot === null || prim.idlMeta?.slot === undefined;
343
+ const secAdopted = !sec || sec.idlMeta?.slot === null || sec.idlMeta?.slot === undefined;
344
+ const adoptedOnly = primAdopted && secAdopted;
345
+
346
+ const primProbe = probeAccount(prim, bytes, forcedName);
347
+ const secProbe = sec ? probeAccount(sec, bytes, forcedName) : null;
348
+
349
+ // A refusal that is really a RENAME must say so (see diagnoseRename): only
350
+ // when the other side identified the bytes do we have a name to compare with.
351
+ // The rename also tells us WHICH account the other copy declares — and that
352
+ // copy may declare a DIFFERENT LAYOUT for it (see diffDeclaredLayout). The
353
+ // report is NOT evidence and never enters the arbitration: it only tells the
354
+ // reader that the answer they got is contested, field by field.
355
+ let renamedTo = null;
356
+ if (primProbe.ok && secProbe && !secProbe.ok) {
357
+ renamedTo = diagnoseRename(secProbe, sec, bytes, primProbe.decoded.name);
358
+ } else if (secProbe?.ok && !primProbe.ok) {
359
+ renamedTo = diagnoseRename(primProbe, prim, bytes, secProbe.decoded.name);
360
+ }
361
+
362
+ const view = {
363
+ primary: accountView(prim, primProbe),
364
+ secondary: accountView(sec, secProbe),
365
+ sourcesAgree: null,
366
+ adoptedOnly,
367
+ additionalSources: additional.map((a) => ({
368
+ label: a.label,
369
+ fingerprint: a.idlMeta?.fingerprint,
370
+ receipt: a.idlMeta?.receipt,
371
+ })),
372
+ };
373
+
374
+ // The layout the OTHER copy declares for the account it could not identify
375
+ // — a report, never evidence: `primary_only`/`fallback_secondary` still stand
376
+ // exactly as the byte arbitration decides them below.
377
+ if (renamedTo) {
378
+ const primaryName = primProbe.ok ? primProbe.decoded.name : renamedTo;
379
+ const secondaryName = secProbe?.ok ? secProbe.decoded.name : renamedTo;
380
+ const declared = diffDeclaredLayout(prim.idl, primaryName, sec.idl, secondaryName);
381
+ if (declared) view.declaredLayout = declared;
382
+ }
383
+
384
+ const finish = (winner, probe, verdict, basis, evidence) => {
385
+ const fields = accountFields(probe, bytes);
386
+ if (trailing === 'error' && fields.trailing.count > 0) {
387
+ throw new UnionRefusal('trailing',
388
+ `account carries ${fields.trailing.count} byte(s) after the structure `
389
+ + `${winner.label} describes`,
390
+ { view });
391
+ }
392
+ view.verdict = verdict;
393
+ view.arbitration = { winner: winner.label, basis, evidence };
394
+ return accountEnvelope(program, fields, winner, view);
395
+ };
396
+
397
+ // Single source: behave like a plain decode, but say which verdict it is.
398
+ if (!sec) {
399
+ if (!primProbe.ok) {
400
+ throw new UnionRefusal('refused',
401
+ `decode refused by the only stored source (${prim.label}): ${primProbe.reason}`,
402
+ { view });
403
+ }
404
+ view.adoptedOnly = adoptedOnly;
405
+ return finish(prim, primProbe, 'single_source', 'only_source',
406
+ primProbe.fullExact ? 'full_roundtrip' : 'described_roundtrip');
407
+ }
408
+
409
+ view.discriminatorDerivation = {
410
+ primary: prim.discriminatorDerivation,
411
+ secondary: sec.discriminatorDerivation,
412
+ };
413
+
414
+ // Both refuse: name both reasons, never one of them.
415
+ if (!primProbe.ok && !secProbe.ok) {
416
+ throw new UnionRefusal('refused',
417
+ `both stored sources refuse this account — ${prim.label}: ${primProbe.reason}; `
418
+ + `${sec.label}: ${secProbe.reason}`,
419
+ { view });
420
+ }
421
+
422
+ // Primary refuses, secondary decodes: byte evidence admits the fallback.
423
+ if (!primProbe.ok && secProbe.ok) {
424
+ if (!secProbe.fullExact && !secProbe.describedExact) {
425
+ throw new UnionRefusal('union_fallback_refused',
426
+ `${prim.label} refuses (${primProbe.reason}); ${sec.label} names `
427
+ + `"${secProbe.decoded.name}" but cannot re-encode these bytes, so the fallback is refused`,
428
+ { view });
429
+ }
430
+ view.sourcesAgree = false;
431
+ return finish(sec, secProbe, 'fallback_secondary', 'primary_refused',
432
+ secProbe.fullExact ? 'full_roundtrip' : 'described_roundtrip');
433
+ }
434
+
435
+ // Secondary refuses, primary decodes: the plain answer, with the refusal kept.
436
+ if (primProbe.ok && !secProbe.ok) {
437
+ view.sourcesAgree = false;
438
+ return finish(prim, primProbe, 'primary_only', 'secondary_refused',
439
+ primProbe.fullExact ? 'full_roundtrip' : 'described_roundtrip');
440
+ }
441
+
442
+ // Both decode. Same 8 bytes matched by two sources -> the NAMES must agree.
443
+ if (primProbe.decoded.name !== secProbe.decoded.name) {
444
+ throw new UnionRefusal('name_conflict',
445
+ `interface conflict on the same account bytes: ${prim.label} says `
446
+ + `"${primProbe.decoded.name}", ${sec.label} says "${secProbe.decoded.name}" — `
447
+ + `refusing instead of picking a winner`,
448
+ { view: { ...view, primary: { ...view.primary, name: primProbe.decoded.name }, secondary: { ...view.secondary, name: secProbe.decoded.name } } });
449
+ }
450
+
451
+ const primFields = accountFields(primProbe, bytes);
452
+ const secFields = accountFields(secProbe, bytes);
453
+ // Same rule as the instruction union: a field one copy spells `lpMint` and
454
+ // the other `lp_mint` is the same field. Raw JSON equality made a naming
455
+ // difference into the verdict `layout_drift` (measured 2026-11-09: 11/11
456
+ // meteora-damm-v1 account rows filled the 20-path cap, and 33 REAL
457
+ // differences were crowded out by spelling paths).
458
+ const diff = diffAccountPaths(primFields.args, secFields.args);
459
+ const argsAgree = !diff.differs;
460
+
461
+ // Arbitration by bytes, strongest evidence first (see the header).
462
+ let winner = prim;
463
+ let basis = 'primary_by_default';
464
+ let evidence = primProbe.fullExact ? 'full_roundtrip'
465
+ : (primProbe.describedExact ? 'described_roundtrip' : 'none');
466
+ const primTrailing = primProbe.decoded.trailing.count;
467
+ const secTrailing = secProbe.decoded.trailing.count;
468
+ if (secProbe.fullExact && !primProbe.fullExact) {
469
+ winner = sec;
470
+ basis = 'byte_evidence_full_roundtrip';
471
+ evidence = 'full_roundtrip';
472
+ } else if (!primProbe.fullExact && !secProbe.fullExact
473
+ && primProbe.describedExact && secProbe.describedExact
474
+ && secTrailing < primTrailing) {
475
+ winner = sec;
476
+ basis = 'byte_evidence_fewer_unexplained_bytes';
477
+ evidence = `trailing ${secTrailing} < ${primTrailing}`;
478
+ } else if (primProbe.fullExact && secProbe.fullExact && !argsAgree) {
479
+ // Both reproduce every byte yet describe different values: the evidence
480
+ // genuinely does not decide (two layouts can serialise to the same bytes).
481
+ basis = 'primary_by_default_evidence_tied';
482
+ evidence = 'both_full_roundtrip';
483
+ }
484
+
485
+ view.sourcesAgree = argsAgree;
486
+ if (!argsAgree) {
487
+ view.verdict = 'layout_drift';
488
+ view.drift = {
489
+ ...diff,
490
+ primaryExplainsFully: primProbe.fullExact,
491
+ secondaryExplainsFully: secProbe.fullExact,
492
+ primaryTrailingBytes: primTrailing,
493
+ secondaryTrailingBytes: secTrailing,
494
+ note: 'both sources decoded the same account bytes to different values; '
495
+ + 'the winner was chosen by byte evidence where it decides, otherwise the primary stands '
496
+ + 'and the drift is reported. `paths` names the fields whose VALUES differ or that only '
497
+ + 'one copy declares; `spellingOnly` names the fields both copies declare under '
498
+ + 'different spellings — a naming style, not a layout difference',
499
+ };
500
+ } else {
501
+ view.verdict = 'agree';
502
+ if (diff.spellingOnly.length > 0) {
503
+ view.naming = {
504
+ spellingOnly: diff.spellingOnly,
505
+ note: 'the two copies spell the same fields differently and decode them to the same values; the naming difference is reported, it is not a disagreement about the data',
506
+ };
507
+ }
508
+ }
509
+ view.arbitration = { winner: winner.label, basis, evidence };
510
+ if (trailing === 'error') {
511
+ const winnerProbe = winner === prim ? primProbe : secProbe;
512
+ const fields = accountFields(winnerProbe, bytes);
513
+ if (fields.trailing.count > 0) {
514
+ throw new UnionRefusal('trailing',
515
+ `account carries ${fields.trailing.count} byte(s) after the structure ${winner.label} describes`,
516
+ { view });
517
+ }
518
+ }
519
+ const fields = winner === prim ? primFields : secFields;
520
+ return accountEnvelope(program, fields, winner, view);
521
+ }
522
+
523
+ export { UnionRefusal } from './union.mjs';