From 1b47994773b4b8f6f958ccfad1237ebb4726e65a Mon Sep 17 00:00:00 2001 From: Jules Date: Mon, 22 Jun 2026 11:50:22 +0000 Subject: [PATCH] feat: implement deterministic ID hashing for design states --- .gitignore | 1 + skills/utils/design-mcts.js | 18 +++++-- skills/utils/test-mcts-hash.js | 87 ++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 skills/utils/test-mcts-hash.js diff --git a/.gitignore b/.gitignore index 59a6acd..1b9b9f4 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ evolution-prompt.md # Test artifacts screenshot-test.png +.vg-canvas/ diff --git a/skills/utils/design-mcts.js b/skills/utils/design-mcts.js index 0fc52ec..32595a3 100644 --- a/skills/utils/design-mcts.js +++ b/skills/utils/design-mcts.js @@ -19,6 +19,7 @@ const path = require('path'); const { spawn } = require('child_process'); const { promisify } = require('util'); const exec = promisify(require('child_process').exec); +const crypto = require('crypto'); // ============================================================================ // DESIGN STATE REPRESENTATION @@ -72,9 +73,20 @@ class DesignState { }; // Metadata - this.id = config.id || `design_${Date.now()}`; this.generation = config.generation || 0; this.parentId = config.parentId || null; + this.id = config.id || this.generateId(); + } + + generateId() { + const data = JSON.stringify({ + colors: this.colors, + typography: this.typography, + layout: this.layout, + animation: this.animation, + components: this.components + }); + return crypto.createHash('sha256').update(data).digest('hex'); } randomOKLCH() { @@ -205,9 +217,9 @@ class DesignState { case 14: newState.components.navigation = newState.randomNavStyle(); break; } - newState.id = `design_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; newState.generation = this.generation + 1; newState.parentId = this.id; + newState.id = newState.generateId(); return newState; } @@ -603,9 +615,9 @@ class MonteCarloTreeSearch { return newState.mutate(); } - newState.id = `design_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; newState.generation = state.generation + 1; newState.parentId = state.id; + newState.id = newState.generateId(); return newState; } diff --git a/skills/utils/test-mcts-hash.js b/skills/utils/test-mcts-hash.js new file mode 100644 index 0000000..c89991f --- /dev/null +++ b/skills/utils/test-mcts-hash.js @@ -0,0 +1,87 @@ +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +// Require the actual classes from design-mcts.js +const mctsCode = fs.readFileSync(path.join(__dirname, 'design-mcts.js'), 'utf8'); +const testContextCode = mctsCode.replace(`main().catch(error => { + console.error('❌ Error:', error); + process.exit(1); +});`, 'module.exports = { DesignState, MonteCarloTreeSearch, MCTSNode };'); + +const tempFile = path.join(__dirname, 'design-mcts-temp.js'); +fs.writeFileSync(tempFile, testContextCode); + +const { DesignState, MonteCarloTreeSearch, MCTSNode } = require('./design-mcts-temp.js'); + +// Cleanup +fs.unlinkSync(tempFile); + +async function runTests() { + console.log('Running MCTS Hashing Unit Tests...'); + + // Test 1: ID logic no longer uses Date.now() or Math.random() + console.log('Test 1: Verification of code changes'); + const mutateCode = DesignState.prototype.mutate.toString(); + assert(!mutateCode.includes('Date.now()'), 'mutate() should not use Date.now()'); + assert(!mutateCode.includes('Math.random().toString'), 'mutate() should not use Math.random() for ID generation'); + console.log('✅ Test 1 Passed'); + + // Test 2: Two design objects with identical properties result in identical ID strings. + console.log('Test 2: Stable ID generation for identical properties'); + + const state1 = new DesignState(); + const state2 = new DesignState(); + + // Manually sync all properties to ensure they are deeply identical + state2.colors = JSON.parse(JSON.stringify(state1.colors)); + state2.typography = JSON.parse(JSON.stringify(state1.typography)); + state2.layout = JSON.parse(JSON.stringify(state1.layout)); + state2.animation = JSON.parse(JSON.stringify(state1.animation)); + state2.components = JSON.parse(JSON.stringify(state1.components)); + + // Re-generate IDs after syncing + state1.id = state1.generateId(); + state2.id = state2.generateId(); + + assert.strictEqual(state1.id, state2.id, 'Identical designs should have identical IDs'); + console.log('✅ Test 2 Passed'); + + // Test 3: Changing a single property results in a different ID. + console.log('Test 3: Collision resistance / Sensitivity'); + const state3 = new DesignState(); + state3.colors = JSON.parse(JSON.stringify(state1.colors)); + state3.typography = JSON.parse(JSON.stringify(state1.typography)); + state3.layout = JSON.parse(JSON.stringify(state1.layout)); + state3.animation = JSON.parse(JSON.stringify(state1.animation)); + state3.components = JSON.parse(JSON.stringify(state1.components)); + + // Change one property + state3.colors.primary = 'oklch(99% 0.1 200)'; + state3.id = state3.generateId(); + + assert.notStrictEqual(state1.id, state3.id, 'Changing one property should result in a different ID'); + console.log('✅ Test 3 Passed'); + + // Test 4: The evaluation cache successfully returns a score when an identical design is encountered. + console.log('Test 4: Cache lookups with deterministic IDs'); + const mcts = new MonteCarloTreeSearch(state1, { iterations: 1 }); + + // Pre-populate cache for state1's ID + mcts.evaluationCache.set(state1.id, 99.9); + + // Create a node with state2 (identical properties, so identical ID) + const node = new MCTSNode(state2); + + const score = await mcts.simulate(node); + assert.strictEqual(score, 99.9, 'Cache should return 99.9 for identical state'); + console.log('✅ Test 4 Passed'); + + console.log('All tests passed successfully! 🎉'); +} + +runTests().catch(err => { + console.error('❌ Test Failed:', err); + process.exit(1); +});