// Oracle-driven IFPS auto-correction against the public Eurocontrol IFPUV. // // Given a route, repeatedly validate and mechanically fix the tractable error // classes until the plan is IFPS-ACCEPTED (or no further fix applies): // * ROUTE130 UNKNOWN DESIGNATOR X -> drop token X from item 15 // * PROF204/205 forbidden FL band -> move the cruise level into the // allowed window (RAD level capping) // One browser session is reused across iterations (fast loop). // // Usage: // node autofix.mjs '{"adep":"LFPG","ades":"EGLL","route":"OPALE DCT KESAX DCT DIMAL DCT ALESO","level":"F360"}' // Prints one JSON line: { accepted, level, route, iterations, log, errors }. import { chromium } from "playwright"; const URL = "https://www.public.nm.eurocontrol.int/PUBPORTAL/gateway/spec/"; const ROUTE_SEL = '[id="FREE_TEXT_EDITOR.FLIGHT_DATA_AREA.GENERAL_DATA_ENTRY.INTRODUCE_FLIGHT_PLAN_FIELD"]'; const VALIDATE_SEL = '[id="FREE_TEXT_EDITOR.FLIGHT_DATA_AREA.VALIDATE_ACTION_LABEL"]'; const MAX_ITERS = 8; function tomorrowDOF() { const d = new Date(Date.now() + 24 * 3600 * 1000); const p = (n) => String(n).padStart(2, "0"); return `${String(d.getUTCFullYear()).slice(2)}${p(d.getUTCMonth() + 1)}${p(d.getUTCDate())}`; } function buildFpl(o, route, flNum) { const type = o.actype || "A320"; const level = `F${String(flNum).padStart(3, "0")}`; return ( `(FPL-TEST01-IS\n` + `-${type}/${o.wtc || "M"}-${o.equip || "SDE1E2E3FGHIRWY/LB1"}\n` + `-${o.adep}${o.eobt || "1200"}\n` + `-${o.speed || "N0450"}${level} ${route.trim()}\n` + `-${o.ades}${o.eet || "0040"}\n` + `-PBN/${o.pbn || "A1B1C1D1O1S2"} REG/${o.reg || "FGKXA"} DOF/${o.dof || tomorrowDOF()})` ); } const HOUSEKEEPING = /^(EFPM(51|234|168|166)|PROF191)$/; function parseErrors(body) { const start = body.indexOf("Validation Results"); const region = start >= 0 ? body.slice(start) : body; const re = /\b([A-Z]{2,6}\d{1,4}):\s*([^\n\t]+)/g; const errors = []; let m; while ((m = re.exec(region))) { if (!HOUSEKEEPING.test(m[1])) errors.push({ code: m[1], msg: m[2].trim() }); } return errors; } // Extract the forbidden FL band(s) from a PROF204/205 message. `Fa..Fb`: // a==0 -> forbidden below b (a floor); b>=600 -> forbidden above a (a ceiling). function bandOf(msg) { const m = msg.match(/F(\d{1,3})\.\.F(\d{3})/); if (!m) return null; const lo = +m[1], hi = +m[2]; if (lo === 0) return { floor: hi }; // forbidden 0..hi => must fly above hi return { ceiling: lo }; // Fa..F999 ceiling, or narrow hole Fa..Fb → fly below a } // Decide the next fix from the current error set. Returns { route?, fl?, note } or null. function planFix(errors, route, fl) { // 1) unknown designators -> strip the offending token const unknown = errors .filter((e) => e.code === "ROUTE130" && /UNKNOWN DESIGNATOR/i.test(e.msg)) .map((e) => e.msg.match(/UNKNOWN DESIGNATOR\s+(\S+)/i)?.[1]) .filter(Boolean); if (unknown.length) { const drop = new Set(unknown); const toks = route.split(/\s+/).filter((t) => !drop.has(t)); // collapse dangling DCT left by removal const cleaned = toks.filter((t, i) => !(t === "DCT" && (i === 0 || i === toks.length - 1 || toks[i - 1] === "DCT"))); return { route: cleaned.join(" "), note: `drop unknown designator(s): ${[...drop].join(", ")}` }; } // 2) FL band restrictions -> move into the allowed window let ceiling = Infinity, floor = 0, sawBand = false; for (const e of errors) { if (e.code !== "PROF204" && e.code !== "PROF205") continue; const b = bandOf(e.msg); if (!b) continue; sawBand = true; if (b.ceiling != null) ceiling = Math.min(ceiling, b.ceiling); if (b.floor != null) floor = Math.max(floor, b.floor); } // Lower the level whenever the current FL violates a RAD ceiling — even if // other (non-level) errors remain; clearing the band errors is strict progress // and the loop stops afterwards if nothing else is fixable. if (sawBand && ceiling !== Infinity && fl >= ceiling) { const target = Math.floor((ceiling - 5) / 10) * 10; // highest 10s FL below the cap if (target > floor && target !== fl && target >= 60) { return { fl: target, note: `RAD level cap ${ceiling} -> retry at F${String(target).padStart(3, "0")}` }; } } return null; } // ── run ──────────────────────────────────────────────────────────────────── const o = JSON.parse(process.argv[2] || '{"adep":"LFPG","ades":"EGLL","route":"OPALE DCT KESAX DCT DIMAL DCT ALESO","level":"F360"}'); let route = o.route.trim(); let fl = parseInt(String(o.level || "F350").replace(/\D/g, ""), 10) || 350; const b = await chromium.launch({ headless: true }); const ctx = await b.newContext({ viewport: { width: 1400, height: 950 } }); const page = await ctx.newPage(); const out = { accepted: false, level: null, route: null, iterations: 0, log: [], errors: [], error: null }; try { await page.goto(URL, { waitUntil: "domcontentloaded", timeout: 60000 }); await page.waitForTimeout(8000); const popupP = ctx.waitForEvent("page", { timeout: 15000 }).catch(() => null); await page.getByText("Free Text Editor", { exact: false }).first().click({ timeout: 15000 }); const ed = (await popupP) || page; await ed.waitForSelector(ROUTE_SEL, { timeout: 20000 }); const submit = async (fpl) => { await ed.locator(ROUTE_SEL).fill(fpl); await ed.locator(VALIDATE_SEL).click({ timeout: 15000 }); await ed.waitForTimeout(6000); return parseErrors(await ed.locator("body").innerText()); }; // Monotonic auto-correction: only *keep* a change if it strictly reduces the // error count. A fix that makes things worse (e.g. lowering FL into a low-level // TMA full of DCT restrictions) is reverted, and we report the best state seen. // The result is never worse than the input. const fl3 = (n) => `F${String(n).padStart(3, "0")}`; let best = null; // { route, fl, errors } let curRoute = route; let curFl = fl; for (let i = 1; i <= MAX_ITERS; i++) { out.iterations = i; const errors = await submit(buildFpl(o, curRoute, curFl)); if (errors.length === 0) { best = { route: curRoute, fl: curFl, errors: [] }; out.log.push(`iter ${i}: ${fl3(curFl)} "${curRoute}" -> ACCEPTED`); break; } if (best !== null && errors.length >= best.errors.length) { // The last applied fix didn't help — revert to the best and stop. out.log.push(`iter ${i}: ${fl3(curFl)} -> ${errors.length} err (not better than ${best.errors.length}) — reverting`); break; } best = { route: curRoute, fl: curFl, errors }; const fix = planFix(errors, curRoute, curFl); out.log.push(`iter ${i}: ${fl3(curFl)} -> ${errors.length} err [${errors.map((e) => e.code).join(",")}]${fix ? " | fix: " + fix.note : " | no auto-fix"}`); if (!fix) break; if (fix.route != null) curRoute = fix.route; if (fix.fl != null) curFl = fix.fl; } best = best || { route: curRoute, fl: curFl, errors: out.errors }; out.accepted = best.errors.length === 0; out.level = fl3(best.fl); out.route = best.route; out.errors = best.errors; } catch (e) { out.error = e.message; } finally { await b.close(); } console.log(JSON.stringify(out));