about summary refs log tree commit diff stats
path: root/js/scripting-lang/web/src
diff options
context:
space:
mode:
Diffstat (limited to 'js/scripting-lang/web/src')
-rw-r--r--js/scripting-lang/web/src/api.js183
-rw-r--r--js/scripting-lang/web/src/app.js286
-rw-r--r--js/scripting-lang/web/src/ast.js161
-rw-r--r--js/scripting-lang/web/src/dev.js268
-rw-r--r--js/scripting-lang/web/src/state.js18
-rw-r--r--js/scripting-lang/web/src/update.js38
-rw-r--r--js/scripting-lang/web/src/view.js198
7 files changed, 1152 insertions, 0 deletions
diff --git a/js/scripting-lang/web/src/api.js b/js/scripting-lang/web/src/api.js
new file mode 100644
index 0000000..cf43178
--- /dev/null
+++ b/js/scripting-lang/web/src/api.js
@@ -0,0 +1,183 @@
+// api.js
+// API fetch logic
+
+/**
+ * Fetch a Pokémon by name from the PokéAPI
+ * @param {string} name
+ * @returns {Promise<object>} Pokémon data
+ */
+export async function fetchPokemon(name) {
+  const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${encodeURIComponent(name.toLowerCase())}`);
+  if (!res.ok) {
+    throw new Error('Pokémon not found');
+  }
+  return await res.json();
+}
+
+/**
+ * Fetch a Pokémon species by name or ID from the PokéAPI
+ * @param {string|number} nameOrId
+ * @returns {Promise<object>} Pokémon species data
+ */
+export async function fetchPokemonSpecies(nameOrId) {
+  const res = await fetch(`https://pokeapi.co/api/v2/pokemon-species/${encodeURIComponent(nameOrId)}`);
+  if (!res.ok) {
+    throw new Error('Pokémon species not found');
+  }
+  return await res.json();
+}
+
+/**
+ * Fetch an evolution chain by ID from the PokéAPI
+ * @param {number} id
+ * @returns {Promise<object>} Evolution chain data
+ */
+export async function fetchEvolutionChain(id) {
+  const res = await fetch(`https://pokeapi.co/api/v2/evolution-chain/${id}`);
+  if (!res.ok) {
+    throw new Error('Evolution chain not found');
+  }
+  return await res.json();
+}
+
+/**
+ * Get evolution chain ID for a Pokémon
+ * @param {string|number} pokemonNameOrId
+ * @returns {Promise<number>} Evolution chain ID
+ */
+export async function getEvolutionChainId(pokemonNameOrId) {
+  try {
+    // First try to get the species data
+    const species = await fetchPokemonSpecies(pokemonNameOrId);
+    return species.evolution_chain.url.split('/').slice(-2, -1)[0];
+  } catch (error) {
+    throw new Error(`Could not find evolution chain for ${pokemonNameOrId}: ${error.message}`);
+  }
+}
+
+/**
+ * Fetch complete evolution data for a Pokémon
+ * @param {string|number} pokemonNameOrId
+ * @returns {Promise<object>} Complete evolution data
+ */
+export async function fetchEvolutionData(pokemonNameOrId) {
+  try {
+    // Get the evolution chain ID
+    const chainId = await getEvolutionChainId(pokemonNameOrId);
+    
+    // Fetch the evolution chain
+    const evolutionChain = await fetchEvolutionChain(chainId);
+    
+    return {
+      chainId,
+      evolutionChain,
+      pokemonName: pokemonNameOrId
+    };
+  } catch (error) {
+    throw new Error(`Failed to fetch evolution data: ${error.message}`);
+  }
+}
+
+// Baba Yaga harness integration
+import { FunctionalHarness } from '../../scripting-harness/core/harness.js';
+
+let harness = null;
+
+/**
+ * Initialize Baba Yaga harness
+ */
+async function initBabaYaga() {
+  // Harness will be created when we have a script
+  return true;
+}
+
+/**
+ * Get the current harness instance for dev mode integration
+ */
+export function getCurrentHarness() {
+  console.log('[API] getCurrentHarness called, harness available:', !!harness);
+  return harness;
+}
+
+/**
+ * Execute a Baba Yaga script with evolution data using the harness
+ * @param {string} script - Baba Yaga script to execute
+ * @param {object} evolutionData - Evolution chain data to work with
+ * @returns {Promise<object>} Script execution results
+ */
+export async function executeBabaYagaScript(script, evolutionData) {
+  try {
+    // Create harness with the script
+    harness = new FunctionalHarness(script, {
+      logStateChanges: false,
+      logCommands: false,
+      debug: false
+    });
+    
+    // IMPORTANT: Initialize the harness before use
+    await harness.initialize();
+    
+    // Process the evolution data through the harness
+    const result = await harness.update(evolutionData);
+    
+    // Extract emitted values from commands
+    const emittedValues = result.commands
+      .filter(cmd => cmd.type === 'emit')
+      .map(cmd => cmd.value);
+    
+
+    
+    return {
+      result: result.model,
+      emitted: emittedValues.length > 0 ? emittedValues : {},
+      evolutionData
+    };
+    
+  } catch (error) {
+    throw new Error(`Baba Yaga script error: ${error.message}`);
+  }
+}
+
+/**
+ * Get example Baba Yaga scripts for evolution data
+ */
+export function getExampleScripts() {
+  return {
+    'Basic Evolution Stages': `
+/* Get evolution stages from the chain */
+state : ..listen;
+/* Extract the evolution chain for easier access */
+chain : state.evolutionChain.chain;
+getSpeciesName : stage -> stage.species.name;
+evolutionStages : map @getSpeciesName chain.evolves_to;
+..emit evolutionStages;
+`,
+    'Evolution Methods': `
+/* Get evolution methods and requirements */
+state : ..listen;
+/* Extract the evolution chain for easier access */
+chain : state.evolutionChain.chain;
+getEvolutionInfo : evo -> {
+  species: evo.species.name,
+  method: evo.evolution_details[0].trigger.name,
+  level: evo.evolution_details[0].min_level
+};
+evolutionMethods : map @getEvolutionInfo chain.evolves_to;
+..emit evolutionMethods;
+`,
+    'Filter by Evolution Method': `
+/* Filter evolutions by method (e.g., level-up only) */
+state : ..listen;
+/* Extract the evolution chain for easier access */
+chain : state.evolutionChain.chain;
+isLevelUp : evo -> 
+  when evo.evolution_details[0].trigger.name is
+    "level-up" then true
+    _ then false;
+levelEvolutions : filter @isLevelUp chain.evolves_to;
+getSpeciesName : evo -> evo.species.name;
+levelEvolutionNames : map @getSpeciesName levelEvolutions;
+..emit levelEvolutionNames;
+`
+  };
+} 
\ No newline at end of file
diff --git a/js/scripting-lang/web/src/app.js b/js/scripting-lang/web/src/app.js
new file mode 100644
index 0000000..086cba1
--- /dev/null
+++ b/js/scripting-lang/web/src/app.js
@@ -0,0 +1,286 @@
+// app.js
+// Entrypoint for the app
+
+import { initialState, cloneState } from './state.js';
+import { update } from './update.js';
+import { view } from './view.js';
+import { fetchPokemon, fetchEvolutionData, executeBabaYagaScript, getExampleScripts, getCurrentHarness } from './api.js';
+import { initDevMode } from './dev.js';
+
+const root = document.getElementById('app');
+let state = cloneState(initialState);
+let dev;
+
+/**
+ * Entrypoint for the app.
+ *
+ * This file implements a minimal Elm-style architecture using only browser APIs and ES modules.
+ * - All state is immutable and updated by a pure update function.
+ * - The entire UI is re-rendered as a string on each state change for simplicity and predictability.
+ * - Event delegation is used to keep wiring minimal and functional.
+ * - No 3rd party code: everything is browser-native for cozy portability and clarity.
+ *
+ * Why this approach?
+ * - Functional, pure update/view logic is easier for me to reason about and test.
+ * - Re-rendering the whole UI avoids bugs from manual DOM updates and keeps state/UI in sync.
+ * - Minimal code and clear data flow make it easy to extend or adapt for new projects.
+ */
+
+// Enable devMode if ?dev=1 is in the URL
+/**
+ * devMode enables logging of all actions and state transitions for debugging.
+ *
+ * Why? This makes the app's state flow transparent, helping you understand and debug the app without extra tooling.
+ */
+const devMode = window.location.search.includes('dev=1');
+
+/**
+ * Generalized render function for Elm-style apps.
+ *
+ * @param {Object} config - Render configuration
+ * @param {HTMLElement} config.root - Root DOM element
+ * @param {any} config.state - Current app state
+ * @param {Function} config.view - View function (state => HTML string)
+ * @param {Array} [config.events] - Array of { selector, event, handler }
+ * @param {Function} [config.postRender] - Optional function({ root, state }) for post-render logic
+ */
+function render({ root, state, view, events = [], postRender }) {
+  root.innerHTML = view(state);
+  events.forEach(({ selector, event, handler }) => {
+    const el = root.querySelector(selector);
+    if (el) el.addEventListener(event, handler);
+  });
+  if (typeof postRender === 'function') {
+    postRender({ root, state });
+  }
+}
+
+// --- App-specific config for render ---
+function postRender({ root, state }) {
+  // Preserve scroll position
+  const scrollPosition = window.scrollY;
+  
+  const input = root.querySelector('#pokemon-query');
+  const error = root.querySelector('.error');
+  
+  // Only handle error focus - don't interfere with user typing
+  if (error) {
+    error.focus();
+  } else if (input && !document.activeElement) {
+    // Only auto-focus search input if nothing is currently focused
+    input.focus();
+    input.value = state.query;
+    input.setSelectionRange(input.value.length, input.value.length);
+  }
+  
+  // Restore scroll position
+  window.scrollTo(0, scrollPosition);
+}
+
+function doRender() {
+  render({
+    root,
+    state,
+    view,
+    events: [
+      { selector: '#search-form', event: 'submit', handler: handleSubmit },
+      { selector: '#pokemon-query', event: 'input', handler: handleInput },
+      { selector: '#execute-script', event: 'click', handler: handleExecuteScript },
+      { selector: '#clear-script', event: 'click', handler: handleClearScript },
+      { selector: '#example-scripts', event: 'change', handler: handleLoadExample },
+      { selector: '#baba-yaga-script', event: 'input', handler: handleScriptInput },
+    ],
+    postRender,
+  });
+}
+
+/**
+ * Dispatches an action to update state and re-render.
+ *
+ * Why centralize dispatch? This enforces a single source of truth for state changes, making the app predictable and easy to debug.
+ *
+ * Why log actions/state in devMode? This provides a transparent, time-travel-like view of app logic without needing any extra tooling.
+ */
+function dispatch(action) {
+  const prevState = state;
+  state = update(state, action);
+  
+  if (devMode && dev && typeof dev.pushState === 'function') {
+    dev.pushState(state);
+    console.groupCollapsed(`Action: ${action.type}`);
+    console.log('Payload:', action.payload);
+    console.log('Prev state:', prevState);
+    console.log('Next state:', state);
+    console.groupEnd();
+  }
+  
+  // Only re-render for actions that actually change the UI
+  const shouldRender = [
+    'FETCH_SUCCESS',
+    'FETCH_ERROR', 
+    'FETCH_EVOLUTION_SUCCESS',
+    'FETCH_EVOLUTION_ERROR',
+    'EXECUTE_SCRIPT_SUCCESS',
+    'EXECUTE_SCRIPT_ERROR',
+    'CLEAR_SCRIPT_OUTPUT',
+    'UPDATE_BABA_YAGA_SCRIPT' // Only when loading examples
+  ].includes(action.type);
+  
+  if (shouldRender) {
+    doRender();
+  }
+}
+
+/**
+ * Handles input events by updating state without re-rendering.
+ */
+function handleInput(e) {
+  // Update state directly without triggering re-render
+  state.query = e.target.value;
+}
+
+/**
+ * Handles script input events by updating state without re-rendering.
+ */
+function handleScriptInput(e) {
+  // Update state directly without triggering re-render
+  state.babaYagaScript = e.target.value;
+}
+
+/**
+ * Handles form submission, triggers async fetch, and dispatches state updates.
+ *
+ * Why handle async here? Keeps update/view pure and centralizes side-effect.
+ */
+async function handleSubmit(e) {
+  e.preventDefault();
+  if (!state.query.trim()) return;
+  dispatch({ type: 'FETCH_START' });
+  try {
+    const data = await fetchPokemon(state.query.trim());
+    dispatch({ type: 'FETCH_SUCCESS', payload: data });
+    
+    // Automatically fetch evolution chain after successful Pokémon search
+    try {
+      dispatch({ type: 'FETCH_EVOLUTION_START' });
+      const evolutionData = await fetchEvolutionData(data.name);
+      dispatch({ type: 'FETCH_EVOLUTION_SUCCESS', payload: evolutionData });
+    } catch (evolutionErr) {
+      dispatch({ type: 'FETCH_EVOLUTION_ERROR', payload: evolutionErr.message });
+    }
+  } catch (err) {
+    dispatch({ type: 'FETCH_ERROR', payload: err.message });
+  }
+}
+
+
+
+/**
+ * Handles Baba Yaga script execution.
+ */
+async function handleExecuteScript(e) {
+  e.preventDefault();
+  if (!state.evolutionChain || !state.babaYagaScript.trim()) return;
+  
+  dispatch({ type: 'EXECUTE_SCRIPT_START' });
+  try {
+    // state.evolutionChain contains the wrapper object, pass it directly
+    const result = await executeBabaYagaScript(state.babaYagaScript, state.evolutionChain);
+    dispatch({ type: 'EXECUTE_SCRIPT_SUCCESS', payload: result });
+    
+    // Update dev mode with the harness instance for enhanced debugging
+    console.log('[App] Checking dev mode integration:', {
+      devMode,
+      dev: !!dev,
+      updateHarness: dev ? typeof dev.updateHarness : 'no dev',
+      updateHarnessValue: dev ? dev.updateHarness : 'no dev'
+    });
+    
+    if (devMode && dev && typeof dev.updateHarness === 'function') {
+      const currentHarness = getCurrentHarness();
+      console.log('[App] Script executed, current harness:', !!currentHarness);
+      try {
+        dev.updateHarness(currentHarness);
+        console.log('[App] updateHarness called successfully');
+      } catch (error) {
+        console.error('[App] Error calling updateHarness:', error);
+      }
+    } else {
+      console.log('[App] Dev mode or updateHarness not available:', {
+        devMode,
+        dev: !!dev,
+        updateHarness: dev ? typeof dev.updateHarness : false
+      });
+      
+      // Try to access the function directly from window.dev
+      if (window.dev && typeof window.dev.updateHarness === 'function') {
+        console.log('[App] Found updateHarness on window.dev, trying direct call');
+        const currentHarness = getCurrentHarness();
+        try {
+          window.dev.updateHarness(currentHarness);
+          console.log('[App] Direct updateHarness call successful');
+        } catch (error) {
+          console.error('[App] Error in direct updateHarness call:', error);
+        }
+      }
+    }
+  } catch (err) {
+    dispatch({ type: 'EXECUTE_SCRIPT_ERROR', payload: err.message });
+  }
+}
+
+/**
+ * Handles script clearing.
+ */
+function handleClearScript(e) {
+  e.preventDefault();
+  dispatch({ type: 'UPDATE_BABA_YAGA_SCRIPT', payload: '' });
+  dispatch({ type: 'CLEAR_SCRIPT_OUTPUT' });
+}
+
+/**
+ * Handles loading example scripts.
+ */
+function handleLoadExample(e) {
+  const selectedExample = e.target.value;
+  if (!selectedExample) return;
+  
+  const examples = getExampleScripts();
+  if (examples[selectedExample]) {
+    // Update state and trigger re-render to show the example
+    state.babaYagaScript = examples[selectedExample];
+    doRender();
+  }
+  
+  // Reset the select
+  e.target.value = '';
+}
+
+// Initialize dev mode before first render
+if (devMode) {
+  dev = initDevMode({
+    getState: () => state,
+    setState: s => { state = s; },
+    render: doRender,
+    harness: null // Will be updated when harness is created
+  });
+}
+
+// Initial render
+doRender();
+
+function updateHistoryInfo() {
+  if (!devMode || !dev) return;
+  dev.update();
+}
+
+function setHistoryPointer(idx) {
+  const info = dev.getHistoryInfo();
+  if (idx < 1 || idx > info.length) return;
+  const newState = dev.setPointer(idx - 1);
+  if (newState) {
+    state = newState;
+    doRender();
+    updateHistoryInfo();
+  }
+}
diff --git a/js/scripting-lang/web/src/ast.js b/js/scripting-lang/web/src/ast.js
new file mode 100644
index 0000000..522d026
--- /dev/null
+++ b/js/scripting-lang/web/src/ast.js
@@ -0,0 +1,161 @@
+// ast.js
+// AST visualization tool for Baba Yaga language
+
+import { lexer, parser } from '../../lang.js';
+
+const examples = {
+  simple: `x : 42;`,
+  
+  when: `result : when x is 42 then "correct" _ then "wrong";`,
+  
+  function: `factorial : n -> 
+  when n is
+    0 then 1
+    _ then n * (factorial (n - 1));`,
+  
+  table: `person : {name: "Baba Yaga", age: 99, active: true};
+numbers : {1, 2, 3, 4, 5};`,
+  
+  arithmetic: `result : 5 + 3 * 2;
+composed : compose @double @increment 5;`,
+  
+  complex: `classify : x y -> 
+  when x y is
+    0 0 then "both zero"
+    0 _ then "x is zero"
+    _ 0 then "y is zero"
+    _ _ then "neither zero";`
+};
+
+// DOM elements - will be initialized when DOM is ready
+let codeInput, generateBtn, examplesSelect, astOutput, tokensOutput, errorOutput, copyAstBtn, copyTokensBtn;
+
+// Initialize when DOM is ready
+document.addEventListener('DOMContentLoaded', () => {
+  // Initialize DOM elements
+  codeInput = document.getElementById('code-input');
+  generateBtn = document.getElementById('generate-btn');
+  examplesSelect = document.getElementById('examples');
+  astOutput = document.getElementById('ast-output');
+  tokensOutput = document.getElementById('tokens-output');
+  errorOutput = document.getElementById('error-output');
+  copyAstBtn = document.getElementById('copy-ast-btn');
+  copyTokensBtn = document.getElementById('copy-tokens-btn');
+
+  // Example selector functionality
+  examplesSelect.addEventListener('change', () => {
+    const selectedExample = examplesSelect.value;
+    if (selectedExample && examples[selectedExample]) {
+      codeInput.value = examples[selectedExample];
+      generateAST();
+    }
+  });
+
+  // Generate button click handler
+  generateBtn.addEventListener('click', generateAST);
+
+  // Copy button click handlers
+  copyAstBtn.addEventListener('click', () => copyToClipboard(astOutput, 'AST'));
+  copyTokensBtn.addEventListener('click', () => copyToClipboard(tokensOutput, 'Tokens'));
+
+  // Auto-generate on Enter key (but not in textarea)
+  document.addEventListener('keydown', (e) => {
+    if (e.key === 'Enter' && e.ctrlKey && document.activeElement !== codeInput) {
+      generateAST();
+    }
+  });
+
+  // Initialize with a default example
+  codeInput.value = examples.when;
+  generateAST();
+});
+
+// Generate AST from code
+function generateAST() {
+  if (!codeInput) return; // DOM not ready yet
+  
+  const code = codeInput.value.trim();
+  
+  if (!code) {
+    showError('Please enter some code to analyze.');
+    return;
+  }
+  
+  try {
+    // Generate tokens
+    const tokens = lexer(code);
+    showTokens(tokens);
+    
+    // Generate AST
+    const ast = parser(tokens);
+    showAST(ast);
+    
+    // Clear any previous errors
+    showError('');
+    
+  } catch (error) {
+    showError(`Parsing Error: ${error.message}`);
+    showAST(null);
+    showTokens(null);
+  }
+}
+
+// Display AST in formatted JSON
+function showAST(ast) {
+  if (!astOutput) return; // DOM not ready yet
+  
+  if (ast) {
+    astOutput.value = JSON.stringify(ast, null, 2);
+  } else {
+    astOutput.value = 'No AST available due to parsing error.';
+  }
+}
+
+// Display tokens in formatted JSON
+function showTokens(tokens) {
+  if (!tokensOutput) return; // DOM not ready yet
+  
+  if (tokens) {
+    tokensOutput.value = JSON.stringify(tokens, null, 2);
+  } else {
+    tokensOutput.value = 'No tokens available due to parsing error.';
+  }
+}
+
+// Display error message
+function showError(message) {
+  if (!errorOutput) return; // DOM not ready yet
+  
+  if (message) {
+    errorOutput.textContent = message;
+    errorOutput.style.display = 'block';
+  } else {
+    errorOutput.style.display = 'none';
+  }
+}
+
+// Copy text to clipboard
+async function copyToClipboard(textarea, label) {
+  if (!textarea || !textarea.value) {
+    showError(`No ${label} content to copy.`);
+    return;
+  }
+  
+  try {
+    await navigator.clipboard.writeText(textarea.value);
+    
+    // Show temporary success message
+    const originalText = errorOutput.textContent;
+    showError(`${label} copied to clipboard!`);
+    
+    // Clear success message after 2 seconds
+    setTimeout(() => {
+      if (errorOutput.textContent === `${label} copied to clipboard!`) {
+        showError('');
+      }
+    }, 2000);
+    
+  } catch (error) {
+    showError(`Failed to copy ${label}: ${error.message}`);
+  }
+} 
\ No newline at end of file
diff --git a/js/scripting-lang/web/src/dev.js b/js/scripting-lang/web/src/dev.js
new file mode 100644
index 0000000..8341d1c
--- /dev/null
+++ b/js/scripting-lang/web/src/dev.js
@@ -0,0 +1,268 @@
+// devMode.js
+// Enhanced dev mode with harness integration for unified debugging
+
+/**
+ * Initialize enhanced dev mode: exposes an API for stepping through state history
+ * with integration to Baba Yaga harness versioning capabilities.
+ * @param {object} opts
+ * @param {function} opts.getState - returns current app state
+ * @param {function} opts.setState - sets app state
+ * @param {function} opts.render - triggers app re-render
+ * @param {object} opts.harness - Baba Yaga FunctionalHarness instance (optional)
+ */
+export function initDevMode({ getState, setState, render, harness = null }) {
+  let history = [];
+  let pointer = -1;
+  let firstLoad = true;
+  let harnessCorrelation = []; // Track web state ↔ harness state correlation
+
+  function pushState(state) {
+    if (pointer < history.length - 1) history = history.slice(0, pointer + 1);
+    history.push(clone(state));
+    pointer = history.length - 1;
+    
+    // Track correlation with harness if available
+    if (harness) {
+      const harnessVersion = harness.currentVersion || 0;
+      harnessCorrelation.push({
+        webVersion: pointer,
+        harnessVersion,
+        timestamp: Date.now()
+      });
+    }
+    
+    logInstructions();
+  }
+
+  function goTo(idx) {
+    if (idx < 0 || idx >= history.length) return;
+    pointer = idx;
+    setState(clone(history[pointer]));
+    render();
+    logInstructions();
+  }
+
+  function next() {
+    if (pointer < history.length - 1) goTo(pointer + 1);
+  }
+
+  function prev() {
+    if (pointer > 0) goTo(pointer - 1);
+  }
+
+  function get() {
+    return history[pointer];
+  }
+
+  function clone(obj) {
+    return JSON.parse(JSON.stringify(obj));
+  }
+
+  function table(obj) {
+    console.table(dev.history);
+  }
+
+  // Harness integration functions
+  function getHarnessHistory() {
+    console.log('[DevMode] getHarnessHistory called, harness available:', !!harness);
+    if (!harness) {
+      console.warn('[DevMode] No harness available for versioning - run a Baba Yaga script first');
+      return [];
+    }
+    const history = harness.getVersionHistory();
+    console.log('[DevMode] Harness history:', history.length, 'versions');
+    return history;
+  }
+
+  function getHarnessDiff(from, to) {
+    if (!harness) {
+      console.warn('[DevMode] No harness available for diffing');
+      return null;
+    }
+    return harness.getStateDiff(from, to);
+  }
+
+  function getCorrelation() {
+    console.log('[DevMode] getCorrelation called, harness available:', !!harness);
+    if (!harness) {
+      console.warn('[DevMode] No harness available for correlation - run a Baba Yaga script first');
+      return null;
+    }
+    
+    const webState = get();
+    const harnessVersions = getHarnessHistory();
+    const currentCorrelation = harnessCorrelation.find(c => c.webVersion === pointer);
+    
+    const result = {
+      webState,
+      webVersion: pointer,
+      harnessVersions,
+      currentCorrelation,
+      allCorrelations: harnessCorrelation
+    };
+    
+    console.log('[DevMode] Correlation result:', {
+      webVersion: result.webVersion,
+      harnessVersions: result.harnessVersions.length,
+      correlations: result.allCorrelations.length
+    });
+    
+    return result;
+  }
+
+  function debugExecution(webVersion, harnessVersion) {
+    if (!harness) {
+      console.warn('[DevMode] No harness available for execution debugging');
+      return null;
+    }
+
+    const webState = history[webVersion];
+    const harnessState = harness.stateHistory.getVersion(harnessVersion);
+    const diff = getHarnessDiff(harnessVersion - 1, harnessVersion);
+    
+    return {
+      webState,
+      harnessState,
+      scriptDiff: diff,
+      correlation: `Web v${webVersion} ↔ Harness v${harnessVersion}`
+    };
+  }
+
+  function stepCombined(direction) {
+    if (direction === 'next') {
+      next();
+      // Could also step harness if correlated
+      const correlation = harnessCorrelation.find(c => c.webVersion === pointer);
+      if (correlation && harness) {
+        console.log(`[DevMode] Web v${pointer} correlates with Harness v${correlation.harnessVersion}`);
+      }
+    } else {
+      prev();
+    }
+  }
+
+  function logInstructions() {
+    if (firstLoad) {
+      console.log('[DevMode] Enhanced state history debugger with harness integration');
+      console.log('Web App Debugging:');
+      console.log('- dev.next()   // step forward');
+      console.log('- dev.prev()   // step backward');
+      console.log('- dev.goTo(n)  // jump to state n (1-based)');
+      console.log('- dev.get()    // get current state');
+      console.log('- dev.table()  // display history as a table');
+      console.log('- dev.history  // array of all states');
+      console.log('- dev.pointer  // current pointer (0-based)');
+      
+      if (harness) {
+        console.log('\nHarness Integration:');
+        console.log('- dev.harnessHistory()  // get harness version history');
+        console.log('- dev.harnessDiff(from, to)  // get state diff');
+        console.log('- dev.correlation()  // show web ↔ harness correlation');
+        console.log('- dev.debugExecution(webVer, harnessVer)  // debug specific execution');
+        console.log('- dev.stepCombined(direction)  // step both systems');
+      }
+      
+      console.log('\nEnhanced Console API:');
+      console.log('- debug.web  // web app debugging');
+      console.log('- debug.harness  // harness debugging');
+      console.log('- debug.combined  // combined debugging');
+      
+      firstLoad = false;
+    }
+  }
+
+  // Function to update harness instance (called after script execution)
+  function updateHarness(newHarness) {
+    console.log('[DevMode] updateHarness called with:', !!newHarness);
+    harness = newHarness;
+    console.log('[DevMode] Harness instance updated for enhanced debugging');
+    
+    // Re-expose the enhanced debug API with updated harness
+    window.debug = enhancedDebug;
+  }
+
+  // Function to check current dev tools status
+  function getStatus() {
+    return {
+      devMode: true,
+      webStates: history.length,
+      currentPointer: pointer,
+      harnessAvailable: !!harness,
+      harnessVersions: harness ? harness.getVersionHistory().length : 0,
+      correlations: harnessCorrelation.length
+    };
+  }
+
+  // Enhanced console API
+  const enhancedDebug = {
+    // Web app debugging
+    web: {
+      next,
+      prev,
+      goTo,
+      get,
+      table,
+      get pointer() { return pointer; },
+      get history() { return history.slice(); },
+    },
+    
+    // Harness debugging
+    harness: {
+      history: getHarnessHistory,
+      diff: getHarnessDiff,
+      correlation: getCorrelation,
+      debugExecution,
+    },
+    
+    // Combined debugging
+    combined: {
+      correlation: getCorrelation,
+      step: stepCombined,
+      execution: debugExecution,
+      status: getStatus,
+    }
+  };
+
+  // Expose API globally for console use
+  window.dev = {
+    next,
+    prev,
+    goTo,
+    get,
+    table,
+    get pointer() { return pointer; },
+    get history() { return history.slice(); },
+    // Harness integration methods
+    harnessHistory: getHarnessHistory,
+    harnessDiff: getHarnessDiff,
+    correlation: getCorrelation,
+    debugExecution,
+    stepCombined,
+    updateHarness,
+    getStatus,
+  };
+
+  // Debug logging to verify function exposure
+  console.log('[DevMode] Dev API functions exposed:', {
+    updateHarness: typeof window.dev.updateHarness,
+    getStatus: typeof window.dev.getStatus,
+    harnessHistory: typeof window.dev.harnessHistory,
+    correlation: typeof window.dev.correlation
+  });
+
+  // Expose enhanced debug API
+  window.debug = enhancedDebug;
+  
+  // Debug logging to verify API exposure
+  console.log('[DevMode] Enhanced debug API exposed:', {
+    debugAvailable: typeof window.debug !== 'undefined',
+    webAvailable: typeof window.debug?.web !== 'undefined',
+    harnessAvailable: typeof window.debug?.harness !== 'undefined',
+    combinedAvailable: typeof window.debug?.combined !== 'undefined'
+  });
+
+  // Initial state
+  pushState(getState());
+
+  return { pushState };
+} 
\ No newline at end of file
diff --git a/js/scripting-lang/web/src/state.js b/js/scripting-lang/web/src/state.js
new file mode 100644
index 0000000..0bfada6
--- /dev/null
+++ b/js/scripting-lang/web/src/state.js
@@ -0,0 +1,18 @@
+// state.js
+// App state definition and helpers
+
+export const initialState = {
+  query: '',
+  pokemon: null,
+  evolutionChain: null,
+  evolutionData: null, // Transformed by Baba Yaga
+  babaYagaScript: '', // User's transformation script
+  scriptOutput: null, // Results from Baba Yaga execution
+  scriptError: null, // Baba Yaga script execution errors
+  loading: false,
+  error: null
+};
+
+export function cloneState(state) {
+  return JSON.parse(JSON.stringify(state));
+} 
\ No newline at end of file
diff --git a/js/scripting-lang/web/src/update.js b/js/scripting-lang/web/src/update.js
new file mode 100644
index 0000000..e13656e
--- /dev/null
+++ b/js/scripting-lang/web/src/update.js
@@ -0,0 +1,38 @@
+// update.js
+// Pure update function
+
+/**
+ * @param {object} state - Current state
+ * @param {object} action - { type, payload }
+ * @returns {object} new state
+ */
+export function update(state, action) {
+  switch (action.type) {
+    case 'UPDATE_QUERY':
+      return { ...state, query: action.payload, error: null };
+    case 'FETCH_START':
+      return { ...state, loading: true, error: null, pokemon: null, evolutionChain: null };
+    case 'FETCH_SUCCESS':
+      return { ...state, loading: false, error: null, pokemon: action.payload };
+    case 'FETCH_ERROR':
+      return { ...state, loading: false, error: action.payload, pokemon: null };
+    case 'FETCH_EVOLUTION_START':
+      return { ...state, loading: true, error: null, evolutionChain: null };
+    case 'FETCH_EVOLUTION_SUCCESS':
+      return { ...state, loading: false, error: null, evolutionChain: action.payload };
+    case 'FETCH_EVOLUTION_ERROR':
+      return { ...state, loading: false, error: action.payload, evolutionChain: null };
+    case 'UPDATE_BABA_YAGA_SCRIPT':
+      return { ...state, babaYagaScript: action.payload, scriptError: null };
+    case 'EXECUTE_SCRIPT_START':
+      return { ...state, scriptError: null, scriptOutput: null };
+    case 'EXECUTE_SCRIPT_SUCCESS':
+      return { ...state, scriptOutput: action.payload, scriptError: null };
+    case 'EXECUTE_SCRIPT_ERROR':
+      return { ...state, scriptError: action.payload, scriptOutput: null };
+    case 'CLEAR_SCRIPT_OUTPUT':
+      return { ...state, scriptOutput: null, scriptError: null };
+    default:
+      return state;
+  }
+} 
\ No newline at end of file
diff --git a/js/scripting-lang/web/src/view.js b/js/scripting-lang/web/src/view.js
new file mode 100644
index 0000000..ab64910
--- /dev/null
+++ b/js/scripting-lang/web/src/view.js
@@ -0,0 +1,198 @@
+// view.js
+// Pure view functions
+
+/**
+ * Pure view functions for the application.
+ *
+ * Why pure functions returning HTML strings? Because Elm does it, tbh.
+ * - Keeps rendering logic stateless and easy to test.
+ * - Ensures the UI is always a direct function of state, which should in theory totally avoid bugs from incremental DOM updates.
+ * - Using template literals is minimal and browser-native, with no dependencies, and is fun.
+ *
+ * Why escape output?
+ * - Prevents XSS and ensures all user/content data is safely rendered.
+ *
+ * Why semantic/accessible HTML?
+ * - Ensures the app is usable for all users, including those using assistive tech, and is easy to reason about.
+ */
+/**
+ * Render the app UI as an HTML string
+ * @param {object} state
+ * @returns {string}
+ */
+export function view(state) {
+  return `
+    <header class="app-header">
+      <h1>Baba Yaga's PokéDex</h1>
+    </header>
+    <container>
+      <form id="search-form" autocomplete="off">
+        <label for="pokemon-query">Pokémon Name (or number)</label>
+        <input id="pokemon-query" type="text" value="${escape(state.query)}" placeholder="e.g. eevee" aria-label="Pokémon Name" required />
+        <button type="submit" ${state.loading ? 'disabled' : ''}>${state.loading ? 'Loading...' : 'Search'}</button>
+      </form>
+      ${state.error ? `<div class="error" role="alert" tabindex="-1">${escape(state.error)}</div>` : ''}
+      
+      ${state.pokemon ? renderPokemonResult(state.pokemon) : ''}
+      ${state.pokemon ? '<div class="workflow-arrow">⬇</div>' : ''}
+      ${state.evolutionChain ? renderEvolutionSection(state) : ''}
+      ${state.evolutionChain ? '<div class="workflow-arrow">⬇</div>' : ''}
+      ${renderBabaYagaSection(state)}
+    </container>
+  `;
+}
+
+function renderPokemonResult(pokemon) {
+  return `
+    <div class="result">
+      <h2>${capitalize(pokemon.name)} (#${pokemon.id})</h2>
+      <img class="pokemon-sprite" src="${pokemon.sprites.front_default}" alt="${escape(pokemon.name)} sprite" />
+      <ul>
+        <li>Type: <b>${pokemon.types.map(t => capitalize(escape(t.type.name))).join(', ')}</b></li>
+        <li>Height: <b>${pokemon.height / 10} m</b></li>
+        <li>Weight: <b>${pokemon.weight / 10} kg</b></li>
+      </ul>
+    </div>
+  `;
+}
+
+function renderEvolutionSection(state) {
+  return `
+    <div class="evolution-section">
+      <h3>Evolution Chain</h3>
+      ${renderEvolutionTree(state.evolutionChain)}
+    </div>
+  `;
+}
+
+function renderEvolutionTree(evolutionChain) {
+  if (!evolutionChain || !evolutionChain.evolutionChain || !evolutionChain.evolutionChain.chain) {
+    return '<p>No evolution data available.</p>';
+  }
+
+  const chain = evolutionChain.evolutionChain.chain;
+  let html = '<div class="evolution-tree">';
+  
+  // Render the base species
+  html += `<div class="evolution-stage base">`;
+  html += `<div class="pokemon-node">${capitalize(chain.species.name)}</div>`;
+  html += `</div>`;
+  
+  // Render evolution stages
+  if (chain.evolves_to && chain.evolves_to.length > 0) {
+    html += renderEvolutionStages(chain.evolves_to, 1);
+  }
+  
+  html += '</div>';
+  return html;
+}
+
+function renderEvolutionStages(evolutions, level) {
+  if (!evolutions || evolutions.length === 0) return '';
+  
+  let html = `<div class="evolution-stage level-${level}">`;
+  
+  evolutions.forEach((evolution, index) => {
+    html += '<div class="evolution-branch">';
+    
+    // Evolution details
+    if (evolution.evolution_details && evolution.evolution_details.length > 0) {
+      const detail = evolution.evolution_details[0];
+      html += `<div class="evolution-detail">`;
+      html += `<span class="evolution-method">${capitalize(detail.trigger.name)}`;
+      if (detail.min_level) {
+        html += ` (Level ${detail.min_level})`;
+      }
+      html += `</span>`;
+      html += `</div>`;
+    }
+    
+    // Pokemon node
+    html += `<div class="pokemon-node">${capitalize(evolution.species.name)}</div>`;
+    
+    // Recursive evolution stages
+    if (evolution.evolves_to && evolution.evolves_to.length > 0) {
+      html += renderEvolutionStages(evolution.evolves_to, level + 1);
+    }
+    
+    html += '</div>';
+  });
+  
+  html += '</div>';
+  return html;
+}
+
+function renderBabaYagaSection(state) {
+  return `
+    <div class="baba-yaga-section">
+      <h3>Baba Yaga Data Transformation</h3>
+      <div class="script-editor">
+        <label for="baba-yaga-script">Transformation Script:</label>
+        <textarea 
+          id="baba-yaga-script" 
+          placeholder="Write your Baba Yaga script here..."
+          rows="8"
+        >${escape(state.babaYagaScript)}</textarea>
+        <div class="script-controls">
+          <button id="execute-script" type="button" ${!state.evolutionChain || !state.babaYagaScript.trim() ? 'disabled' : ''}>
+            Execute Script
+          </button>
+          <button id="clear-script" type="button">Clear</button>
+          <select id="example-scripts">
+            <option value="">Load Example...</option>
+            <option value="Basic Evolution Stages">Basic Evolution Stages</option>
+            <option value="Evolution Methods">Evolution Methods</option>
+            <option value="Filter by Evolution Method">Filter by Evolution Method</option>
+          </select>
+        </div>
+        ${!state.evolutionChain ? '<p class="help-text">💡 Search for a Pokémon to automatically load its evolution chain and enable script execution.</p>' : ''}
+        ${state.evolutionChain && !state.babaYagaScript.trim() ? '<p class="help-text">💡 Write a Baba Yaga script or load an example to enable execution.</p>' : ''}
+      </div>
+      
+      ${state.scriptError ? `<div class="script-error" role="alert">${escape(state.scriptError)}</div>` : ''}
+      ${state.scriptOutput ? renderScriptOutput(state.scriptOutput) : ''}
+    </div>
+  `;
+}
+
+function renderScriptOutput(scriptOutput) {
+  let html = '<div class="script-output">';
+  html += '<h4>Script Results:</h4>';
+  
+  // Display emitted data
+  if (scriptOutput.emitted && Object.keys(scriptOutput.emitted).length > 0) {
+    html += '<div class="emitted-data">';
+    html += '<h5>Emitted Data:</h5>';
+    Object.entries(scriptOutput.emitted).forEach(([event, data]) => {
+      html += `<div class="emitted-event">`;
+      html += `<h6>${escape(event)}:</h6>`;
+      html += `<pre>${escape(JSON.stringify(data, null, 2))}</pre>`;
+      html += `</div>`;
+    });
+    html += '</div>';
+  }
+  
+  // Display script result
+  if (scriptOutput.result !== undefined) {
+    html += '<div class="script-result">';
+    html += '<h5>Script Result:</h5>';
+    html += `<pre>${escape(JSON.stringify(scriptOutput.result, null, 2))}</pre>`;
+    html += '</div>';
+  }
+  
+  html += '</div>';
+  return html;
+}
+
+function escape(str) {
+  /**
+   * Escapes HTML special characters to prevent XSS.
+   *
+   * Why escape here? Keeps all rendering safe by default, so no accidental injection is possible.
+   */
+  return String(str).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
+}
+
+function capitalize(str) {
+  return str.charAt(0).toUpperCase() + str.slice(1);
+} 
\ No newline at end of file