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
package/src/shank.mjs ADDED
@@ -0,0 +1,73 @@
1
+ // Shank / Kinobi dialect support for IDL-driven decoding.
2
+ //
3
+ // Why this exists: not every published IDL is an Anchor IDL. Shank-generated
4
+ // IDLs (and the Codama/Kinobi clients built from them) describe the SAME Borsh
5
+ // field layouts but a DIFFERENT dispatch and length dialect:
6
+ // - instructions carry `discriminant: { type: "u8", value: N }` — a single
7
+ // variant-order byte — instead of an 8-byte Anchor sighash;
8
+ // - enums are Borsh 1-byte variant indexes (sememe already decoded these);
9
+ // - the lighthouse family encodes array/string lengths as LEB128
10
+ // (`CompactU64`, `CompactBytes`, `*Assertions` — custom types the IDL
11
+ // references but does not define; the wire form is pinned upstream in
12
+ // lighthouse_common::CompactU64 / LEB128Vec and mirrored by the program's
13
+ // own generated JS client, clients/js/src/hooked.ts at Jac0xb/lighthouse).
14
+ //
15
+ // Measured ground truth (2026-11-04): all 42 Lighthouse payloads in the
16
+ // 156-fixture foreign sweep (step-ingestooor @001cf39) dispatch on their shank
17
+ // discriminant byte (0x06 = AssertAccountInfoMulti, 0x0a =
18
+ // AssertTokenAccountMulti — both present in the first-party IDL) and decode +
19
+ // re-encode byte-exactly under this dialect.
20
+ //
21
+ // Rules, all falsifiable:
22
+ // - A shank discriminant is NEVER overwritten by Anchor's sighash derivation:
23
+ // the two schemes are independent; deriving one from the other produces a
24
+ // table that matches no real wire. (The pre-fix behavior filled all 18
25
+ // lighthouse instructions with bogus sighashes — measured wrong.)
26
+ // - The hooked LEB128 types engage only for an IDL in the shank dialect and
27
+ // only for a name the IDL does not define itself; a name defined in the
28
+ // IDL's own `types` always wins, and an unknown undefined name still
29
+ // refuses (`unknown defined type`). Nothing is guessed from shape.
30
+
31
+ const isPlainObject = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
32
+
33
+ /** True when `instruction` carries a shank 1-byte discriminant. */
34
+ export function isShankDiscriminant(instruction) {
35
+ const raw = instruction?.discriminant;
36
+ return isPlainObject(raw) && raw.type === 'u8'
37
+ && Number.isInteger(raw.value) && raw.value >= 0 && raw.value <= 255;
38
+ }
39
+
40
+ /** The 1-byte discriminator as a Buffer, or null when not shank-shaped. */
41
+ export function shankDiscriminatorBytes(instruction) {
42
+ return isShankDiscriminant(instruction) ? Buffer.from([instruction.discriminant.value]) : null;
43
+ }
44
+
45
+ /**
46
+ * The hooked LEB128 types, pinned from lighthouse_common (Jac0xb/lighthouse,
47
+ * programs/lighthouse/src/instruction.rs: `CompactU64`, `LEB128Vec<T>`) and
48
+ * the program's generated clients/js/src/hooked.ts:
49
+ * - scalar: unsigned LEB128 (7-bit groups, little-endian, max 10 bytes)
50
+ * - bytes: unsigned LEB128 length prefix + that many raw bytes
51
+ * - vec: unsigned LEB128 length prefix + that many Borsh elements
52
+ * The `*Assertions` element types stay in the IDL's own `types`; only the
53
+ * length dialect is hooked.
54
+ */
55
+ export const SHANK_LEB128_TYPES = new Map([
56
+ ['CompactU64', { kind: 'scalar' }],
57
+ ['CompactBytes', { kind: 'bytes' }],
58
+ ['AccountInfoAssertions', { kind: 'vec', element: 'AccountInfoAssertion' }],
59
+ ['AccountDataAssertions', { kind: 'vec', element: 'AccountDataAssertion' }],
60
+ ['MintAccountAssertions', { kind: 'vec', element: 'MintAccountAssertion' }],
61
+ ['TokenAccountAssertions', { kind: 'vec', element: 'TokenAccountAssertion' }],
62
+ ['StakeAccountAssertions', { kind: 'vec', element: 'StakeAccountAssertion' }],
63
+ ['UpgradeableLoaderStateAssertions', { kind: 'vec', element: 'UpgradeableLoaderStateAssertion' }],
64
+ ]);
65
+
66
+ /**
67
+ * Whether an IDL is in the shank dialect: declared via metadata.origin, or
68
+ * shaped so (at least one instruction carries a 1-byte discriminant).
69
+ */
70
+ export function isShankIdl(idl) {
71
+ if (idl?.metadata?.origin === 'shank') return true;
72
+ return (idl?.instructions ?? []).some((instruction) => isShankDiscriminant(instruction));
73
+ }
@@ -0,0 +1,209 @@
1
+ // Chain-verified short wire forms: payloads the DEPLOYED program executed that
2
+ // the stored interface describes as one field LONGER than the bytes on the wire.
3
+ //
4
+ // Why this table exists, and why it is not simply "decode when a payload ends
5
+ // at a field boundary":
6
+ //
7
+ // Byte-exact re-encoding is NOT sufficient evidence for an absent field. With
8
+ // `trailing` it is: a complete described prefix that re-encodes exactly means
9
+ // the extra bytes are extra, and the described part is whole. With `absent` it
10
+ // is not — a payload truncated by a capture bug also ends on a field boundary
11
+ // and also re-encodes byte-exactly to the shortened bytes. So offline, on bytes
12
+ // alone, "the deployed program accepts a shorter form" and "this payload was
13
+ // cut short" are indistinguishable. src/codec.mjs therefore keeps
14
+ // `absent: 'error'` as the default and refuses both.
15
+ //
16
+ // What makes the shorter form KNOWABLE is a chain witness. Each entry below was
17
+ // measured on mainnet by tools/short-form-wire-forms.mjs against
18
+ // research/short-form-wire-forms-2026-11-03.json (the 4-block window) and
19
+ // research/short-form-wire-forms-2026-11-04.json (the 32-block window):
20
+ //
21
+ // - the transaction's `err` field is null (the chain accepted it),
22
+ // - the witness instruction bytes are byte-IDENTICAL to the fixture payload,
23
+ // - the program's own log line names the instruction the decode will report.
24
+ //
25
+ // On the 2026-11-03 12,572-payload sample that resolved 171 of the 172
26
+ // refusals: 0 of them were genuine chain-rejected refusals. The 2026-11-04
27
+ // 89,546-payload window resolved 350 of 350 refusals the same way, and found
28
+ // three MORE short forms the first window had never seen — because the table
29
+ // is keyed on program + discriminator + exact length, and the first window
30
+ // simply did not contain them: pump.fun's bonding curve executes the 24-byte
31
+ // `buy` form that had only been witnessed on PumpSwap, plus `buy_exact_sol_in`
32
+ // and the 146-byte `create_v2`, which omits THREE declared fields.
33
+ //
34
+ // The table is the product form of those measurements — the evidence travels
35
+ // with the decoder, so `absent: 'verified'` can name these forms while still
36
+ // refusing every other payload that happens to end on a field boundary.
37
+ //
38
+ // A payload must match program + discriminator + EXACT wire length. Matching
39
+ // the discriminator alone is not enough: the 25-byte form of
40
+ // e1f7501ed5b38488 is a different, fully-described shape and must not be routed
41
+ // through the absent path (it is not absent — both OptionBools are present).
42
+
43
+ /**
44
+ * Verified short wire forms.
45
+ *
46
+ * `program` deployed program address the witness belongs to
47
+ * `bytes` exact payload length, discriminator included
48
+ * `absent` declared fields the wire does not carry, in declaration order —
49
+ * named here so a decode that reports a DIFFERENT absent set does
50
+ * not silently count as verified
51
+ */
52
+ const VERIFIED_SHORT_FORMS = [
53
+ {
54
+ program: '6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P',
55
+ discriminator: 'e1f7501ed5b38488',
56
+ bytes: 24,
57
+ instruction: 'buy_exact_quote_in_v3',
58
+ absent: ['partial_fill'],
59
+ witnesses: 152,
60
+ },
61
+ {
62
+ program: '6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P',
63
+ discriminator: '07051dc4f5176550',
64
+ bytes: 24,
65
+ instruction: 'buy_v3',
66
+ absent: ['partial_fill'],
67
+ witnesses: 1,
68
+ },
69
+ {
70
+ program: 'pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA',
71
+ discriminator: 'c62e1552b4d9e870',
72
+ bytes: 24,
73
+ instruction: 'buy_exact_quote_in',
74
+ absent: ['track_volume'],
75
+ witnesses: 17,
76
+ },
77
+ {
78
+ program: 'pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA',
79
+ discriminator: '66063d1201daebea',
80
+ bytes: 24,
81
+ instruction: 'buy',
82
+ absent: ['track_volume'],
83
+ witnesses: 1,
84
+ },
85
+ // --- 2026-11-04 window (research/short-form-wire-forms-2026-11-04.json) ---
86
+ // The same 24-byte `buy` form, on pump.fun's BONDING CURVE program. The
87
+ // discriminator and the length match the PumpSwap entry above, but the
88
+ // program is a different deployment, so the 2026-11-03 window could not
89
+ // witness it: the key is program + discriminator + exact length, and only
90
+ // one of the two programs was measured then.
91
+ {
92
+ program: '6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P',
93
+ discriminator: '66063d1201daebea',
94
+ bytes: 24,
95
+ instruction: 'buy',
96
+ absent: ['track_volume'],
97
+ witnesses: 7,
98
+ },
99
+ {
100
+ program: '6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P',
101
+ discriminator: '38fc74089edfcd5f',
102
+ bytes: 24,
103
+ instruction: 'buy_exact_sol_in',
104
+ absent: ['track_volume'],
105
+ witnesses: 1,
106
+ },
107
+ // The first short form that omits MORE THAN ONE declared field: the
108
+ // interface declares three, the wire carries none of them.
109
+ {
110
+ program: '6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P',
111
+ discriminator: 'd6904cec5f8b31b4',
112
+ bytes: 146,
113
+ instruction: 'create_v2',
114
+ absent: ['is_cashback_enabled', 'creator_fee_bps', 'is_holder_reward'],
115
+ witnesses: 1,
116
+ },
117
+ ];
118
+
119
+ export const SHORT_FORM_SOURCE =
120
+ 'research/short-form-wire-forms-2026-11-03.json + research/short-form-wire-forms-2026-11-04.json '
121
+ + '(chain witnesses: err:null, byte-identical payload, matching program log line)';
122
+
123
+ export const VERIFIED = VERIFIED_SHORT_FORMS;
124
+ export { VERIFIED_SHORT_FORMS };
125
+
126
+ /** Look up a payload in the chain-verified short-form table; null when absent. */
127
+ export function verifiedShortForm(program, data) {
128
+ if (!program || !(data instanceof Uint8Array)) return null;
129
+ if (data.length < 8) return null;
130
+ const discriminator = Buffer.from(data.subarray(0, 8)).toString('hex');
131
+ return VERIFIED_SHORT_FORMS.find((entry) =>
132
+ entry.program === program
133
+ && entry.discriminator === discriminator
134
+ && entry.bytes === data.length) ?? null;
135
+ }
136
+
137
+ /**
138
+ * Resolve an `absent` policy into the codec policy for one payload.
139
+ *
140
+ * 'error' never decode absent fields (the strict default)
141
+ * 'report' decode any payload that ends on a field boundary (caller risk)
142
+ * 'verified' decode only forms in the chain-verified table; anything else
143
+ * stays strict — the evidence-gated policy
144
+ */
145
+ export function resolveAbsentPolicy(program, data, policy) {
146
+ if (policy === undefined || policy === null || policy === 'error') return { policy: 'error', form: null };
147
+ if (policy === 'report') return { policy: 'report', form: null };
148
+ if (policy !== 'verified') {
149
+ throw new Error(`unknown absent policy ${JSON.stringify(policy)}`);
150
+ }
151
+ const form = verifiedShortForm(program, data);
152
+ return { policy: form ? 'report' : 'error', form };
153
+ }
154
+
155
+ /**
156
+ * Assert that a completed decode reproduces the measured wire form.
157
+ *
158
+ * Throws when it does not. This is the single place the rule lives, so the
159
+ * product paths (single-source and union) cannot drift apart from it, and so
160
+ * the rule can be tested directly with synthetic decodes — a different missing
161
+ * set, a different name — instead of only through a fixture store.
162
+ */
163
+ export function assertShortFormEvidence(form, decoded, bytes) {
164
+ const evidence = describeShortForm(form, decoded, bytes);
165
+ if (!evidence.verified) {
166
+ throw new Error(
167
+ `short-form evidence does not cover this decode: measured absent `
168
+ + `${JSON.stringify(form.absent)} for ${form.instruction}, decoded `
169
+ + `${JSON.stringify(evidence.decodeAbsence)} — refusing instead of trusting stale evidence`);
170
+ }
171
+ return evidence;
172
+ }
173
+
174
+ /**
175
+ * Does a completed decode reproduce the measured wire form?
176
+ *
177
+ * Pure on purpose: the rules for trusting old chain evidence are worth testing
178
+ * directly with synthetic decodes (a different missing set, a different name)
179
+ * rather than only through a fixture store. Returns the evidence object with
180
+ * `verified` set — it never throws, so a caller can choose to refuse.
181
+ *
182
+ * `decoded` may be a codec result (`instruction`/`event`) or a union result
183
+ * (`name`); both carry `absent`.
184
+ */
185
+ export function describeShortForm(form, decoded, bytes) {
186
+ const named = decoded.absent ?? [];
187
+ const sameSet = named.length === form.absent.length
188
+ && form.absent.every((field) => named.includes(field));
189
+ const decodedName = decoded.name
190
+ ?? (decoded.kind === 'event' ? decoded.event : decoded.instruction);
191
+ return {
192
+ // Verified means: THIS decode named exactly the fields the chain witness
193
+ // saw missing, under the name the witness's log line carried. A decoder
194
+ // that reports a different absence has not reproduced the measured form —
195
+ // the IDL gained or lost a field since the witness was taken — and stale
196
+ // evidence must not be spent on a shape nobody measured.
197
+ verified: sameSet && (decodedName === undefined || decodedName === form.instruction),
198
+ program: form.program,
199
+ discriminator: form.discriminator,
200
+ bytes: form.bytes,
201
+ instruction: form.instruction,
202
+ absent: form.absent,
203
+ decodeAbsence: named,
204
+ decodedName,
205
+ witnesses: form.witnesses,
206
+ source: SHORT_FORM_SOURCE,
207
+ wireBytes: bytes.length,
208
+ };
209
+ }
@@ -0,0 +1,253 @@
1
+ // Read one stored IDL for a program, verified against its receipt.
2
+ //
3
+ // The store layout is the one produced by `sememe pull` / `sememe adopt`:
4
+ // <store>/receipts/<program>-<slot>-<source>-<fingerprint>.json
5
+ // <store>/objects/<fingerprint>.json
6
+ // The receipt binds the program id to the object, so decoding is offline and
7
+ // reproducible: same store + same payload => same result, no network call.
8
+ //
9
+ // The receipt is a claim; the recomputed hash is the check. Every step that can
10
+ // lie on the way to a decode is refused with a named reason instead of being
11
+ // tolerated:
12
+ // - a receipt whose program field is not the program asked for (a receipt
13
+ // swapped in from another program's file)
14
+ // - a receipt pointing at an object that is not in the store
15
+ // - an object that does not re-hash to the fingerprint the receipt claims
16
+ // (the defect measured 2026-10-30: before this check, a tampered object —
17
+ // valid JSON, one instruction renamed — was decoded happily and its
18
+ // tampered name reported as provenance)
19
+ // - an object declaring a different program than the receipt binds
20
+ // - a receipt whose DECLARED origin or body no longer matches its own
21
+ // digest (src/receipt-digest.mjs): the fingerprint covers the OBJECT, so
22
+ // before the receipt carried a digest, rewriting `origin` in the receipt
23
+ // left `sememe verify` green over a rewritten provenance label — measured
24
+ // 2026-11-07: violations 0 after `origin` was changed to
25
+ // `attacker/rewritten-origin`. A legacy receipt without digests is read as
26
+ // it always was: its origin is then declared WITHOUT a binding.
27
+ // An unreadable or tampered store must never become another source's names.
28
+
29
+ import { createHash } from 'node:crypto';
30
+ import { readFile, readdir } from 'node:fs/promises';
31
+ import path from 'node:path';
32
+ import { canonicalJson } from './canonical.mjs';
33
+ import { compareReceiptNames } from './receipt-order.mjs';
34
+ import { classifyReceiptFiles, isReceiptFile } from './receipt-name.mjs';
35
+ import { receiptIntegrity } from './receipt-digest.mjs';
36
+ import { assertIdlShape, assertReadableDialect, declaredProgram } from './dialect.mjs';
37
+
38
+ export { canonicalJson } from './canonical.mjs';
39
+
40
+ /** Read and verify one receipt+object pair. Throws with a named reason.
41
+ *
42
+ * Exported for `src/lock.mjs`: the lock must re-check every receipt through
43
+ * THIS reader — a second, weaker copy of the checks would recreate the "one
44
+ * store, two strengths" failure the header warns about. */
45
+ export async function readVerified(storeDir, program, receiptName) {
46
+ let receipt;
47
+ try {
48
+ receipt = JSON.parse(await readFile(path.join(storeDir, 'receipts', receiptName), 'utf8'));
49
+ } catch (error) {
50
+ throw Error(`store receipt ${receiptName} is unreadable: ${String(error?.message ?? error).slice(0, 140)}`);
51
+ }
52
+ if (receipt?.program !== program) {
53
+ throw Error(`store receipt ${receiptName} names program ${receipt?.program} — it is not a receipt for ${program}`);
54
+ }
55
+ // The receipt's OWN integrity, before it is allowed to name anything. A
56
+ // rewritten declared origin is refused HERE, on every read — decode, union
57
+ // and verify all see the same refusal, and the reason says which kind of
58
+ // rewrite happened (the lock reports the two differently).
59
+ const integrity = receiptIntegrity(receipt);
60
+ if (integrity) {
61
+ const error = Error(`store receipt ${receiptName}: ${integrity.reason}`);
62
+ error.code = integrity.code;
63
+ throw error;
64
+ }
65
+ const fingerprint = String(receipt.fingerprint).replace(/^sha256:/, '');
66
+ if (!/^[a-f0-9]{64}$/.test(fingerprint)) throw Error(`store receipt has a malformed fingerprint: ${receipt.fingerprint}`);
67
+ const objectPath = path.join(storeDir, 'objects', `${fingerprint}.json`);
68
+ let raw;
69
+ try { raw = await readFile(objectPath); }
70
+ catch (error) {
71
+ if (error?.code === 'ENOENT') {
72
+ throw Error(`store receipt ${receiptName} claims object ${fingerprint} but the store object is missing`);
73
+ }
74
+ throw Error(`store object ${fingerprint}.json is unreadable: ${String(error?.message ?? error).slice(0, 140)}`);
75
+ }
76
+ let idl;
77
+ try { idl = JSON.parse(raw); }
78
+ catch (error) {
79
+ throw Error(`store object ${fingerprint}.json is not JSON: ${String(error?.message ?? error).slice(0, 140)}`);
80
+ }
81
+ if (!Array.isArray(idl.instructions)) throw Error(`stored object for ${program} has no instructions`);
82
+ // Same two checks as the write path (src/adopt.mjs), applied on every
83
+ // read: a dialect NO engine of the product can read is refused by NAME
84
+ // before any decode can crash unnamed, and a declared program that
85
+ // differs from the receipt's program is a re-labelling no matter which
86
+ // field carries the declaration (`address`, `metadata.address` or
87
+ // `program_id`). Since the PULS-Z wiring a stored raw-LE object reads
88
+ // through the engine (src/rawle.mjs) — a store that refused it by name
89
+ // now decodes instead, which is the point of the wiring.
90
+ assertReadableDialect(idl, (reason) => {
91
+ const error = Error(`stored object ${fingerprint} for ${program}: ${reason}`);
92
+ error.code = 'unsupported-idl-dialect';
93
+ return error;
94
+ });
95
+ const declared = declaredProgram(idl);
96
+ if (declared && declared !== program) {
97
+ throw Error(`stored object ${fingerprint} declares program ${declared}, not the receipt's ${program}`);
98
+ }
99
+ if (idl.address && idl.address !== program) {
100
+ throw Error(`stored object ${fingerprint} declares program ${idl.address}, not the receipt's ${program}`);
101
+ }
102
+ // `pull` writes sha256(canonicalJson(idl) + '\n'), so verify with the same
103
+ // canonicalizer rather than with JSON.stringify, whose key order is not the
104
+ // stored order. A receipt without this check is a filename, not provenance.
105
+ const actual = createHash('sha256').update(`${canonicalJson(idl)}\n`).digest('hex');
106
+ if (actual !== fingerprint) {
107
+ throw Error(
108
+ `stored object for ${program} re-hashes to sha256:${actual} but receipt ${receiptName} `
109
+ + `claims sha256:${fingerprint}: object and receipt disagree, refusing to decode`,
110
+ );
111
+ }
112
+ return { idl, receipt, receiptName, objectPath };
113
+ }
114
+
115
+ async function receiptNames(storeDir, program) {
116
+ const receipts = path.join(storeDir, 'receipts');
117
+ let names;
118
+ try { names = await readdir(receipts); }
119
+ catch (error) {
120
+ if (error?.code === 'ENOENT') {
121
+ // `code` names the KIND of failure so a caller can distinguish "this
122
+ // store does not exist" from the absence of "this program is not in it"
123
+ // WITHOUT parsing prose — src/conformance.mjs needs exactly that
124
+ // (measured 2026-11-10: it swallowed both and reported a green gate).
125
+ // The message is unchanged: it is quoted by bin/sememe.mjs, the README
126
+ // and several tests.
127
+ const missing = Error(`no sememe store at ${storeDir}: run "sememe pull" first`);
128
+ missing.code = 'no-sememe-store';
129
+ throw missing;
130
+ }
131
+ throw error;
132
+ }
133
+ const candidates = names.filter((name) => name.startsWith(`${program}-`) && isReceiptFile(name));
134
+ if (!candidates.length) {
135
+ // The store EXISTS and simply holds no receipt for this program: an
136
+ // acquisition gap, never damage. Callers must be able to report it as
137
+ // such instead of mistaking it for a reader refusal.
138
+ const absent = Error(`no stored IDL for program ${program} in ${storeDir}`);
139
+ absent.code = 'no-stored-idl';
140
+ throw absent;
141
+ }
142
+ // Numeric slot order, NOT byte order: the slot in the filename is a decimal
143
+ // NUMBER, and "99999999" sorts after "123456789" as a string — so a plain
144
+ // sort picks the OLDER receipt as "last = newest" whenever two slot strings
145
+ // differ in length (measured 2026-11-07: plain decode and lock said slot
146
+ // 99999999, the union path — which sorts by numeric slot — said 123456789).
147
+ // ONE ordering, shared with src/lock.mjs via src/receipt-order.mjs.
148
+ candidates.sort(compareReceiptNames);
149
+ return candidates;
150
+ }
151
+
152
+ /** Find the newest stored IDL for a program id, verified against its receipt. */
153
+ export async function findStoredIdl(storeDir, program) {
154
+ const candidates = await receiptNames(storeDir, program);
155
+ return readVerified(storeDir, program, candidates[candidates.length - 1]);
156
+ }
157
+
158
+ /**
159
+ * Find the store's sources for a program, split by provenance.
160
+ *
161
+ * `primary` is the newest ON-CHAIN receipt (a real slot) and `secondary` is the
162
+ * newest ADOPTED receipt (a published copy, slot null). The split is what keeps
163
+ * the union honest: a published copy must never silently become the default
164
+ * single source, and an on-chain pull must never be dropped because a file was
165
+ * adopted later.
166
+ *
167
+ * TWO RULES THAT MUST NOT DIVERGE (measured 2026-11-07, this host):
168
+ *
169
+ * 1. Adopted copies are ordered by the SHARED receipt ordering
170
+ * (src/receipt-order.mjs) — the exact rule `findStoredIdl` (the plain
171
+ * decode) and `src/lock.mjs` apply. They used to be ordered by their
172
+ * human-readable LABEL, which embeds the ORIGIN the caller typed: with
173
+ * two adopted copies the union path read the copy the origin spelling
174
+ * pointed at while the plain decode and the lock read the copy the
175
+ * fingerprint pointed at. Same store, same bytes, two answers —
176
+ * plain `decode` said `sell_v3`, `decode --union` refused the payload.
177
+ * 2. When the store holds NO on-chain receipt and MULTIPLE adopted copies,
178
+ * the newest adopted copy is the `primary` and the next one the
179
+ * `secondary`, so the union path arbitrates the copies the store actually
180
+ * holds instead of consulting one and ignoring the rest. With exactly one
181
+ * adopted copy and no pull, `primary` stays null (the sole-source case):
182
+ * nothing on chain means there is no on-chain primary.
183
+ *
184
+ * Everything the pair does not consult is returned as `additional`, so the
185
+ * caller can NAME it instead of pretending the store holds two sources.
186
+ *
187
+ * Both are re-hashed against their receipts here; an unreadable or tampered
188
+ * object is refused by name, never skipped silently.
189
+ */
190
+ export async function findStoredIdls(storeDir, program) {
191
+ const candidates = await receiptNames(storeDir, program);
192
+ const onChain = [];
193
+ const adopted = [];
194
+ for (const name of candidates) {
195
+ // The slot segment is the second field: `<program>-<slot>-<source>-<hash>`.
196
+ const rest = name.slice(`${program}-`.length);
197
+ const slotSegment = rest.split('-')[0];
198
+ const source = await readVerified(storeDir, program, name);
199
+ const entry = {
200
+ // Human-readable provenance; the receipt filename stays in idlMeta.receipt.
201
+ // An ADOPTED origin is DECLARED, never verified — the label says so, so no
202
+ // reader can present a caller-typed string as a checked fact. A legacy
203
+ // receipt without originStatus is declared too (adopt never verified it).
204
+ label: source.receipt.slot === null || source.receipt.slot === undefined
205
+ ? `${source.receipt.source} (adopted, declared origin: ${source.receipt.origin ?? source.receipt.fingerprint.slice(0, 12)})`
206
+ : `${source.receipt.source} @slot ${source.receipt.slot}`,
207
+ idl: source.idl,
208
+ idlMeta: {
209
+ fingerprint: source.receipt.fingerprint,
210
+ slot: source.receipt.slot ?? null,
211
+ source: source.receipt.source,
212
+ origin: source.receipt.origin ?? null,
213
+ originStatus: source.receipt.originStatus
214
+ ?? (source.receipt.origin !== undefined && source.receipt.origin !== null ? 'declared' : null),
215
+ receipt: source.receiptName,
216
+ },
217
+ objectPath: source.objectPath,
218
+ };
219
+ if (source.receipt.slot === null || source.receipt.slot === undefined) adopted.push(entry);
220
+ else if (/^\d+$/.test(slotSegment)) onChain.push(entry);
221
+ else adopted.push(entry);
222
+ }
223
+ // Ordered by the ONE shared rule (numeric slot, filename tiebreak) — the
224
+ // same order `candidates` already arrives in from `receiptNames`, and the
225
+ // same order `findStoredIdl` and src/lock.mjs apply. Never the label (it
226
+ // carries the origin string the caller typed) and never the raw string (it
227
+ // orders by digits, not by value).
228
+ onChain.sort((a, b) => compareReceiptNames(a.idlMeta.receipt, b.idlMeta.receipt));
229
+ // Adopted copies are ordered by the SAME shared rule — numeric slot first
230
+ // (they all carry filename segment 0), then the filename byte-wise. Never
231
+ // the label: it carries the origin string the caller typed, and two copies
232
+ // of one program then answered from two different orderings.
233
+ adopted.sort((a, b) => compareReceiptNames(a.idlMeta.receipt, b.idlMeta.receipt));
234
+
235
+ let primary = onChain.length ? onChain[onChain.length - 1] : null;
236
+ let secondary = adopted.length ? adopted[adopted.length - 1] : null;
237
+ if (!primary && adopted.length >= 2) {
238
+ // No pull in the store, but several verified published copies: they ARE the
239
+ // union's two sources. Ranking them by the shared rule keeps the primary
240
+ // equal to the copy `findStoredIdl` reads, so plain and union decode agree.
241
+ primary = adopted[adopted.length - 1];
242
+ secondary = adopted[adopted.length - 2];
243
+ }
244
+ const consulted = new Set([primary, secondary].filter(Boolean));
245
+ const additional = [...onChain, ...adopted].filter((entry) => !consulted.has(entry));
246
+ return {
247
+ primary,
248
+ secondary,
249
+ additional,
250
+ onChain,
251
+ adopted,
252
+ };
253
+ }
@@ -0,0 +1,46 @@
1
+ // Atomic no-replace writer for store objects and receipts.
2
+ //
3
+ // Extracted from src/pull.mjs so that `sememe adopt` (a published copy with no
4
+ // RPC involved) can share the exact same write contract as `sememe pull`:
5
+ // - temp file created O_EXCL with 0600
6
+ // - fsync before install
7
+ // - hard-link install: another writer (or a symlink planted at the target)
8
+ // cannot be overwritten; a losing writer verifies the installed bytes
9
+ // - any existing destination with DIFFERENT bytes is an error, never replaced
10
+ // A store entry is content-addressed: overwriting one digest with other bytes
11
+ // would break every receipt that points at it.
12
+
13
+ import { link, lstat, open, readFile, rm } from 'node:fs/promises';
14
+
15
+ export async function writeOnce(destination, content) {
16
+ try {
17
+ const entry = await lstat(destination);
18
+ if (!entry.isFile() || entry.isSymbolicLink()) throw Error(`non-regular store entry: ${destination}`);
19
+ const current = await readFile(destination);
20
+ if (!current.equals(content)) throw Error(`digest collision or corrupted local object: ${destination}`);
21
+ return;
22
+ } catch (error) {
23
+ if (error?.code !== 'ENOENT') throw error;
24
+ }
25
+ const temporary = `${destination}.tmp-${process.pid}-${Math.random().toString(16).slice(2)}`;
26
+ let handle;
27
+ try {
28
+ handle = await open(temporary, 'wx', 0o600);
29
+ await handle.writeFile(content);
30
+ await handle.sync();
31
+ await handle.close(); handle = null;
32
+ // Atomic no-replace installation: hard-link fails if another writer won.
33
+ // Rename would overwrite a concurrently created or symlinked digest path.
34
+ try { await link(temporary, destination); }
35
+ catch (error) {
36
+ if (error?.code !== 'EEXIST') throw error;
37
+ const entry = await lstat(destination);
38
+ if (!entry.isFile() || entry.isSymbolicLink()) throw Error(`non-regular store entry: ${destination}`);
39
+ const current = await readFile(destination);
40
+ if (!current.equals(content)) throw Error(`digest collision or corrupted local object: ${destination}`);
41
+ }
42
+ } finally {
43
+ if (handle) await handle.close();
44
+ await rm(temporary, { force: true });
45
+ }
46
+ }