import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { AtendimentoAvaliacao } from "../src/models/AtendimentoAvaliacao.model.js"; import { AtendimentoGoldenLabel } from "../src/models/AtendimentoGoldenLabel.model.js"; import { SETOR_NOMES } from "../src/config/setores.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const APLICAR = process.argv.includes("--apply"); const tamanhoIdx = process.argv.indexOf("--tamanho"); const TAMANHO = tamanhoIdx > -1 ? Number(process.argv[tamanhoIdx + 1]) : 40; const minimoIdx = process.argv.indexOf("--minimo"); const MINIMO = minimoIdx > -1 ? Number(process.argv[minimoIdx + 1]) : 3; const expectIdx = process.argv.indexOf("--expect-count"); const EXPECT_COUNT = expectIdx > -1 ? Number(process.argv[expectIdx + 1]) : null; const SETORES = Object.keys(SETOR_NOMES); async function volumePorSetor() { const linhas = await AtendimentoAvaliacao.query() .where("Avaliavel", true) .join("atendimentos as a", "a.Id", "atendimento_avaliacoes.AtendimentoId") .whereIn("a.Setor", SETORES) .groupBy("a.Setor") .select("a.Setor") .count("atendimento_avaliacoes.AtendimentoId as total"); const volume = Object.fromEntries(SETORES.map((s) => [s, 0])); for (const l of linhas) volume[l.Setor] = Number(l.total); return volume; } async function jaAmostradoPorSetor() { const linhas = await AtendimentoGoldenLabel.query() .join("atendimentos as a", "a.Id", "atendimento_golden_labels.AtendimentoId") .whereIn("a.Setor", SETORES) .groupBy("a.Setor") .select("a.Setor") .count("atendimento_golden_labels.AtendimentoId as total"); const jaAmostrado = Object.fromEntries(SETORES.map((s) => [s, 0])); for (const l of linhas) jaAmostrado[l.Setor] = Number(l.total); return jaAmostrado; } // aloca MINIMO fixo por setor, distribui o restante proporcionalmente ao volume // (método do maior resto), sem nunca alocar mais que o volume disponível no setor function calcularAlocacao(volume) { const alocacao = Object.fromEntries(SETORES.map((s) => [s, Math.min(MINIMO, volume[s])])); let restante = TAMANHO - Object.values(alocacao).reduce((a, b) => a + b, 0); if (restante < 0) restante = 0; const volumeTotal = SETORES.reduce((a, s) => a + volume[s], 0) || 1; const fracoes = SETORES.map((s) => { const proporcional = (volume[s] / volumeTotal) * restante; const inteiro = Math.floor(proporcional); return { setor: s, inteiro, resto: proporcional - inteiro }; }); for (const f of fracoes) alocacao[f.setor] += f.inteiro; let distribuido = fracoes.reduce((a, f) => a + f.inteiro, 0); let sobra = restante - distribuido; fracoes.sort((a, b) => b.resto - a.resto); for (const f of fracoes) { if (sobra <= 0) break; if (alocacao[f.setor] >= volume[f.setor]) continue; alocacao[f.setor] += 1; sobra -= 1; } for (const s of SETORES) alocacao[s] = Math.min(alocacao[s], volume[s]); return alocacao; } async function sortearPorSetor(setor, quantidade) { if (quantidade <= 0) return []; const jaNaAmostra = AtendimentoGoldenLabel.query().select("AtendimentoId"); return AtendimentoAvaliacao.query() .where("Avaliavel", true) .join("atendimentos as a", "a.Id", "atendimento_avaliacoes.AtendimentoId") .where("a.Setor", setor) .whereNotIn("atendimento_avaliacoes.AtendimentoId", jaNaAmostra) .orderByRaw("RAND()") .limit(quantidade) .select("atendimento_avaliacoes.AtendimentoId as AtendimentoId", "a.Codigo as Codigo", "a.Setor as Setor"); } async function main() { const volume = await volumePorSetor(); const jaAmostrado = await jaAmostradoPorSetor(); const alocacao = calcularAlocacao(volume); console.log(`[gerar-amostra-golden-set] alvo total: ${TAMANHO} | mínimo por setor: ${MINIMO}`); console.log("[gerar-amostra-golden-set] volume disponível / já amostrado / alocação-alvo por setor:"); for (const s of SETORES) { console.log(` ${s}: volume=${volume[s]} | já amostrado=${jaAmostrado[s]} | alocação-alvo=${alocacao[s]}`); } const amostraFinal = []; const faltantePorSetor = {}; for (const s of SETORES) { const faltam = Math.max(0, alocacao[s] - jaAmostrado[s]); faltantePorSetor[s] = faltam; if (faltam > 0) { const sorteados = await sortearPorSetor(s, faltam); amostraFinal.push(...sorteados); } } console.log(`[gerar-amostra-golden-set] ${amostraFinal.length} atendimentos novos serão adicionados à amostra.`); if (EXPECT_COUNT !== null && amostraFinal.length !== EXPECT_COUNT) { console.error(`[gerar-amostra-golden-set] abortando: esperava ${EXPECT_COUNT}, achei ${amostraFinal.length}.`); process.exit(1); } const backupDir = path.resolve(__dirname, "logs"); fs.mkdirSync(backupDir, { recursive: true }); const backupPath = path.join(backupDir, `gerar-amostra-golden-set-${new Date().toISOString().replace(/[:.]/g, "-")}.json`); fs.writeFileSync( backupPath, JSON.stringify( { config: { tamanho: TAMANHO, minimo: MINIMO }, volumePorSetor: volume, jaAmostradoPorSetor: jaAmostrado, alocacaoAlvoPorSetor: alocacao, faltantePorSetor, amostraFinal }, null, 2 ) ); console.log(`[gerar-amostra-golden-set] detalhe do sorteio salvo em ${backupPath}`); if (!APLICAR) { console.log("[gerar-amostra-golden-set] dry-run (nenhuma alteração feita). Use --apply para gravar."); process.exit(0); } if (amostraFinal.length === 0) { console.log("[gerar-amostra-golden-set] nada para inserir."); process.exit(0); } for (const { AtendimentoId } of amostraFinal) { await AtendimentoGoldenLabel.query().insert({ AtendimentoId, Status: "pendente" }); } console.log(`[gerar-amostra-golden-set] ${amostraFinal.length} linhas inseridas com Status='pendente'.`); process.exit(0); } main().catch((err) => { console.error("[gerar-amostra-golden-set] falha fatal:", err); process.exit(1); });