Monorepo for Aesthetic.Computer
aesthetic.computer
1// Generate Short Code, 25.10.22
2// Shared code generator for paintings, tapes, and kidlisp
3// Supports both random and inferred (source-aware) code generation
4
5import { customAlphabet } from 'nanoid';
6
7// Code generator alphabets - try lowercase first, add uppercase only on collision
8const lowercaseConsonants = 'bcdfghjklmnpqrstvwxyz';
9const lowercaseVowels = 'aeiou';
10const uppercaseConsonants = 'BCDFGHJKLMNPQRSTVWXYZ';
11const uppercaseVowels = 'AEIOU';
12const numbers = '23456789'; // Exclude 0,1 (look like O,l)
13
14// Lowercase-only alphabet for initial attempts
15const lowercaseAlphabet = lowercaseConsonants + lowercaseVowels + numbers;
16// Full alphabet with uppercase for collisions
17const fullAlphabet = lowercaseConsonants + lowercaseVowels + uppercaseConsonants + uppercaseVowels + numbers;
18
19const CODE_LENGTH = 3;
20const LOWERCASE_ATTEMPTS = 50; // Try lowercase-only first
21const MAX_COLLISION_ATTEMPTS = 100;
22
23const lowercaseNanoid = customAlphabet(lowercaseAlphabet, CODE_LENGTH);
24const fullNanoid = customAlphabet(fullAlphabet, CODE_LENGTH);
25
26/**
27 * Generate a unique short code with MongoDB collision checking
28 * @param {Object} collection - MongoDB collection to check for existing codes
29 * @param {Object} options - Generation options
30 * @param {string} options.mode - 'random' or 'inferred' (default: 'random')
31 * @param {string} options.sourceText - Source text for inference (required if mode='inferred')
32 * @param {string} options.type - Content type: 'kidlisp', 'painting', or 'tape'
33 * @returns {Promise<string>} Unique 3-character code (or longer if exhausted)
34 */
35export async function generateUniqueCode(collection, options = {}) {
36 const { mode = 'random', sourceText, type } = options;
37
38 // For inferred mode, try intelligent codes first
39 if (mode === 'inferred' && sourceText && type === 'kidlisp') {
40 console.log(`🧠 Attempting inferred code generation from source...`);
41 const inferredCodes = await generateInferredCodes(sourceText, type);
42
43 // Try each inferred code
44 for (const inferredCode of inferredCodes) {
45 const existing = await collection.findOne({ code: inferredCode });
46 if (!existing) {
47 console.log(`✨ Using inferred code: ${inferredCode}`);
48 return inferredCode;
49 }
50 }
51
52 console.log(`⚠️ All inferred codes taken, falling back to random generation`);
53 }
54
55 // Random generation (or fallback from inferred)
56 return await generateRandomCode(collection);
57}
58
59/**
60 * Generate random pronounceable code with collision checking
61 * @param {Object} collection - MongoDB collection to check for existing codes
62 * @returns {Promise<string>} Unique code
63 */
64async function generateRandomCode(collection) {
65 // First 50 attempts: lowercase only
66 for (let attempt = 0; attempt < LOWERCASE_ATTEMPTS; attempt++) {
67 const code = lowercaseNanoid();
68
69 const existing = await collection.findOne({ code });
70 if (!existing) {
71 return code;
72 }
73
74 console.log(`⚠️ Lowercase code collision detected: ${code}, retrying...`);
75 }
76
77 // Next 50 attempts: include uppercase
78 for (let attempt = LOWERCASE_ATTEMPTS; attempt < MAX_COLLISION_ATTEMPTS; attempt++) {
79 const code = fullNanoid();
80
81 const existing = await collection.findOne({ code });
82 if (!existing) {
83 console.log(`⚠️ Using uppercase in code after ${LOWERCASE_ATTEMPTS} lowercase attempts: ${code}`);
84 return code;
85 }
86
87 console.log(`⚠️ Full alphabet code collision detected: ${code}, retrying...`);
88 }
89
90 // If we hit max attempts, use a longer code with full alphabet
91 const longerNanoid = customAlphabet(fullAlphabet, CODE_LENGTH + 1);
92 const longerCode = longerNanoid();
93 console.log(`⚠️ Max collisions reached, using longer code: ${longerCode}`);
94 return longerCode;
95}
96
97/**
98 * Generate inferred codes from source text (KidLisp source code)
99 * Analyzes text to create meaningful, pronounceable codes
100 * @param {string} sourceText - Source code to analyze
101 * @param {string} type - Content type (currently only 'kidlisp' supported)
102 * @returns {Promise<string[]>} Array of candidate codes, sorted by quality
103 */
104async function generateInferredCodes(sourceText, type) {
105 const codes = [];
106 const cleanSource = sourceText.trim().toLowerCase();
107
108 // Temporal hints (day of week, date markers)
109 const now = new Date();
110 const dayOfWeek = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'][now.getDay()];
111 const dayChar = dayOfWeek.charAt(0); // 's', 'm', 't', 'w', etc.
112 const monthChar = ['j', 'f', 'm', 'a', 'm', 'j', 'j', 'a', 's', 'o', 'n', 'd'][now.getMonth()];
113 const dayOfMonth = now.getDate();
114 const isWeekend = now.getDay() === 0 || now.getDay() === 6;
115
116 // Phonemic helpers for better human readability
117 const vowels = 'aeiou';
118 const consonants = 'bcdfghjklmnpqrstvwxyz';
119
120 // Check if a string has good vowel-consonant balance for pronunciation
121 function hasGoodPhonetics(str) {
122 const hasVowel = /[aeiou]/.test(str);
123 const hasConsonant = /[bcdfghjklmnpqrstvwxyz]/.test(str);
124 return hasVowel && hasConsonant;
125 }
126
127 // Create pronounceable combinations by inserting vowels
128 function makePronounceable(consonantString, targetLength = 3) {
129 if (consonantString.length === 0) return '';
130 if (consonantString.length >= targetLength && hasGoodPhonetics(consonantString)) {
131 return consonantString.substring(0, targetLength);
132 }
133
134 let result = consonantString.charAt(0);
135 const vowelChoices = ['a', 'e', 'i', 'o', 'u'];
136
137 for (let i = 1; i < consonantString.length && result.length < targetLength; i++) {
138 if (result.length < targetLength - 1) {
139 result += vowelChoices[i % vowelChoices.length];
140 }
141 if (result.length < targetLength) {
142 result += consonantString.charAt(i);
143 }
144 }
145
146 while (result.length < targetLength) {
147 result += vowelChoices[result.length % vowelChoices.length];
148 }
149
150 return result.substring(0, targetLength);
151 }
152
153 // KidLisp-specific inference
154 if (type === 'kidlisp') {
155 // Common KidLisp functions
156 const kidlispFunctions = [
157 'wipe', 'ink', 'line', 'box', 'circle', 'rect', 'def', 'later',
158 'scroll', 'resolution', 'gap', 'frame', 'brush', 'clear', 'repeat'
159 ];
160
161 const colors = ['red', 'blue', 'green', 'yellow', 'white', 'black', 'gray', 'purple', 'orange'];
162
163 // Find functions in source
164 const foundFunctions = kidlispFunctions.filter(fn =>
165 cleanSource.includes(`(${fn}`) || cleanSource.includes(` ${fn} `)
166 );
167
168 const foundColors = colors.filter(color => cleanSource.includes(color));
169
170 // Strategy 1: First letters of functions with vowel insertion
171 if (foundFunctions.length >= 2) {
172 const firstLetters = foundFunctions.slice(0, 3).map(fn => fn.charAt(0)).join('');
173 codes.push(makePronounceable(firstLetters, 3));
174 codes.push(makePronounceable(firstLetters, 4));
175 }
176
177 // Strategy 2: Function + color combinations
178 if (foundFunctions.length > 0 && foundColors.length > 0) {
179 const f = foundFunctions[0].charAt(0);
180 const c = foundColors[0].charAt(0);
181 codes.push(f + 'a' + c); // "wab" for wipe blue
182 codes.push(f + 'i' + c); // "wib"
183 codes.push(f + 'o' + c); // "wob"
184 }
185
186 // Strategy 3: Line-by-line first letters
187 const lines = cleanSource.split('\n').map(l => l.trim()).filter(l => l.length > 0);
188 if (lines.length >= 2 && lines.length <= 6) {
189 const lineWords = lines.map(line => (line.match(/[a-z]+/g) || [])[0]).filter(w => w);
190 if (lineWords.length >= 2) {
191 const lineCode = lineWords.map(w => w.charAt(0)).join('');
192 codes.push(makePronounceable(lineCode, 3));
193 }
194 }
195
196 // Strategy 4: Extract numbers
197 const numbers = cleanSource.match(/\d+/g) || [];
198 if (numbers.length > 0 && foundFunctions.length > 0) {
199 const digit = numbers[0].charAt(0);
200 const f = foundFunctions[0].charAt(0);
201 codes.push(f + digit + (foundFunctions[1] ? foundFunctions[1].charAt(0) : 'a'));
202 }
203
204 // Temporal hints - VERY sparingly, only 2 codes, lowest priority
205 // These appear after all meaningful strategies as a last resort
206 if (foundFunctions.length > 0) {
207 const f = foundFunctions[0].charAt(0);
208 codes.push(f + dayChar + 'a'); // e.g., "wwa" for wipe-Wednesday
209 if (dayOfMonth < 10) {
210 codes.push(f + dayOfMonth + 'a'); // e.g., "w2a" for wipe on 2nd
211 }
212 }
213 }
214
215 // Filter and score codes
216 const validCodes = codes.filter(code =>
217 /^[a-z0-9]+$/.test(code) &&
218 code.length >= 3 &&
219 code.length <= 4 &&
220 /[a-z]/.test(code)
221 );
222
223 // Score codes by phonetic quality
224 const scorePhonetics = (code) => {
225 let score = 0;
226 if (hasGoodPhonetics(code)) score += 10;
227 if (/^[bcdfghjklmnpqrstvwxyz][aeiou]/.test(code)) score += 5;
228 if (/[aeiou][bcdfghjklmnpqrstvwxyz]$/.test(code)) score += 3;
229 if (code.length === 3) score += 2;
230 return score;
231 };
232
233 return [...new Set(validCodes)]
234 .sort((a, b) => scorePhonetics(b) - scorePhonetics(a))
235 .slice(0, 20); // Return top 20 candidates
236}