gerarAmostraGoldenSet.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. import fs from "node:fs";
  2. import path from "node:path";
  3. import { fileURLToPath } from "node:url";
  4. import { AtendimentoAvaliacao } from "../src/models/AtendimentoAvaliacao.model.js";
  5. import { AtendimentoGoldenLabel } from "../src/models/AtendimentoGoldenLabel.model.js";
  6. import { SETOR_NOMES } from "../src/config/setores.js";
  7. const __dirname = path.dirname(fileURLToPath(import.meta.url));
  8. const APLICAR = process.argv.includes("--apply");
  9. const tamanhoIdx = process.argv.indexOf("--tamanho");
  10. const TAMANHO = tamanhoIdx > -1 ? Number(process.argv[tamanhoIdx + 1]) : 40;
  11. const minimoIdx = process.argv.indexOf("--minimo");
  12. const MINIMO = minimoIdx > -1 ? Number(process.argv[minimoIdx + 1]) : 3;
  13. const expectIdx = process.argv.indexOf("--expect-count");
  14. const EXPECT_COUNT = expectIdx > -1 ? Number(process.argv[expectIdx + 1]) : null;
  15. const SETORES = Object.keys(SETOR_NOMES);
  16. async function volumePorSetor() {
  17. const linhas = await AtendimentoAvaliacao.query()
  18. .where("Avaliavel", true)
  19. .join("atendimentos as a", "a.Id", "atendimento_avaliacoes.AtendimentoId")
  20. .whereIn("a.Setor", SETORES)
  21. .groupBy("a.Setor")
  22. .select("a.Setor")
  23. .count("atendimento_avaliacoes.AtendimentoId as total");
  24. const volume = Object.fromEntries(SETORES.map((s) => [s, 0]));
  25. for (const l of linhas) volume[l.Setor] = Number(l.total);
  26. return volume;
  27. }
  28. async function jaAmostradoPorSetor() {
  29. const linhas = await AtendimentoGoldenLabel.query()
  30. .join("atendimentos as a", "a.Id", "atendimento_golden_labels.AtendimentoId")
  31. .whereIn("a.Setor", SETORES)
  32. .groupBy("a.Setor")
  33. .select("a.Setor")
  34. .count("atendimento_golden_labels.AtendimentoId as total");
  35. const jaAmostrado = Object.fromEntries(SETORES.map((s) => [s, 0]));
  36. for (const l of linhas) jaAmostrado[l.Setor] = Number(l.total);
  37. return jaAmostrado;
  38. }
  39. // aloca MINIMO fixo por setor, distribui o restante proporcionalmente ao volume
  40. // (método do maior resto), sem nunca alocar mais que o volume disponível no setor
  41. function calcularAlocacao(volume) {
  42. const alocacao = Object.fromEntries(SETORES.map((s) => [s, Math.min(MINIMO, volume[s])]));
  43. let restante = TAMANHO - Object.values(alocacao).reduce((a, b) => a + b, 0);
  44. if (restante < 0) restante = 0;
  45. const volumeTotal = SETORES.reduce((a, s) => a + volume[s], 0) || 1;
  46. const fracoes = SETORES.map((s) => {
  47. const proporcional = (volume[s] / volumeTotal) * restante;
  48. const inteiro = Math.floor(proporcional);
  49. return { setor: s, inteiro, resto: proporcional - inteiro };
  50. });
  51. for (const f of fracoes) alocacao[f.setor] += f.inteiro;
  52. let distribuido = fracoes.reduce((a, f) => a + f.inteiro, 0);
  53. let sobra = restante - distribuido;
  54. fracoes.sort((a, b) => b.resto - a.resto);
  55. for (const f of fracoes) {
  56. if (sobra <= 0) break;
  57. if (alocacao[f.setor] >= volume[f.setor]) continue;
  58. alocacao[f.setor] += 1;
  59. sobra -= 1;
  60. }
  61. for (const s of SETORES) alocacao[s] = Math.min(alocacao[s], volume[s]);
  62. return alocacao;
  63. }
  64. async function sortearPorSetor(setor, quantidade) {
  65. if (quantidade <= 0) return [];
  66. const jaNaAmostra = AtendimentoGoldenLabel.query().select("AtendimentoId");
  67. return AtendimentoAvaliacao.query()
  68. .where("Avaliavel", true)
  69. .join("atendimentos as a", "a.Id", "atendimento_avaliacoes.AtendimentoId")
  70. .where("a.Setor", setor)
  71. .whereNotIn("atendimento_avaliacoes.AtendimentoId", jaNaAmostra)
  72. .orderByRaw("RAND()")
  73. .limit(quantidade)
  74. .select("atendimento_avaliacoes.AtendimentoId as AtendimentoId", "a.Codigo as Codigo", "a.Setor as Setor");
  75. }
  76. async function main() {
  77. const volume = await volumePorSetor();
  78. const jaAmostrado = await jaAmostradoPorSetor();
  79. const alocacao = calcularAlocacao(volume);
  80. console.log(`[gerar-amostra-golden-set] alvo total: ${TAMANHO} | mínimo por setor: ${MINIMO}`);
  81. console.log("[gerar-amostra-golden-set] volume disponível / já amostrado / alocação-alvo por setor:");
  82. for (const s of SETORES) {
  83. console.log(` ${s}: volume=${volume[s]} | já amostrado=${jaAmostrado[s]} | alocação-alvo=${alocacao[s]}`);
  84. }
  85. const amostraFinal = [];
  86. const faltantePorSetor = {};
  87. for (const s of SETORES) {
  88. const faltam = Math.max(0, alocacao[s] - jaAmostrado[s]);
  89. faltantePorSetor[s] = faltam;
  90. if (faltam > 0) {
  91. const sorteados = await sortearPorSetor(s, faltam);
  92. amostraFinal.push(...sorteados);
  93. }
  94. }
  95. console.log(`[gerar-amostra-golden-set] ${amostraFinal.length} atendimentos novos serão adicionados à amostra.`);
  96. if (EXPECT_COUNT !== null && amostraFinal.length !== EXPECT_COUNT) {
  97. console.error(`[gerar-amostra-golden-set] abortando: esperava ${EXPECT_COUNT}, achei ${amostraFinal.length}.`);
  98. process.exit(1);
  99. }
  100. const backupDir = path.resolve(__dirname, "logs");
  101. fs.mkdirSync(backupDir, { recursive: true });
  102. const backupPath = path.join(backupDir, `gerar-amostra-golden-set-${new Date().toISOString().replace(/[:.]/g, "-")}.json`);
  103. fs.writeFileSync(
  104. backupPath,
  105. JSON.stringify(
  106. {
  107. config: { tamanho: TAMANHO, minimo: MINIMO },
  108. volumePorSetor: volume,
  109. jaAmostradoPorSetor: jaAmostrado,
  110. alocacaoAlvoPorSetor: alocacao,
  111. faltantePorSetor,
  112. amostraFinal
  113. },
  114. null,
  115. 2
  116. )
  117. );
  118. console.log(`[gerar-amostra-golden-set] detalhe do sorteio salvo em ${backupPath}`);
  119. if (!APLICAR) {
  120. console.log("[gerar-amostra-golden-set] dry-run (nenhuma alteração feita). Use --apply para gravar.");
  121. process.exit(0);
  122. }
  123. if (amostraFinal.length === 0) {
  124. console.log("[gerar-amostra-golden-set] nada para inserir.");
  125. process.exit(0);
  126. }
  127. for (const { AtendimentoId } of amostraFinal) {
  128. await AtendimentoGoldenLabel.query().insert({ AtendimentoId, Status: "pendente" });
  129. }
  130. console.log(`[gerar-amostra-golden-set] ${amostraFinal.length} linhas inseridas com Status='pendente'.`);
  131. process.exit(0);
  132. }
  133. main().catch((err) => {
  134. console.error("[gerar-amostra-golden-set] falha fatal:", err);
  135. process.exit(1);
  136. });