| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 |
- 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);
- });
|