Seções

Documentação

Código vira circuito — e a verificação fecha o loop.

Início rápido§

O Circuit Forge é um simulador de circuitos analógicos baseado em código que roda 100% no navegador, sem backend. Você descreve o circuito em TypeScript — com pinos nomeados como anode/cathode e +/- — e o motor numérico (Modified Nodal Analysis + Newton-Raphson, análises DC e transiente) resolve tudo localmente. O editor visual em canvas, com probes e osciloscópio, é uma projeção do código, nunca o contrário.

Além do simulador, existe o forge: um loop de verificação pensado para agentes LLM (e humanos). Uma bancada .bench.ts descreve circuito + spec; o CLI roda a escada lint → op → tran e devolve diagnósticos com código estável (tipo F105), localização no código-fonte, hints de dimensionamento verificados por simulação e exit codes com semântica — para que um agente convirja para um circuito funcional em poucas iterações, sem parsear texto solto.

Como rodar§

terminal
$ npm install
$ npm run dev                                          # app em http://localhost:5173
$ npm test                                             # suíte Vitest
$ npm run forge -- verify bench/led-driver.bench.ts    # escada de verificação

Primeiro circuito no simulador§

No editor de código do simulador (aba Simulador), este divisor de tensão é o "olá, mundo": crie o circuito, conecte os pinos, analise e leia os resultados no console.

const circuit = new Circuit();
const gnd = new Ground();

const v1 = new VoltageSource(10);
const r1 = new Resistor(1000);
const r2 = new Resistor(2000);

v1.pin('+').connect(r1.pin('1'));
r1.pin('2').connect(r2.pin('1'));
r2.pin('2').connect(gnd.pin('1'));
v1.pin('-').connect(gnd.pin('1'));

circuit.analyze('dc');

console.log('V_out (R2):', r2.voltage.toFixed(3), 'V');
console.log('Corrente:', (r1.current * 1000).toFixed(3), 'mA');

Você deve ver V_out (R2): 6.667 V — o divisor 1k/2k sobre 10 V. A partir daí, abra os exemplos prontos ou vá direto para as bancadas.

Bancadas§

Uma bancada (arquivo .bench.ts) é o artefato completo de verificação: circuito + parâmetros livres + asserções. A função bench(nome, fn) recebe um testbench builder tb com esta API:

  • tb.add(id, componente) — registra um componente com id estável ('R1', 'D1'…). Esse id é a identidade nos diagnósticos, nas sondas e no --set.
  • tb.gnd — o nó de referência (terra). Todo circuito precisa de caminho DC até ele.
  • tb.name(nome, pin) — dá nome estável a um net (ex.: 'out') para poder sondá-lo em asserções op.
  • tb.param(id, {min, max, scale}) — delegação de dimensionamento: declara o parâmetro livre. LLMs são bons em topologia e ruins em dimensionamento contínuo; quando uma asserção falha, a plataforma varre a faixa, simula cada ponto e devolve a faixa que passa (hint verificado) — o agente nunca chuta números.
  • tb.expect.op(...) / tb.expect.tran(...) — as asserções: o spec como teste.

Unidades§

Valores aceitam número ou string com sufixo de engenharia: '4.7k', '10m', '2u', '1M', '1e-6'.

Atenção: m ≠ M — m é mili (10⁻³) e M é mega (10⁶). '10m' são 10 mΩ/mA/ms; '1M' é 1 MΩ.

bench/led-driver.bench.ts comentado§

import { bench, VoltageSource, Resistor, LED } from 'circuit-forge';

/**
 * Driver de LED: fonte de 5 V, resistor em série, LED para o terra.
 * O "olá, mundo" canônico da escada de verificação — passa como está.
 */
export default bench('led-driver', (tb) => {
  // ids estáveis: são a identidade nos diagnósticos, sondas e --set
  const v1 = tb.add('V1', new VoltageSource(5));
  const r1 = tb.add('R1', new Resistor('330'));
  const d1 = tb.add('D1', new LED());

  // pinos nomeados eliminam a classe de erro mais comum dos LLMs
  v1.pin('+').connect(r1.pin('1'));
  r1.pin('2').connect(d1.pin('anode'));
  d1.pin('cathode').connect(tb.gnd);
  v1.pin('-').connect(tb.gnd);

  // net nomeado: agora 'led' é endereçável nas asserções op
  tb.name('led', d1.pin('anode'));

  // dimensionamento é da plataforma: se uma asserção op falhar,
  // ela varre esta faixa e devolve um hint VERIFICADO por simulação
  tb.param('R1', { min: '100', max: '10k', scale: 'log' });

  // o spec como teste
  tb.expect.op('D1.i').toBeWithin('5m', '15m'); // LED aceso, mas seguro
  tb.expect.op('led').toBeWithin(1.8, 2.6);     // queda direta plausível
});

Rode com npm run forge -- verify bench/led-driver.bench.ts. Há exemplos quebrados de propósito em bench/broken/ para ver cada diagnóstico em ação.

Sondas§

Toda asserção mede uma sonda. A gramática é curta:

SondaMedeExemploOnde vale
'<id>.i'corrente através do componente'D1.i'op e tran
'<id>.v'tensão sobre o componente'R1.v'op e tran
'<net>'tensão de um net nomeado via tb.name(...)'out'apenas op

Nets automáticos (n1, n2, …) não são endereçáveis de propósito: a numeração muda quando você adiciona ou remove componentes, então uma sonda 'n3' quebraria silenciosamente entre edições. Nomeie o net com tb.name('out', pin) para garantir estabilidade — e por isso o padrão n<dígitos> é reservado e não pode ser usado como nome de net.

tb.name('out', r1.pin('2'));          // nomeia o net do pino
tb.expect.op('out').toBeCloseTo(2.5, 0.1); // agora é sondável

Asserções§

Estágio op — ponto de operação DC§

Formato: tb.expect.op(sonda).método(...).

MétodoO que medeExemplo
toBeWithin(min, max)valor DC dentro do intervalo fechadotb.expect.op('D1.i').toBeWithin('8m', '12m')
toBeCloseTo(v, tol)valor DC igual a v com tolerância ± toltb.expect.op('out').toBeCloseTo(5, 0.01)
toBeAbove(v)valor DC acima de vtb.expect.op('R1.v').toBeAbove(1.2)
toBeBelow(v)valor DC abaixo de vtb.expect.op('D1.i').toBeBelow('20m')

Estágio tran — transiente§

Formato: tb.expect.tran({tstop, dt}).probe(sonda).método(...) — encadeável, com múltiplas sondas e checagens na mesma janela.

MétodoO que medeExemplo
toSettleWithin({target, tol, by})o sinal estabiliza em target ± tol (fração) até o instante by; mede o tempo de acomodação.toSettleWithin({target: 5, tol: 0.05, by: '4m'})
toNeverExceed(v)o sinal nunca ultrapassa v em toda a janela (overshoot/pico).toNeverExceed('25m')
toStayAbove(v, {from})o sinal permanece acima de v (opcionalmente a partir de from).toStayAbove(4.5, {from: '2m'})
toStayBelow(v, {from})o sinal permanece abaixo de v (opcionalmente a partir de from).toStayBelow('6m')
toRippleBelow(amp, {from})a amplitude pico-a-pico do ripple fica abaixo de amp (a partir de from).toRippleBelow('50m', {from: '3m'})
toEndCloseTo(v, tol)o valor final da janela é v ± tol.toEndCloseTo(5, 0.1)

Exemplo real, de bench/rc-lowpass.bench.ts (R = 1k, C = 1µ, τ = 1 ms):

tb.expect
  .tran({ tstop: '6m', dt: '10u' })
  .probe('C1.v')
  .toSettleWithin({ target: 5, tol: 0.05, by: '4m' })
  .toNeverExceed(5.5)
  .toEndCloseTo(5, 0.1)
  .probe('R1.i')
  .toStayBelow('6m'); // corrente de inrush limitada por V/R

Diagnósticos§

Todo erro tem código estável (nunca renumerado — parsers e agentes podem depender dele), localização no código-fonte e correção. forge explain <código> traz a explicação longa com a física por trás de cada um.

Faixas§

FaixaFaseSignificado
F1xxlint (pré-solver)topologia estrutural
S1xxsolver linearsistema insolúvel
S2xxsolver não-linearNewton-Raphson
A3xxasserçõesspec não atingido
T0xxferramentaerro de autoria da bancada

Códigos ativos§

CódigoSlugSeveridadeQuando dispara
F101floating-neterrornet sem caminho DC até o ground (isolada por capacitor/fonte de corrente)
F103unreached-componenterrorcomponente (ou ilha) sem conexão com o circuito aterrado — seria silenciosamente ignorado pelo solver
F104voltage-source-short / voltage-source-looperrorfonte ideal em curto, ou loop de fontes ideais (indutor conta como fonte 0 V em DC)
F105source-clamped-diodeerrordiodo/LED polarizado direto grampeado numa fonte ideal sem resistência em série
F106dangling-pinwarningpino sem nenhuma conexão
S101unsolvable-systemerrormatriz singular que o lint não modelou (raro; reportar)
S201nr-nonconvergenceerrorNR não convergiu no ponto DC — carrega componente culpado, ΔV e histórico de oscilação
S202nr-nonconvergenceerrorNR não convergiu num passo do transiente
A301assertion-failedfailasserção falhou — carrega medido, esperado, margem e hint verificado quando há tb.param
T001bench-errorerrora própria bancada lançou exceção na elaboração
T002stage-crasherrorum estágio (op/tran) quebrou após a elaboração — bug de plataforma; o RunRecord ainda é emitido (exit 3)

Contrato de confiança dos fixes§

Todo fix carrega confidence:

verified

A plataforma simulou a correção e ela passa em todas as asserções do estágio. O agente deve aplicar diretamente.

mechanical

Transformação determinística (não simulada, mas sem julgamento).

suggested

Heurística; o agente deve raciocinar antes de aplicar.

Invariantes (aplicados em code review): nunca emitir fix que referencia flag ou capacidade que não existe; nunca emitir número não simulado sem rótulo suggested.

Exit codes§

CódigoSignificadoAção do agente
0todos os estágios pedidos passarampronto
1asserção falhou (circuito válido)ajustar valores (use o hint!) ou topologia
2circuito inválido/insolúvel (F/S com severidade error)consertar a estrutura antes de tudo
3erro de ferramenta (uso incorreto, bancada lançou)consertar a bancada/invocação

Notas de semântica§

  • O lint sempre roda (mesmo com --stage op): resolver um circuito que o lint rejeitaria significaria descartar componentes silenciosamente e reportar números fabricados.
  • settleWithin que nunca estabiliza reporta measured = fim da janela (finito) — nunca Infinity, que viraria null no JSON.
  • O hint.passingRange cobre apenas a maior sequência contígua de amostras aprovadas (cada amostra passa no lint E em todas as asserções op); os valores exatos simulados estão em passingSamples.

CLI§

O forge roda via npm run forge -- <comando>. A saída é determinística — sem timestamps e sem cores no texto —, então dois runs idênticos fazem diff limpo.

forge — comandos
USAGE
  forge verify <bench.ts> [opções]    # roda a escada: lint → op → tran
  forge log list [--json]             # lista runs do ledger (.forge/runs.jsonl)
  forge log show <runId> [--json]     # RunRecord completo de um run passado
  forge explain <código>              # docs de um diagnóstico, ex.: forge explain F101
  forge catalog [tipo] [--json]       # datasheet dos componentes + gramática

OPÇÕES DO VERIFY
  --stage op,tran        # roda só estes estágios (o lint SEMPRE roda)
  --set R1=330,V1=5      # sobrescreve valores sem editar a bancada
  --json                 # emite o RunRecord completo em JSON no stdout
  --no-hints             # pula hints verificados de dimensionamento (mais rápido)
  --no-ledger            # não grava este run em .forge/runs.jsonl

Sessão completa: fail com hint → --set → pass§

Com bench/broken/led-overcurrent.bench.ts — resistor de série pequeno demais (LED a ~28 mA contra spec de 8–12 mA):

sessão forge
$ npm run forge -- verify bench/broken/led-overcurrent.bench.ts

bench led-overcurrent  netlist cb4f1e998fc8  ([email protected])

  ✓ lint  pass
  ✗ op    fail
        A301 a1: D1.i within [8m, 12m] — measured 28.04m (outside by 16.04m, 2.34x)
          at bench/broken/led-overcurrent.bench.ts:22
          hint (verified): R1 in [261, 316.2] passes all op assertions — try R1 = 261
  – tran  skipped (op assertions failed)

FAIL · 1 problem · first failure: a1 · exit 1
run cb4f1e998fc8-001 → .forge/runs.jsonl (forge log show cb4f1e998fc8-001)

$ npm run forge -- verify bench/broken/led-overcurrent.bench.ts --set R1=280

bench led-overcurrent  netlist cb4f1e998fc8  set R1=280  ([email protected])

  ✓ lint  pass
  ✓ op    pass
        1 assertion passed

PASS · exit 0

Exit code 1 disse "circuito válido, spec não atingido"; o hint verificado deu a faixa que passa; --set R1=280 aplicou a correção sem editar código. Para agentes, --json devolve o mesmo conteúdo como RunRecord estruturado, e forge log list mantém o histórico do que já foi tentado fora do contexto (anti context attrition).

Componentes§

O catálogo abaixo é o mesmo que forge catalog devolve — o "datasheet" que o agente consulta. Valores aceitam número ou string com unidades ('4.7k', '10m', '2u', '1M'; lembre: m = mili, M = mega).

TipoExemploPinosValorComportamento DC
Resistor tb.add('R1', new Resistor('4.7k')) '1', '2' resistência em ohms (finita, > 0); aceita string com unidades conduz; varrível via tb.param
Capacitor tb.add('C1', new Capacitor('10u')) '1', '2' capacitância em farads (finita, > 0) ABERTO em DC — nets isolados atrás dele precisam de caminho DC (ver F101); varrível
Inductor tb.add('L1', new Inductor('10m')) '1', '2' indutância em henries (finita, > 0) curto de 0 V em DC — em paralelo com fonte ideal dispara F104; varrível
VoltageSource tb.add('V1', new VoltageSource(5)) '+', '-' volts DC (finito); defina .acAmplitude/.frequency para senoide no transiente fonte ideal — fontes em paralelo disparam F104; varrível
CurrentSource tb.add('I1', new CurrentSource('1m')) '+', '-' ampères (finito); a corrente flui para o pino + externamente não fixa tensões de nó — nó alimentado só por ela flutua (F101); varrível
Diode tb.add('D1', new Diode({ Is: 1e-14, n: 1 })) 'anode', 'cathode' parâmetros de Shockley {Is, n, Vt}; não-linear (Newton-Raphson) precisa de resistência em série ou o NR pode não convergir (S201)
LED tb.add('D1', new LED()) 'anode', 'cathode' diodo com defaults de LED (~2 V de queda direta em correntes de mA) SEMPRE use resistor em série; sonde D1.i para a corrente de operação
Switch tb.add('SW1', new Switch(false)) 'com', 'a', 'b' SPDT: closed=false roteia com→a, closed=true roteia com→b; .toggle() conduz (canal 1 mΩ fechado / 1 TΩ aberto)
Button tb.add('BT1', new Button(false)) '1', '2' momentâneo: conduz enquanto .pressed === true conduz (1 mΩ pressionado / 1 TΩ solto)

Além dos built-in, componentes customizados podem ser definidos via defineComponent({ pins, params, stamp, onStep, ... }) — com estado interno e flag nonlinear — como no exemplo MCU + LEDs.

Roadmap§

O v0.1 já entrega o loop completo: motor MNA validado (DC + transiente, Newton-Raphson), DSL bench(), lint estrutural pré-solver, hints verificados por simulação e o CLI com ledger. Daqui em diante:

FaseEntregaPor quê
v0.2Estágio L2 (sweep/curvas de transferência), forge sweep, forge probe sobre o ledgercompleta a escada de verificação em estágios
v0.3Relatório com orçamento de tokens (failures-only + drill-down), --watch com timeout duro de solveiteração sub-segundo; nunca travar o tool call do agente
v0.4Servidor MCP (forge mcp) com o mesmo schema do CLIo agente usa como ferramenta nativa
v0.5forge optimize (Nelder-Mead sobre a escada), gmin/source stepping no NRagente nunca mais escolhe resistor na mão
v0.6UI = viewer do RunRecord: chips de asserção no schematic, bandas no osciloscópio, timeline do ledgerhumano e agente veem o mesmo artefato
v0.7Determinismo forte, export SPICE + cross-check ngspice em CIcredibilidade numérica pública
v0.8MOSFET nível 1, BJT Ebers-Moll, fontes comportamentais, macro-modelo de op-ampdestrava specs reais
v1.0CircuitBench: eval público de "iterações-até-verde" por modelo; freeze do schema forge-run/1.0o padrão vence porque os agentes comprovadamente convergem nele