Uma função auxiliar passa a retornar segundos em vez de milissegundos. Os argumentos e o tipo retornado continuam iguais. Um chamador deixa de solicitar atualização, outro continua ordenando corretamente e uma coluna CSV fica silenciosamente mil vezes menor. Uma lista de referências não informa, sozinha, quais desses resultados ocorreram.

Suponha uma aplicação que lista sessões, decide quando atualizá-las e exporta suas idades. É um exemplo construído para aprendizado; os resultados abaixo vêm da execução do JavaScript com entradas fixas e asserções inalteradas. Baixe o exemplo completo para reproduzi-los.

Defina a mudança antes de listar chamadores

O download mantém duas versões da função em elapsed.mjs, permitindo executar os mesmos consumidores com ambas:

export function elapsedBefore(nowMs, startedMs) {
  return nowMs - startedMs;
}

export function elapsedAfter(nowMs, startedMs) {
  return (nowMs - startedMs) / 1000;
}

As duas entradas continuam sendo timestamps em milissegundos. O retorno proposto é em segundos, incluindo frações: uma idade de 2_500 milissegundos vira 2.5, não 2. Com nowMs = 60_000 e startedMs = 15_000, o resultado muda de 45_000 para 45.

Isso torna a pergunta precisa: quais consumidores dependem da unidade antiga e quais dependem apenas de uma propriedade preservada pela conversão? Uma verificação de tipos que vê números nas duas versões não responde.

Se as entradas forem desconhecidas, primeiro siga um valor desde sua origem pelo código. Mudar a unidade de saída e mudar a unidade de entrada são edições distintas; este exemplo muda apenas a saída.

Use referências para montar uma lista de candidatos

Busque elapsed no editor ou execute:

rg -n 'elapsed' --glob '*.mjs' .

Abra consumers.mjs. Dois chamadores merecem atenção:

export function needsRefresh(elapsed, nowMs, lastSyncMs) {
  return elapsed(nowMs, lastSyncMs) >= 30_000;
}

export function oldestFirst(elapsed, nowMs, sessions) {
  return [...sessions]
    .sort((a, b) => elapsed(nowMs, b.startedMs) - elapsed(nowMs, a.startedMs))
    .map((session) => session.id);
}

needsRefresh compara o valor com um limite fixo. oldestFirst compara dois valores produzidos pela mesma função. Marque ambos como candidatos e registre o que prometem aos chamadores: atualizar ao atingir trinta segundos e retornar IDs da sessão mais antiga à mais nova sem alterar o array de entrada.

Referências do servidor de linguagem ajudam a navegar quando ele está instalado e configurado. Trate os resultados como pistas. Aliases, funções injetadas, registros e configuração em execução ainda exigem inspeção; a visão de referências não inventaria todos os consumidores que uma implantação pode selecionar.

Siga a função através do registro

app.mjs passa a função para além dos dois consumidores diretos. Seleciona uma fábrica pela configuração e fornece a ela a função com outro nome:

const factory = registry.get(config.exporter);
if (!factory) throw new Error(`Unknown exporter: ${config.exporter}`);
const exporter = factory({ measureAge: elapsed });

Siga register(registry) antes, no mesmo arquivo. Nos modos before e after, chama registerAgeColumn, implementada em age-column.mjs:

export function registerAgeColumn(registry) {
  registry.set('age-column', ({ measureAge }) => ({
    kind: 'age-column',
    rows(nowMs, sessions) {
      return [
        'id,age_seconds',
        ...sessions.map(
          (session) =>
            `${session.id},${measureAge(nowMs, session.startedMs) / 1000}`,
        ),
      ].join('\n');
    },
  }));
}

O cabeçalho CSV promete segundos. Com a função antiga, dividir por 1000 os produz. Com a nova, a divisão acontece duas vezes.

Buscar apenas elapsed não revela a chamada measureAge(...) neste arquivo. Amplie a busca para o alias e a chave de registro após descobri-los:

rg -n 'measureAge|age-column|exporter' --glob '*.mjs' .

A aplicação de teste seleciona explicitamente { exporter: 'age-column' } e confere selectedExporter. Isso conecta o registro ao caminho realmente executado. Encontrar uma fábrica registrada sem conferir a seleção deixaria seu uso indefinido. Em um servidor, o trabalho equivalente pode começar pela rota e configuração que selecionam o caminho da requisição.

Execute as mesmas expectativas nas duas versões

Descompacte e entre em code-change-impact-analysis. Não há dependências externas. A execução registrada usou Node.js 22.22.1; use Node.js 22 ou posterior e execute os comandos separadamente no terminal:

node run.mjs before
node run.mjs after
node run.mjs migrated

Espera-se que after saia com código 1. Não una os comandos com &&, pois isso interromperia antes da versão migrada. Os três modos carregam as mesmas asserções de tests.mjs.

Versão Passaram Falharam Código de saída
before: função em milissegundos, consumidores originais 7 0 0
after: função em segundos, consumidores originais 4 3 1, falha esperada
migrated: função em segundos, consumidores dependentes de unidade atualizados 7 0 0

As três falhas em after são as decisões de atualização em 30_000 e 31_000 milissegundos e a saída CSV. O caso logo abaixo do limite continua passando. Os dois testes de ordenação passam, assim como a rejeição de um exportador desconhecido.

Para examinar os valores junto às asserções, execute:

node demo.mjs

Com nowMs = 60_000, a sessão alpha começa em 15_000 e beta em 57_500. As observações impressas incluem:

Observação before after migrated
Atualização aos 30 segundos true false true
IDs do mais antigo ao mais novo alpha, beta alpha, beta alpha, beta
Idade CSV de alpha 45 0.045 45
Idade CSV de beta 2.5 0.0025 2.5

demo.mjs imprime observações; tests.mjs decide se satisfazem o comportamento esperado. O padrão node --test run.mjs executa apenas before, então uma execução padrão verde não valida a proposta.

Separe consumidores quebrados dos testados sem regressão

O resultado é mais útil do que uma contagem de referências:

Consumidor ou área Constatação Evidência
needsRefresh Afetado, confirmado Os testes inalterados do limite de trinta segundos falham com a função em segundos.
oldestFirst Regressão descartada para as entradas testadas Idades distintas, frações, empates, diferenças de um milissegundo e listas vazias ou unitárias mantêm a ordem; a entrada não muda.
Exportador age-column selecionado Afetado nesta configuração, confirmado O teste seleciona a fábrica e compara o CSV completo. As duas idades ficam mil vezes menores após a mudança.
Outras configurações ou consumidores fora do download Desconhecido Não foram fornecidos nem executados.

Há também uma razão para a ordenação: a comparação usa a diferença entre duas idades. Dividir ambas pelo mesmo fator positivo preserva o sinal para essas entradas. Ela precisa da ordem relativa; o limite de atualização e o exportador precisam de uma unidade específica.

Esse raciocínio ajuda a escolher testes. Não permite afirmar que todos os números possíveis foram verificados. As fixtures usam timestamps inteiros pequenos e finitos em milissegundos. Valores extremos de ponto flutuante, entradas inválidas, agendamento assíncrono de atualização e um leitor CSV real ficam fora da execução. O download contém um exportador; não afirma cobrir um inventário de plugins de produção.

Migre os consumidores dependentes da unidade

Mantenha elapsedAfter. Em consumers.mjs, a função de atualização migrada compara segundos com segundos:

export function needsRefreshSeconds(elapsed, nowMs, lastSyncMs) {
  return elapsed(nowMs, lastSyncMs) >= 30;
}

Em age-column.mjs, registerAgeColumnSeconds emite measureAge(...) diretamente, removendo a segunda divisão. app.mjs escolhe essas duas implementações quando o modo é migrated. oldestFirst permanece inalterada.

Os testes continuam exigindo os mesmos resultados visíveis: atualização aos trinta segundos, os mesmos IDs ordenados e segundos no CSV. As sete verificações finais passando mostram que esses resultados foram restaurados para a fixture. Se seu patch também muda resultados esperados, revise as asserções junto à implementação antes de considerar a execução verde equivalente.

Use a IA para sugerir o próximo arquivo

Durante a leitura, uma pergunta opcional pode restringir a busca:

Siga a função passada como elapsed, incluindo aliases e fábricas registradas. Para cada consumidor candidato, mostre o código que presume uma unidade ou usa apenas ordenação. Identifique a seleção em execução e a entrada de teste que permitem verificar a consequência. Separe chamadores não testados dos resultados executados.

Confira os caminhos propostos no código e execute os comandos.