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/drift.mjs ADDED
@@ -0,0 +1,172 @@
1
+ // Which differences between two decoded argument trees are LAYOUT differences?
2
+ //
3
+ // Why this file exists (measured 2026-11-09): two verified copies of the same
4
+ // program can spell the same field differently — the on-chain copy of
5
+ // meteora-damm-v1 says `lpMint`, its published copy says `lp_mint`. The union
6
+ // reported that as a LAYOUT DRIFT and listed the fields one by one, so a
7
+ // reader was told "the two copies disagree about the data" about a field that
8
+ // both copies decoded to the SAME value.
9
+ //
10
+ // The measurement (tools/measure-drift-naming.mjs, research/drift-naming-2026-11-09.json)
11
+ // over every program in the repo that holds two verified copies:
12
+ //
13
+ // pump.fun 88 instruction drift rows, 6 account drift rows: all real differences
14
+ // meteora-damm-v1 643 instruction drift rows on the pre-fix tree, ALL of them
15
+ // NOTHING BUT spelling differences (`inAmount` vs `in_amount`,
16
+ // `claimFee` vs `claim_fee`) that raw JSON equality turned
17
+ // into `layout_drift`
18
+ // meteora-damm-v1 11 account drift rows, every row hitting the 20-path cap (220 paths);
19
+ // 33 REAL differences were crowded out by spelling paths
20
+ //
21
+ // The rule this file applies is the one the declared-layout report already
22
+ // states in the README: NAMING STYLE IS NOT A LAYOUT DIFFERENCE. Fields are
23
+ // paired by normalised spelling (`_padding0` and `padding0` are the same
24
+ // field; `totalAmount` and `total_amount` are the same field), and only a
25
+ // difference in the VALUE or in the SHAPE of a paired field is a difference
26
+ // in the data.
27
+ //
28
+ // Two things this file does NOT do:
29
+ //
30
+ // * it does not change WHICH source won — arbitration is by bytes, and this
31
+ // file only describes what the disagreement was;
32
+ // * it does not hide the spelling difference — it is reported, in its own
33
+ // list, under `spellingOnly`. The reader learns that the two copies call
34
+ // the same field different things; they just do not get told the two
35
+ // copies disagree about the data when they do not.
36
+ //
37
+ // A field only one side declares is still a real difference and is still
38
+ // reported: pairing by name does not make a missing field present.
39
+
40
+ /** The bound on how many differences one report lists. */
41
+ export const MAX_DRIFT_PATHS = 20;
42
+
43
+ /**
44
+ * Normalise a field name so that spelling styles pair up:
45
+ * leading underscores are dropped and `snake_case` becomes `camelCase`.
46
+ * `totalAmount` / `total_amount` / `_total_amount` all normalise to
47
+ * `totalAmount`.
48
+ */
49
+ export function normaliseFieldName(name) {
50
+ return String(name)
51
+ .replace(/^_+/, '')
52
+ .replace(/_+([a-z0-9])/g, (m, c) => c.toUpperCase());
53
+ }
54
+
55
+ /** Pair two sides' keys by normalised spelling, in declaration order. */
56
+ function pairKeys(a, b) {
57
+ const group = (obj) => {
58
+ const map = new Map();
59
+ for (const key of Object.keys(obj)) {
60
+ const nk = normaliseFieldName(key);
61
+ if (!map.has(nk)) map.set(nk, []);
62
+ map.get(nk).push(key);
63
+ }
64
+ return map;
65
+ };
66
+ const aMap = group(a);
67
+ const bMap = group(b);
68
+ const pairs = [];
69
+ const seen = new Set();
70
+ for (const nk of [...aMap.keys(), ...bMap.keys()]) {
71
+ if (seen.has(nk)) continue;
72
+ seen.add(nk);
73
+ const aKeys = aMap.get(nk) ?? [];
74
+ const bKeys = bMap.get(nk) ?? [];
75
+ const n = Math.min(aKeys.length, bKeys.length);
76
+ for (let i = 0; i < n; i += 1) pairs.push({ a: aKeys[i], b: bKeys[i], paired: true });
77
+ for (let i = n; i < aKeys.length; i += 1) pairs.push({ a: aKeys[i], b: null, paired: false });
78
+ for (let i = n; i < bKeys.length; i += 1) pairs.push({ a: null, b: bKeys[i], paired: false });
79
+ }
80
+ return pairs;
81
+ }
82
+
83
+ const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
84
+ const sameValue = (a, b) => JSON.stringify(a) === JSON.stringify(b);
85
+
86
+ /**
87
+ * Compare two decoded argument trees.
88
+ *
89
+ * @returns {{
90
+ * paths: string[],
91
+ * truncated: boolean,
92
+ * differs: boolean,
93
+ * spellingOnly: Array<{ path: string, primary: string, secondary: string }>,
94
+ * details: Array<Record<string, unknown>>,
95
+ * }}
96
+ * `paths` are the REAL differences (value or shape), named in the primary's
97
+ * spelling where the field exists on both sides; `spellingOnly` are the
98
+ * fields both sides declare under different names. `differs` is the verdict
99
+ * input: true only when `paths` is non-empty, so a spelling difference can
100
+ * never make the union say the two copies disagree about the data.
101
+ */
102
+ export function diffArgsByNormalisedName(primaryArgs, secondaryArgs, { max = MAX_DRIFT_PATHS } = {}) {
103
+ const paths = [];
104
+ const details = [];
105
+ const spellingOnly = [];
106
+ let truncated = false;
107
+
108
+ const push = (path, detail) => {
109
+ if (paths.length >= max) { truncated = true; return; }
110
+ paths.push(path);
111
+ details.push(detail);
112
+ };
113
+
114
+ const compareArrays = (a, b, path) => {
115
+ const n = Math.min(a.length, b.length);
116
+ let differing = 0;
117
+ for (let i = 0; i < n; i += 1) {
118
+ if (!sameValue(a[i], b[i])) differing += 1;
119
+ }
120
+ const extra = Math.abs(a.length - b.length);
121
+ if (differing === 0 && extra === 0) return;
122
+ push(path || '(root)', {
123
+ path: path || '(root)',
124
+ kind: 'array',
125
+ primaryLength: a.length,
126
+ secondaryLength: b.length,
127
+ differingElements: differing + extra,
128
+ });
129
+ };
130
+
131
+ const walk = (a, b, path) => {
132
+ if (truncated) return;
133
+ if (Array.isArray(a) || Array.isArray(b)) {
134
+ if (Array.isArray(a) && Array.isArray(b)) { compareArrays(a, b, path); return; }
135
+ push(path || '(root)', { path: path || '(root)', kind: 'shape' });
136
+ return;
137
+ }
138
+ if (!isPlainObject(a) || !isPlainObject(b)) {
139
+ if (!sameValue(a, b)) {
140
+ push(path || '(root)', { path: path || '(root)', kind: 'value', primary: a, secondary: b });
141
+ }
142
+ return;
143
+ }
144
+ for (const { a: aKey, b: bKey, paired } of pairKeys(a, b)) {
145
+ if (truncated) return;
146
+ const display = aKey ?? bKey;
147
+ const childPath = path ? `${path}.${display}` : display;
148
+ if (!paired) {
149
+ push(childPath, { path: childPath, kind: 'missing', side: aKey === null ? 'secondary' : 'primary' });
150
+ continue;
151
+ }
152
+ if (aKey !== bKey) {
153
+ // Same field, different spelling. It is still reported as a naming
154
+ // difference; it only counts as a difference in the DATA when the
155
+ // values behind the two names are not the same.
156
+ spellingOnly.push({ path: childPath, primary: aKey, secondary: bKey });
157
+ if (sameValue(a[aKey], b[bKey])) continue;
158
+ }
159
+ walk(a[aKey], b[bKey], childPath);
160
+ }
161
+ };
162
+
163
+ walk(primaryArgs, secondaryArgs, '');
164
+
165
+ return {
166
+ paths,
167
+ truncated,
168
+ differs: paths.length > 0,
169
+ spellingOnly,
170
+ details,
171
+ };
172
+ }
package/src/gap.mjs ADDED
@@ -0,0 +1,205 @@
1
+ // `sememe gap <program>` — why is there no interface here? Asked BEFORE pull.
2
+ //
3
+ // WHY THIS FILE EXISTS
4
+ //
5
+ // The full foreign sweep (research/step-ingestooor-full-sweep-2026-11-04.json)
6
+ // ended with 306 no-interface payloads across 70 programs, and the reflex is to
7
+ // call them all "acquisition gaps ... next derivation candidates". That label
8
+ // conflates two problems that need OPPOSITE fixes:
9
+ //
10
+ // - a program that PUBLISHES an on-chain IDL at one of the two addresses
11
+ // sememe can read is a NEVER-PULLED gap: `sememe pull` closes it, no
12
+ // derivation exists to do;
13
+ // - a program with neither address answering publishes nothing: pulling it
14
+ // forever cannot help (it needs `sememe adopt` of a first-party published
15
+ // copy, or a pinned upstream derivation, or a vendored wire table).
16
+ //
17
+ // Measured 2026-11-04 against live mainnet over all 70 gap programs:
18
+ // 34 publish an IDL (177 payloads) — including Jupiter DCA, which the card had
19
+ // listed as a derivation candidate — and 36 publish nothing (129 payloads),
20
+ // among them the largest single gap (L2TExM, 42 payloads, Lighthouse).
21
+ //
22
+ // WHY IT IS A TOOL AND NOT A PARAGRAPH
23
+ //
24
+ // 1. An RPC failure is NEVER recorded as "publishes nothing". The crates.io
25
+ // 403 incident (signerd, 2026-11-03) is the standing example: a client
26
+ // failure that reads like an absence. Unknown rows are counted separately
27
+ // and the verdict is withheld (`no-verdict`) so a partial probe cannot be
28
+ // quoted as a complete one.
29
+ // 2. A program sememe answers WITHOUT an IDL (System, ComputeBudget, SPL Token,
30
+ // Token-2022, Raydium AMM v4) is `vendored`, not a gap: telling a reader to
31
+ // pull it would send them after a receipt that can never exist.
32
+ // 3. An account that EXISTS but cannot be decoded is `present-undecodable`, its
33
+ // own verdict, never folded into "publishes nothing": an unusable interface
34
+ // is a different finding from an absent one.
35
+ // 4. Payload counts come from the committed sweep report, and its
36
+ // `noInterface` headline must equal the sum of the per-program counts —
37
+ // a hand-edited artifact is refused before it can be probed.
38
+ //
39
+ // This file lives in src/ so the shipped CLI can offer it: tools/ is not part
40
+ // of the published package (see "files" in package.json), and a consumer who
41
+ // hits `no stored IDL for program X` must be able to ask WHY from the same
42
+ // install they run `sememe decode` from. The acquisition dependency
43
+ // (@solana/web3.js, via ./acquire.mjs) is loaded lazily by the caller exactly
44
+ // like `pull`, so the decode-only install path stays dependency-free.
45
+
46
+ import { addresses, inspect, rpcAccounts } from './acquire.mjs';
47
+ import { LIGHTHOUSE_PROGRAM } from './lighthouse.mjs';
48
+ import { isNativeProgram } from './native.mjs';
49
+ import { isTokenProgram } from './token.mjs';
50
+ import { isRaydiumProgram } from './raydium.mjs';
51
+ import { isAtaProgram } from './ata.mjs';
52
+
53
+ const VENDORED = 'vendored';
54
+
55
+ /** Programs sememe answers with a pinned wire table, so there is no IDL to fetch. */
56
+ export function vendoredProgram(program) {
57
+ if (isNativeProgram(program)) return 'native';
58
+ if (isTokenProgram(program)) return 'token';
59
+ if (isRaydiumProgram(program)) return 'raydium';
60
+ if (isAtaProgram(program)) return 'ata';
61
+ return null;
62
+ }
63
+
64
+ /**
65
+ * Classify ONE gap program from its two probe accounts (metadata, anchor).
66
+ *
67
+ * Every path returns a verdict; nothing throws on bad data, so a caller can
68
+ * aggregate instead of losing a whole run to one malformed account.
69
+ */
70
+ export function classify(program, accounts, hints = {}) {
71
+ const vendored = vendoredProgram(program);
72
+ if (vendored) return { program, verdict: VENDORED, kind: vendored };
73
+ const c = hints.canonical ?? inspect(accounts?.[0], 'metadata', program);
74
+ const l = hints.legacy ?? inspect(accounts?.[1], 'anchor', program);
75
+ const usable = c.state === 'usable' ? 'metadata' : l.state === 'usable' ? 'anchor' : null;
76
+ if (usable) {
77
+ const chosen = usable === 'metadata' ? c : l;
78
+ return {
79
+ program, verdict: 'pullable', source: usable,
80
+ instructions: chosen.instructions,
81
+ decodableStoredOnly: chosen.decodableStoredOnly,
82
+ canonical: c.state, legacy: l.state,
83
+ };
84
+ }
85
+ if (c.state === 'missing' && l.state === 'missing') {
86
+ return { program, verdict: 'no-interface-published', canonical: c.state, legacy: l.state };
87
+ }
88
+ return {
89
+ program, verdict: 'present-undecodable',
90
+ canonical: c.state, legacy: l.state,
91
+ error: c.state === 'undecodable' ? c.error : l.state === 'undecodable' ? l.error : undefined,
92
+ };
93
+ }
94
+
95
+ const chunk = (items, size) => {
96
+ const out = [];
97
+ for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
98
+ return out;
99
+ };
100
+
101
+ /**
102
+ * Probe a list of `{ program, payloads }` entries against the chain.
103
+ *
104
+ * 25 programs per request = 50 keys; the public endpoint rejects larger
105
+ * getMultipleAccounts batches (-32602), measured while building this.
106
+ */
107
+ export async function probeGap(programs, options = {}) {
108
+ const {
109
+ url = 'https://api.mainnet-beta.solana.com',
110
+ fetcher = fetch,
111
+ batchSize = 25,
112
+ tries = 3,
113
+ } = options;
114
+ if (!Array.isArray(programs) || !programs.length) throw Error('programs required');
115
+ const rows = [];
116
+ let slot = null;
117
+ for (const group of chunk(programs, batchSize)) {
118
+ const locations = await Promise.all(group.map(async ({ program }) => addresses(program)));
119
+ const keys = locations.flatMap(({ metadata, anchor }) => [metadata, anchor]);
120
+ const response = await rpcAccounts(url, keys, fetcher, tries);
121
+ if (response.error) {
122
+ // The whole batch is UNKNOWN — and unknown must never become absence.
123
+ for (const { program, payloads } of group) {
124
+ rows.push({ program, payloads, verdict: 'unknown', error: response.error });
125
+ }
126
+ continue;
127
+ }
128
+ slot = response.slot ?? slot;
129
+ group.forEach(({ program, payloads }, index) => {
130
+ const accounts = [response.accounts[2 * index], response.accounts[2 * index + 1]];
131
+ rows.push({ program, payloads, slot, ...classify(program, accounts) });
132
+ });
133
+ }
134
+ return { rows, summary: summarize(rows), slot };
135
+ }
136
+
137
+ export function summarize(rows) {
138
+ const byVerdict = {};
139
+ const payloadsByVerdict = {};
140
+ for (const row of rows) {
141
+ byVerdict[row.verdict] = (byVerdict[row.verdict] ?? 0) + 1;
142
+ payloadsByVerdict[row.verdict] = (payloadsByVerdict[row.verdict] ?? 0) + (row.payloads ?? 0);
143
+ }
144
+ const unknown = byVerdict.unknown ?? 0;
145
+ return {
146
+ programs: rows.length,
147
+ payloads: rows.reduce((sum, row) => sum + (row.payloads ?? 0), 0),
148
+ byVerdict, payloadsByVerdict,
149
+ unknown,
150
+ // Withhold the verdict when any probe failed: a partial probe is not a
151
+ // statement about the rows it failed on, and must not be quoted as one.
152
+ verdict: unknown > 0 ? 'no-verdict' : 'measured',
153
+ pullablePrograms: byVerdict.pullable ?? 0,
154
+ pullablePayloads: payloadsByVerdict.pullable ?? 0,
155
+ publishesNothingPrograms: byVerdict['no-interface-published'] ?? 0,
156
+ publishesNothingPayloads: payloadsByVerdict['no-interface-published'] ?? 0,
157
+ undecodablePrograms: byVerdict['present-undecodable'] ?? 0,
158
+ vendoredPrograms: byVerdict[VENDORED] ?? 0,
159
+ knownPrograms: rows.length - unknown,
160
+ };
161
+ }
162
+
163
+ /**
164
+ * Turn a committed sweep report's `noInterfaceByProgram` map into the entries
165
+ * probeGap takes. The report is the denominator: its headline must equal the
166
+ * sum of the per-program counts, or the artifact has been hand-edited.
167
+ */
168
+ export function programsFromReport(report) {
169
+ const perProgram = report?.noInterfaceByProgram;
170
+ if (!perProgram || typeof perProgram !== 'object' || Array.isArray(perProgram)) {
171
+ throw Error('report has no noInterfaceByProgram map');
172
+ }
173
+ const programs = Object.entries(perProgram).map(([program, payloads]) => ({ program, payloads }));
174
+ if (!programs.length) throw Error('report has no gap programs');
175
+ const sum = programs.reduce((total, { payloads }) => total + payloads, 0);
176
+ if (sum !== report.noInterface) {
177
+ throw Error(`gap counts sum to ${sum} but report.noInterface says ${report.noInterface} — hand-edited artifact`);
178
+ }
179
+ return programs;
180
+ }
181
+
182
+ /** The `next` line for one verdict — the fix that actually applies. */
183
+ export function nextStep(row, program) {
184
+ switch (row.verdict) {
185
+ case 'pullable':
186
+ return `sememe pull ${program}`;
187
+ case 'no-interface-published':
188
+ // Lighthouse is the one measured case where sememe SHIPS the verified
189
+ // first-party copy (vendor/lighthouse, hash-pinned): the general advice
190
+ // would send the user off to find a file the package already carries.
191
+ if (program === LIGHTHOUSE_PROGRAM) {
192
+ return 'this program publishes no on-chain interface at either address — pulling it can never succeed. '
193
+ + 'sememe ships its first-party published copy (Jac0xb/lighthouse, MIT, hash-pinned): '
194
+ + 'use sememe adopt-lighthouse --store <dir>, then sememe decode --union.';
195
+ }
196
+ return 'this program publishes no on-chain interface at either address — pulling it can never succeed. '
197
+ + 'Use sememe adopt <program> <file> with a first-party published copy, or a pinned upstream derivation.';
198
+ case 'present-undecodable':
199
+ return 'an interface account exists but its schema is unusable — pull it to inspect; decode will refuse by name';
200
+ case VENDORED:
201
+ return 'answered by the pinned wire table shipped with sememe; there is no IDL to store';
202
+ default:
203
+ return 'the RPC did not answer — absence is NOT established; retry before believing anything about the chain';
204
+ }
205
+ }
@@ -0,0 +1,168 @@
1
+ // The Lighthouse acquisition: a pinned, hash-verified first-party published
2
+ // IDL for the program that was the LARGEST remaining acquisition gap after
3
+ // ATA (measured 2026-11-11: L2TExMFKdjpN9kozasaurPirfHy9P8sbXoAN1qA3S95,
4
+ // 1,214 payloads in 6 finalized blocks, no IDL account at either pullable
5
+ // address — research/ecosystem-coverage-2026-11-11.json, acquisitionQueue[0]).
6
+ //
7
+ // WHY THIS FILE EXISTS
8
+ //
9
+ // `sememe adopt` stores any file the caller names, and its origin is DECLARED,
10
+ // never verified (src/adopt.mjs says so on every surface). For ATA the answer
11
+ // was a derived wire table pinned to the vendored upstream Rust. For Lighthouse
12
+ // the answer already exists as a FIRST-PARTY PUBLISHED COPY: the program's own
13
+ // repository ships programs/lighthouse/lighthouse.json (shank dialect, which
14
+ // src/shank.mjs decodes — measured 2026-11-04 on 42 payloads of the foreign
15
+ // sweep). The gap was never "no interface exists"; it was "no one can adopt it
16
+ // reproducibly". This module makes the adoption checkable:
17
+ //
18
+ // - the copy is VENDORED under vendor/lighthouse/<commit>/ with the commit
19
+ // it was read at and the sha256 of every file (license MIT, LICENSE
20
+ // vendored beside it);
21
+ // - loadVendoredLighthouse() re-hashes the vendored bytes against the pin
22
+ // and REFUSES by name on any mismatch, missing file, unparseable JSON, a
23
+ // declared address that disagrees, or a non-shank instruction table —
24
+ // the same two-check contract src/native.mjs / src/token.mjs /
25
+ // src/raydium.mjs / src/ata.mjs prove;
26
+ // - the shank discriminants themselves are checked for self-consistency
27
+ // (every instruction declares {type:'u8',value:N}; N must be the
28
+ // declaration index) so a doctored IDL cannot claim 0x00 for two
29
+ // instructions.
30
+ //
31
+ // What is deliberately NOT claimed: the vendored bytes are not proven to come
32
+ // from the commit by this module (GitHub blobs are content-addressed, git
33
+ // commits are not signed here); the receipt will say originStatus 'declared'
34
+ // and name repo + commit + file. Chain proof stays with the decode: every
35
+ // payload must still re-encode byte-exactly or the union refuses.
36
+
37
+ import { readFileSync } from 'node:fs';
38
+ import { createHash } from 'node:crypto';
39
+ import path from 'node:path';
40
+ import { fileURLToPath } from 'node:url';
41
+
42
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
43
+ export const REPO_ROOT = path.resolve(HERE, '..');
44
+
45
+ export const LIGHTHOUSE_PROGRAM = 'L2TExMFKdjpN9kozasaurPirfHy9P8sbXoAN1qA3S95';
46
+
47
+ export const LIGHTHOUSE_PIN = {
48
+ repo: 'Jac0xb/lighthouse',
49
+ commit: '4c579479c98635e419b1b167f08be02a71604a71',
50
+ license: 'MIT',
51
+ // sha256 of the bytes AS PUBLISHED at the commit, read via the GitHub
52
+ // contents API at that ref (not from a mutable working tree).
53
+ files: [
54
+ {
55
+ file: 'programs/lighthouse/lighthouse.json',
56
+ role: 'first-party published IDL (shank dialect: per-instruction u8 discriminant)',
57
+ sha256: '9c54749abd27c09cf8d61e50016b625b5ae4aeb5cb1b8baf295199a1d7ecfd48',
58
+ },
59
+ {
60
+ file: 'LICENSE',
61
+ role: 'the license the vendored copy ships under',
62
+ sha256: '64d73100cafc1f5cb148cbbd5b3c7fc74bafc46f156646b0861c698570d02327',
63
+ },
64
+ ],
65
+ origin: `github.com/${'Jac0xb/lighthouse'}@4c579479c98635e419b1b167f08be02a71604a71:programs/lighthouse/lighthouse.json`,
66
+ };
67
+
68
+ export function vendoredLighthousePath(file, root = REPO_ROOT) {
69
+ return path.join(root, 'vendor', 'lighthouse', LIGHTHOUSE_PIN.commit, path.basename(file));
70
+ }
71
+
72
+ export function sha256File(filePath) {
73
+ return createHash('sha256').update(readFileSync(filePath)).digest('hex');
74
+ }
75
+
76
+ export class LighthousePinError extends Error {
77
+ constructor(reason, detail) {
78
+ super(detail ? `${reason}: ${detail}` : reason);
79
+ this.name = 'LighthousePinError';
80
+ this.reason = reason;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Read the vendored IDL and verify it against the pin. Every refusal is
86
+ * NAMED — a caller can tally failure faces without parsing prose.
87
+ * Returns { idl, path, sha256, bytes } on success.
88
+ */
89
+ export function loadVendoredLighthouse(root = REPO_ROOT, pin = LIGHTHOUSE_PIN) {
90
+ const pinned = pin.files.find((f) => f.file.endsWith('lighthouse.json'));
91
+ const vendoredPath = path.join(root, 'vendor', 'lighthouse', pin.commit, path.basename(pinned.file));
92
+ let raw;
93
+ try {
94
+ raw = readFileSync(vendoredPath);
95
+ } catch (error) {
96
+ throw new LighthousePinError(
97
+ 'lighthouse-vendor-missing',
98
+ `${vendoredPath} is not present (${error.code ?? error.message})`,
99
+ );
100
+ }
101
+ const now = createHash('sha256').update(raw).digest('hex');
102
+ if (now !== pinned.sha256) {
103
+ throw new LighthousePinError(
104
+ 'lighthouse-vendor-hash-mismatch',
105
+ `pinned ${pinned.sha256}, hashed now ${now} — refusing to adopt bytes that are not the reviewed copy`,
106
+ );
107
+ }
108
+ let idl;
109
+ try {
110
+ idl = JSON.parse(raw.toString('utf8'));
111
+ } catch (error) {
112
+ throw new LighthousePinError('lighthouse-vendor-unparseable', String(error.message ?? error));
113
+ }
114
+ if (!idl || typeof idl !== 'object' || Array.isArray(idl)) {
115
+ throw new LighthousePinError('lighthouse-vendor-unparseable', 'not an IDL object');
116
+ }
117
+ // Address binding: this file claims Lighthouse by its own metadata, and the
118
+ // program id this module pins is the one the measurement saw. A file that
119
+ // declares a different program refuses, it is never re-labelled.
120
+ const declared = idl.metadata?.address ?? idl.address ?? null;
121
+ if (declared !== LIGHTHOUSE_PROGRAM) {
122
+ throw new LighthousePinError(
123
+ 'lighthouse-address-disagrees',
124
+ `vendored file declares ${declared}, pin binds ${LIGHTHOUSE_PROGRAM}`,
125
+ );
126
+ }
127
+ if (!Array.isArray(idl.instructions) || idl.instructions.length === 0) {
128
+ throw new LighthousePinError('lighthouse-no-instructions', 'not an interface copy');
129
+ }
130
+ // Shank table integrity: every instruction must declare a u8 discriminant
131
+ // equal to its declaration index (measured truth at the pin: 0..17 in
132
+ // order). This is the IDL checking ITSELF, not a transcription.
133
+ idl.instructions.forEach((instruction, index) => {
134
+ const disc = instruction?.discriminant;
135
+ if (!disc || disc.type !== 'u8' || disc.value !== index) {
136
+ throw new LighthousePinError(
137
+ 'lighthouse-discriminant-table',
138
+ `instruction ${index} (${instruction?.name}) discriminant ` +
139
+ JSON.stringify(disc ?? null) + ` is not the shank u8 value ${index}`,
140
+ );
141
+ }
142
+ });
143
+ return { idl, path: vendoredPath, sha256: now, bytes: raw.length };
144
+ }
145
+
146
+ /** The origin label adopt records for a load that passed the pin checks. */
147
+ export function lighthouseOriginLabel() {
148
+ return `github.com/${LIGHTHOUSE_PIN.repo}@${LIGHTHOUSE_PIN.commit}:programs/lighthouse/lighthouse.json`;
149
+ }
150
+
151
+ /**
152
+ * Adopt the verified first-party copy into a store, through the PRODUCT's one
153
+ * write path (src/adopt.mjs — the same path `sememe adopt` uses).
154
+ *
155
+ * Order matters and is the point of this function: the pin is checked FIRST,
156
+ * so a tampered or missing vendored file means NOTHING was written. The
157
+ * receipt's origin stays `declared` (adopt's own rule): the pin binds the
158
+ * BYTES to a commit, not the bytes to the deployed program.
159
+ */
160
+ export async function adoptLighthouse(storeDir, { adopt } = {}) {
161
+ if (typeof adopt !== 'function') throw Error('adoptLighthouse needs the product write path (src/adopt.mjs)');
162
+ const loaded = loadVendoredLighthouse();
163
+ const receipt = await adopt(LIGHTHOUSE_PROGRAM, loaded.path, storeDir, {
164
+ source: 'published',
165
+ origin: lighthouseOriginLabel(),
166
+ });
167
+ return { receipt, loaded };
168
+ }