export const SCENARIO_DETAILS=[
 ['basic','01 · Из HTTP в бизнес-операцию','Один запрос, родительский SERVER span и две внутренние операции. Начните с дерева и откройте атрибуты дочернего span.'],
 ['distributed','02 · Два сервиса и traceparent','Checkout вызывает Pricing. CLIENT и SERVER связаны одним trace ID; полученный traceparent сохранён в реальном HTTP-ответе.'],
 ['sequential','03 · Последовательные вызовы','Два учебных HTTP-вызова выполняются друг за другом. Задержки 45/80 мс задаёт код стенда; на шкале показана фактическая измеренная длительность.'],
 ['parallel','04 · Параллельные ветви','Те же два HTTP-вызова запущены через Promise.all. Сравните перекрытие ветвей с предыдущим опытом, а не сумму всех span.'],
 ['error','05 · Ошибка и связанный лог','Pricing намеренно возвращает 500. Посмотрите ERROR, событие exception и строку журнала с тем же trace ID.'],
 ['async-context','06 · Контекст после await','Два запроса выполнены одновременно. Проверка после await и setImmediate подтверждает сохранение своего span без смешивания запросов.'],
 ['worker','07 · Контекст в worker_threads','CPU-задача действительно выполнена в отдельном потоке. traceparent передан явно в workerData, затем извлечён отдельным SDK.'],
 ['sampling','08 · Родительское решение о выборке','Два настоящих запроса получили входной sampled-флаг 1 и 0. Несэмплированный запрос обработан, но его spans не экспортированы. Не путайте отсутствие трассы с отсутствием запроса.'],
 ['privacy','09 · Поля по списку и тихий health','Вымышленные email/token не попали в атрибуты. /health обслужен без span. Посмотрите фактический набор полей и проверки записи.'],
 ['shutdown','10 · Завершение и отправка буфера','После последнего запроса процессы прекращают приём, завершают активную работу и вызывают sdk.shutdown(). Последние spans присутствуют в JSONL и проверяются по ID.']
];
export const ms=hr=>hr[0]*1000+hr[1]/1e6;
export function flattenTrace(spans){
 const ids=new Map(spans.map(s=>[s.spanId,s]));
 const depth=s=>{let n=0,p=s.parentSpanId,seen=new Set([s.spanId]);while(p&&ids.has(p)&&!seen.has(p)){seen.add(p);n++;p=ids.get(p).parentSpanId;}return n;};
 const children=new Map();for(const s of spans){const key=ids.has(s.parentSpanId)?s.parentSpanId:null;if(!children.has(key))children.set(key,[]);children.get(key).push(s);}
 const out=[],seen=new Set();const visit=(parent)=>{for(const s of (children.get(parent)||[]).sort((a,b)=>ms(a.startTime)-ms(b.startTime))){if(seen.has(s.spanId))continue;seen.add(s.spanId);out.push({...s,depth:depth(s)});visit(s.spanId);}};visit(null);return out;
}
export function traceBounds(spans){if(!spans.length)return{start:0,end:0,duration:0};const start=Math.min(...spans.map(s=>ms(s.startTime))),end=Math.max(...spans.map(s=>ms(s.endTime)));return{start,end,duration:Math.max(0,end-start)};}
export function filterSpans(spans,{service='',errors=false,search=''}={}){const text=search.toLowerCase().trim();return spans.filter(s=>(!service||s.resource['service.name']===service)&&(!errors||s.status.code===2)&&(!text||[s.name,s.spanId,s.traceId].some(x=>x.toLowerCase().includes(text))));}
