cryptoseed 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,187 @@
1
+ /**
2
+ * CryptoSeedRecovery - Spelling Correction and Suggestion Module
3
+ * Uses QWERTY keyboard adjacency, Levenshtein distance, and edit transpositions.
4
+ */
5
+
6
+ // QWERTY keyboard adjacency mapping for adjacent-key typos
7
+ const KEYBOARD_ADJACENCY = {
8
+ 'q': 'wa', 'w': 'qeas', 'e': 'wrds', 'r': 'etfg', 't': 'rygh', 'y': 'tuhj', 'u': 'yijk', 'i': 'uokl', 'o': 'ipl', 'p': 'o',
9
+ 'a': 'qwsz', 's': 'wedaxz', 'd': 'erfcsx', 'f': 'rtgvcd', 'g': 'tyhbvf', 'h': 'yujnbg', 'j': 'uikmnh', 'k': 'ijlm', 'l': 'kop',
10
+ 'z': 'asx', 'x': 'zsdc', 'c': 'xdfv', 'v': 'cfgb', 'b': 'vghn', 'n': 'bhjm', 'm': 'njk'
11
+ };
12
+
13
+ /**
14
+ * Computes the Levenshtein distance between two strings.
15
+ * @param {string} s1
16
+ * @param {string} s2
17
+ * @returns {number}
18
+ */
19
+ function levenshtein(s1, s2) {
20
+ if (s1 === s2) return 0;
21
+ if (!s1) return s2.length;
22
+ if (!s2) return s1.length;
23
+
24
+ let prevRow = Array(s2.length + 1);
25
+ let currRow = Array(s2.length + 1);
26
+
27
+ for (let j = 0; j <= s2.length; j++) {
28
+ prevRow[j] = j;
29
+ }
30
+
31
+ for (let i = 1; i <= s1.length; i++) {
32
+ currRow[0] = i;
33
+ for (let j = 1; j <= s2.length; j++) {
34
+ const cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
35
+ currRow[j] = Math.min(
36
+ prevRow[j] + 1, // Deletion
37
+ currRow[j - 1] + 1, // Insertion
38
+ prevRow[j - 1] + cost // Substitution
39
+ );
40
+ }
41
+ prevRow = [...currRow];
42
+ }
43
+ return prevRow[s2.length];
44
+ }
45
+
46
+ /**
47
+ * Generates spelling mutations with edit distance 1.
48
+ * @param {string} word
49
+ * @returns {string[]} - List of mutated words
50
+ */
51
+ function generateMutations(word) {
52
+ const mutations = new Set();
53
+ const chars = word.split('');
54
+
55
+ // 1. Transpositions (swap adjacent characters)
56
+ for (let i = 0; i < chars.length - 1; i++) {
57
+ const nextChars = [...chars];
58
+ const temp = nextChars[i];
59
+ nextChars[i] = nextChars[i + 1];
60
+ nextChars[i + 1] = temp;
61
+ mutations.add(nextChars.join(''));
62
+ }
63
+
64
+ // 2. Keyboard adjacent replacements
65
+ for (let i = 0; i < chars.length; i++) {
66
+ const char = chars[i];
67
+ const adj = KEYBOARD_ADJACENCY[char];
68
+ if (adj) {
69
+ for (let j = 0; j < adj.length; j++) {
70
+ const nextChars = [...chars];
71
+ nextChars[i] = adj[j];
72
+ mutations.add(nextChars.join(''));
73
+ }
74
+ }
75
+ }
76
+
77
+ // 3. Deletions
78
+ for (let i = 0; i < chars.length; i++) {
79
+ const nextChars = [...chars];
80
+ nextChars.splice(i, 1);
81
+ mutations.add(nextChars.join(''));
82
+ }
83
+
84
+ // 4. Double-character reductions (e.g. "committ" -> "commit")
85
+ for (let i = 0; i < chars.length - 1; i++) {
86
+ if (chars[i] === chars[i + 1]) {
87
+ const nextChars = [...chars];
88
+ nextChars.splice(i, 1);
89
+ mutations.add(nextChars.join(''));
90
+ }
91
+ }
92
+
93
+ return Array.from(mutations);
94
+ }
95
+
96
+ /**
97
+ * Returns a list of ranked spelling suggestions for a given input word.
98
+ * @param {string} inputWord
99
+ * @param {string[]} wordlist
100
+ * @param {number} maxResults
101
+ * @returns {string[]}
102
+ */
103
+ function getSuggestions(inputWord, wordlist, maxResults = 5) {
104
+ if (typeof inputWord !== 'string') return [];
105
+ const cleanWord = inputWord.toLowerCase().trim();
106
+ if (cleanWord.length === 0) return [];
107
+
108
+ // If the word is already in the list, no suggestions needed
109
+ if (wordlist.includes(cleanWord)) {
110
+ return [cleanWord];
111
+ }
112
+
113
+ const results = new Set();
114
+
115
+ // Step 1: Check generated direct edit distance 1 mutations (very fast)
116
+ const mutations = generateMutations(cleanWord);
117
+ for (const mut of mutations) {
118
+ if (wordlist.includes(mut)) {
119
+ results.add(mut);
120
+ }
121
+ }
122
+
123
+ // Step 2: Fallback to full Levenshtein and prefix matching if we need more results
124
+ if (results.size < maxResults) {
125
+ const candidates = [];
126
+ for (const dictWord of wordlist) {
127
+ // Exclude words that are too far in length to optimize
128
+ if (Math.abs(dictWord.length - cleanWord.length) > 2) continue;
129
+
130
+ const dist = levenshtein(cleanWord, dictWord);
131
+ if (dist <= 2) {
132
+ let score = dist * 10;
133
+ // Reward prefix match (often typos happen at the end)
134
+ if (dictWord.startsWith(cleanWord.substring(0, 3))) {
135
+ score -= 3;
136
+ }
137
+ candidates.push({ word: dictWord, score });
138
+ }
139
+ }
140
+
141
+ // Sort candidates by score ascending (lowest score is best)
142
+ candidates.sort((a, b) => a.score - b.score);
143
+
144
+ for (const cand of candidates) {
145
+ results.add(cand.word);
146
+ if (results.size >= maxResults) break;
147
+ }
148
+ }
149
+
150
+ return Array.from(results).slice(0, maxResults);
151
+ }
152
+
153
+ /**
154
+ * Prefix-based fallback search (comer por letras)
155
+ * Increments prefix characters from left to right until there are no matches,
156
+ * then returns the matching words starting with the last successful prefix.
157
+ * If no letters match, returns empty array.
158
+ * @param {string} inputWord
159
+ * @param {string[]} wordlist
160
+ * @returns {string[]}
161
+ */
162
+ function getPrefixSuggestions(inputWord, wordlist) {
163
+ if (typeof inputWord !== 'string') return [];
164
+ const cleanWord = inputWord.toLowerCase().trim();
165
+ if (cleanWord.length === 0) return [];
166
+
167
+ let lastMatches = [];
168
+
169
+ for (let i = 1; i <= cleanWord.length; i++) {
170
+ const prefix = cleanWord.substring(0, i);
171
+ const matches = wordlist.filter(w => w.startsWith(prefix));
172
+ if (matches.length > 0) {
173
+ lastMatches = matches;
174
+ } else {
175
+ break;
176
+ }
177
+ }
178
+
179
+ return lastMatches;
180
+ }
181
+
182
+ module.exports = {
183
+ levenshtein,
184
+ generateMutations,
185
+ getSuggestions,
186
+ getPrefixSuggestions
187
+ };