#!/usr/bin/env node

/**
 * One-shot CDP driver for the development-live legacy parity surface.
 *
 * The companion PowerShell wrapper pins the package/app/PGM identities and calls
 * this script once per user-visible action.  This driver deliberately has no
 * retry loop for playout commands.  A timeout, OutcomeUnknown state, unexpected
 * native message, or probe replacement makes the invocation fail so that the
 * wrapper can latch the run in a stopped state.
 */

const PROBE_KEY = '__codexLegacyPgmParityProbe';
const PROBE_VERSION = '2026-07-22.3';
const DEFAULT_PORT = 9339;
const TARGET_URL = 'https://legacy-parity.mbn.local/index.html';
const RPC_TIMEOUT_MS = 15_000;
const STATE_TIMEOUT_MS = 180_000;
const BUILD_TIMEOUT_MS = 45_000;

const THIRTY_CODES = [
  '5016', '50160', '5078', '8067', '5086', '50860', '5023', '5024', '5077',
  '5082', '5083', '5084', '5085', '5088', '6067', '8035', '5011', '8003',
  '5037', '8001', '5026', '5087', '5029', '8032', '5032', '5076', '5079',
  '5080', '5081', '5025',
];

const THIRTY_MANIFEST = [
  ['5016', '\ubbf8\uad6d \uc9c0\uc218[\ub2e4\uc6b0, \ub098\uc2a4\ub2e5, S&P]', '3\uc5f4\ud310'],
  ['50160', '\ub18d\uc0b0\ubb3c[\uc6d0\uba74, \ubaa9\ud654(\uba74\ud654)]', '2\uc5f4\ud310-'],
  ['5078', '\ub2e4\uc6b0', '\ubbf8\uad6d\uc5c5\uc885'],
  ['8067', '\uae00\ub85c\ubc8c \uc99d\uc2dc \uc9c0\ub3c4', '\uae00\ub85c\ubc8c \uc99d\uc2dc \uc9c0\ub3c4'],
  ['5086', '\uc6d0\ub2ec\ub7ec_5\uc77c', '\uc218\uc775\ub960 \uadf8\ub798\ud504'],
  ['50860', '\uc6d0\ub2ec\ub7ec_5\uc77c', '\ub77c\uc778 \uadf8\ub798\ud504'],
  ['5023', '\ucf54\uc2a4\ud53c \ub9e4\ub9e4\ub3d9\ud5a5_\ud45c\uadf8\ub798\ud504', '\ub9e4\ub9e4\ub3d9\ud5a5'],
  ['5024', '\ucf54\uc2a4\ud53c \ub9e4\ub9e4\ub3d9\ud5a5_\ub9c9\ub300\uadf8\ub798\ud504', '\ub9e4\ub9e4\ub3d9\ud5a5'],
  ['5077', '\ucf54\uc2a4\ud53c \uc2dc\uac00\ucd1d\uc561', '6\uc885\ubaa9 \ud604\uc7ac\uac00'],
  ['5082', '\uc8fc\uccb4\ubcc4 \ub9e4\ub9e4\ub3d9\ud5a5_\ud45c\uadf8\ub798\ud504', '\ub9e4\ub9e4\ub3d9\ud5a5'],
  ['5083', '\uac1c\uc778 \ub9e4\ub9e4\ub3d9\ud5a5_\ub77c\uc778\uadf8\ub798\ud504', '\ub9e4\ub9e4\ub3d9\ud5a5'],
  ['5084', '\ucf54\uc2a4\ud53c \ub9e4\ub9e4\ub3d9\ud5a5_\ub77c\uc778\uadf8\ub798\ud504', '\ub9e4\ub9e4\ub3d9\ud5a5'],
  ['5085', '\ud504\ub85c\uadf8\ub7a8 \ub9e4\ub9e4_\ud45c\uadf8\ub798\ud504', '\ub9e4\ub9e4\ub3d9\ud5a5'],
  ['5088', '\ucf54\uc2a4\ud53c \uc2dc\uac00\ucd1d\uc561', '12\uc885\ubaa9 \ud604\uc7ac\uac00'],
  ['6067', '\uae30\uad00 \uc21c\ub9e4\uc218 \ud604\ud669_\ud45c\uadf8\ub798\ud504', '\ub9e4\ub9e4\ub3d9\ud5a5'],
  ['8035', '\uce94\ub4e4\uadf8\ub798\ud504_\uc77c\ubd09', '\ucf54\uc2a4\ud53c'],
  ['5011', '\uc0bc\uc131\uc804\uc790', '1\uc5f4\ud310\uc0c1\uc138'],
  ['8003', '\uc0bc\uc131\uc804\uc790', '\ud638\uac00\ucc3d'],
  ['5037', '\uc0bc\uc131\uc804\uc790', '\uac70\ub798\uc6d0'],
  ['8001', '\ucf54\uc2a4\ud53c \ub124\ubaa8\uadf8\ub798\ud504', '\ub124\ubaa8\uadf8\ub798\ud504'],
  ['5026', '\uc0bc\uc131\uc99d\uad8c \u2194 \uc0bc\uc131\ucd9c\ud310\uc0ac', 's5026'],
  ['5087', '\uc0bc\uc131\uc99d\uad8c \u2194 \uc0bc\uc131\ucd9c\ud310\uc0ac', 's5087'],
  ['5029', '\uc0bc\uc131\uc99d\uad8c \u2194 \uc0bc\uc131\ucd9c\ud310\uc0ac', 's5029'],
  ['8032', '\ucf54\uc2a4\ub2e5 \uc9c0\uc218 \u2194 \ucf54\uc2a4\ud53c \uc9c0\uc218', 's8018'],
  ['5032', '\uc120\ubb3c \uc9c0\uc218 \u2194 \ucf54\uc2a4\ud53c \uc9c0\uc218', 's5032'],
  ['5076', 'APS', '\uc8fc\uc694\ub9e4\ucd9c \uad6c\uc131'],
  ['5079', 'AJ\ub124\ud2b8\uc6cd\uc2a4', '\uc131\uc7a5\uc131 \uc9c0\ud45c'],
  ['5080', '3S', '\ub9e4\ucd9c\uc561'],
  ['5081', '3S', '\uc601\uc5c5\uc774\uc775'],
  ['5025', '\uac1c\uc778 \uc21c\ub9e4\ub3c4 \uc0c1\uc704(\uc218\ub3d9)', '\uc21c\ub9e4\ub3c4 \uc0c1\uc704'],
].map(([code, stockName, graphicType]) => ({ code, stockName, graphicType }));

const ROUTE_ROW_IDENTITIES = new Map(THIRTY_MANIFEST.map(item => [item.code, item]));
const FIXED_VISIBLE_ROW_IDENTITIES = new Map([
  ['5016', { market: '\ud574\uc678\uc9c0\uc218', stockName: '\ubbf8\uad6d \uc9c0\uc218[\ub2e4\uc6b0, \ub098\uc2a4\ub2e5, S&P]', graphicType: '3\uc5f4\ud310', subtype: '' }],
  ['50160', { market: '\ud574\uc678\uc9c0\uc218', stockName: '\ub18d\uc0b0\ubb3c[\uc6d0\uba74, \ubaa9\ud654(\uba74\ud654)]', graphicType: '2\uc5f4\ud310-', subtype: '' }],
  ['5078', { market: '\ud574\uc678\uc9c0\uc218', stockName: '\ub2e4\uc6b0', graphicType: '\ubbf8\uad6d\uc5c5\uc885', subtype: '' }],
  ['8067', { market: '\ud574\uc678\uc9c0\uc218', stockName: '\uae00\ub85c\ubc8c \uc99d\uc2dc \uc9c0\ub3c4', graphicType: '\uae00\ub85c\ubc8c \uc99d\uc2dc \uc9c0\ub3c4', subtype: '' }],
  ['5086', { market: '\ud658\uc728', stockName: '\uc6d0\ub2ec\ub7ec', graphicType: '\uc218\uc775\ub960 \uadf8\ub798\ud504', subtype: '5\uc77c' }],
  ['50860', { market: '\ud658\uc728', stockName: '\uc6d0\ub2ec\ub7ec', graphicType: '\ub77c\uc778 \uadf8\ub798\ud504', subtype: '5\uc77c' }],
  ['5023', { market: '\ucf54\uc2a4\ud53c', stockName: '\uc9c0\uc218', graphicType: '\ucf54\uc2a4\ud53c \ub9e4\ub9e4\ub3d9\ud5a5', subtype: '\ud45c\uadf8\ub798\ud504' }],
  ['5024', { market: '\ucf54\uc2a4\ud53c', stockName: '\uc9c0\uc218', graphicType: '\ucf54\uc2a4\ud53c \ub9e4\ub9e4\ub3d9\ud5a5', subtype: '\ub9c9\ub300\uadf8\ub798\ud504' }],
  ['5077', { market: '\ucf54\uc2a4\ud53c', stockName: '\uc9c0\uc218', graphicType: '6\uc885\ubaa9 \ud604\uc7ac\uac00', subtype: '\ucf54\uc2a4\ud53c \uc2dc\uac00\ucd1d\uc561' }],
  ['5082', { market: '\uc804\uccb4', stockName: '\uc9c0\uc218', graphicType: '\uc8fc\uccb4\ubcc4 \ub9e4\ub9e4\ub3d9\ud5a5', subtype: '\ud45c\uadf8\ub798\ud504' }],
  ['5083', { market: '\uc804\uccb4', stockName: '\uc9c0\uc218', graphicType: '\uac1c\uc778 \ub9e4\ub9e4\ub3d9\ud5a5', subtype: '\ub77c\uc778\uadf8\ub798\ud504' }],
  ['5084', { market: '\ucf54\uc2a4\ud53c', stockName: '\uc9c0\uc218', graphicType: '\ucf54\uc2a4\ud53c \ub9e4\ub9e4\ub3d9\ud5a5', subtype: '\ub77c\uc778\uadf8\ub798\ud504' }],
  ['5085', { market: '\uc804\uccb4', stockName: '\uc9c0\uc218', graphicType: '\ud504\ub85c\uadf8\ub7a8 \ub9e4\ub9e4', subtype: '\ud45c\uadf8\ub798\ud504' }],
  ['5088', { market: '\ucf54\uc2a4\ud53c', stockName: '\uc9c0\uc218', graphicType: '12\uc885\ubaa9 \ud604\uc7ac\uac00', subtype: '\ucf54\uc2a4\ud53c \uc2dc\uac00\ucd1d\uc561' }],
  ['6067', { market: '\uc804\uccb4', stockName: '\uc9c0\uc218', graphicType: '\uae30\uad00 \uc21c\ub9e4\uc218 \ud604\ud669', subtype: '\ud45c\uadf8\ub798\ud504' }],
  ['8035', { market: '\ucf54\uc2a4\ud53c', stockName: '\uc9c0\uc218', graphicType: '\uce94\ub4e4\uadf8\ub798\ud504', subtype: '\uc77c\ubd09' }],
]);
for (const [code, identity] of FIXED_VISIBLE_ROW_IDENTITIES) {
  ROUTE_ROW_IDENTITIES.set(code, { code, ...identity });
}
ROUTE_ROW_IDENTITIES.set('5001', {
  code: '5001', stockName: '\uc0bc\uc131\uc804\uc790', graphicType: '1\uc5f4\ud310\uae30\ubcf8', subtype: '\ud604\uc7ac\uac00',
});
ROUTE_ROW_IDENTITIES.set('5074', {
  code: '5074', stockName: '\ucf54\uc2a4\ud53c 5\ub2e8 \ud45c\uadf8\ub798\ud504', graphicType: '5\ub2e8 \ud45c\uadf8\ub798\ud504',
});

const FIXED_ACTIONS = [
  ['overseas', 'fixed-047'], ['overseas', 'fixed-044'],
  ['overseas', 'fixed-076'], ['overseas', 'fixed-063'],
  ['exchange', 'fixed-085'], ['exchange', 'fixed-105'],
  ['index', 'fixed-234'], ['index', 'fixed-236'], ['index', 'fixed-275'],
  ['index', 'fixed-233'], ['index', 'fixed-240'], ['index', 'fixed-235'],
  ['index', 'fixed-244'], ['index', 'fixed-302'], ['index', 'fixed-243'],
  ['index', 'fixed-136'],
];

const FINANCIAL_ACTIONS = [
  ['revenue-composition', 'APS'],
  ['growth-metrics', 'AJ\ub124\ud2b8\uc6cd\uc2a4'],
  ['sales', '3S'],
  ['operating-profit', '3S'],
];

const S6001_ASSET_INDEPENDENT_ACTIONS = [
  { actionId: 'fixed-072', stockName: '\uc720\uac00 \ub450\ubc14\uc774 \uc9c0\uc218', subtype: 'DubaiCrude' },
  { actionId: 'fixed-073', stockName: '\uc720\uac00 WTI \uc9c0\uc218', subtype: 'WtiCrude' },
  { actionId: 'fixed-074', stockName: '\uc720\uac00 \ube0c\ub80c\ud2b8\uc720 \uc9c0\uc218', subtype: 'BrentCrude' },
  { actionId: 'fixed-075', stockName: '\uae08 \uc9c0\uc218', subtype: 'Gold' },
];

const ROW_ID_PATTERN = /^legacy-(?:fixed|stock|industry|comparison|manual-financial|manual-list)-\d{8}$/;

function fail(message, code = 'STAGE_FAILED', details = undefined) {
  const error = new Error(message);
  error.code = code;
  error.details = details;
  throw error;
}

function parseArguments(argv) {
  const result = { port: DEFAULT_PORT, action: 'snapshot', routeIndex: null, pageIndex: null };
  for (let index = 2; index < argv.length; index += 1) {
    const token = argv[index];
    const value = argv[index + 1];
    if (token === '--port' && value) {
      result.port = Number(value);
      index += 1;
    } else if (token === '--action' && value) {
      result.action = value;
      index += 1;
    } else if (token === '--route-index' && value) {
      result.routeIndex = Number(value);
      index += 1;
    } else if (token === '--page-index' && value) {
      result.pageIndex = Number(value);
      index += 1;
    } else {
      fail(`Unknown or incomplete argument: ${token}`, 'INVALID_ARGUMENT');
    }
  }
  if (!Number.isInteger(result.port) || result.port <= 0 || result.port > 65535) {
    fail(`Invalid CDP port: ${result.port}`, 'INVALID_ARGUMENT');
  }
  return result;
}

async function fetchJson(url, timeoutMs = RPC_TIMEOUT_MS) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);
  try {
    const response = await fetch(url, { signal: controller.signal });
    if (!response.ok) {
      fail(`HTTP ${response.status} from ${url}`, 'CDP_DISCOVERY_FAILED');
    }
    return await response.json();
  } finally {
    clearTimeout(timer);
  }
}

async function openTarget(port) {
  const targets = await fetchJson(`http://127.0.0.1:${port}/json/list`);
  const pages = targets.filter((target) => target.type === 'page' && target.url === TARGET_URL);
  if (pages.length !== 1) {
    fail(`Expected exactly one parity page at ${TARGET_URL}; found ${pages.length}.`, 'TARGET_IDENTITY_MISMATCH', {
      targets: targets.map(({ id, type, url, title }) => ({ id, type, url, title })),
    });
  }
  if (!pages[0].webSocketDebuggerUrl) {
    fail('Parity page has no WebSocket debugger URL.', 'TARGET_IDENTITY_MISMATCH');
  }
  const endpoint = new URL(pages[0].webSocketDebuggerUrl);
  if (endpoint.protocol !== 'ws:' || endpoint.hostname !== '127.0.0.1' ||
      Number(endpoint.port) !== port || endpoint.pathname !== `/devtools/page/${pages[0].id}` ||
      endpoint.username || endpoint.password || endpoint.search || endpoint.hash) {
    fail('Parity page WebSocket endpoint identity is not exact.', 'TARGET_IDENTITY_MISMATCH', {
      id: pages[0].id,
      webSocketDebuggerUrl: pages[0].webSocketDebuggerUrl,
    });
  }
  return pages[0];
}

class CdpClient {
  constructor(url) {
    this.url = url;
    this.socket = null;
    this.sequence = 0;
    this.pending = new Map();
  }

  async connect() {
    this.socket = new WebSocket(this.url);
    await new Promise((resolve, reject) => {
      const timer = setTimeout(() => reject(new Error('CDP WebSocket connection timed out.')), RPC_TIMEOUT_MS);
      this.socket.addEventListener('open', () => {
        clearTimeout(timer);
        resolve();
      }, { once: true });
      this.socket.addEventListener('error', () => {
        clearTimeout(timer);
        reject(new Error('CDP WebSocket connection failed.'));
      }, { once: true });
    });
    this.socket.addEventListener('message', (event) => {
      const message = JSON.parse(event.data);
      if (!message.id) return;
      const pending = this.pending.get(message.id);
      if (!pending) return;
      this.pending.delete(message.id);
      clearTimeout(pending.timer);
      if (message.error) pending.reject(new Error(message.error.message || 'CDP RPC failed.'));
      else pending.resolve(message.result);
    });
    this.socket.addEventListener('close', () => {
      for (const pending of this.pending.values()) {
        clearTimeout(pending.timer);
        pending.reject(new Error('CDP WebSocket closed while an RPC was pending.'));
      }
      this.pending.clear();
    });
  }

  send(method, params = {}, timeoutMs = RPC_TIMEOUT_MS) {
    const id = ++this.sequence;
    return new Promise((resolve, reject) => {
      const timer = setTimeout(() => {
        this.pending.delete(id);
        const error = new Error(`CDP RPC timed out: ${method}`);
        error.code = 'OUTCOME_UNKNOWN';
        reject(error);
      }, timeoutMs);
      this.pending.set(id, { resolve, reject, timer });
      this.socket.send(JSON.stringify({ id, method, params }));
    });
  }

  async evaluate(expression, awaitPromise = true, timeoutMs = RPC_TIMEOUT_MS) {
    const result = await this.send('Runtime.evaluate', {
      expression,
      awaitPromise,
      returnByValue: true,
      userGesture: true,
    }, timeoutMs);
    if (result.exceptionDetails) {
      const description = result.exceptionDetails.exception?.description
        || result.exceptionDetails.text
        || 'Runtime.evaluate failed.';
      throw new Error(description);
    }
    return result.result?.value;
  }

  close() {
    if (this.socket && this.socket.readyState <= WebSocket.OPEN) this.socket.close();
  }
}

const probeInstallerSource = String.raw`
(() => {
  const key = ${JSON.stringify(PROBE_KEY)};
  const version = ${JSON.stringify(PROBE_VERSION)};
  const current = globalThis[key];
  if (current) {
    if (current.version !== version || window.chrome.webview.postMessage !== current.wrapper) {
      throw new Error('Parity probe exists but its version or wrapper identity changed.');
    }
    return { installed: false, version: current.version };
  }

  if (globalThis.__legacyDevelopmentLiveProbe) {
    throw new Error('A different development-live probe is already installed; start a fresh app instance.');
  }

  if (!window.chrome?.webview) throw new Error('WebView bridge is unavailable.');
  const original = window.chrome.webview.postMessage;
  const allowed = new Set([
    'ready', 'select-tab', 'activate-fixed-action', 'activate-playlist-row',
    'search-stocks', 'select-stock', 'cut-pointer-down', 'drop-selected-cuts',
    'activate-industry-action', 'search-comparison-domestic',
    'choose-comparison-domestic', 'choose-comparison-fixed',
    'clear-comparison-targets', 'add-comparison-pair', 'select-comparison-pair',
    'delete-selected-comparison-pair', 'activate-comparison-action',
    'open-manual-financial', 'search-manual-financial',
    'activate-manual-financial-result', 'close-manual-financial',
    'add-manual-net-sell-cut', 'close-manual-list',
    'take-in', 'next-playout', 'take-out'
  ]);
  const playout = new Set(['take-in', 'next-playout', 'take-out']);
  const fixedIds = new Set([
    'fixed-047', 'fixed-044', 'fixed-076', 'fixed-063', 'fixed-085', 'fixed-105',
    'fixed-234', 'fixed-236', 'fixed-275', 'fixed-233', 'fixed-240', 'fixed-235',
    'fixed-244', 'fixed-302', 'fixed-243', 'fixed-136', 'fixed-245',
    'fixed-072', 'fixed-073', 'fixed-074', 'fixed-075'
  ]);
  const tabs = new Set(['overseas', 'exchange', 'index', 'kospiIndustry', 'comparison']);
  const comparisonActions = new Set(['comparison-candle', 'comparison-line', 'return-5d', 'two-column-current']);
  const financialScreens = new Set(['revenue-composition', 'growth-metrics', 'sales', 'operating-profit']);
  const financialQueries = new Set(['APS', 'AJ\ub124\ud2b8\uc6cd\uc2a4', '3S']);
  const blocked = [];
  const outbound = [];
  const violations = [];
  const nativeStates = [];
  let state = null;

  const validToken = (value, max = 160) => typeof value === 'string' && value.length > 0 && value.length <= max &&
    !Array.from(value).some(character => {
      const code = character.charCodeAt(0);
      return code < 32 || code === 127 || code === 0xfffd;
    });
  const empty = payload => payload && typeof payload === 'object' && !Array.isArray(payload) && Object.keys(payload).length === 0;
  const validate = (message) => {
    if (!message || typeof message !== 'object' || !validToken(message.type, 64) || !allowed.has(message.type)) return false;
    const payload = message.payload;
    switch (message.type) {
      case 'ready': case 'drop-selected-cuts': case 'clear-comparison-targets':
      case 'add-comparison-pair': case 'delete-selected-comparison-pair':
      case 'close-manual-financial': case 'add-manual-net-sell-cut':
      case 'close-manual-list': case 'take-in': case 'next-playout':
      case 'take-out': return empty(payload);
      case 'select-tab': return tabs.has(payload?.tabId);
      case 'activate-fixed-action': return fixedIds.has(payload?.actionId);
      case 'activate-playlist-row': return /^legacy-(?:fixed|stock|industry|comparison|manual-financial|manual-list)-[0-9]{8}$/u.test(payload?.rowId || '');
      case 'search-stocks': return payload?.text === '\uc0bc\uc131\uc804\uc790' && payload?.trigger === 'button';
      case 'select-stock': return Number.isInteger(payload?.resultIndex) && payload.resultIndex >= 0;
      case 'cut-pointer-down': return [1, 5, 31, 32].includes(payload?.physicalIndex) &&
        payload?.control === false && payload?.shift === false && payload?.allowAppend === true &&
        Number.isFinite(payload?.timestampMilliseconds);
      case 'activate-industry-action': return ['five-row', 'square'].includes(payload?.actionId);
      case 'search-comparison-domestic': return payload?.query === '\uc0bc\uc131';
      case 'choose-comparison-domestic': return validToken(payload?.selectionId);
      case 'choose-comparison-fixed': return ['kospi', 'kosdaq', 'futures'].includes(payload?.targetId);
      case 'select-comparison-pair': return /^comparison-pair-[0-9]{8}$/u.test(payload?.rowId || '');
      case 'activate-comparison-action': return comparisonActions.has(payload?.actionId);
      case 'open-manual-financial': return financialScreens.has(payload?.screen);
      case 'search-manual-financial': return financialQueries.has(payload?.query);
      case 'activate-manual-financial-result': return validToken(payload?.resultId);
      default: return false;
    }
  };

  const wrapper = (message) => {
    const payload = message?.payload;
    if (!validate(message)) {
      blocked.push({ at: new Date().toISOString(), reason: 'not-allowlisted', type: message?.type || null, payload });
      throw new Error('Blocked unexpected native message.');
    }
    outbound.push({ at: new Date().toISOString(), type: message.type, payload: structuredClone(payload) });
    if (outbound.length > 1200) outbound.shift();
    return original.call(window.chrome.webview, message);
  };

  window.chrome.webview.postMessage = wrapper;
  window.chrome.webview.addEventListener('message', (event) => {
    const message = event.data;
    if (!message || message.type !== 'state' || !message.payload) return;
    state = structuredClone(message.payload);
    nativeStates.push({ at: new Date().toISOString(), state });
    if (nativeStates.length > 500) nativeStates.shift();
    if (!state.playout || state.playout.outcomeUnknown === true) {
      violations.push({ at: new Date().toISOString(), revision: state.revision ?? null,
        reason: !state.playout ? 'missing-playout' : 'outcome-unknown' });
    }
  });

  globalThis[key] = {
    version, original, wrapper, allowed, playout, blocked, outbound,
    violations, nativeStates, get state() { return state; }
  };
  return { installed: true, version };
})()
`;

function js(value) {
  return JSON.stringify(value);
}

function normalizeValue(value) {
  return value == null ? null : String(value);
}

async function installOrVerifyProbe(client) {
  return await client.evaluate(probeInstallerSource);
}

async function snapshot(client) {
  return await client.evaluate(`(() => {
    const key = ${js(PROBE_KEY)};
    const probe = globalThis[key];
    const app = document.querySelector('main.operator-shell');
    const native = probe?.state || null;
    const playlist = native?.playlist || [];
    const activeNative = playlist.find(row => row.isActive) || null;
    const playout = native?.playout || null;
    const settled = Boolean(playout) && native?.isBusy !== true && document.body.getAttribute('aria-busy') !== 'true' &&
      playout.isBusy !== true && playout.isPlayCompletionPending !== true &&
      playout.isTakeOutCompletionPending !== true && playout.refreshActive !== true;
    const normalize = (value) => (value == null ? null : String(value));
    return {
      href: location.href,
      readyState: document.readyState,
      appPresent: Boolean(app),
      bodyBusy: document.body.getAttribute('aria-busy'),
      probe: probe ? {
        version: probe.version,
        wrapperExact: window.chrome.webview.postMessage === probe.wrapper,
        blockedCount: probe.blocked.length,
        violationCount: probe.violations.length,
        nativeStateCount: probe.nativeStates.length,
        outboundCount: probe.outbound.length,
        lastOutbound: probe.outbound.slice(-8),
        violations: probe.violations.slice(-8)
      } : null,
      buttons: Object.fromEntries(['playout-take-in', 'playout-next', 'playout-take-out']
        .map(id => [id, {
          disabled: document.getElementById(id)?.disabled === true,
          hidden: document.getElementById(id)?.hidden === true
        }])),
      state: native ? {
        revision: native.revision ?? null,
        isBusy: native.isBusy === true,
        status: normalize(native.statusMessage),
        statusKind: normalize(native.statusKind),
        dialog: native.dialog ? structuredClone(native.dialog) : null,
        commandResult: native.commandResult ? structuredClone(native.commandResult) : null,
        mode: normalize(playout?.mode),
        connectionState: normalize(playout?.connectionState),
        connected: playout?.connectionState === 'connected',
        isConnected: playout?.isConnected === true,
        isCommandAvailable: playout?.isCommandAvailable === true,
        phase: normalize(playout?.phase),
        settled,
        pendingCommand: playout?.isPlayCompletionPending === true ? 'play-completion'
          : playout?.isTakeOutCompletionPending === true ? 'take-out-completion'
            : playout?.refreshActive === true ? 'refresh' : null,
        outcomeUnknown: playout?.outcomeUnknown === true,
        liveTakeInAllowed: playout?.liveTakeInAllowed === true,
        currentCode: normalize(playout?.onAirCode),
        preparedCode: normalize(playout?.preparedCode),
        currentCueIndex: Number.isInteger(playout?.currentCueIndexZeroBased) ? playout.currentCueIndexZeroBased : -1,
        currentEntryId: normalize(playout?.currentEntryId),
        nextKind: normalize(playout?.nextKind),
        refreshCount: Number(playout?.refreshCompletedCount || 0),
        refreshLimitReached: playout?.refreshLimitReached === true,
        activeRowId: normalize(activeNative?.rowId),
        pageText: normalize(activeNative?.pageText),
        pageIndex: Number.isInteger(playout?.pageIndexZeroBased) ? playout.pageIndexZeroBased + 1 : 0,
        pageCount: Number(playout?.pageCount || 0),
        pageSize: Number(playout?.pageSize || 0),
        itemCount: Number(playout?.itemCount || 0),
        currentPageItemCount: Number(playout?.currentPageItemCount || 0),
        playout: playout ? structuredClone(playout) : null,
        rows: playlist.map((row) => ({
          id: normalize(row.rowId), graphicCode: normalize(row.sceneAlias ?? row.cutCode),
          market: normalize(row.marketText), name: normalize(row.stockName),
          graphicType: normalize(row.graphicType), subtype: normalize(row.subtype),
          pageText: normalize(row.pageText), enabled: row.isEnabled === true,
          active: row.isActive === true, selected: row.isSelected === true,
          resolved: row.isSceneResolved === true
        })),
        active: activeNative ? {
          id: normalize(activeNative.rowId), graphicCode: normalize(activeNative.sceneAlias ?? activeNative.cutCode),
          market: normalize(activeNative.marketText), name: normalize(activeNative.stockName),
          graphicType: normalize(activeNative.graphicType), subtype: normalize(activeNative.subtype),
          pageText: normalize(activeNative.pageText)
        } : null,
        tabs: (native.tabs || []).map(tab => ({ id: normalize(tab.id), active: tab.isActive === true })),
        searchResults: (native.searchResults || []).map((item) => ({
          index: Number(item.index), name: normalize(item.displayName)
        })),
        selectedStockIndex: Number.isInteger(native.selectedStockIndex) ? native.selectedStockIndex : null,
        selectedCuts: (native.cutRows || []).filter(cut => cut.isSelected).map((cut) => ({
          physicalIndex: Number(cut.physicalIndex), label: normalize(cut.rawLabel)
        })),
        cutRows: (native.cutRows || []).map(cut => ({
          physicalIndex: Number(cut.physicalIndex), label: normalize(cut.rawLabel), selected: cut.isSelected === true
        })),
        fixedActions: (native.fixedCatalog?.sections || []).flatMap(section =>
          (section.actions || []).map(item => ({ id: normalize(item.id), available: item.isAvailable === true }))),
        industry: native.industry ? structuredClone(native.industry) : null,
        comparison: native.comparison ? structuredClone(native.comparison) : null,
        comparisonPairs: (native.comparison?.savedPairs || []).map(pair => ({
          id: normalize(pair.rowId ?? pair.id ?? pair.pairId),
          rowNumber: Number(pair.rowNumber),
          displayLabel: normalize(pair.displayLabel),
          selected: pair.isSelected === true
        })),
        manualFinancial: native.manualFinancial ? structuredClone(native.manualFinancial) : null,
        manualLists: native.manualLists ? structuredClone(native.manualLists) : null
      } : null,
      dom: {
        connectionText: document.getElementById('playout-connection-state')?.textContent?.trim() || null,
        statusText: document.getElementById('playout-status')?.textContent?.trim() || null,
        activeWorkspace: window.LegacyWorkspaceNavigation?.active?.() || null,
        activeMarket: window.LegacyWorkspaceNavigation?.activeMarket?.() || null,
        catalogVisible: (() => {
          const workspace = document.getElementById('catalog-workspace');
          return Boolean(workspace && workspace.hidden !== true &&
            !workspace.hasAttribute('inert') && workspace.getAttribute('aria-hidden') === 'false');
        })()
      }
    };
  })()`);
}

function assertHealthy(result, { requireIdle = false, allowPending = false, allowMissingState = false } = {}) {
  if (result.href !== TARGET_URL || !result.appPresent || result.readyState !== 'complete') {
    fail('Parity WebView document identity is not exact.', 'TARGET_IDENTITY_MISMATCH', result);
  }
  if (!result.probe || result.probe.version !== PROBE_VERSION || !result.probe.wrapperExact) {
    fail('Parity safety probe is missing or replaced.', 'PROBE_IDENTITY_MISMATCH', result.probe);
  }
  if (result.probe.blockedCount !== 0 || result.probe.violationCount !== 0) {
    fail('Parity safety probe recorded a blocked message or state violation.', 'PROBE_VIOLATION', result.probe);
  }
  const state = result.state;
  if (allowMissingState && !state) return;
  if (!state || state.mode !== 'live' || state.connectionState !== 'connected' ||
      !state.connected || !state.isConnected) {
    fail('Development-live state is not connected and authorized.', 'LIVE_STATE_MISMATCH', state);
  }
  if (state.dialog != null) fail('An operator dialog is present.', 'OPERATOR_DIALOG_PRESENT', state.dialog);
  if (state.outcomeUnknown) fail('Native state reports OutcomeUnknown.', 'OUTCOME_UNKNOWN', state);
  if (!allowPending && (!state.isCommandAvailable || !state.liveTakeInAllowed || state.pendingCommand || !state.settled)) {
    fail('Native playout state is not settled.', 'PLAYOUT_NOT_SETTLED', state);
  }
  if (requireIdle && state.phase !== 'idle') {
    fail(`Expected IDLE phase; found ${state.phase}.`, 'PLAYOUT_PHASE_MISMATCH', state);
  }
}

async function waitFor(client, predicate, description, timeoutMs = STATE_TIMEOUT_MS,
    { allowMissingState = false } = {}) {
  const deadline = Date.now() + timeoutMs;
  let last = null;
  while (Date.now() < deadline) {
    last = await snapshot(client);
    assertHealthy(last, { allowPending: true, allowMissingState });
    if (predicate(last)) return last;
    await new Promise((resolve) => setTimeout(resolve, 125));
  }
  const error = new Error(`Timed out waiting for ${description}.`);
  error.code = 'OUTCOME_UNKNOWN';
  error.details = last;
  throw error;
}

async function postAndWait(client, type, payload, predicate, description, timeoutMs = BUILD_TIMEOUT_MS) {
  const before = await snapshot(client);
  assertHealthy(before);
  const beforeRevision = Number(before.state.revision || 0);
  await client.evaluate(`(() => {
    window.chrome.webview.postMessage(${js({ type, payload })});
    return true;
  })()`);
  return await waitFor(
    client,
    (current) => Number(current.state.revision || 0) > beforeRevision && predicate(current, before),
    description,
    timeoutMs,
  );
}

async function resolvePoint(client, selector) {
  const point = await client.evaluate(`(() => {
    const element = document.querySelector(${js(selector)});
    if (!element) return null;
    const rect = element.getBoundingClientRect();
    const style = getComputedStyle(element);
    if (rect.width <= 0 || rect.height <= 0 || style.visibility === 'hidden' || style.display === 'none') return null;
    element.scrollIntoView({ block: 'center', inline: 'center' });
    const next = element.getBoundingClientRect();
    return { x: next.left + next.width / 2, y: next.top + next.height / 2 };
  })()`);
  if (!point) fail(`Visible click target was not found: ${selector}`, 'CLICK_TARGET_MISSING');
  return point;
}

async function physicalClick(client, selector) {
  const point = await resolvePoint(client, selector);
  await client.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: point.x, y: point.y, button: 'none' });
  await client.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: point.x, y: point.y, button: 'left', clickCount: 1 });
  await client.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: point.x, y: point.y, button: 'left', clickCount: 1 });
}

async function physicalDoubleClickAndWait(
  client, selector, predicate, description, timeoutMs = BUILD_TIMEOUT_MS) {
  const before = await snapshot(client);
  assertHealthy(before);
  const outboundBefore = before.probe.outboundCount;
  const point = await resolvePoint(client, selector);
  await client.send('Input.dispatchMouseEvent', {
    type: 'mouseMoved', x: point.x, y: point.y, button: 'none',
  });
  await client.send('Input.dispatchMouseEvent', {
    type: 'mousePressed', x: point.x, y: point.y, button: 'left', clickCount: 1,
  });
  await client.send('Input.dispatchMouseEvent', {
    type: 'mouseReleased', x: point.x, y: point.y, button: 'left', clickCount: 1,
  });
  await new Promise((resolve) => setTimeout(resolve, 50));
  await client.send('Input.dispatchMouseEvent', {
    type: 'mousePressed', x: point.x, y: point.y, button: 'left', clickCount: 2,
  });
  await client.send('Input.dispatchMouseEvent', {
    type: 'mouseReleased', x: point.x, y: point.y, button: 'left', clickCount: 2,
  });
  return await waitFor(client, (current) =>
    current.probe.outboundCount > outboundBefore && predicate(current, before),
  description, timeoutMs);
}

async function physicalClickAndWait(client, selector, predicate, description, timeoutMs = STATE_TIMEOUT_MS) {
  const before = await snapshot(client);
  assertHealthy(before);
  const outboundBefore = before.probe.outboundCount;
  await physicalClick(client, selector);
  return await waitFor(client, (current) => {
    if (current.probe.outboundCount <= outboundBefore) return false;
    return predicate(current, before);
  }, description, timeoutMs);
}

async function physicalDragStockCut(client, physicalIndex, label) {
  const before = await snapshot(client);
  assertHealthy(before);
  const geometry = await client.evaluate(`(() => {
    const source = document.querySelector('#cut-list .cut-row[data-physical-index=${js(String(physicalIndex))}]');
    const target = document.getElementById('playlist-drop-zone');
    if (!source || !target) return null;
    source.scrollIntoView({ block: 'center', inline: 'center' });
    const sourceRect = source.getBoundingClientRect();
    const targetRect = target.getBoundingClientRect();
    const sourceStyle = getComputedStyle(source);
    const targetStyle = getComputedStyle(target);
    if (sourceRect.width <= 0 || sourceRect.height <= 0 || targetRect.width <= 0 || targetRect.height <= 0 ||
        sourceStyle.visibility === 'hidden' || sourceStyle.display === 'none' ||
        targetStyle.visibility === 'hidden' || targetStyle.display === 'none') return null;
    const start = { x: sourceRect.left + sourceRect.width / 2, y: sourceRect.top + sourceRect.height / 2 };
    return {
      start,
      threshold: { x: start.x + 20, y: start.y + 20 },
      target: { x: targetRect.left + targetRect.width / 2, y: targetRect.top + Math.min(80, targetRect.height / 2) }
    };
  })()`);
  if (!geometry) fail(`Visible stock cut drag target was not found: ${label}`, 'DRAG_TARGET_MISSING');

  await client.send('Input.dispatchMouseEvent', { type: 'mouseMoved', ...geometry.start, button: 'none' });
  await client.send('Input.dispatchMouseEvent', {
    type: 'mousePressed', ...geometry.start, button: 'left', buttons: 1, clickCount: 1,
  });
  await client.send('Input.dispatchMouseEvent', {
    type: 'mouseMoved', ...geometry.threshold, button: 'left', buttons: 1,
  });
  await client.send('Input.dispatchMouseEvent', {
    type: 'mouseMoved', ...geometry.target, button: 'left', buttons: 1,
  });
  await client.send('Input.dispatchMouseEvent', {
    type: 'mouseReleased', ...geometry.target, button: 'left', buttons: 0, clickCount: 1,
  });

  const result = await waitFor(client, (current) => !current.state.isBusy &&
    current.state.rows.length === before.state.rows.length + 1, `stock cut drag ${label}`, BUILD_TIMEOUT_MS);
  const outboundDelta = result.probe.outboundCount - before.probe.outboundCount;
  const types = result.probe.lastOutbound.slice(-outboundDelta).map(item => item.type);
  if (outboundDelta !== 2 || types[0] !== 'cut-pointer-down' || types[1] !== 'drop-selected-cuts') {
    fail(`Stock cut drag did not produce the exact native gesture sequence: ${label}`,
      'DRAG_MESSAGE_SEQUENCE_MISMATCH', { outboundDelta, types });
  }
  return result;
}

function assertRowIds(rows) {
  const invalid = rows.filter((row) => !ROW_ID_PATTERN.test(row.id || ''));
  if (invalid.length > 0) fail('Playlist contains a non in-memory legacy row identifier.', 'PLAYLIST_ID_MISMATCH', invalid);
}

function assertExactCodes(result, expected) {
  if (result.state.rows.length !== expected.length) {
    fail(`Playlist route count mismatch. Expected ${expected.length}; got ${result.state.rows.length}.`,
      'PLAYLIST_ROUTE_MISMATCH', { expected, rows: result.state.rows });
  }
  result.state.rows.forEach((row, index) => {
    assertRowMatchesCode(row, expected[index], index);
  });
  assertRowIds(result.state.rows);
}

function assertRowMatchesCode(row, code, index = null) {
  const expected = ROUTE_ROW_IDENTITIES.get(code);
  if (!expected) fail(`No row identity is registered for route ${code}.`, 'PLAYLIST_ROUTE_MISMATCH');
  if ((expected.market != null && row.market !== expected.market) ||
      row.name !== expected.stockName || row.graphicType !== expected.graphicType ||
      (expected.subtype != null && row.subtype !== expected.subtype) || row.enabled !== true || row.resolved !== true) {
    fail(`Playlist row identity mismatch for route ${code}${index == null ? '' : ` at index ${index}`}.`,
      'PLAYLIST_ROW_MISMATCH', { code, expected, row });
  }
}

function assertTwoSceneManifest(result) {
  assertExactCodes(result, ['5001', '5074']);
  const [first, second] = result.state.rows;
  if (first.name !== '\uc0bc\uc131\uc804\uc790' || first.market !== '\ucf54\uc2a4\ud53c' ||
      first.graphicType !== '1\uc5f4\ud310\uae30\ubcf8' || first.subtype !== '\ud604\uc7ac\uac00' ||
      second.market !== '\uc5c5\uc885_\ucf54\uc2a4\ud53c' || second.name !== '\ucf54\uc2a4\ud53c 5\ub2e8 \ud45c\uadf8\ub798\ud504' ||
      second.graphicType !== '5\ub2e8 \ud45c\uadf8\ub798\ud504') {
    fail('5001/5074 playlist row identity mismatch.', 'PLAYLIST_ROW_MISMATCH', result.state.rows);
  }
}

function assertThirtyManifest(result) {
  assertExactCodes(result, THIRTY_CODES);
  result.state.rows.forEach((row, index) => {
    if (!row.enabled || !/^1\/(?:1|20)$/u.test(row.pageText || '')) {
      fail(`30-route playlist page identity mismatch at index ${index}.`, 'PLAYLIST_ROW_MISMATCH', { row });
    }
  });
}

function assertPagedManifest(result) {
  assertExactCodes(result, ['5077', '5088']);
  const [first, second] = result.state.rows;
  if (!/^(?:[1-9]|1[0-9]|20)\/20$/u.test(first.pageText || '') ||
      second.pageText !== '1/20' || !first.enabled || !second.enabled) {
    fail('5077/5088 page-boundary playlist identity is not exact.', 'PLAYLIST_PAGE_FIXTURE_MISMATCH', result.state.rows);
  }
}

function assertS6001Row(row, expected, index = null) {
  if (!row || row.market !== '\ud574\uc678\uc9c0\uc218' || row.name !== expected.stockName ||
      row.graphicType !== '\uc774\ubbf8\uc9c0 \uadf8\ub798\ud504' || row.subtype !== '' ||
      row.pageText !== '1/1' || row.enabled !== true || row.resolved !== true ||
      !ROW_ID_PATTERN.test(row.id || '')) {
    fail(`s6001 playlist row identity mismatch${index == null ? '' : ` at index ${index}`}.`,
      'PLAYLIST_ROW_MISMATCH', { expected, row });
  }
}

function assertS6001Manifest(result) {
  if (result.state.rows.length !== S6001_ASSET_INDEPENDENT_ACTIONS.length) {
    fail('s6001 asset-independent playlist must contain exactly four rows.',
      'PLAYLIST_ROUTE_MISMATCH', result.state.rows);
  }
  result.state.rows.forEach((row, index) =>
    assertS6001Row(row, S6001_ASSET_INDEPENDENT_ACTIONS[index], index));
}

async function selectTab(client, tabId) {
  const current = await snapshot(client);
  if (current.state.tabs.some(tab => tab.id === tabId && tab.active)) return current;
  return await postAndWait(client, 'select-tab', { tabId },
    (value) => value.state.tabs.some(tab => tab.id === tabId && tab.active) && !value.state.isBusy,
    `tab ${tabId}`);
}

async function physicalSelectCatalogTab(client, tabId) {
  const before = await snapshot(client);
  assertHealthy(before);
  if (!before.state.tabs.some(tab => tab.id === tabId)) {
    fail(`Catalog tab is not registered: ${tabId}`, 'CATALOG_TAB_MISSING');
  }

  const outboundBefore = before.probe.outboundCount;
  const nativeStateBefore = before.probe.nativeStateCount;
  await physicalClick(client, `#market-tabs button[data-tab-id=${js(tabId)}]`);
  const result = await waitFor(client, (current) =>
    current.probe.outboundCount > outboundBefore &&
      current.probe.nativeStateCount > nativeStateBefore &&
      !current.state.isBusy &&
      current.state.tabs.some(tab => tab.id === tabId && tab.active) &&
      current.dom.activeWorkspace === 'catalog' &&
      current.dom.activeMarket === tabId &&
      current.dom.catalogVisible === true,
  `physical catalog tab ${tabId}`, BUILD_TIMEOUT_MS);

  const outboundDelta = result.probe.outboundCount - outboundBefore;
  const types = result.probe.lastOutbound.slice(-outboundDelta).map(item => item.type);
  if (outboundDelta !== 1 || types[0] !== 'select-tab') {
    fail(`Catalog tab click did not produce exactly one select-tab intent: ${tabId}`,
      'CATALOG_TAB_INTENT_MISMATCH', { outboundDelta, types });
  }
  return result;
}

async function selectSamsung(client) {
  const searched = await postAndWait(client, 'search-stocks', { text: '\uc0bc\uc131\uc804\uc790', trigger: 'button' },
    (current) => !current.state.isBusy &&
      current.state.searchResults.filter(item => item.name === '\uc0bc\uc131\uc804\uc790').length === 1,
    'Samsung stock search');
  const stock = searched.state.searchResults.find(item => item.name === '\uc0bc\uc131\uc804\uc790');
  if (!stock || !Number.isInteger(stock.index)) fail('Samsung search result identity is not unique.', 'SEARCH_RESULT_MISMATCH');
  await postAndWait(client, 'select-stock', { resultIndex: stock.index },
    (current) => !current.state.isBusy && current.state.selectedStockIndex === stock.index,
    'Samsung stock selection');
}

async function addStockCut(client, physicalIndex, label, expectedCode) {
  const result = await physicalDragStockCut(client, physicalIndex, label);
  assertRowMatchesCode(result.state.rows.at(-1), expectedCode);
}

async function addFixedAction(client, section, actionId, expectedCode) {
  const current = await selectTab(client, section);
  if (!current.state.fixedActions.some(item => item.id === actionId && item.available)) {
    fail(`Fixed action ${section}/${actionId} is unavailable.`, 'FIXED_ACTION_UNAVAILABLE');
  }
  const result = await postAndWait(client, 'activate-fixed-action', { actionId },
    (value, before) => !value.state.isBusy && value.state.rows.length === before.state.rows.length + 1,
    `fixed action ${section}/${actionId}`);
  assertRowMatchesCode(result.state.rows.at(-1), expectedCode);
}

async function addIndustryAction(client, actionId, expectedCode) {
  const current = await selectTab(client, 'kospiIndustry');
  if (!current.state.industry?.actions?.some(item => item.actionId === actionId)) {
    fail(`Industry action ${actionId} is unavailable.`, 'INDUSTRY_ACTION_UNAVAILABLE');
  }
  const result = await postAndWait(client, 'activate-industry-action', { actionId },
    (value, before) => !value.state.isBusy && value.state.rows.length === before.state.rows.length + 1,
    `industry action ${actionId}`);
  assertRowMatchesCode(result.state.rows.at(-1), expectedCode);
}

async function clearComparison(client) {
  const current = await snapshot(client);
  if (current.state.comparison?.firstTarget == null && current.state.comparison?.secondTarget == null) return current;
  return await postAndWait(client, 'clear-comparison-targets', {},
    (value) => value.state.comparison?.firstTarget == null && value.state.comparison?.secondTarget == null,
    'clear comparison targets');
}

async function chooseComparison(client, type, payload, slot) {
  return await postAndWait(client, type, payload,
    (value) => value.state.comparison?.[slot] != null,
    `comparison ${slot}`);
}

async function addComparisonAction(client, actionId, expectedCode) {
  const current = await snapshot(client);
  if (!current.state.comparison?.actions?.some(item => item.actionId === actionId && item.isAvailable)) {
    fail(`Comparison action ${actionId} is unavailable.`, 'COMPARISON_ACTION_UNAVAILABLE');
  }
  const result = await postAndWait(client, 'activate-comparison-action', { actionId },
    (value, before) => !value.state.isBusy && value.state.rows.length === before.state.rows.length + 1,
    `comparison action ${actionId}`);
  assertRowMatchesCode(result.state.rows.at(-1), expectedCode);
  return result;
}

function comparisonPairRows(comparison) {
  return (comparison?.savedPairs || []).map((pair) => ({
    rowId: normalizeValue(pair.rowId ?? pair.id ?? pair.pairId),
    rowNumber: Number(pair.rowNumber),
    displayLabel: normalizeValue(pair.displayLabel),
  }));
}

function comparisonPairsEqual(left, right) {
  return JSON.stringify(left) === JSON.stringify(right);
}

async function commitComparisonPair(client, description) {
  const before = await snapshot(client);
  assertHealthy(before);
  const baseline = comparisonPairRows(before.state.comparison);
  const baselineIds = new Set(baseline.map((pair) => pair.rowId));
  const result = await postAndWait(client, 'add-comparison-pair', {},
    (value, before) => !value.state.isBusy &&
      (value.state.comparison?.savedPairs?.length || 0) === (before.state.comparison?.savedPairs?.length || 0) + 1 &&
      value.state.comparison?.selectedPairId != null,
    `comparison pair ${description}`);
  const after = comparisonPairRows(result.state.comparison);
  const addedPairId = normalizeValue(result.state.comparison?.selectedPairId);
  if (!comparisonPairsEqual(after.slice(0, -1), baseline) ||
      !addedPairId || baselineIds.has(addedPairId) || after.at(-1)?.rowId !== addedPairId) {
    fail(`Temporary comparison pair identity changed the saved baseline: ${description}`,
      'COMPARISON_COMMIT_MISMATCH', { baseline, after, addedPairId });
  }
  return { result, baseline, addedPairId };
}

async function deleteTemporaryComparisonPair(client, description, baseline, addedPairId) {
  const before = await snapshot(client);
  assertHealthy(before);
  if (normalizeValue(before.state.comparison?.selectedPairId) !== addedPairId) {
    fail(`The temporary comparison pair is no longer selected: ${description}`,
      'COMPARISON_TEMP_SELECTION_MISMATCH', before.state.comparison);
  }
  const result = await postAndWait(client, 'delete-selected-comparison-pair', {},
    (value) => !value.state.isBusy &&
      comparisonPairRows(value.state.comparison).length === baseline.length &&
      !comparisonPairRows(value.state.comparison).some((pair) => pair.rowId === addedPairId),
    `comparison pair cleanup ${description}`);
  const after = comparisonPairRows(result.state.comparison);
  if (!comparisonPairsEqual(after, baseline)) {
    fail(`Temporary comparison pair cleanup changed the saved baseline: ${description}`,
      'COMPARISON_CLEANUP_MISMATCH', { baseline, after, addedPairId });
  }
  return result;
}

async function restoreComparisonSelection(client, selectedPairId) {
  const current = await snapshot(client);
  const currentSelection = normalizeValue(current.state.comparison?.selectedPairId);
  if (currentSelection === selectedPairId || (!selectedPairId && !currentSelection)) return current;
  if (!selectedPairId || !comparisonPairRows(current.state.comparison).some((pair) => pair.rowId === selectedPairId)) {
    fail('The original saved comparison selection cannot be restored.',
      'COMPARISON_SELECTION_RESTORE_MISMATCH', { selectedPairId, comparison: current.state.comparison });
  }
  return await postAndWait(client, 'select-comparison-pair', { rowId: selectedPairId },
    (value) => !value.state.isBusy && value.state.comparison?.selectedPairId === selectedPairId,
    'restore saved comparison selection');
}

async function setup5001And5074(client) {
  const initial = await snapshot(client);
  assertHealthy(initial, { requireIdle: true });
  if (initial.state.rows.length !== 0) fail('5001/5074 setup requires an empty in-memory playlist.', 'PLAYLIST_NOT_EMPTY');
  await selectSamsung(client);
  await addStockCut(client, 1, '1\uc5f4\ud310\uae30\ubcf8_\ud604\uc7ac\uac00', '5001');
  await addIndustryAction(client, 'five-row', '5074');
  const result = await snapshot(client);
  assertTwoSceneManifest(result);
  return result;
}

async function buildThirtyBase(client) {
  const initial = await snapshot(client);
  assertHealthy(initial, { requireIdle: true });
  if (initial.state.rows.length !== 0) fail('30-route base build requires an empty in-memory playlist.', 'PLAYLIST_NOT_EMPTY');
  for (let index = 0; index < FIXED_ACTIONS.length; index += 1) {
    await addFixedAction(client, FIXED_ACTIONS[index][0], FIXED_ACTIONS[index][1], THIRTY_CODES[index]);
  }
  await selectSamsung(client);
  await addStockCut(client, 5, '1\uc5f4\ud310\uc0c1\uc138_\uc561\uba74\uac00', '5011');
  await addStockCut(client, 31, '\ud638\uac00\ucc3d_\ud45c\uadf8\ub798\ud504', '8003');
  await addStockCut(client, 32, '\uac70\ub798\uc6d0_\ud45c\uadf8\ub798\ud504', '5037');
  await addIndustryAction(client, 'square', '8001');
  const result = await snapshot(client);
  assertExactCodes(result, THIRTY_CODES.slice(0, 20));
  return result;
}

async function buildThirtyComparison(client) {
  const initial = await snapshot(client);
  assertHealthy(initial, { requireIdle: true });
  assertExactCodes(initial, THIRTY_CODES.slice(0, 20));

  await selectTab(client, 'comparison');
  let current = await snapshot(client);
  const savedPairBaseline = comparisonPairRows(current.state.comparison);
  const selectedPairBaseline = normalizeValue(current.state.comparison?.selectedPairId);
  await clearComparison(client);
  current = await postAndWait(client, 'search-comparison-domestic', { query: '\uc0bc\uc131' },
    (value) => !value.state.isBusy && value.state.comparison?.domesticSearch?.query === '\uc0bc\uc131' &&
      value.state.comparison?.domesticSearch?.results?.length > 1,
    'comparison domestic search');
  const firstId = current.state.comparison.domesticSearch.results[0].selectionId;
  const secondId = current.state.comparison.domesticSearch.results[1].selectionId;
  if (!firstId || !secondId || firstId === secondId) fail('Comparison search selections are not exact.', 'COMPARISON_SELECTION_MISMATCH');
  await chooseComparison(client, 'choose-comparison-domestic', { selectionId: firstId }, 'firstTarget');
  await chooseComparison(client, 'choose-comparison-domestic', { selectionId: secondId }, 'secondTarget');
  let temporaryPair = await commitComparisonPair(client, 'Samsung pair');
  await addComparisonAction(client, 'comparison-candle', '5026');
  await addComparisonAction(client, 'comparison-line', '5087');
  await addComparisonAction(client, 'return-5d', '5029');
  await deleteTemporaryComparisonPair(client, 'Samsung pair', temporaryPair.baseline, temporaryPair.addedPairId);

  await clearComparison(client);
  await chooseComparison(client, 'choose-comparison-fixed', { targetId: 'kospi' }, 'firstTarget');
  await chooseComparison(client, 'choose-comparison-fixed', { targetId: 'kosdaq' }, 'secondTarget');
  temporaryPair = await commitComparisonPair(client, 'KOSPI/KOSDAQ');
  await addComparisonAction(client, 'two-column-current', '8032');
  await deleteTemporaryComparisonPair(client, 'KOSPI/KOSDAQ', temporaryPair.baseline, temporaryPair.addedPairId);

  await clearComparison(client);
  await chooseComparison(client, 'choose-comparison-fixed', { targetId: 'kospi' }, 'firstTarget');
  await chooseComparison(client, 'choose-comparison-fixed', { targetId: 'futures' }, 'secondTarget');
  temporaryPair = await commitComparisonPair(client, 'KOSPI/futures');
  await addComparisonAction(client, 'two-column-current', '5032');
  await deleteTemporaryComparisonPair(client, 'KOSPI/futures', temporaryPair.baseline, temporaryPair.addedPairId);

  await restoreComparisonSelection(client, selectedPairBaseline);

  const result = await snapshot(client);
  assertExactCodes(result, THIRTY_CODES.slice(0, 25));
  if (!comparisonPairsEqual(comparisonPairRows(result.state.comparison), savedPairBaseline) ||
      normalizeValue(result.state.comparison?.selectedPairId) !== selectedPairBaseline) {
    fail('Temporary comparison pairs were not removed without changing the saved baseline.',
      'COMPARISON_CLEANUP_MISMATCH', { savedPairBaseline, selectedPairBaseline, comparison: result.state.comparison });
  }
  return result;
}

async function buildThirtyFinancial(client) {
  const initial = await snapshot(client);
  assertHealthy(initial, { requireIdle: true });
  assertExactCodes(initial, THIRTY_CODES.slice(0, 25));
  let current = initial;
  if (current.state.manualFinancial != null) {
    current = await postAndWait(client, 'close-manual-financial', {},
      (value) => !value.state.isBusy && value.state.manualFinancial == null,
      'stale manual financial close');
  }
  for (let index = 0; index < FINANCIAL_ACTIONS.length; index += 1) {
    const [screen, query] = FINANCIAL_ACTIONS[index];
    const beforeLength = current.state.rows.length;
    current = await postAndWait(client, 'open-manual-financial', { screen },
      (value) => !value.state.isBusy && value.state.manualFinancial?.screen === screen,
      `manual financial ${screen} open`);
    current = await postAndWait(client, 'search-manual-financial', { query },
      (value) => !value.state.isBusy && value.state.manualFinancial?.screen === screen &&
        value.state.manualFinancial?.query === query &&
        value.state.manualFinancial?.rows?.some(row => row.stockName === query),
      `manual financial ${screen} search`);
    const exactRows = current.state.manualFinancial.rows.filter(row => row.stockName === query);
    if (exactRows.length !== 1 || !exactRows[0].resultId) {
      fail(`Manual financial ${screen}/${query} result identity is not unique.`, 'FINANCIAL_RESULT_MISMATCH', exactRows);
    }
    current = await postAndWait(client, 'activate-manual-financial-result', { resultId: exactRows[0].resultId },
      (value) => !value.state.isBusy && value.state.manualFinancial == null && value.state.rows.length === beforeLength + 1,
      `manual financial ${screen} playlist`);
    assertRowMatchesCode(current.state.rows.at(-1), THIRTY_CODES[25 + index]);
  }
  const result = current;
  assertExactCodes(result, THIRTY_CODES.slice(0, 29));
  return result;
}

async function buildThirtyManual(client) {
  const initial = await snapshot(client);
  assertHealthy(initial, { requireIdle: true });
  assertExactCodes(initial, THIRTY_CODES.slice(0, 29));
  await selectTab(client, 'index');
  const opened = await postAndWait(client, 'activate-fixed-action', { actionId: 'fixed-245' },
    (value) => !value.state.isBusy && value.state.manualLists?.isOpen === true &&
      value.state.manualLists?.screen === 'net-sell' && value.state.manualLists?.audience === 'individual' &&
      value.state.manualLists?.netRowsAreFresh === true && value.state.manualLists?.canMaterializePlaylist === true &&
      value.state.manualLists?.netRows?.length === 5,
    'manual net-sell fresh read');
  if (!opened.state.manualLists?.canMaterializePlaylist) fail('Manual net-sell list cannot materialize a playlist row.', 'MANUAL_LIST_UNAVAILABLE');
  const result = await postAndWait(client, 'add-manual-net-sell-cut', {},
    (current, before) => !current.state.isBusy && current.state.rows.length === before.state.rows.length + 1 &&
      current.state.manualLists?.isOpen === true,
    'manual net-sell cut addition');
  assertRowMatchesCode(result.state.rows.at(-1), '5025');
  await postAndWait(client, 'close-manual-list', {},
    (value) => !value.state.isBusy && value.state.manualLists?.isOpen === false,
    'manual list close');
  const final = await snapshot(client);
  assertThirtyManifest(final);
  return final;
}

async function buildPaged(client) {
  const initial = await snapshot(client);
  assertHealthy(initial, { requireIdle: true });
  if (initial.state.rows.length !== 0) fail('Paged-boundary build requires an empty in-memory playlist.', 'PLAYLIST_NOT_EMPTY');
  await addFixedAction(client, 'index', 'fixed-275', '5077');
  await addFixedAction(client, 'index', 'fixed-302', '5088');
  const result = await snapshot(client);
  assertPagedManifest(result);
  if (result.state.rows.some(row => row.pageText !== '1/20')) {
    fail('5077/5088 must both materialize with an exact 1/20 page label.',
      'PLAYLIST_PAGE_FIXTURE_MISMATCH', result.state.rows);
  }
  return result;
}

async function buildS6001AssetIndependent(client) {
  const initial = await snapshot(client);
  assertHealthy(initial, { requireIdle: true });
  if (initial.state.rows.length !== 0) {
    fail('s6001 asset-independent build requires an empty in-memory playlist.',
      'PLAYLIST_NOT_EMPTY');
  }
  await physicalSelectCatalogTab(client, 'overseas');
  for (let index = 0; index < S6001_ASSET_INDEPENDENT_ACTIONS.length; index += 1) {
    const expected = S6001_ASSET_INDEPENDENT_ACTIONS[index];
    const selector = `button[data-action-id=${js(expected.actionId)}]`;
    await client.evaluate(`(() => {
      const button = document.querySelector(${js(selector)});
      const details = button?.closest('details');
      if (details) details.open = true;
      return Boolean(button && details);
    })()`);
    const result = await physicalDoubleClickAndWait(client, selector,
      (current, before) => !current.state.isBusy &&
        current.state.rows.length === before.state.rows.length + 1,
      `s6001 physical double-click ${expected.actionId}`);
    assertS6001Row(result.state.rows.at(-1), expected, index);
  }
  const result = await snapshot(client);
  assertS6001Manifest(result);
  return result;
}

async function activateRow(client, row, expectedCode, manifestCheck) {
  manifestCheck(await snapshot(client));
  if (!ROW_ID_PATTERN.test(row.id || '')) fail('Target row is not an in-memory legacy row.', 'PLAYLIST_ID_MISMATCH', row);
  return await physicalClickAndWait(client, `#playlist-rows .playlist-data-row[data-row-id=${js(row.id)}] .playlist-row-header`,
    (current) => current.state.activeRowId === row.id,
    `row activation ${expectedCode}`, BUILD_TIMEOUT_MS);
}

async function clickPlayout(client, buttonId, predicate, description) {
  return await physicalClickAndWait(client, `#${buttonId}`,
    (current) => predicate(current), description, STATE_TIMEOUT_MS);
}

async function runAction(client, options) {
  const action = options.action;
  if (action === 'bootstrap') {
    let current = await snapshot(client);
    if (!current.state) {
      await client.evaluate(`window.chrome.webview.postMessage({ type: 'ready', payload: {} })`);
      current = await waitFor(client, (value) => Boolean(value.state), 'initial native state',
        STATE_TIMEOUT_MS, { allowMissingState: true });
    }
    assertHealthy(current, { requireIdle: true });
    if (current.state.rows.length !== 0 || current.state.currentCode != null || current.state.preparedCode != null) {
      fail('Bootstrap requires exact connected IDLE with an empty in-memory playlist.', 'BOOTSTRAP_STATE_MISMATCH', current.state);
    }
    return current;
  }
  const initial = await snapshot(client);
  assertHealthy(initial, { allowPending: action.startsWith('wait-') });

  switch (action) {
    case 'snapshot': return initial;
    case 'setup-5001-5074': return await setup5001And5074(client);
    case 'take-in-5001':
      assertTwoSceneManifest(initial);
      if (initial.state.phase !== 'idle' || initial.state.activeRowId !== initial.state.rows[0].id ||
          initial.state.currentCueIndex !== -1 || initial.buttons['playout-take-in']?.disabled ||
          initial.buttons['playout-take-in']?.hidden) {
        fail('5001 must be active in exact IDLE before TAKE IN.', 'ACTIVE_ROUTE_MISMATCH');
      }
      return await clickPlayout(client, 'playout-take-in',
        (current) => current.state.phase === 'program' && current.state.currentCode === '5001' &&
          current.state.currentCueIndex === 0 && current.state.pageIndex === 1 && current.state.pageCount === 1 &&
          current.state.nextKind === 'playlistNext' && current.state.settled &&
          current.state.refreshCount === 1 && current.state.refreshLimitReached,
        '5001 TAKE IN');
    case 'next-5074':
      assertTwoSceneManifest(initial);
      if (initial.state.phase !== 'program' || initial.state.currentCode !== '5001' ||
          initial.state.currentCueIndex !== 0 || initial.state.nextKind !== 'playlistNext') {
        fail('NEXT requires settled 5001 PROGRAM with playlistNext.', 'PLAYOUT_PHASE_MISMATCH');
      }
      return await clickPlayout(client, 'playout-next',
        (current) => current.state.phase === 'program' && current.state.currentCode === '5074' &&
          current.state.currentCueIndex === 1 && current.state.pageIndex === 1 &&
          current.state.pageCount >= 2 && current.state.pageSize === 5 &&
          current.state.itemCount >= 5 && current.state.currentPageItemCount === 5 &&
          current.state.nextKind === 'pageNext' && current.state.settled,
        'NEXT to 5074');
    case 'page-next-5074': {
      assertTwoSceneManifest(initial);
      if (initial.state.phase !== 'program' || initial.state.currentCode !== '5074' ||
          initial.state.currentCueIndex !== 1 || initial.state.pageIndex !== 1 || initial.state.nextKind !== 'pageNext') {
        fail('Page NEXT requires 5074 page 1 on PROGRAM.', 'ACTIVE_ROUTE_MISMATCH');
      }
      return await clickPlayout(client, 'playout-next',
        (current) => current.state.phase === 'program' && current.state.currentCode === '5074' &&
          current.state.currentCueIndex === 1 && current.state.pageIndex === 2 &&
          current.state.pageSize === 5 && current.state.currentPageItemCount === 5 &&
          current.state.nextKind === 'pageNext' && current.state.settled,
        '5074 Page NEXT');
    }
    case 'take-out-5074':
      assertTwoSceneManifest(initial);
      if (initial.state.phase !== 'program' || initial.state.currentCode !== '5074' || initial.state.pageIndex !== 2) {
        fail('TAKE OUT requires 5074 page 2 on PROGRAM.', 'ACTIVE_ROUTE_MISMATCH');
      }
      return await clickPlayout(client, 'playout-take-out',
        (current) => current.state.phase === 'idle' && current.state.currentCode == null &&
          current.state.preparedCode == null && current.state.currentCueIndex === -1 && current.state.settled,
        '5074 TAKE OUT');
    case 'build-30-base': return await buildThirtyBase(client);
    case 'build-30-comparison': return await buildThirtyComparison(client);
    case 'build-30-financial': return await buildThirtyFinancial(client);
    case 'build-30-manual': return await buildThirtyManual(client);
    case 'resume-30-manifest':
      assertThirtyManifest(initial);
      if (initial.state.phase !== 'idle' || initial.state.currentCode != null ||
          initial.state.preparedCode != null || initial.state.currentCueIndex !== -1) {
        fail('30-route resume requires the preserved manifest in exact IDLE.', 'PLAYOUT_PHASE_MISMATCH');
      }
      return initial;
    case 'take-out-recovery-8001': {
      if (options.routeIndex !== 19) {
        fail('take-out-recovery-8001 requires --route-index 19.', 'INVALID_ARGUMENT');
      }
      assertThirtyManifest(initial);
      const row = initial.state.rows[19];
      if (initial.state.phase !== 'program' || initial.state.currentCode !== '8001' ||
          initial.state.currentCueIndex !== 19 || initial.state.currentEntryId !== row.id ||
          !initial.state.settled || initial.state.pendingCommand != null ||
          initial.state.refreshCount !== 1 || !initial.state.refreshLimitReached) {
        fail('Recovery TAKE OUT requires known settled 8001 PROGRAM after its one authorized refresh.',
          'PLAYOUT_PHASE_MISMATCH', initial.state);
      }
      return await clickPlayout(client, 'playout-take-out',
        (current) => current.state.phase === 'idle' && current.state.currentCode == null &&
          current.state.preparedCode == null && current.state.currentCueIndex === -1 && current.state.settled,
        'known 8001 recovery TAKE OUT');
    }
    case 'take-out-recovery-5077-page2': {
      if (options.pageIndex !== 2) {
        fail('take-out-recovery-5077-page2 requires --page-index 2.', 'INVALID_ARGUMENT');
      }
      assertPagedManifest(initial);
      const row = initial.state.rows[0];
      if (initial.state.phase !== 'program' || initial.state.currentCode !== '5077' ||
          initial.state.currentCueIndex !== 0 || initial.state.currentEntryId !== row.id ||
          initial.state.activeRowId !== row.id || initial.state.pageIndex !== 2 ||
          initial.state.pageCount !== 20 || initial.state.pageSize !== 6 ||
          initial.state.itemCount !== 120 || initial.state.currentPageItemCount !== 6 ||
          !initial.state.settled || initial.state.pendingCommand != null ||
          initial.state.refreshCount !== 1 || !initial.state.refreshLimitReached) {
        fail('Recovery TAKE OUT requires known settled 5077 page 2/20 PROGRAM.',
          'PLAYOUT_PHASE_MISMATCH', initial.state);
      }
      return await clickPlayout(client, 'playout-take-out',
        (current) => current.state.phase === 'idle' && current.state.currentCode == null &&
          current.state.preparedCode == null && current.state.currentCueIndex === -1 && current.state.settled,
        'known 5077 page 2 recovery TAKE OUT');
    }
    case 'activate-30': {
      if (!Number.isInteger(options.routeIndex) || options.routeIndex < 0 || options.routeIndex >= THIRTY_CODES.length) {
        fail('activate-30 requires --route-index 0..29.', 'INVALID_ARGUMENT');
      }
      assertThirtyManifest(initial);
      if (initial.state.phase !== 'idle' || initial.state.currentCode != null || initial.state.preparedCode != null) {
        fail('Route activation requires exact IDLE.', 'PLAYOUT_PHASE_MISMATCH');
      }
      return await activateRow(client, initial.state.rows[options.routeIndex], THIRTY_CODES[options.routeIndex], assertThirtyManifest);
    }
    case 'take-in-30-fast': {
      if (!Number.isInteger(options.routeIndex) || options.routeIndex < 0 || options.routeIndex >= THIRTY_CODES.length) {
        fail('take-in-30-fast requires --route-index 0..29.', 'INVALID_ARGUMENT');
      }
      assertThirtyManifest(initial);
      const code = THIRTY_CODES[options.routeIndex];
      const row = initial.state.rows[options.routeIndex];
      if (initial.state.activeRowId !== row.id || initial.state.phase !== 'idle' ||
          initial.state.currentCueIndex !== -1 || initial.buttons['playout-take-in']?.disabled ||
          initial.buttons['playout-take-in']?.hidden) {
        fail(`TAKE IN requires active ${code} in exact IDLE.`, 'ACTIVE_ROUTE_MISMATCH');
      }
      return await clickPlayout(client, 'playout-take-in',
        (current) => current.state.phase === 'program' && current.state.currentCode === code &&
          current.state.currentCueIndex === options.routeIndex && current.state.currentEntryId === row.id &&
          current.state.pageIndex === 1,
        `${code} fast TAKE IN`);
    }
    case 'wait-30': {
      if (!Number.isInteger(options.routeIndex) || options.routeIndex < 0 || options.routeIndex >= THIRTY_CODES.length) {
        fail('wait-30 requires --route-index 0..29.', 'INVALID_ARGUMENT');
      }
      assertThirtyManifest(initial);
      const code = THIRTY_CODES[options.routeIndex];
      const row = initial.state.rows[options.routeIndex];
      return await waitFor(client,
        (current) => current.state.phase === 'program' && current.state.currentCode === code &&
          current.state.currentCueIndex === options.routeIndex && current.state.currentEntryId === row.id &&
          current.state.settled,
        `${code} settled PROGRAM`);
    }
    case 'take-out-30': {
      if (!Number.isInteger(options.routeIndex) || options.routeIndex < 0 || options.routeIndex >= THIRTY_CODES.length) {
        fail('take-out-30 requires --route-index 0..29.', 'INVALID_ARGUMENT');
      }
      assertThirtyManifest(initial);
      const code = THIRTY_CODES[options.routeIndex];
      if (initial.state.phase !== 'program' || initial.state.currentCode !== code ||
          initial.state.currentCueIndex !== options.routeIndex || !initial.state.settled) {
        fail(`TAKE OUT requires settled ${code} PROGRAM.`, 'PLAYOUT_PHASE_MISMATCH');
      }
      return await clickPlayout(client, 'playout-take-out',
        (current) => current.state.phase === 'idle' && current.state.currentCode == null &&
          current.state.preparedCode == null && current.state.currentCueIndex === -1 && current.state.settled,
        `${code} TAKE OUT`);
    }
    case 'build-paged': return await buildPaged(client);
    case 'build-s6001-asset-independent': return await buildS6001AssetIndependent(client);
    case 'activate-s6001': {
      if (!Number.isInteger(options.routeIndex) || options.routeIndex < 0 ||
          options.routeIndex >= S6001_ASSET_INDEPENDENT_ACTIONS.length) {
        fail('activate-s6001 requires --route-index 0..3.', 'INVALID_ARGUMENT');
      }
      assertS6001Manifest(initial);
      if (initial.state.phase !== 'idle' || initial.state.currentCode != null ||
          initial.state.preparedCode != null) {
        fail('s6001 route activation requires exact IDLE.', 'PLAYOUT_PHASE_MISMATCH');
      }
      return await activateRow(
        client,
        initial.state.rows[options.routeIndex],
        '6001',
        assertS6001Manifest);
    }
    case 'take-in-s6001': {
      if (!Number.isInteger(options.routeIndex) || options.routeIndex < 0 ||
          options.routeIndex >= S6001_ASSET_INDEPENDENT_ACTIONS.length) {
        fail('take-in-s6001 requires --route-index 0..3.', 'INVALID_ARGUMENT');
      }
      assertS6001Manifest(initial);
      const row = initial.state.rows[options.routeIndex];
      if (initial.state.activeRowId !== row.id || initial.state.phase !== 'idle' ||
          initial.state.currentCueIndex !== -1 || initial.buttons['playout-take-in']?.disabled ||
          initial.buttons['playout-take-in']?.hidden) {
        fail('s6001 TAKE IN requires its selected row in exact IDLE.', 'ACTIVE_ROUTE_MISMATCH');
      }
      return await clickPlayout(client, 'playout-take-in',
        (current) => current.state.phase === 'program' && current.state.currentCode === '6001' &&
          current.state.currentCueIndex === options.routeIndex &&
          current.state.currentEntryId === row.id && current.state.pageIndex === 1,
        `${S6001_ASSET_INDEPENDENT_ACTIONS[options.routeIndex].subtype} s6001 TAKE IN`);
    }
    case 'wait-s6001': {
      if (!Number.isInteger(options.routeIndex) || options.routeIndex < 0 ||
          options.routeIndex >= S6001_ASSET_INDEPENDENT_ACTIONS.length) {
        fail('wait-s6001 requires --route-index 0..3.', 'INVALID_ARGUMENT');
      }
      assertS6001Manifest(initial);
      const row = initial.state.rows[options.routeIndex];
      return await waitFor(client,
        (current) => current.state.phase === 'program' && current.state.currentCode === '6001' &&
          current.state.currentCueIndex === options.routeIndex &&
          current.state.currentEntryId === row.id && current.state.settled,
        `${S6001_ASSET_INDEPENDENT_ACTIONS[options.routeIndex].subtype} s6001 settled PROGRAM`);
    }
    case 'take-out-s6001': {
      if (!Number.isInteger(options.routeIndex) || options.routeIndex < 0 ||
          options.routeIndex >= S6001_ASSET_INDEPENDENT_ACTIONS.length) {
        fail('take-out-s6001 requires --route-index 0..3.', 'INVALID_ARGUMENT');
      }
      assertS6001Manifest(initial);
      if (initial.state.phase !== 'program' || initial.state.currentCode !== '6001' ||
          initial.state.currentCueIndex !== options.routeIndex || !initial.state.settled) {
        fail('s6001 TAKE OUT requires its settled PROGRAM row.', 'PLAYOUT_PHASE_MISMATCH');
      }
      return await clickPlayout(client, 'playout-take-out',
        (current) => current.state.phase === 'idle' && current.state.currentCode == null &&
          current.state.preparedCode == null && current.state.currentCueIndex === -1 && current.state.settled,
        `${S6001_ASSET_INDEPENDENT_ACTIONS[options.routeIndex].subtype} s6001 TAKE OUT`);
    }
    case 'take-in-5077':
      assertPagedManifest(initial);
      if (initial.state.phase !== 'idle' || initial.state.activeRowId !== initial.state.rows[0].id) {
        fail('5077 must be active in exact IDLE.', 'ACTIVE_ROUTE_MISMATCH');
      }
      return await clickPlayout(client, 'playout-take-in',
        (current) => current.state.phase === 'program' && current.state.currentCode === '5077' &&
          current.state.currentCueIndex === 0 && current.state.pageIndex === 1 && current.state.pageCount === 20 &&
          current.state.pageSize === 6 && current.state.itemCount === 120 &&
          current.state.currentPageItemCount === 6 && current.state.nextKind === 'pageNext' && current.state.settled,
        '5077 TAKE IN');
    case 'page-next-5077': {
      if (!Number.isInteger(options.pageIndex) || options.pageIndex < 1 || options.pageIndex > 19) {
        fail('page-next-5077 requires --page-index 1..19.', 'INVALID_ARGUMENT');
      }
      assertPagedManifest(initial);
      if (initial.state.currentCode !== '5077' || initial.state.pageIndex !== options.pageIndex ||
          initial.state.pageCount !== 20 || initial.state.phase !== 'program' || initial.state.nextKind !== 'pageNext') {
        fail(`Page NEXT ${options.pageIndex} requires 5077 page ${options.pageIndex}/20 on PROGRAM.`, 'ACTIVE_ROUTE_MISMATCH');
      }
      return await clickPlayout(client, 'playout-next',
        (current) => current.state.phase === 'program' && current.state.currentCode === '5077' &&
          current.state.currentCueIndex === 0 && current.state.pageIndex === options.pageIndex + 1 &&
          current.state.pageCount === 20 && current.state.pageSize === 6 && current.state.itemCount === 120 &&
          current.state.currentPageItemCount === 6 && current.state.settled,
        `5077 Page NEXT ${options.pageIndex}/20 -> ${options.pageIndex + 1}/20`);
    }
    case 'next-5088':
      assertPagedManifest(initial);
      if (initial.state.phase !== 'program' || initial.state.currentCode !== '5077' ||
          initial.state.pageIndex !== 20 || initial.state.pageCount !== 20 || initial.state.nextKind !== 'playlistNext') {
        fail('NEXT requires 5077 page 20/20 and playlistNext.', 'ACTIVE_ROUTE_MISMATCH');
      }
      return await clickPlayout(client, 'playout-next',
        (current) => current.state.phase === 'program' && current.state.currentCode === '5088' &&
          current.state.currentCueIndex === 1 && current.state.pageIndex === 1 && current.state.pageCount === 20 &&
          current.state.pageSize === 12 && current.state.itemCount === 240 &&
          current.state.currentPageItemCount === 12 && current.state.settled,
        'NEXT 5077 page 20 to 5088 page 1');
    case 'take-out-5088':
      assertPagedManifest(initial);
      if (initial.state.phase !== 'program' || initial.state.currentCode !== '5088' ||
          initial.state.currentCueIndex !== 1) {
        fail('TAKE OUT requires 5088 on PROGRAM.', 'ACTIVE_ROUTE_MISMATCH');
      }
      return await clickPlayout(client, 'playout-take-out',
        (current) => current.state.phase === 'idle' && current.state.currentCode == null &&
          current.state.preparedCode == null && current.state.currentCueIndex === -1 && current.state.settled,
        '5088 TAKE OUT');
    default: fail(`Unknown action: ${action}`, 'INVALID_ARGUMENT');
  }
}

const options = parseArguments(process.argv);
let client = null;
let target = null;
try {
  target = await openTarget(options.port);
  client = new CdpClient(target.webSocketDebuggerUrl);
  await client.connect();
  await client.send('Runtime.enable');
  const probe = await installOrVerifyProbe(client);
  const result = await runAction(client, options);
  process.stdout.write(`${JSON.stringify({
    ok: true,
    action: options.action,
    routeIndex: options.routeIndex,
    pageIndex: options.pageIndex,
    target: { id: target.id, title: target.title, url: target.url },
    probe,
    result,
    completedAtUtc: new Date().toISOString(),
  })}\n`);
} catch (error) {
  let finalSnapshot = null;
  if (client) {
    try { finalSnapshot = await snapshot(client); } catch { /* preserve the original failure */ }
  }
  process.stderr.write(`${JSON.stringify({
    ok: false,
    action: options.action,
    routeIndex: options.routeIndex,
    pageIndex: options.pageIndex,
    code: error.code || 'STAGE_FAILED',
    message: error.message,
    details: error.details,
    target: target ? { id: target.id, title: target.title, url: target.url } : null,
    finalSnapshot,
    failedAtUtc: new Date().toISOString(),
  })}\n`);
  process.exitCode = 1;
} finally {
  client?.close();
}
