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.
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.
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).
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. |
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.
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.
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 emhigh_synchrony;(0, 1)cai emlow_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 |
|---|---|
| null | Ambos os lados responderam 0. Sem percepção de relação. |
| low_synchrony | Concordam em baixa intensidade. |
| mid_synchrony | Concordam em intensidade média. |
| high_synchrony | Concordam em alta intensidade — núcleo da rede. |
| high_low_asynchrony | Um vê alta, o outro vê baixa. |
| mid_null_asynchrony | Um vê média, o outro não vê relação. |
| high_null_asynchrony | Um 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:
|5 − 1| = 4, então cai no ramo assincrônico.Math.abs(5 − 1) * 2 + 3 − Math.max(5, 1) = 4 * 2 + 3 − 5 = 6.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.
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 dá.
- in_degree
-
Soma de
(answerTS − answerST)quando positivo. Quanto os outros me avaliam acima do que eu os avalio. Altoin_degreeindica reconhecimento percebido externamente. - out_degree
-
Soma de
(answerST − answerTS)quando positivo. Quanto eu avalio os outros acima do que sou avaliado. Altoout_degreeindica visão generosa ou superestimação da rede. - connections_ranking
-
Ordenação dos respondentes por contagem total de
high_synchronyrecebidas — usado no card "ranking" do dashboard. - response_percentage
- Percentual de respondentes que efetivamente preencheram a pesquisa em relação ao total cadastrado.
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.
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.
// 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)
);
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.
Fluxo de dados: frontend → Lambda → Supabase
Dashboard
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
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)
- Frontend chama
dashboard-processore recebe o JSON do Neurocalc. - Esse JSON é repassado para
excel-processorjunto donome_arquivoe doreportType(fullouexecutive). - O Lambda gera o
.xlsx, faz upload para o bucketrelatoriose devolve uma signed URL com validade de 1h.
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.
|
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
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.
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 |
Deploy & dependências
Pacotes Node usados pelas Lambdas
@supabase/supabase-js— query nas tabelasmydb.*.@aws-sdk/client-secrets-manager— busca a service role key.xlsx(somente excel-processor) — gera workbook.
Variáveis de ambiente do frontend
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
SUPABASE_URL=https://<project>.supabase.co
SUPABASE_SECRET_NAME=supabase-service-key # nome do segredo no Secrets Manager
Build & empacotamento
npm run build # esbuild, marca @aws-sdk como external
npm run zip # gera function.zip pronto para upload
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.
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.
{
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:
{
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, ... },
},
}
Performance & limites práticos
O custo do Neurocalc é dominado por dois fatores: o número de pares
(N² 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 respondentes | 2.500 | ~300 ms | Sem cuidados |
| 200 respondentes | 40.000 | ~1,2 s | Cache obrigatório no segundo acesso |
| 500 respondentes | 250.000 | ~6 s | Pré-aquecer cache antes do horário de pico |
| 1.000+ respondentes | 1.000.000+ | > 25 s | Considerar paralelizar por pergunta |
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.
Quirks importantes
- Pesquisa sem respondentes
-
Tanto
dashboard.tsquantograph.tsretornam um JSON mínimo (estrutura vazia) sem lançar erro. O frontend trata como "sem dados". - Falha de cache não bloqueia resposta
-
Os
UPSERTemsurvey_cachee o upload do dataset bruto para Storage estão emtry/catchseparados — 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-processorremapeia os IDs originais dos respondentes para uma sequência1..Nantes de devolver os nós, para que o gráfico mostre identificadores curtos e estáveis independentemente dorespondent.idreal. Mantenha o mapeamento ao depurar correspondências respondente ↔ nó. - RPC
get_survey_cacheambíguo -
Há sobrecargas com tipos ambíguos. O
graph-processorevita o RPC e fazSELECTdireto 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.