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§
$ 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çõesop.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'.
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:
| Sonda | Mede | Exemplo | Onde 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étodo | O que mede | Exemplo |
|---|---|---|
toBeWithin(min, max) | valor DC dentro do intervalo fechado | tb.expect.op('D1.i').toBeWithin('8m', '12m') |
toBeCloseTo(v, tol) | valor DC igual a v com tolerância ± tol | tb.expect.op('out').toBeCloseTo(5, 0.01) |
toBeAbove(v) | valor DC acima de v | tb.expect.op('R1.v').toBeAbove(1.2) |
toBeBelow(v) | valor DC abaixo de v | tb.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étodo | O que mede | Exemplo |
|---|---|---|
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§
| Faixa | Fase | Significado |
|---|---|---|
| F1xx | lint (pré-solver) | topologia estrutural |
| S1xx | solver linear | sistema insolúvel |
| S2xx | solver não-linear | Newton-Raphson |
| A3xx | asserções | spec não atingido |
| T0xx | ferramenta | erro de autoria da bancada |
Códigos ativos§
| Código | Slug | Severidade | Quando dispara |
|---|---|---|---|
| F101 | floating-net | error | net sem caminho DC até o ground (isolada por capacitor/fonte de corrente) |
| F103 | unreached-component | error | componente (ou ilha) sem conexão com o circuito aterrado — seria silenciosamente ignorado pelo solver |
| F104 | voltage-source-short / voltage-source-loop | error | fonte ideal em curto, ou loop de fontes ideais (indutor conta como fonte 0 V em DC) |
| F105 | source-clamped-diode | error | diodo/LED polarizado direto grampeado numa fonte ideal sem resistência em série |
| F106 | dangling-pin | warning | pino sem nenhuma conexão |
| S101 | unsolvable-system | error | matriz singular que o lint não modelou (raro; reportar) |
| S201 | nr-nonconvergence | error | NR não convergiu no ponto DC — carrega componente culpado, ΔV e histórico de oscilação |
| S202 | nr-nonconvergence | error | NR não convergiu num passo do transiente |
| A301 | assertion-failed | fail | asserção falhou — carrega medido, esperado, margem e hint verificado quando há tb.param |
| T001 | bench-error | error | a própria bancada lançou exceção na elaboração |
| T002 | stage-crash | error | um 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ódigo | Significado | Ação do agente |
|---|---|---|
0 | todos os estágios pedidos passaram | pronto |
1 | asserção falhou (circuito válido) | ajustar valores (use o hint!) ou topologia |
2 | circuito inválido/insolúvel (F/S com severidade error) | consertar a estrutura antes de tudo |
3 | erro 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. settleWithinque nunca estabiliza reportameasured= fim da janela (finito) — nuncaInfinity, que virarianullno JSON.- O
hint.passingRangecobre 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 empassingSamples.
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.
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):
$ 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).
| Tipo | Exemplo | Pinos | Valor | Comportamento 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:
| Fase | Entrega | Por quê |
|---|---|---|
v0.2 | Estágio L2 (sweep/curvas de transferência), forge sweep, forge probe sobre o ledger | completa a escada de verificação em estágios |
v0.3 | Relatório com orçamento de tokens (failures-only + drill-down), --watch com timeout duro de solve | iteração sub-segundo; nunca travar o tool call do agente |
v0.4 | Servidor MCP (forge mcp) com o mesmo schema do CLI | o agente usa como ferramenta nativa |
v0.5 | forge optimize (Nelder-Mead sobre a escada), gmin/source stepping no NR | agente nunca mais escolhe resistor na mão |
v0.6 | UI = viewer do RunRecord: chips de asserção no schematic, bandas no osciloscópio, timeline do ledger | humano e agente veem o mesmo artefato |
v0.7 | Determinismo forte, export SPICE + cross-check ngspice em CI | credibilidade numérica pública |
v0.8 | MOSFET nível 1, BJT Ebers-Moll, fontes comportamentais, macro-modelo de op-amp | destrava specs reais |
v1.0 | CircuitBench: eval público de "iterações-até-verde" por modelo; freeze do schema forge-run/1.0 | o padrão vence porque os agentes comprovadamente convergem nele |