{"version":3,"file":"aggregate-index-recommendations.cjs","names":[],"sources":["../../src/action-plan/aggregate-index-recommendations.ts"],"sourcesContent":["import type { IndexRecommendation } from \"../query.js\";\nimport type { AggregatedIndexRecommendation } from \"./index-coverage.js\";\n\n/**\n * Minimal per-query shape the action-plan consolidation reads. Structurally\n * compatible with both the live-query optimizer state and the CI run query\n * result, so callers on either side can pass their records directly without an\n * adapter. Only the `improvements_available` state carries index actions; every\n * other state is ignored.\n */\nexport interface ActionPlanQuery {\n  hash: string;\n  optimization: {\n    state: string;\n    cost?: number;\n    optimizedCost?: number;\n    costReductionPercentage?: number;\n    indexRecommendations?: IndexRecommendation[];\n  };\n}\n\n/**\n * Collapse a set of analyzed queries into one entry per unique index\n * definition, scored by the percentage cost reduction of the queries it helps.\n *\n * This is the shared primitive behind the live-query and CI-run recommendation\n * views. Ranking here is the legacy \"most queries, then biggest single\n * percentage\" order; the absolute-cost ranking the dashboard action plan needs\n * lives in {@link buildActionPlan}.\n */\nexport function aggregateIndexRecommendations(\n  queries: readonly ActionPlanQuery[],\n): AggregatedIndexRecommendation[] {\n  const indexMap = new Map<\n    string,\n    {\n      index: IndexRecommendation;\n      queryHashes: string[];\n      costReductions: number[];\n    }\n  >();\n\n  for (const query of queries) {\n    if (query.optimization.state !== \"improvements_available\") continue;\n\n    const { indexRecommendations, costReductionPercentage } =\n      query.optimization;\n    if (!indexRecommendations || costReductionPercentage === undefined)\n      continue;\n\n    for (const indexRec of indexRecommendations) {\n      const key = indexRec.definition;\n      const existing = indexMap.get(key);\n\n      if (existing) {\n        existing.queryHashes.push(query.hash);\n        existing.costReductions.push(costReductionPercentage);\n      } else {\n        indexMap.set(key, {\n          index: indexRec,\n          queryHashes: [query.hash],\n          costReductions: [costReductionPercentage],\n        });\n      }\n    }\n  }\n\n  const aggregated: AggregatedIndexRecommendation[] = [];\n\n  for (const data of indexMap.values()) {\n    const averageCostReduction =\n      data.costReductions.reduce((sum, val) => sum + val, 0) /\n      data.costReductions.length;\n    const bestCostReduction = Math.max(...data.costReductions);\n\n    aggregated.push({\n      index: data.index,\n      affectedQueryCount: data.queryHashes.length,\n      averageCostReduction,\n      bestCostReduction,\n      affectedQueryHashes: data.queryHashes,\n    });\n  }\n\n  // Most affected queries first, breaking ties by the biggest single reduction.\n  aggregated.sort((a, b) => {\n    if (a.affectedQueryCount !== b.affectedQueryCount) {\n      return b.affectedQueryCount - a.affectedQueryCount;\n    }\n    return b.bestCostReduction - a.bestCostReduction;\n  });\n\n  return aggregated;\n}\n"],"mappings":";;;;;;;;;;;AA8BA,SAAgB,8BACd,SACiC;CACjC,MAAM,2BAAW,IAAI,KAOlB;AAEH,MAAK,MAAM,SAAS,SAAS;AAC3B,MAAI,MAAM,aAAa,UAAU,yBAA0B;EAE3D,MAAM,EAAE,sBAAsB,4BAC5B,MAAM;AACR,MAAI,CAAC,wBAAwB,4BAA4B,KAAA,EACvD;AAEF,OAAK,MAAM,YAAY,sBAAsB;GAC3C,MAAM,MAAM,SAAS;GACrB,MAAM,WAAW,SAAS,IAAI,IAAI;AAElC,OAAI,UAAU;AACZ,aAAS,YAAY,KAAK,MAAM,KAAK;AACrC,aAAS,eAAe,KAAK,wBAAwB;SAErD,UAAS,IAAI,KAAK;IAChB,OAAO;IACP,aAAa,CAAC,MAAM,KAAK;IACzB,gBAAgB,CAAC,wBAAwB;IAC1C,CAAC;;;CAKR,MAAM,aAA8C,EAAE;AAEtD,MAAK,MAAM,QAAQ,SAAS,QAAQ,EAAE;EACpC,MAAM,uBACJ,KAAK,eAAe,QAAQ,KAAK,QAAQ,MAAM,KAAK,EAAE,GACtD,KAAK,eAAe;EACtB,MAAM,oBAAoB,KAAK,IAAI,GAAG,KAAK,eAAe;AAE1D,aAAW,KAAK;GACd,OAAO,KAAK;GACZ,oBAAoB,KAAK,YAAY;GACrC;GACA;GACA,qBAAqB,KAAK;GAC3B,CAAC;;AAIJ,YAAW,MAAM,GAAG,MAAM;AACxB,MAAI,EAAE,uBAAuB,EAAE,mBAC7B,QAAO,EAAE,qBAAqB,EAAE;AAElC,SAAO,EAAE,oBAAoB,EAAE;GAC/B;AAEF,QAAO"}