Neuroredes
Módulos do Produto

Neurocalc

Motor de cálculo de sincronia e assincronia entre pares de respondentes. Não é um pacote isolado: é a lógica compartilhada que vive embutida em três Lambdas (dashboard-processor, graph-processor, excel-processor) e alimenta o dashboard analítico, o grafo da rede e o relatório Excel a partir dos mesmos dados brutos.

Capítulo 01

O que é o Neurocalc

Cada respondente avalia os demais (e a si mesmo) em até cinco perguntas com escala de 0 a 6. O Neurocalc consome esse conjunto de respostas par-a-par e produz, para cada pergunta, uma classificação da conexão entre os dois lados — quão alinhadas (sincrônicas) ou desalinhadas (assincrônicas) as visões são — além de rankings, percentuais de resposta e agregações por atributo (área, cargo, etc.).

A função canônica é createJSON() em lambda/dashboard-processor/src/utils/neurocalc.ts. O graph-processor reaproveita a mesma lógica em graph-calc.ts, e o excel-processor consome o JSON já calculado pelo dashboard para montar planilhas.

Destaque

Não há biblioteca externa de análise de rede (graphology, NetworkX, d3-graph). Toda a combinatória — pares, métricas, agrupamentos — é JavaScript puro sobre arrays e objetos. Isso simplifica o deploy mas exige cuidado com performance em pesquisas grandes (N² no número de respondentes).

Capítulo 02

As três Lambdas que usam Neurocalc

Todas são acionadas via Lambda Function URL (HTTP direto, sem API Gateway). O frontend descobre cada URL pelas variáveis NEXT_PUBLIC_*_PROCESSOR_URL. As Lambdas autenticam no Supabase com a service role key, buscada do AWS Secrets Manager a cada cold start.

Lambda Entrada / Saída Cache
dashboard-processor In: { surveyId, forceRefresh? }
Out: JSON com overall_rating, question_1..5, relations, connections_ranking, response_percentage, relations_by_*, inter_relations_by_*.
mydb.survey_cache.neurocalc_result (JSONB)
graph-processor In: { surveyId } (rota /graph)
Out: nodes[], edges{} por pergunta, legend e respondentStats.
mydb.survey_cache.graph_result (JSONB)
excel-processor In: { json, nome_arquivo, reportType? }
Out: { signedUrl, fileName } apontando para o bucket relatorios no Supabase Storage.
Sem cache — gera .xlsx efêmero, signed URL válida por 1h.
Nota

O excel-processor não chama o Neurocalc diretamente. Ele recebe o JSON já calculado pelo dashboard (o frontend faz um GET no dashboard primeiro, depois POST no excel-processor com o resultado). Ver services/report.service.ts:49–163.

Capítulo 03

Algoritmo de sincronia / assincronia

Para cada par ordenado de respondentes (S, T) e cada pergunta, o Neurocalc compara a resposta de S sobre T (answerST) com a resposta de T sobre S (answerTS) e classifica a conexão em uma de sete categorias.

lambda/dashboard-processor/src/utils/neurocalc.ts typescript
function calcConnection(answerST: number, answerTS: number): string {
  const connectionList = [
    "null",
    "low_synchrony",
    "mid_synchrony",
    "high_synchrony",
    "high_low_asynchrony",
    "mid_null_asynchrony",
    "high_null_asynchrony",
  ];
  if (Math.abs(answerST - answerTS) <= 1) {
    return connectionList[Math.min(answerST, answerTS)];
  } else {
    return connectionList[Math.abs(answerST - answerTS) * 2 + 3 - Math.max(answerST, answerTS)];
  }
}

Como ler o resultado

  • Quando a diferença é pequena (|S − T| ≤ 1): conexão sincrônica. O índice usado é o menor dos dois valores, então respostas (5, 5) e (5, 6) caem em high_synchrony; (0, 1) cai em low_synchrony.
  • Quando a diferença é grande (|S − T| ≥ 2): conexão assincrônica. A fórmula mistura a magnitude da diferença com o teto da escala para distinguir se a divergência é alto-vs-nulo, médio-vs-nulo ou alto-vs-baixo.
Classe Significado
nullAmbos os lados responderam 0. Sem percepção de relação.
low_synchronyConcordam em baixa intensidade.
mid_synchronyConcordam em intensidade média.
high_synchronyConcordam em alta intensidade — núcleo da rede.
high_low_asynchronyUm vê alta, o outro vê baixa.
mid_null_asynchronyUm vê média, o outro não vê relação.
high_null_asynchronyUm vê alta, o outro não vê — divergência máxima.

Trace passo a passo

Para uma resposta (answerST = 5, answerTS = 1) na escala 0–6:

  1. |5 − 1| = 4, então cai no ramo assincrônico.
  2. Math.abs(5 − 1) * 2 + 3 − Math.max(5, 1) = 4 * 2 + 3 − 5 = 6.
  3. connectionList[6]high_null_asynchrony.

Esse mapeamento numérico é frágil em código (mexer na ordem do connectionList reescreve o significado de todas as métricas históricas). Se for refatorar, garanta que o cache survey_cache.neurocalc_result seja invalidado para todas as pesquisas, senão o frontend lê resultados antigos com classes deslocadas.

Capítulo 04

Métricas por respondente

Para cada respondente, o Neurocalc devolve um agregado por pergunta com a contagem de conexões em cada classe e dois índices direcionais derivados da diferença bruta entre o que ele recebe e o que ele .

in_degree
Soma de (answerTS − answerST) quando positivo. Quanto os outros me avaliam acima do que eu os avalio. Alto in_degree indica reconhecimento percebido externamente.
out_degree
Soma de (answerST − answerTS) quando positivo. Quanto eu avalio os outros acima do que sou avaliado. Alto out_degree indica visão generosa ou superestimação da rede.
connections_ranking
Ordenação dos respondentes por contagem total de high_synchrony recebidas — usado no card "ranking" do dashboard.
response_percentage
Percentual de respondentes que efetivamente preencheram a pesquisa em relação ao total cadastrado.
Capítulo 05

Métricas por grupo (atributos)

Respondentes têm até sete atributos custom (attribute_1..attribute_7) configurados por pesquisa — tipicamente "área", "cargo", "região", "nível". O Neurocalc agrega as conexões por valor de atributo e por par de valores distintos.

relations_by_attribute_N
Contagem total de cada classe de conexão dentro de cada grupo de atributo. Ex.: { "Engenharia": { high_synchrony: 42, ... }, "Produto": { ... } }.
inter_relations_by_attribute_N
Contagem das classes entre grupos distintos do mesmo atributo (Engenharia ↔ Produto, Engenharia ↔ Vendas, etc.). Usado para identificar silos ou pontes entre áreas.
attribute_details
Para cada participante, breakdown das conexões alto/médio/baixo segmentado pelo valor de atributo do outro lado da conexão.

Atributos multivalorados

Atributos podem conter múltiplos valores separados por / , ; | ou barra invertida. O Neurocalc faz o split e trata cada participante como pertencente a todos os grupos simultaneamente, então as somas por grupo podem totalizar mais do que o número de respondentes.

Capítulo 06

Quirk: respostas codificadas como número concatenado

Por motivos históricos, cada par (respondent_id, evaluatee_id) guarda as cinco respostas como um único número cujos dígitos são as respostas de cada pergunta na ordem. Ex.: respostas [2, 3, 1, 5, 4] viram 23154.

lambda/dashboard-processor/src/index.ts typescript
// Codificação: array → número
const encoded = arr.every((v) => v === 0) ? 0 : Number(arr.join(""));

// Extração: dígito da pergunta `questionPos` (1..5)
const answer = Math.floor(
  (responseValue % Math.pow(10, questionPos)) / Math.pow(10, questionPos - 1)
);
Atenção

O dashboard converte para number; o graph-processor mantém como string (linha 143 de graph.ts) justamente para preservar zeros à esquerda — uma resposta 02345 vira 2345 se convertida para número. Ao tocar nesse trecho, verifique nos dois lados.

Os atributos também aparecem em duas grafias: o Neurocalc usa nomes em português (atributo_1..atributo_6) e o graph-calc usa inglês (attribute_1..attribute_6). O index.ts do dashboard mapeia ambos antes de passar adiante. Não unifique sem revisar os dois.

Capítulo 07

Fluxo de dados: frontend → Lambda → Supabase

Dashboard

pesquisa/services/dashboard.service.ts typescript
const response = await fetch(process.env.NEXT_PUBLIC_DASHBOARD_PROCESSOR_URL!, {
  method: "POST",
  body: JSON.stringify({ surveyId, forceRefresh }),
});
const neurocalcResult = await response.json();

Gráfico

pesquisa/services/graph.service.ts typescript
const response = await fetch(`${process.env.NEXT_PUBLIC_GRAPH_PROCESSOR_URL}/graph`, {
  method: "POST",
  body: JSON.stringify({ surveyId }),
});
const { nodes, edges, legend, respondentStats } = await response.json();

Relatório Excel (dois saltos)

  1. Frontend chama dashboard-processor e recebe o JSON do Neurocalc.
  2. Esse JSON é repassado para excel-processor junto do nome_arquivo e do reportType (full ou executive).
  3. O Lambda gera o .xlsx, faz upload para o bucket relatorios e devolve uma signed URL com validade de 1h.
Capítulo 08

Cache & invalidação

Resultados do Neurocalc são caros (N² no número de respondentes) e mudam pouco depois que a pesquisa fecha. Por isso o Lambda mantém um cache em mydb.survey_cache com duas colunas jsonb: neurocalc_result (dashboard) e graph_result (gráfico).

Situação Comportamento
Cache hit Lambda retorna o JSON guardado e encerra. Custo: 1 SELECT.
Cache miss Roda o cálculo completo, faz UPSERT com updated_at = now().
forceRefresh: true Dashboard ignora cache, recalcula e sobrescreve. Útil ao depurar.
Nova resposta chega Trigger trigger_invalidate_cache em mydb.survey_responses marca survey_processing_results.processing_status = 'stale'. O cache não é apagado — a invalidação é preguiçosa.
Atenção

O flag stale existe em survey_processing_results mas não é lido pelo Lambda hoje. Na prática, o cache só é regenerado quando o usuário passa forceRefresh: true ou quando o registro survey_cache é apagado manualmente. Antes de assumir que o dashboard está "atualizado", confirme a data em survey_cache.updated_at.

Trigger de invalidação

mydb.invalidate_survey_cache() sql
CREATE OR REPLACE FUNCTION mydb.invalidate_survey_cache()
RETURNS trigger AS $$
BEGIN
  UPDATE mydb.survey_processing_results
     SET processing_status = 'stale',
         updated_at = now()
   WHERE survey_id = COALESCE(NEW.survey_id, OLD.survey_id);
  RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trigger_invalidate_cache
AFTER INSERT OR UPDATE ON mydb.survey_responses
FOR EACH ROW EXECUTE FUNCTION mydb.invalidate_survey_cache();

O trigger é defensivo — preserva a possibilidade de migrar para um worker que leia o flag e regenere proativamente. Hoje a regeneração é manual; futuras versões podem consumir survey_processing_results como fila.

Capítulo 09

Modelo de dados consumido

O Neurocalc lê quatro tabelas. As Lambdas paginam survey_responses em blocos de 500 para não estourar limite de payload do Supabase.

Tabela Colunas relevantes
mydb.surveys id, title, question_1..5, start_date, end_date
mydb.survey_respondents id, name, attribute_1..7, phone, email, active, survey_id
mydb.survey_responses respondent_id, evaluatee_id, question_1..5 (codificado, ver Cap. 06)
mydb.survey_attributes Nomes amigáveis de cada atributo por pesquisa, via RPC get_survey_attributes
Capítulo 10

Deploy & dependências

Pacotes Node usados pelas Lambdas

  • @supabase/supabase-js — query nas tabelas mydb.*.
  • @aws-sdk/client-secrets-manager — busca a service role key.
  • xlsx (somente excel-processor) — gera workbook.

Variáveis de ambiente do frontend

.env.local bash
NEXT_PUBLIC_DASHBOARD_PROCESSOR_URL=https://<id>.lambda-url.us-east-1.on.aws/
NEXT_PUBLIC_GRAPH_PROCESSOR_URL=https://<id>.lambda-url.us-east-1.on.aws/
NEXT_PUBLIC_EXCEL_PROCESSOR_URL=https://<id>.lambda-url.us-east-1.on.aws/

Variáveis de ambiente do Lambda

Lambda runtime bash
SUPABASE_URL=https://<project>.supabase.co
SUPABASE_SECRET_NAME=supabase-service-key   # nome do segredo no Secrets Manager

Build & empacotamento

lambda/<processor>/package.json bash
npm run build    # esbuild, marca @aws-sdk como external
npm run zip      # gera function.zip pronto para upload
Nota

CORS é configurado direto na Lambda Function URL (CDK / console AWS), não no handler. Os comentários do index.ts avisam: não adicione headers CORS no código, ou o navegador receberá headers duplicados.

Capítulo 11

Forma do JSON de saída (dashboard)

O dashboard-processor devolve um objeto com chave por pergunta mais um bloco overall_rating com agregados gerais. Cada pergunta carrega todos os ângulos que o dashboard precisa renderizar — não há cálculos adicionais no frontend.

Resposta resumida (dashboard-processor) typescript
{
  survey_info: {
    id: 123,
    title: "Pesquisa 2026.Q1",
    start_date: "2026-04-01",
    end_date:   "2026-05-31",
    attribute_names: { attribute_1: "Área", attribute_2: "Senioridade", ... },
  },
  overall_rating: {
    response_percentage: 0.78,
    whoAnswered:        [ { id, name, attribute_1..6 }, ... ],
    whoDidntAnswered:   [ ... ],
  },
  question_1: {
    text: "Quanto eu confio nesta pessoa?",
    relations: {
      high_synchrony:        421,
      mid_synchrony:         312,
      low_synchrony:         108,
      high_low_asynchrony:    44,
      mid_null_asynchrony:    22,
      high_null_asynchrony:   12,
      null:                   89,
    },
    connections_ranking: [
      { respondent_id: 17, name: "...", high_synchrony: 14, ... },
      // top N por high_synchrony recebidas
    ],
    relations_by_attribute_1: {
      "Engenharia": { high_synchrony: 142, ... },
      "Produto":    { ... },
    },
    inter_relations_by_attribute_1: {
      "Engenharia × Produto": { high_synchrony: 23, ... },
      // pares ordenados de grupos distintos
    },
    attribute_details: { /* breakdown por participante */ },
  },
  question_2: { ... }, // mesma forma
  // ...
}

O graph-processor devolve uma forma diferente, focada em desenho:

Resposta resumida (graph-processor) typescript
{
  nodes: [
    { id: 1, label: "Ana",   attributes: { area: "Eng", ... }, originalId: 4287 },
    { id: 2, label: "Bruno", attributes: { ... },              originalId: 4288 },
    // IDs sequenciais 1..N (ver Cap. 12)
  ],
  edges: {
    question_1: [
      { source: 1, target: 2, connection: "high_synchrony", color: "#1f9d55" },
      // ...
    ],
    question_2: [ ... ],
  },
  legend: {
    high_synchrony:      { label: "Sincronia alta",  color: "#1f9d55" },
    mid_synchrony:       { ... },
    // ...
  },
  respondentStats: {
    "1": { in_degree: 8.4, out_degree: 5.1, high_synchrony: 12, ... },
  },
}
Capítulo 12

Performance & limites práticos

O custo do Neurocalc é dominado por dois fatores: o número de pares ( no número de respondentes ativos) e o fan-out por atributo (cada atributo multiplica o trabalho de agregação por K valores distintos).

Tamanho da pesquisa Pares (N²) Tempo típico (cold start) Recomendação
50 respondentes2.500~300 msSem cuidados
200 respondentes40.000~1,2 sCache obrigatório no segundo acesso
500 respondentes250.000~6 sPré-aquecer cache antes do horário de pico
1.000+ respondentes1.000.000+> 25 sConsiderar paralelizar por pergunta
Atenção

Lambda Function URL tem timeout fixo em 30s (independente do timeout configurado na função). Pesquisas com mais de 800 respondentes podem estourar — sintoma: o frontend recebe 504 sem mensagem clara. Mitigação atual: regenerar o cache de madrugada ou usar uma escala intermediária de invalidação.

Memória & pagination

As Lambdas paginam survey_responses em blocos de 500 linhas para respeitar o limite de payload do Supabase (~6MB por resposta REST). Para uma pesquisa de 200 respondentes, isso significa 80 round-trips, somando ~400ms de I/O — não é o gargalo principal, mas vale lembrar ao auditar logs.

Capítulo 13

Quirks importantes

Pesquisa sem respondentes
Tanto dashboard.ts quanto graph.ts retornam um JSON mínimo (estrutura vazia) sem lançar erro. O frontend trata como "sem dados".
Falha de cache não bloqueia resposta
Os UPSERT em survey_cache e o upload do dataset bruto para Storage estão em try/catch separados — a resposta HTTP devolve o JSON mesmo se o cache falhar. Erros aparecem só nos logs do CloudWatch.
Remap de IDs no gráfico
O graph-processor remapeia os IDs originais dos respondentes para uma sequência 1..N antes de devolver os nós, para que o gráfico mostre identificadores curtos e estáveis independentemente do respondent.id real. Mantenha o mapeamento ao depurar correspondências respondente ↔ nó.
RPC get_survey_cache ambíguo
Há sobrecargas com tipos ambíguos. O graph-processor evita o RPC e faz SELECT direto na tabela. Replicando essa escolha você economiza uma rodada de debug.
Modos do Excel
reportType: 'full' gera todas as abas (uma por pergunta + agregados). reportType: 'executive' gera versão enxuta com só os agregados principais.