(function (root, factory) {
  "use strict";

  const api = Object.freeze(factory());
  if (typeof module === "object" && module.exports) module.exports = api;
  if (root) root.MbnLegacyFixedWorkflow = api;
})(typeof globalThis === "object" ? globalThis : this, function () {
  "use strict";

  const DEFAULT_FADE_DURATION = 6;
  const identifierPattern = /^[A-Za-z0-9_-]{1,128}$/;
  const controlCharacterPattern = /[\u0000-\u001f\u007f]/;
  const datePattern = /^\d{4}-\d{2}-\d{2}$/;
  const fixedMarkets = Object.freeze(["overseas", "exchange", "index"]);
  const runtimeAssets = Object.freeze({
    overseas: Object.freeze({
      relativePath: "bin/Debug/Res/해외.ini",
      sha256: "DBDC4EAC28BED9FD0DEABED7FE880F21CB49E48503FD3CCB95C0A29128818B2A"
    }),
    exchange: Object.freeze({
      relativePath: "bin/Debug/Res/환율.ini",
      sha256: "7A99F8DFB5DD8D8DB514605F232A0A7973545CA6A4A46ABE2D102ADD025B586C"
    }),
    index: Object.freeze({
      relativePath: "bin/Debug/Res/지수.ini",
      sha256: "E90ED317FD3ECC1A716AD17DD9BC21FE8B838574E1C20854BD0A886A96BA17E6"
    })
  });

  const rawActions = [];

  function cloneSelection(value) {
    return {
      groupCode: String(value.groupCode || ""),
      subject: String(value.subject || ""),
      graphicType: String(value.graphicType || ""),
      subtype: String(value.subtype || ""),
      dataCode: String(value.dataCode || "")
    };
  }

  function addAction(market, section, label, definition) {
    const ordinal = rawActions.length + 1;
    rawActions.push({
      id: `fixed-${String(ordinal).padStart(3, "0")}`,
      market,
      section,
      label,
      detail: definition.detail || section,
      category: definition.category || "market",
      builderKey: definition.builderKey,
      code: definition.code,
      aliases: definition.aliases || [definition.code],
      available: definition.available !== false,
      prerequisite: definition.prerequisite || "",
      selection: cloneSelection(definition.selection || {}),
      dynamicDate: definition.dynamicDate === true,
      dynamicNxtSession: definition.dynamicNxtSession === true
    });
  }

  const foreignIndices = [
    ["다우", "Dow"], ["나스닥", "Nasdaq"], ["S&P", "Sp500"],
    ["독일", "GermanyDax"], ["영국", "UnitedKingdomFtse"], ["프랑스", "FranceCac"],
    ["니케이", "Nikkei"], ["중국 상해", "ShanghaiComposite"], ["홍콩 항셍", "HangSeng"],
    ["대만 가권", "TaiwanWeighted"], ["싱가포르 지수", "SingaporeStraitsTimes"],
    ["태국 지수", "ThailandSet"], ["필리핀 지수", "PhilippinesComposite"],
    ["말레이시아 지수", "MalaysiaKlse"], ["인도네시아 지수", "IndonesiaComposite"]
  ];
  const commodities = [
    ["소맥", "FeedWheat"], ["옥수수", "Corn"], ["밀", "Wheat"], ["대두", "Soybean"],
    ["콩", "Bean"], ["현미", "BrownRice"], ["커피", "Coffee"], ["코코아", "Cocoa"],
    ["설탕", "Sugar"], ["생우(소)", "LiveCattle"], ["비육우", "FeederCattle"],
    ["돈육(돼지)", "LeanHogs"], ["구리", "Copper"], ["철광석", "IronOre"],
    ["니켈", "Nickel"], ["천연가스", "NaturalGas"], ["백금", "Platinum"],
    ["팔라듐", "Palladium"], ["납", "Lead"], ["아연", "Zinc"], ["주석", "Tin"],
    ["원면", "RawCotton"], ["목화(면화)", "Cotton"],
    ["국제 금", "InternationalGold"], ["국제 은", "InternationalSilver"],
    ["국내 금", "DomesticGold"], ["국내 은", "DomesticSilver"]
  ];

  for (const [label, subject] of foreignIndices) {
    addAction("overseas", "1열판기본", label, {
      builderKey: "s5001", code: "5001", category: "plate",
      selection: { groupCode: "FOREIGN_INDEX", subject, graphicType: "1열판기본", subtype: "CURRENT" }
    });
  }
  addAction("overseas", "1열판기본", "BDI 지수", {
    builderKey: "s5001", code: "5001", category: "plate",
    selection: { groupCode: "FOREIGN_INDEX", subject: "BALTIC_DRY_INDEX", graphicType: "1열판기본", subtype: "CURRENT" }
  });
  for (const [label, subject] of commodities) {
    addAction("overseas", "1열판기본", label, {
      builderKey: "s5001", code: "5001", category: "plate",
      selection: { groupCode: "COMMODITY", subject, graphicType: "1열판기본", subtype: "CURRENT" }
    });
  }

  [
    ["농산물[원면, 목화(면화)]", "Cotton"],
    ["원자재[국제 금, 국제 은]", "InternationalPreciousMetals"],
    ["원자재[국내 금, 국내 은]", "DomesticPreciousMetals"]
  ].forEach(([label, subtype]) => addAction("overseas", "2열판-", label, {
    builderKey: "s50160", code: "50160", category: "plate",
    selection: { groupCode: "ALL", subject: "INDEX", graphicType: "2열판-", subtype }
  }));

  [
    ["미국 지수[다우, 나스닥, S&P]", "UnitedStatesIndices"],
    ["중화권 지수[홍콩항셍, 상해종합, 대만]", "GreaterChinaIndices"],
    ["유럽권 지수[영국, 프랑스, 독일]", "EuropeanIndices"],
    ["아시아 지수[상해, 대만, 일본]", "AsianIndices"],
    ["미국국채[2년, 3년, 5년]", "UnitedStatesTreasuries"],
    ["채권금리(CD,CP,콜금리)", "BondYields"],
    ["유가[두바이, 브렌트유, WTI]", "Oil"],
    ["광물[금, 은, 구리]", "Minerals"],
    ["식자재[대두, 옥수수, 밀]", "FoodMaterials"],
    ["농산물[소맥, 옥수수, 밀]", "AgricultureWheatCorn"],
    ["농산물[대두, 콩, 현미]", "AgricultureSoyRice"],
    ["농산물[커피, 코코아, 설탕]", "AgricultureCoffeeCocoaSugar"],
    ["농산물[생우(소), 비육우, 돈육(돼지)]", "AgricultureLivestock"],
    ["원자재[구리, 철광석, 니켈]", "RawMaterialsCopperIronNickel"],
    ["원자재[천연가스, 백금, 팔라듐]", "RawMaterialsGasPlatinumPalladium"],
    ["원자재[납, 아연, 주석]", "RawMaterialsLeadZincTin"]
  ].forEach(([label, subtype]) => addAction("overseas", "3열판", label, {
    builderKey: "s5016", code: "5016", category: "plate",
    selection: { groupCode: "ALL", subject: "INDEX", graphicType: "3열판", subtype }
  }));

  [
    ["글로벌 증시 지도", "8067"], ["아시아 증시 지도", "5072"],
    ["미국 증시 지도", "5068"], ["유럽 증시 지도", "5070"]
  ].forEach(([label, code]) => addAction("overseas", "글로벌 증시 지도", label, {
    builderKey: "s8067", code, aliases: [code],
    selection: { groupCode: "ALL", subject: "INDEX", graphicType: "WORLD_MAP", subtype: code }
  }));

  [
    ["미국 다우 지수", "DowJones"], ["미국 나스닥 지수", "Nasdaq"],
    ["미국 S&P 지수", "StandardAndPoor500"], ["중국 상해 지수", "China"],
    ["일본 닛케이 지수", "Japan"], ["유가 두바이 지수", "DubaiCrude"],
    ["유가 WTI 지수", "WtiCrude"], ["유가 브렌트유 지수", "BrentCrude"],
    ["금 지수", "Gold"]
  ].forEach(([label, subtype]) => addAction("overseas", "이미지 그래프", label, {
    builderKey: "s6001", code: "6001",
    selection: { groupCode: "FOREIGN_INDEX", subject: label, graphicType: "IMAGE_GRAPH", subtype }
  }));

  [["다우", "DOW"], ["나스닥", "NASDAQ"], ["S&P", "S&P500"]]
    .forEach(([label, subject]) => addAction("overseas", "미국업종", label, {
      builderKey: "s5078", code: "5078", category: "chart",
      selection: { groupCode: "FOREIGN_INDEX", subject, graphicType: "US_SECTOR_INDEX", subtype: "" }
    }));

  const exchangeTargets = [
    ["원달러", "WonDollar"], ["원엔", "WonYen"],
    ["원위엔", "WonYuan"], ["원유로", "WonEuro"]
  ];
  for (const [label, subject] of exchangeTargets) {
    addAction("exchange", "1열판기본", label, {
      builderKey: "s5001", code: "5001", category: "plate",
      selection: { groupCode: "EXCHANGE_RATE", subject, graphicType: "1열판기본", subtype: "CURRENT" }
    });
  }
  [
    ["해외환율[엔달러, 위안화달러, 유로달러]", "OverseasExchangeRates"],
    ["환율[원달러, 원엔, 원유로]", "DomesticExchangeRates"]
  ].forEach(([label, subtype]) => addAction("exchange", "3열판", label, {
    builderKey: "s5016", code: "5016", category: "plate",
    selection: { groupCode: "ALL", subject: "INDEX", graphicType: "3열판", subtype }
  }));

  const yieldPeriods = [
    ["5일", "FiveDays"], ["20일", "TwentyDays"], ["60일", "SixtyDays"],
    ["120일", "OneHundredTwentyDays"], ["240일", "TwoHundredFortyDays"]
  ];
  for (const [label, subject] of exchangeTargets) {
    for (const [periodLabel, subtype] of yieldPeriods) {
      addAction("exchange", "수익률 그래프", `${label}_${periodLabel}`, {
        builderKey: "s5086", code: "5086", category: "chart",
        selection: { groupCode: "EXCHANGE_RATE", subject, graphicType: "YIELD_GRAPH", subtype }
      });
    }
  }
  for (const [label, subject] of exchangeTargets) {
    for (const [periodLabel, subtype] of yieldPeriods) {
      addAction("exchange", "라인 그래프", `${label}_${periodLabel}`, {
        builderKey: "s50860", code: "50860", category: "chart",
        selection: { groupCode: "EXCHANGE_RATE", subject, graphicType: "LINE_GRAPH", subtype }
      });
    }
  }

  [
    ["3열판_주요지수[코스피, 코스닥, 코스피200]", "MajorKospiKosdaqKospi200"],
    ["3열판_주요지수[코스피, 코스닥, 선물]", "MajorKospiKosdaqFutures"],
    ["3열판_예상체결지수[코스피, 코스닥, 코스피200]", "ExpectedKospiKosdaqKospi200"],
    ["3열판_예상체결지수[코스피, 코스닥, 선물]", "ExpectedKospiKosdaqFutures"]
  ].forEach(([label, subtype]) => addAction("index", "주요지수", label, {
    builderKey: "s5016", code: "5016", category: "plate",
    selection: { groupCode: "ALL", subject: "INDEX", graphicType: "3열판", subtype }
  }));

  const candlePeriods = [
    ["일봉", "8035"], ["5일", "8061"], ["20일", "8040"],
    ["60일", "8046"], ["120일", "8051"], ["240일", "8056"]
  ];
  function addIndexMarket(section, groupCode, candleGroup, options) {
    addAction("index", section, "1열판기본_예상지수", {
      builderKey: "s5001", code: "5001", category: "plate",
      selection: { groupCode, subject: "INDEX", graphicType: "1열판기본", subtype: "EXPECTED_INDEX" }
    });
    addAction("index", section, "1열판기본_현재지수", {
      builderKey: "s5001", code: "5001", category: "plate",
      selection: { groupCode, subject: "INDEX", graphicType: "1열판기본", subtype: "CURRENT" }
    });
    if (options.yield) {
      for (const [periodLabel, subtype] of yieldPeriods) {
        addAction("index", section, `수익률그래프_${periodLabel}`, {
          builderKey: "s5086", code: "5086", category: "chart",
          selection: { groupCode, subject: "INDEX", graphicType: "YIELD_GRAPH", subtype }
        });
      }
    }
    for (const [periodLabel, code] of candlePeriods) {
      addAction("index", section, `캔들그래프_${periodLabel}`, {
        builderKey: "s8010", code, category: "chart",
        selection: { groupCode: candleGroup, subject: "INDEX", graphicType: "", subtype: "PRICE" }
      });
    }
    if (options.volume) {
      for (const [periodLabel, code] of candlePeriods) {
        addAction("index", section, `캔들그래프(거래량)_${periodLabel}`, {
          builderKey: "s8010", code, category: "chart",
          selection: { groupCode: candleGroup, subject: "INDEX", graphicType: "", subtype: "VOLUME" }
        });
      }
    }
    if (options.expected) {
      for (const [periodLabel, code] of candlePeriods.slice(1)) {
        addAction("index", section, `캔들그래프(예상지수)_${periodLabel}`, {
          builderKey: "s8010", code, category: "chart",
          selection: { groupCode: candleGroup, subject: "INDEX", graphicType: "", subtype: "EXPECTED" }
        });
      }
    }
  }
  addIndexMarket("코스피", "KOSPI", "KOSPI_INDEX", { yield: true, volume: true, expected: true });
  addIndexMarket("코스닥", "KOSDAQ", "KOSDAQ_INDEX", { yield: true, volume: true, expected: true });
  addIndexMarket("코스피200", "KOSPI200", "KOSPI200_INDEX", { yield: true, volume: true, expected: true });
  addIndexMarket("선물", "FUTURES", "FUTURES_INDEX", { yield: false, volume: false, expected: true });
  addIndexMarket("KRX100", "KRX100", "KRX100_INDEX", { yield: true, volume: true, expected: false });

  function trend(label, builderKey, code, groupCode, graphicType, subtype, available = true) {
    addAction("index", "매매동향", label, {
      builderKey, code, available, category: "market",
      prerequisite: available ? "" : "원본 수동 순매도 데이터 파일과 검증된 입력 화면이 필요합니다.",
      selection: { groupCode, subject: "INDEX", graphicType, subtype }
    });
  }
  trend("주체별 매매동향_표그래프", "s5082", "5082", "ALL", "TRADING_BY_PARTICIPANT", "TABLE_GRAPH");
  trend("코스피 매매동향_표그래프", "s5023", "5023", "KOSPI", "KOSPI_TRADING_TREND", "TABLE_GRAPH");
  trend("코스피 매매동향_라인그래프", "s5084", "5084", "KOSPI", "KOSPI_TRADING_TREND", "LINE_GRAPH");
  trend("코스피 매매동향_막대그래프", "s5024", "5024", "KOSPI", "KOSPI_TRADING_TREND", "BAR_GRAPH");
  trend("코스닥 매매동향_표그래프", "s5023", "5023", "KOSDAQ", "KOSDAQ_TRADING_TREND", "TABLE_GRAPH");
  trend("코스닥 매매동향_라인그래프", "s5084", "5084", "KOSDAQ", "KOSDAQ_TRADING_TREND", "LINE_GRAPH");
  trend("코스닥 매매동향_막대그래프", "s5024", "5024", "KOSDAQ", "KOSDAQ_TRADING_TREND", "BAR_GRAPH");
  trend("개인 매매동향_라인그래프", "s5083", "5083", "ALL", "INDIVIDUAL_TRADING_TREND", "LINE_GRAPH");
  trend("외국인 매매동향_라인그래프", "s5083", "5083", "ALL", "FOREIGN_TRADING_TREND", "LINE_GRAPH");
  trend("기관 매매동향_라인그래프", "s5083", "5083", "ALL", "INSTITUTION_TRADING_TREND", "LINE_GRAPH");
  trend("기관 순매수 현황_표그래프", "s6067", "6067", "ALL", "INSTITUTION_NET_BUY", "TABLE_GRAPH");
  trend("프로그램 매매_표그래프", "s5085", "5085", "ALL", "PROGRAM_TRADING", "TABLE_GRAPH");
  trend("개인 순매도 상위(수동)", "s5025", "5025", "INDIVIDUAL", "MANUAL_NET_SELL", "MANUAL_NET_SELL", false);
  trend("외국인 순매도 상위(수동)", "s5025", "5025", "FOREIGN", "MANUAL_NET_SELL", "MANUAL_NET_SELL", false);
  trend("기관 순매도 상위(수동)", "s5025", "5025", "INSTITUTION", "MANUAL_NET_SELL", "MANUAL_NET_SELL", false);

  const fiveRows = [
    "코스피 시가총액", "코스피 예상가 시가총액 상위", "코스피 상승률 상위", "코스피 하락률 상위",
    "코스피 시간외 상승률 상위", "코스피 시간외 하락률 상위",
    "코스닥 시가총액", "코스닥 예상가 시가총액 상위", "코스닥 상승률 상위", "코스닥 하락률 상위",
    "코스닥 시간외 상승률 상위", "코스닥 시간외 하락률 상위", "코스피 200 종목", "ETF 종목", "VI 발동(수동)",
    "코스피_NXT 시가총액", "코스피_NXT 상승률 상위", "코스피_NXT 하락률 상위",
    "코스피_NXT 시간외 상승률 상위", "코스피_NXT 시간외 하락률 상위", "코스피_NXT 거래량 상위",
    "코스닥_NXT 시가총액", "코스닥_NXT 상승률 상위", "코스닥_NXT 하락률 상위",
    "코스닥_NXT 시간외 상승률 상위", "코스닥_NXT 시간외 하락률 상위", "코스닥_NXT 거래량 상위"
  ];
  const sixAndTwelveRows = [
    "코스피 시가총액", "코스피 예상가 시가총액 상위", "코스피 상승률 상위", "코스피 하락률 상위",
    "코스피 52주 신고가", "코스피 52주 신저가", "코스피 시간외 상승률 상위", "코스피 시간외 하락률 상위",
    "코스닥 시가총액", "코스닥 예상가 시가총액 상위", "코스닥 상승률 상위", "코스닥 하락률 상위",
    "코스닥 52주 신고가", "코스닥 52주 신저가", "코스닥 시간외 상승률 상위", "코스닥 시간외 하락률 상위",
    "코스피 200 종목", "ETF 종목", "VI 발동(수동)",
    "코스피_NXT 시가총액", "코스피_NXT 상승률 상위", "코스피_NXT 하락률 상위", "코스피_NXT 거래량 상위",
    "코스닥_NXT 시가총액", "코스닥_NXT 상승률 상위", "코스닥_NXT 하락률 상위", "코스닥_NXT 거래량 상위"
  ];

  function pagedDefinition(label, builderKey, code) {
    if (label === "VI 발동(수동)") {
      return {
        // VILIST removes the placeholder row and always creates the legacy
        // five-row outcome, even when the placeholder came from the 6/12 section.
        builderKey: "s5074", code: "5074", available: false, category: "plate",
        prerequisite: "검증된 VI 종목 목록 입력 화면이 필요합니다.",
        selection: { groupCode: "PAGED_VI", subject: "", graphicType: "INPUT_ORDER", subtype: "CURRENT" }
      };
    }
    let groupCode;
    if (label.startsWith("코스피_NXT")) groupCode = "PAGED_NXT_KOSPI";
    else if (label.startsWith("코스닥_NXT")) groupCode = "PAGED_NXT_KOSDAQ";
    else if (label.startsWith("코스닥")) groupCode = "PAGED_DOMESTIC_KOSDAQ";
    else groupCode = "PAGED_DOMESTIC_KOSPI";

    let subtype;
    if (label.includes("예상가 시가총액")) subtype = "EXPECTED_MARKET_CAP";
    else if (label.includes("시가총액")) subtype = "MARKET_CAP";
    else if (label.includes("시간외 상승률")) subtype = "AFTER_HOURS_GAIN_RATE";
    else if (label.includes("시간외 하락률")) subtype = "AFTER_HOURS_LOSS_RATE";
    else if (label.includes("상승률")) subtype = "GAIN_RATE";
    else if (label.includes("하락률")) subtype = "LOSS_RATE";
    else if (label.includes("52주 신고가")) subtype = "52_WEEK_HIGH";
    else if (label.includes("52주 신저가")) subtype = "52_WEEK_LOW";
    else if (label.includes("200 종목")) subtype = "KOSPI200";
    else if (label.startsWith("ETF")) subtype = "ETF";
    else if (label.includes("거래량")) subtype = "VOLUME";
    else throw new Error(`Unknown paged legacy action: ${label}`);

    const dynamicNxtSession = groupCode.startsWith("PAGED_NXT_");
    return {
      builderKey, code, category: "plate",
      dynamicDate: !dynamicNxtSession,
      dynamicNxtSession,
      selection: {
        groupCode,
        subject: "INDEX",
        graphicType: dynamicNxtSession ? "$NXT_SESSION" : "",
        subtype,
        dataCode: dynamicNxtSession ? "" : "$TODAY"
      }
    };
  }

  function addPagedSection(section, labels, builderKey, code) {
    for (const label of labels) addAction("index", section, label, pagedDefinition(label, builderKey, code));
  }
  addPagedSection("5단 표그래프", fiveRows, "s5074", "5074");
  addPagedSection("6종목 현재가", sixAndTwelveRows, "s5077", "5077");
  addPagedSection("12종목 현재가", sixAndTwelveRows, "s5088", "5088");

  const actions = Object.freeze(rawActions.map(value => Object.freeze({
    ...value,
    aliases: Object.freeze([...value.aliases]),
    selection: Object.freeze({ ...value.selection })
  })));
  if (actions.length !== 328) throw new Error("The three fixed legacy tabs must contain exactly 328 actions.");
  if (actions.filter(action => action.available).length !== 322) {
    throw new Error("Exactly six fixed-tab actions must remain trusted manual prerequisites.");
  }
  const byId = new Map(actions.map(action => [action.id, action]));

  function localDateText(now = new Date()) {
    const year = String(now.getFullYear()).padStart(4, "0");
    const month = String(now.getMonth() + 1).padStart(2, "0");
    const day = String(now.getDate()).padStart(2, "0");
    return `${year}-${month}-${day}`;
  }

  function currentNxtSession(now = new Date()) {
    return now.getHours() <= 12 ? "PRE_MARKET" : "AFTER_MARKET";
  }

  function materializeSelection(action, now, movingAverages) {
    const selection = cloneSelection(action.selection);
    if (action.dynamicDate) selection.dataCode = localDateText(now);
    if (action.dynamicNxtSession) selection.graphicType = currentNxtSession(now);
    if (action.builderKey === "s8010") {
      const flags = [];
      if (movingAverages.ma5) flags.push("MA5");
      if (movingAverages.ma20) flags.push("MA20");
      selection.graphicType = flags.join(",");
    }
    return selection;
  }

  function requireOptions(options) {
    if (!options || typeof options !== "object" || Array.isArray(options)) {
      throw new TypeError("Fixed playlist options are required.");
    }
    const id = typeof options.id === "string" ? options.id.trim() : "";
    if (!identifierPattern.test(id)) throw new TypeError("A safe fixed playlist entry id is required.");
    const fadeDuration = options.fadeDuration === undefined ? DEFAULT_FADE_DURATION : Number(options.fadeDuration);
    if (!Number.isInteger(fadeDuration) || fadeDuration < 0 || fadeDuration > 60) {
      throw new RangeError("Fade duration must be an integer from 0 through 60.");
    }
    const now = options.now instanceof Date && !Number.isNaN(options.now.valueOf()) ? options.now : new Date();
    const ma5 = options.ma5 === undefined ? false : options.ma5;
    const ma20 = options.ma20 === undefined ? false : options.ma20;
    if (typeof ma5 !== "boolean" || typeof ma20 !== "boolean") {
      throw new TypeError("Moving-average flags must be boolean values.");
    }
    return { id, fadeDuration, now, ma5, ma20 };
  }

  function createFixedPlaylistEntry(actionId, options) {
    const action = typeof actionId === "string" ? byId.get(actionId) : null;
    if (!action) throw new RangeError("The selected fixed legacy action is unknown.");
    if (!action.available) throw new RangeError(action.prerequisite || "The selected action requires trusted manual data.");
    const normalized = requireOptions(options);
    const movingAverages = Object.freeze({
      ma5: action.builderKey === "s8010" && normalized.ma5,
      ma20: action.builderKey === "s8010" && normalized.ma20
    });
    const selection = Object.freeze(materializeSelection(action, normalized.now, movingAverages));
    return {
      id: normalized.id,
      builderKey: action.builderKey,
      code: action.code,
      aliases: [...action.aliases],
      title: action.label,
      detail: `${action.section} · ${action.label}`,
      category: action.category,
      market: action.market,
      selection,
      enabled: true,
      fadeDuration: normalized.fadeDuration,
      graphicType: selection.graphicType,
      subtype: selection.subtype,
      operator: Object.freeze({
        source: "legacy-fixed-workflow",
        actionId: action.id,
        legacyTab: action.market,
        legacySection: action.section,
        legacyLabel: action.label,
        assetPath: runtimeAssets[action.market].relativePath,
        assetSha256: runtimeAssets[action.market].sha256,
        movingAverages
      })
    };
  }

  function sameStringArray(left, right) {
    return Array.isArray(left) && Array.isArray(right) &&
      left.length === right.length && left.every((value, index) => value === right[index]);
  }

  function normalizeStoredMovingAverages(value, action) {
    const stored = value?.operator?.movingAverages;
    if (stored !== undefined) {
      if (!stored || typeof stored !== "object" || Array.isArray(stored) ||
          typeof stored.ma5 !== "boolean" || typeof stored.ma20 !== "boolean" ||
          Object.keys(stored).length !== 2) return null;
      if (action.builderKey !== "s8010" && (stored.ma5 || stored.ma20)) return null;
      return Object.freeze({ ma5: stored.ma5, ma20: stored.ma20 });
    }
    if (action.builderKey !== "s8010") return Object.freeze({ ma5: false, ma20: false });
    const graphicType = value?.selection?.graphicType;
    if (!["", "MA5", "MA20", "MA5,MA20"].includes(graphicType)) return null;
    return Object.freeze({
      ma5: graphicType === "MA5" || graphicType === "MA5,MA20",
      ma20: graphicType === "MA20" || graphicType === "MA5,MA20"
    });
  }

  function storedSelectionMatches(value, action, movingAverages) {
    if (!value || typeof value !== "object" || Array.isArray(value)) return false;
    for (const key of ["groupCode", "subject", "graphicType", "subtype", "dataCode"]) {
      const text = value[key];
      if (typeof text !== "string" || text.length > 256 || controlCharacterPattern.test(text)) return false;
      if (action.dynamicDate && key === "dataCode") {
        if (!datePattern.test(text)) return false;
      } else if (action.dynamicNxtSession && key === "graphicType") {
        if (!(["PRE_MARKET", "AFTER_MARKET"].includes(text))) return false;
      } else if (action.builderKey === "s8010" && key === "graphicType") {
        const flags = [];
        if (movingAverages.ma5) flags.push("MA5");
        if (movingAverages.ma20) flags.push("MA20");
        if (text !== flags.join(",")) return false;
      } else if (text !== action.selection[key]) return false;
    }
    return true;
  }

  function restoreFixedPlaylistEntry(value, options = {}) {
    if (!value || typeof value !== "object" || Array.isArray(value) || typeof value.enabled !== "boolean") return null;
    const operator = value.operator;
    if (!operator || typeof operator !== "object" || operator.source !== "legacy-fixed-workflow") return null;
    const action = byId.get(operator.actionId);
    const movingAverages = action ? normalizeStoredMovingAverages(value, action) : null;
    if (!action || !action.available || !movingAverages ||
        !storedSelectionMatches(value.selection, action, movingAverages)) return null;
    if (operator.legacyTab !== action.market || operator.legacySection !== action.section ||
        operator.legacyLabel !== action.label || operator.assetPath !== runtimeAssets[action.market].relativePath ||
        operator.assetSha256 !== runtimeAssets[action.market].sha256) return null;
    if (value.builderKey !== action.builderKey || value.code !== action.code ||
        value.title !== action.label || value.detail !== `${action.section} · ${action.label}` ||
        value.category !== action.category || value.market !== action.market ||
        !sameStringArray(value.aliases, action.aliases)) return null;
    let recreated;
    try {
      recreated = createFixedPlaylistEntry(action.id, {
        id: value.id,
        fadeDuration: value.fadeDuration,
        now: options.now,
        ma5: movingAverages.ma5,
        ma20: movingAverages.ma20
      });
    } catch {
      return null;
    }
    return { ...recreated, enabled: value.enabled };
  }

  function actionsForMarket(market) {
    return fixedMarkets.includes(market) ? actions.filter(action => action.market === market) : [];
  }

  return {
    runtimeAssets,
    fixedMarkets,
    actions,
    actionsForMarket,
    localDateText,
    currentNxtSession,
    createFixedPlaylistEntry,
    restoreFixedPlaylistEntry,
    refreshFixedPlaylistEntry: restoreFixedPlaylistEntry
  };
});
