{"_id":"@aylonmuramatsu/flow-pipes","_rev":"3-0658982103ad045751bf0d118445adde","name":"@aylonmuramatsu/flow-pipes","dist-tags":{"latest":"1.1.0"},"versions":{"1.0.0":{"name":"@aylonmuramatsu/flow-pipes","version":"1.0.0","keywords":["flow","pipeline","workflow","orchestration","typescript","issues","parallel"],"author":{"name":"Aylon Muramatsu"},"license":"MIT","_id":"@aylonmuramatsu/flow-pipes@1.0.0","maintainers":[{"name":"aylonmuramatsu","email":"aylon.muramatsu@gmail.com"}],"homepage":"https://github.com/aylonmuramatsu/flow-pipes#readme","bugs":{"url":"https://github.com/aylonmuramatsu/flow-pipes/issues"},"dist":{"shasum":"0f806e1398c9567ed32f0b70dcffc34b74964a34","tarball":"https://registry.npmjs.org/@aylonmuramatsu/flow-pipes/-/flow-pipes-1.0.0.tgz","fileCount":6,"integrity":"sha512-6US8CdkNJgljxXkdS5gGatVpNmLG4kf8BkE/lExA81Z2A+cVF1sZOefHgnGVG+pNcNot+4dPA1qqt7ZOwqVnvA==","signatures":[{"sig":"MEUCIQDVGLXoG41nEHBe+ZaassErtkm3cy3RpNP4XgftZ74cHAIgRjn2Lk9F6migotRJ1w7kCHOUM31MD7raxh1FsrRW04g=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":52794},"main":"./index.js","types":"./index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./index.d.ts","default":"./index.js","require":"./index.js"}},"_npmUser":{"name":"aylonmuramatsu","email":"aylon.muramatsu@gmail.com"},"repository":{"url":"git+https://github.com/aylonmuramatsu/flow-pipes.git","type":"git"},"_npmVersion":"11.8.0","description":"Motor de pipelines tipado com contexto compartilhado, Issues e execução paralela","directories":{},"sideEffects":false,"_nodeVersion":"24.13.1","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/flow-pipes_1.0.0_1784856478934_0.6475231394434682","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@aylonmuramatsu/flow-pipes","version":"1.0.1","keywords":["flow","pipeline","workflow","orchestration","typescript","issues","parallel"],"author":{"name":"Aylon Muramatsu"},"license":"MIT","_id":"@aylonmuramatsu/flow-pipes@1.0.1","maintainers":[{"name":"aylonmuramatsu","email":"aylon.muramatsu@gmail.com"}],"homepage":"https://github.com/aylonmuramatsu/flow-pipes#readme","bugs":{"url":"https://github.com/aylonmuramatsu/flow-pipes/issues"},"dist":{"shasum":"10d3da38dd1d4ea6395749a0f652096854318c55","tarball":"https://registry.npmjs.org/@aylonmuramatsu/flow-pipes/-/flow-pipes-1.0.1.tgz","fileCount":7,"integrity":"sha512-CtdINxNle9qBFgotlDNCktbAlg9GI3GMSF/OlP4etcSjvlTZqkbxXd8cOxw3jUu9UHgfGE+ZgC+zY9+qShXyAQ==","signatures":[{"sig":"MEYCIQDEWuP3zSdrCbS+ODrpC1sP23r1gKZykIvAbRknM/eaAwIhAM2JVRiW1otDtnk2ihpDMkBO7SGYg5sYP3C4bhc3XWuV","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":71515},"main":"./index.js","types":"./index.d.ts","module":"./index.mjs","engines":{"node":">=18"},"exports":{".":{"types":"./index.d.ts","import":"./index.mjs","default":"./index.mjs","require":"./index.js"}},"_npmUser":{"name":"aylonmuramatsu","email":"aylon.muramatsu@gmail.com"},"repository":{"url":"git+https://github.com/aylonmuramatsu/flow-pipes.git","type":"git"},"_npmVersion":"11.8.0","description":"Motor de pipelines tipado com contexto compartilhado, Issues e execução paralela","directories":{},"sideEffects":false,"_nodeVersion":"24.13.1","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/flow-pipes_1.0.1_1784861186765_0.14523511056394023","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@aylonmuramatsu/flow-pipes","version":"1.1.0","description":"Motor de pipelines tipado com contexto compartilhado, Issues e execução paralela","license":"MIT","author":{"name":"Aylon Muramatsu"},"keywords":["flow","pipeline","workflow","orchestration","typescript","issues","parallel"],"engines":{"node":">=18"},"sideEffects":false,"main":"./index.js","module":"./index.mjs","types":"./index.d.ts","exports":{".":{"types":"./index.d.ts","import":"./index.mjs","require":"./index.js","default":"./index.mjs"}},"repository":{"type":"git","url":"git+https://github.com/aylonmuramatsu/flow-pipes.git"},"homepage":"https://github.com/aylonmuramatsu/flow-pipes#readme","bugs":{"url":"https://github.com/aylonmuramatsu/flow-pipes/issues"},"publishConfig":{"registry":"https://registry.npmjs.org/","access":"public"},"_id":"@aylonmuramatsu/flow-pipes@1.1.0","_nodeVersion":"24.13.1","_npmVersion":"11.8.0","dist":{"integrity":"sha512-7Y4vImN2dAZEXAu0zF62lcue+TA3WxC9FSEohMMOwIt2/QNM1EaoaHqRQOFeIZJhTQWD58CEWaMuYKglq9xr3A==","shasum":"590f3bbdea64e96dd3f4e2178ccb72630b646c9a","tarball":"https://registry.npmjs.org/@aylonmuramatsu/flow-pipes/-/flow-pipes-1.1.0.tgz","fileCount":9,"unpackedSize":134639,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQD5qkwX9P81ijTlfek6wjLLXUuj1SE4md/zoNLNXXs0qAIgEVR8fstxWxBiC9Fppl6UG11aYCtDi4rgQmLrNC7epew="}]},"_npmUser":{"name":"aylonmuramatsu","email":"aylon.muramatsu@gmail.com"},"directories":{},"maintainers":[{"name":"aylonmuramatsu","email":"aylon.muramatsu@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/flow-pipes_1.1.0_1784901273030_0.6363564891983886"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-24T01:27:58.776Z","modified":"2026-07-24T13:54:33.308Z","1.0.0":"2026-07-24T01:27:59.094Z","1.0.1":"2026-07-24T02:46:26.896Z","1.1.0":"2026-07-24T13:54:33.161Z"},"bugs":{"url":"https://github.com/aylonmuramatsu/flow-pipes/issues"},"author":{"name":"Aylon Muramatsu"},"license":"MIT","homepage":"https://github.com/aylonmuramatsu/flow-pipes#readme","keywords":["flow","pipeline","workflow","orchestration","typescript","issues","parallel"],"repository":{"type":"git","url":"git+https://github.com/aylonmuramatsu/flow-pipes.git"},"description":"Motor de pipelines tipado com contexto compartilhado, Issues e execução paralela","maintainers":[{"name":"aylonmuramatsu","email":"aylon.muramatsu@gmail.com"}],"readme":"# Flow Pipes\n\n**Languages:** [English](./README.md) · **Português (Brasil)**\n\n> Por que criar services e fluxos complexos não pode ser divertido e simples — como uma pipeline?\n\nCansou de service que só “conversa” na base do `throw`? A gente também.\n\n**Flow Pipes** é um motor de pipelines tipado pra Node.js / TypeScript. A ideia é quase ofensiva de tão simples: monta uma **esteira de etapas**, compartilha estado sem drama, trata erro de negócio como **Issue** (sem derrubar o fluxo por birra) e **você** decide quando a festa acaba.\n\nMenos try/catch em cascata. Mais código que dá pra ler sem café intravenoso.\n\n## Instalação (a parte chata, rápida)\n\n```bash\nyarn add @aylonmuramatsu/flow-pipes\n```\n\nSó pede **Node.js >= 18**. Sem ritual.\n\n---\n\n## Novidades 1.1.0\n\n| Feature | Uso |\n| ------- | --- |\n| `ctx.issue({ ... })` | objeto → a lib cria a classe (sem declarar Issue no app) |\n| `.step([a, b, c])` | vários steps em sequência sem encadear |\n| `.when(pred, step\\|steps)` / `{ when }` | step condicional (`skipped` na trail) |\n| `.each(..., { fatalScope })` | `\"flow\"` (default) para o lote; `\"item\"` só o item |\n| `ctx.stop({ reason, code })` | freio com `stopInfo` (≠ Fatal) |\n| `issue.code` + metadata tipada | Issues genéricas + `code` first-class |\n| `FieldRequiredIssue` | `message` + metadata livre; default `Error` |\n| `traceId` + `durationMs` | correlação do run + duração na trail |\n| `retry` no step | só em **throw**; fixed / exponential |\n| `run(input, { signal, timeoutMs, traceId })` | abort / timeout → `ABORTED` |\n\n---\n\n## Por que isso existe (spoiler: dor de verdade)\n\n### Exception não é contrato de negócio\n\nQuando um service só fala com o outro via exception, qualquer caminho “silencioso” vira festival de try/catch. Você queria processar pedido; terminou de bombeiro de stack trace às 18h47.\n\nAqui o domínio **anota Issues** no contexto. Você processa. No final: commit, rollback, resposta parcial ou “pode parar, pessoal”.\n\n### Excel com 800 linhas e a regra é “não pode morrer na primeira”\n\nJá viveu isso? Regras por campo, precisa **passar por tudo**, fazer staging — e só no fim cancelar os inserts e devolver **quais erros** atrapalharam a festa.\n\n“Estoura na linha 2” não é estratégia. É preguiça com stack. `.each` + Issues por linha + decisão no fim: aí sim.\n\n### `if` perdido no meio do service? A gente sente\n\nValidação espalhada é esconde-esconde de bug. Cada preocupação vira um **`step`**. Issue soft? Segue o baile. `FatalIssue` / `ctx.stop()`? Agora encerra **de propósito**, não por acidente.\n\n### Uma “ação” que você reaproveita de verdade (não aquele ctrl+c emocionado)\n\nAgrupa operações num Flow. Outro service chama a mesma ação. Integração deixa de ser fanfic copiada e vira composição. Seu eu do futuro vai te mandar um obrigado (ou pelo menos não vai te xingar no code review).\n\n### Shared: adeus variável global disfarçada de “tô só passando a ref”\n\nSem gambiarra de parâmetro em parâmetro. O **`shared`** é o estado do fluxo. Steps leem e escrevem ali. O motor cuida de `ctx.result` (trail, falhas). Domínio de um lado, fofoca do motor do outro.\n\n### Paralelo quando o relógio aperta\n\n`.parallel` pra fan-out de I/O. `.each({ concurrency })` pra lote com workers. `Flow.runAll` pra vários fluxos independentes. Otimiza sem inventar worker pool toda sprint como se fosse hobby.\n\n### Mental model invertido (e bem melhor)\n\n1. Monta a esteira\n2. Processa\n3. Olha Issues / `stopped` / `result`\n4. **Aí** commit, rollback ou responde o cliente\n\nPrimeiro o trabalho. Depois o veredito. Tipo série boa: sem spoiler no episódio 1.\n\n---\n\n## Conceitos essenciais (versão café)\n\n| Peça                        | Em uma frase                                          |\n| --------------------------- | ----------------------------------------------------- |\n| `Flow`                      | Sua esteira — a “ação” completa                       |\n| `step`                      | Uma etapa, uma responsabilidade. Sem novela mexicana. |\n| `ctx.input`                 | O que entrou nesse step (no `.each`, é o item da vez) |\n| `ctx.shared`                | Onde o domínio mora durante o rolê                    |\n| `ctx.issue(...)`            | “Anota aí: deu ruim de negócio”                       |\n| `Issue`                     | Problema de domínio. Não é exception de fantasia.     |\n| `FatalIssue` / `ctx.stop({ reason?, code? })` | “Pode parar, pessoal.” (+ `stopInfo`) |\n| `ctx.traceId` / trail `durationMs` | Correlação do run + quanto cada step demorou |\n| `ctx.result`                | O que o motor viu depois do `run`                     |\n| `ctx.stopped` / `stopInfo`  | Alguém puxou o freio (e por quê)                      |\n| `.each` / `fatalScope`      | Um filho por item; fatal no item ou no lote           |\n| `.when` / `retry`           | Condicional e re-tentativa em throw                   |\n| `.parallel` / `concurrency` | Mais rápido, menos fila no caixa                      |\n\nSteps se falam pelo `shared`. O motor monta o `result` no final. Cada um no seu quadrado.\n\n---\n\n## Import (o kit básico do herói)\n\n```ts\nimport {\n  Flow,\n  FlowContext,\n  FatalIssue,\n  BusinessRuleIssue,\n  FieldRequiredIssue,\n  Severity,\n  Issue,\n  buildFailureReport,\n  collectAllIssues,\n  collectTrail,\n} from \"@aylonmuramatsu/flow-pipes\";\n```\n\n---\n\n## Issues, validations e rules (o kit completo)\n\nAqui mora o coração da lib: **não jogue exception pra tudo**. Modele o problema, registre, continue (ou pare) com intenção.\n\n### O caminho feliz: objeto no `ctx.issue` (sem classe no seu app)\n\nA **classe** é obrigação da lib. Você passa o descritor:\n\n```ts\nctx.issue({\n  type: \"FieldRequired\", // ou omitir (= BusinessRule); custom: \"MinAge\", \"InsufficientStock\"\n  message: \"Email não preenchido.\",\n  code: \"FIELD_REQUIRED\",\n  metadata: { path: \"email\" },\n});\n\nctx.issue({\n  type: \"Fatal\",\n  message: \"Lock negado.\",\n  code: \"JOB_LOCKED\",\n  metadata: { jobId },\n});\n\n// filtrar sem instanceof:\nctx.hasCode(\"FIELD_REQUIRED\");\nctx.findByCode(\"MIN_AGE\");\nctx.findByType(\"MinAge\");\n\n// built-ins ainda batem com classe (se precisar):\nctx.has(FieldRequiredIssue);\n```\n\n`new FieldRequiredIssue(...)` continua válido — opcional, avançado.\n\n### Anatomia de uma Issue\n\nToda Issue:\n\n1. **estende** a classe abstrata `Issue`\n2. declara um **`severity`** (`info` | `warning` | `error` | `fatal`)\n3. passa **`message` + `metadata`** no `super`\n4. ganha de brinde: `toJSON()`, `createdAt`, `isFatal()`, `code?`, `name` = nome da classe\n\n```ts\nimport { Issue, Severity } from \"@aylonmuramatsu/flow-pipes\";\n\nexport class MinAgeIssue extends Issue {\n  public readonly severity = Severity.Error;\n\n  constructor(age: number, line: number) {\n    super(\"Cliente menor de idade.\", { code: \"MIN_AGE\", age, line, minAge: 18 });\n  }\n}\n```\n\n`metadata` é o seu JSON de bolso pro front, log e suporte. Coloque `path`, `field`, `sku`, `code` — o que for útil depois.  \nSe `metadata.code` for string, vira também `issue.code` (e aparece no `toJSON()`).\n\n### Severidade: o que cada uma _quer dizer_\n\n| Severity  | Clima             | Flow continua?                                         |\n| --------- | ----------------- | ------------------------------------------------------ |\n| `info`    | “Só avisando”     | Sim                                                    |\n| `warning` | “Hmm, olha isso”  | Sim                                                    |\n| `error`   | “Regra quebrada”  | Sim (até você dar `stop()`)                            |\n| `fatal`   | “Sem recuperação” | **Não** — `ctx.issue(fatal)` já chama `stop()` sozinho |\n\nDuas alavancas, dois papéis:\n\n- **`severity`** → relatório, filtro, HTTP mapping\n- **`ctx.stop({ reason, code })`** → “não rode o próximo step” + `stopInfo` tipável\n\n`FatalIssue` / `severity === fatal` puxa o freio automático (com `code` da Issue). Nas outras, **você** decide se só anota ou se encerra.\n\n### Issues padrão (tempero de cozinha)\n\n| Issue                | Quando usar                                                                 |\n| -------------------- | --------------------------------------------------------------------------- |\n| `FieldRequiredIssue` | Campo obrigatório / não preenchido — `message` + metadata livre (tipável) |\n| `BusinessRuleIssue`  | Regra genérica (“menor de idade”, “CPF duplicado”) — também tipável       |\n| `FatalIssue`         | Lock negado, estado impossível, “não tem como seguir” — também tipável    |\n\n```ts\nnew FieldRequiredIssue(\"Email não preenchido.\", {\n  code: \"FIELD_REQUIRED\",\n  path: \"email\",\n});\n\n// Excel ainda funciona — você escolhe o shape:\nnew FieldRequiredIssue(\"O campo 'cpf' é obrigatório.\", {\n  code: \"FIELD_REQUIRED\",\n  line: 3,\n  field: \"cpf\",\n});\n\n// Soft em import parcial:\nnew FieldRequiredIssue(\"Coluna opcional vazia.\", { field: \"notes\" }, Severity.Warning);\n```\n\nÓtimos pra começar. Ruins pra virar deus de mil nomes — quando a regra tem identidade (estoque, gateway, KYC), **crie a classe dela**.\n\n### Criando Issues custom (receitas do mundo real)\n\n```ts\nimport { Issue, Severity } from \"@aylonmuramatsu/flow-pipes\";\n\n/** Validação de formato / domínio tipado */\nexport class InvalidCpfIssue extends Issue {\n  public readonly severity = Severity.Error;\n  constructor(cpf: string, line: number) {\n    super(\"CPF inválido.\", { cpf, line, code: \"INVALID_CPF\" });\n  }\n}\n\n/** Regra de negócio com contexto rico */\nexport class InsufficientStockIssue extends Issue {\n  public readonly severity = Severity.Error;\n  constructor(sku: string, requested: number, available: number) {\n    super(`Estoque insuficiente para ${sku}.`, {\n      sku,\n      requested,\n      available,\n      code: \"INSUFFICIENT_STOCK\",\n    });\n  }\n}\n\n/** Aviso que não bloqueia (relatório / compliance) */\nexport class SuspiciousAmountIssue extends Issue {\n  public readonly severity = Severity.Warning;\n  constructor(amount: number) {\n    super(\"Valor fora do padrão histórico.\", {\n      amount,\n      code: \"SUSPICIOUS_AMOUNT\",\n    });\n  }\n}\n\n/** Integração externa sem volta */\nexport class PaymentGatewayIssue extends Issue {\n  public readonly severity = Severity.Fatal;\n  constructor(gatewayCode: string, raw?: unknown) {\n    super(`Gateway recusou (${gatewayCode}).`, {\n      gatewayCode,\n      raw,\n      code: \"PAYMENT_GATEWAY\",\n    });\n  }\n}\n\n/** Política / autorização */\nexport class ForbiddenOperationIssue extends Issue {\n  public readonly severity = Severity.Fatal;\n  constructor(action: string, userId: string) {\n    super(`Operação não permitida: ${action}.`, {\n      action,\n      userId,\n      code: \"FORBIDDEN\",\n    });\n  }\n}\n\nexport class DuplicateCpfIssue extends Issue {\n  public readonly severity = Severity.Error;\n  constructor(cpf: string, line: number) {\n    super(`CPF duplicado: ${cpf}`, { cpf, line, code: \"DUPLICATE_CPF\" });\n  }\n}\n```\n\nDica de ouro: um campo `code` estável no `metadata` faz o front/i18n/monitoramento felizes. A `message` pode ser humana; o `code` é contrato.\n\n### Padrões de validation (steps que só julgam)\n\nValidação boa é **chata e previsível**: lê `input` / `shared`, emite Issue, às vezes marca `metadata`, às vezes dá `stop()`. Não grava no banco. Não chama gateway. Só julga, igual a vizinha na janela! brincadeirinha! haha.\n\n#### 1) Acumular tudo (ótimo pra formulário / Excel)\n\nNão para no primeiro erro — junta a lista completa pra devolver de uma vez.\n\n```ts\nasync function validateCustomer(ctx: FlowContext<CustomerInput, Shared>) {\n  const { name, email, document } = ctx.input;\n\n  if (!name)\n    ctx.issue(\n      new FieldRequiredIssue(\"Nome não preenchido.\", {\n        code: \"FIELD_REQUIRED\",\n        path: \"name\",\n      }),\n    );\n  if (!email)\n    ctx.issue(\n      new FieldRequiredIssue(\"Email não preenchido.\", {\n        code: \"FIELD_REQUIRED\",\n        path: \"email\",\n      }),\n    );\n  if (!document)\n    ctx.issue(\n      new FieldRequiredIssue(\"Documento não preenchido.\", {\n        code: \"FIELD_REQUIRED\",\n        path: \"document\",\n      }),\n    );\n  if (document && !isValidCpf(document)) {\n    ctx.issue(new InvalidCpfIssue(document, 0));\n  }\n\n  if (ctx.issues.length > 0) {\n    ctx.metadata.set(\"invalid\", true);\n    ctx.stop({\n      code: \"VALIDATION_STOP\",\n      reason: \"validation_failed\",\n    });\n  }\n}\n```\n\n#### 2) Soft no `.each` (linha zoada não mata o lote)\n\n```ts\nasync function validateRow(ctx: FlowContext<ExcelRow, ImportShared>) {\n  const row = ctx.input;\n\n  if (!row.name) {\n    ctx.issue(new FieldRequiredIssue(\"O campo 'name' é obrigatório.\", { code: \"FIELD_REQUIRED\", line: row.line, field: \"name\" }));\n  }\n  if (row.age != null && row.age < 18) {\n    ctx.issue(new MinAgeIssue(row.age, row.line));\n  }\n  if (row.cpf && ctx.shared.seenCpfs.has(row.cpf)) {\n    ctx.issue(new DuplicateCpfIssue(row.cpf, row.line));\n  } else if (row.cpf) {\n    ctx.shared.seenCpfs.add(row.cpf);\n  }\n\n  if (ctx.issues.length > 0) {\n    ctx.metadata.set(\"invalid\", true);\n    // sem stop no filho → próximas linhas seguem\n  }\n}\n\nasync function stageIfValid(ctx: FlowContext<ExcelRow, ImportShared>) {\n  if (ctx.metadata.get(\"invalid\")) return;\n  // staging / insert “provisório”\n}\n```\n\n#### 3) Fail-fast quando o resto não faz sentido\n\n```ts\nasync function ensureJobLock(ctx) {\n  if (!(await tryLock(ctx.input.jobId))) {\n    ctx.issue(new FatalIssue(\"job already locked\", { jobId: ctx.input.jobId }));\n    // FatalIssue já dá stop() — próximos steps nem acordam\n  }\n}\n```\n\n#### 4) Pipeline de validation (vários steps, uma responsabilidade cada)\n\n```ts\nFlow.create<Shared>()\n  .shared({ ... })\n  .step(validateRequiredFields)   // presence\n  .step(validateFormats)          // CPF, email, UUID\n  .step(validateBusinessRules)    // idade, estoque, status\n  .step(validatePermissions)      // authz\n  .step(persist)\n  .run(input);\n```\n\nCada step pequeno = testável, reutilizável, sem novela de 200 linhas.\n\n### Padrões de rules (regras de negócio)\n\nRule ≠ “if solto”. Rule é **decisão de domínio** com Issue nomeada (e, se precisar, efeito no `shared`).\n\n```ts\nasync function applyDiscountRules(ctx: FlowContext<CartInput, CheckoutShared>) {\n  const { coupon, total } = ctx.shared;\n\n  if (coupon && coupon.expired) {\n    ctx.issue(\n      new BusinessRuleIssue(\"Cupom expirado.\", { code: \"COUPON_EXPIRED\" }),\n    );\n    ctx.shared.coupon = null; // regra também limpa estado\n    return;\n  }\n\n  if (coupon && total < coupon.minAmount) {\n    ctx.issue(\n      new BusinessRuleIssue(\"Cupom exige valor mínimo.\", {\n        code: \"COUPON_MIN_AMOUNT\",\n        minAmount: coupon.minAmount,\n        total,\n      }),\n    );\n    return;\n  }\n\n  if (total > 10_000) {\n    ctx.issue(new SuspiciousAmountIssue(total)); // warning: segue, mas fica no radar\n  }\n}\n\nasync function reserveStock(ctx: FlowContext<OrderInput, OrderShared>) {\n  for (const item of ctx.shared.items) {\n    const available = await stock.of(item.sku);\n    if (available < item.qty) {\n      ctx.issue(new InsufficientStockIssue(item.sku, item.qty, available));\n    }\n  }\n  if (ctx.has(InsufficientStockIssue)) {\n    ctx.stop(); // não cobra, não emite NF\n  }\n}\n```\n\n### Consultando Issues depois do `run` (o buffet)\n\n```ts\nconst ctx = await flow.run(input);\n\nctx.has(FieldRequiredIssue); // boolean\nctx.find(FieldRequiredIssue); // lista tipada\nctx.first(BusinessRuleIssue); // a primeira\nctx.count(InvalidCpfIssue); // quantas\nctx.issues; // todas deste contexto\ncollectAllIssues(ctx); // pai + children (`.each`)\n\n// HTTP / API\nif (ctx.has(FieldRequiredIssue) || ctx.metadata.get(\"invalid\")) {\n  return {\n    status: 400,\n    body: { issues: collectAllIssues(ctx).map((i) => i.toJSON()) },\n  };\n}\n\nif (ctx.stopped) {\n  return {\n    status: 422,\n    body: {\n      failure: ctx.result?.failure ?? buildFailureReport(ctx),\n      issues: collectAllIssues(ctx).map((i) => i.toJSON()),\n    },\n  };\n}\n```\n\n`toJSON()` já serializa `type`, `severity`, `message`, `metadata`, `createdAt` — pronto pra log e response.\n\n### Monte o Flow como “casos de uso”\n\nTrês jeitos que funcionam bem em time real:\n\n```ts\n// 1) Factory da ação (reuso entre HTTP, fila, cron)\nexport function importCustomersFlow(db: Db) {\n  return Flow.create<ImportShared>()\n    .shared({ db, rows: [], seenCpfs: new Set(), staged: [] })\n    .step(readFile)\n    .each((c) => c.shared.rows, [validateRow, applyRowRules, stageRow])\n    .step(decideCommitOrStop);\n}\n\n// 2) Validations puras + rules + side-effects separados\nFlow.create()\n  .step(validateInput) // presence + format\n  .step(applyDomainRules) // estoque, cupom, KYC\n  .step(chargePayment) // I/O\n  .step(persistOrder);\n\n// 3) Soft no lote + hard no final\nFlow.create()\n  .each((c) => c.shared.rows, [validateRow, stageRow])\n  .step(async (ctx) => {\n    if (collectAllIssues(ctx).some((i) => i.severity === \"error\")) {\n      ctx.shared.errors = collectAllIssues(ctx).map((i) => i.toJSON());\n      ctx.stop(); // chamador: rollback\n    }\n  });\n```\n\n### Mini-mapa: quando usar o quê\n\n| Situação                          | O que fazer                                              |\n| --------------------------------- | -------------------------------------------------------- |\n| Campo vazio                       | `FieldRequiredIssue` (ou custom de campo)                |\n| Formato inválido                  | Issue tipada (`InvalidCpfIssue`, …)                      |\n| Regra de domínio                  | `BusinessRuleIssue` ou Issue dedicada                    |\n| Dá pra seguir, mas quero rastrear | `Severity.Warning` + sem `stop`                          |\n| Não faz sentido continuar         | `FatalIssue` **ou** Issue + `ctx.stop()`                 |\n| Lote: uma linha ruim              | Issue no filho, `metadata.invalid`, **sem** stop no each |\n| Lote: no fim cancela tudo         | `collectAllIssues` + `stop` no step final                |\n| Lib externa estourou              | deixa throwar — runner vira Fatal + `result.failure`     |\n| Resposta HTTP 400 rica            | `find` / `collectAllIssues` + `toJSON()`                 |\n\n---\n\n## Exemplos (cole, adapte, seja feliz)\n\nIsso aqui é **referência**, não um reality show com botão “Run”. Copia pro seu projeto e faz a mágica.\n\n### 1. Flow básico — “oi, mundo”, mas com atitude\n\n```ts\ninterface Shared {\n  messages: string[];\n}\n\nasync function greet(ctx: FlowContext<{ name: string }, Shared>) {\n  if (!ctx.input.name) {\n    ctx.issue(new FieldRequiredIssue(\"Nome não preenchido.\", { code: \"FIELD_REQUIRED\", path: \"name\" }));\n    ctx.stop();\n    return;\n  }\n\n  ctx.shared.messages.push(`Olá, ${ctx.input.name}`);\n}\n\nconst ctx = await Flow.create<Shared>()\n  .shared({ messages: [] })\n  .step(greet)\n  .run({ name: \"Ana\" });\n\nconsole.log(ctx.shared.messages);\nconsole.log(ctx.result);\n```\n\n### 2. Service / API — porque o HTTP também merece esteira\n\nLib externa estourou? **O processo Node não cai junto.** O runner vira `FatalIssue` e te devolve o contexto pra você responder com classe (ou com 503, sem julgamentos).\n\n```ts\ninterface CreateOrderInput {\n  customerId: string;\n  amount: number;\n}\n\ninterface OrderShared {\n  total: number;\n  orderId?: string;\n}\n\nasync function validate(ctx: FlowContext<CreateOrderInput, OrderShared>) {\n  if (!ctx.input.customerId) {\n    ctx.issue(new FieldRequiredIssue(\"customerId obrigatório.\", { code: \"FIELD_REQUIRED\", path: \"customerId\" }));\n    ctx.stop();\n  }\n}\n\nasync function charge(ctx: FlowContext<CreateOrderInput, OrderShared>) {\n  if (ctx.input.amount < 0) {\n    throw new Error(\"Gateway indisponível\");\n  }\n  ctx.shared.total = ctx.input.amount;\n}\n\nasync function persist(ctx: FlowContext<CreateOrderInput, OrderShared>) {\n  ctx.shared.orderId = `ord_${Date.now()}`;\n}\n\nexport class CreateOrderService {\n  async execute(input: CreateOrderInput) {\n    const ctx = await Flow.create<OrderShared>()\n      .shared({ total: 0 })\n      .step(validate)\n      .step(charge)\n      .step(persist)\n      .run(input);\n\n    if (ctx.has(FieldRequiredIssue)) {\n      return {\n        status: 400,\n        body: { issues: ctx.find(FieldRequiredIssue).map((i) => i.toJSON()) },\n      };\n    }\n\n    if (ctx.stopped || ctx.has(FatalIssue)) {\n      return {\n        status: 503,\n        body: {\n          error: ctx.result?.failure?.message ?? ctx.first(FatalIssue)?.message,\n        },\n      };\n    }\n\n    return {\n      status: 201,\n      body: { data: ctx.shared },\n    };\n  }\n}\n```\n\n### 3. Importação com relatório — o clássico “processa tudo, chora depois”\n\nValida o lote, faz staging, e só no fim: rollback + “olha a lista do que deu errado”.\n\n```ts\ninterface Row {\n  line: number;\n  name: string;\n  cpf: string;\n}\n\ninterface Shared {\n  rows: Row[];\n  staged: Row[];\n  errors: unknown[];\n}\n\nasync function loadRows(ctx) {\n  ctx.shared.rows = ctx.input.rows;\n}\n\nasync function validateRow(ctx) {\n  const row = ctx.input as Row;\n  if (!row.name) {\n    ctx.issue(new FieldRequiredIssue(\"O campo 'name' é obrigatório.\", { code: \"FIELD_REQUIRED\", line: row.line, field: \"name\" }));\n  }\n  if (!row.cpf) {\n    ctx.issue(new FieldRequiredIssue(\"O campo 'cpf' é obrigatório.\", { code: \"FIELD_REQUIRED\", line: row.line, field: \"cpf\" }));\n  }\n}\n\nasync function stageIfValid(ctx) {\n  if (ctx.issues.length) return;\n  ctx.shared.staged.push(ctx.input as Row);\n}\n\nasync function decide(ctx) {\n  const issues = collectAllIssues(ctx);\n  if (issues.length > 0) {\n    ctx.shared.errors = issues.map((i) => i.toJSON());\n    ctx.stop();\n  }\n}\n\nconst ctx = await Flow.create<Shared>()\n  .shared({ rows: [], staged: [], errors: [] })\n  .step(loadRows)\n  .each((c) => c.shared.rows, [validateRow, stageIfValid])\n  .step(decide)\n  .run({\n    rows: [\n      /* ... */\n    ],\n  });\n\nif (ctx.stopped) {\n  // rollback + manda ctx.shared.errors pro cliente\n} else {\n  // commit de ctx.shared.staged e vai tomar café\n}\n```\n\n### 4. Lote com `.each` — um por um, sem drama coletivo\n\nCada item ganha um contexto filho (`ctx.children`) dividindo o mesmo `shared`. Uma linha zoada não precisa cancelar o passeio inteiro.\n\n```ts\ninterface Row {\n  id: string;\n  name: string;\n}\n\ninterface Shared {\n  rows: Row[];\n  saved: string[];\n}\n\nasync function saveRow(ctx: FlowContext<Row, Shared>) {\n  if (!ctx.input.name) {\n    ctx.issue(new FieldRequiredIssue(\"Nome não preenchido.\", { code: \"FIELD_REQUIRED\", path: \"name\" }));\n    return; // só essa linha fica de fora\n  }\n  ctx.shared.saved.push(ctx.input.id);\n}\n\nconst ctx = await Flow.create<Shared>()\n  .shared({\n    rows: [\n      { id: \"1\", name: \"Ana\" },\n      { id: \"2\", name: \"\" },\n      { id: \"3\", name: \"Bruno\" },\n    ],\n    saved: [],\n  })\n  .each((c) => c.shared.rows, [saveRow], {\n    name: \"persistRows\",\n    concurrency: 4, // opcional; default 1 (modo zen)\n  })\n  .run();\n\nconsole.log(ctx.shared.saved);\nconsole.log(ctx.children.map((c) => c.issues));\n```\n\n### 5. `.parallel` — porque esperar em fila é coisa do século passado\n\nDica de ouro: **chaves distintas** no `shared` (`profile`, `wallet`, `score`). Senão vira briga de vizinho no mesmo endereço.\n\n```ts\ninterface Shared {\n  profile?: { name: string };\n  wallet?: { balance: number };\n  score?: { value: number };\n}\n\nasync function fetchProfile(ctx: FlowContext<{ userId: string }, Shared>) {\n  ctx.shared.profile = { name: `User ${ctx.input.userId}` };\n}\n\nasync function fetchWallet(ctx: FlowContext<{ userId: string }, Shared>) {\n  ctx.shared.wallet = { balance: 1500 };\n}\n\nasync function fetchScore(ctx: FlowContext<{ userId: string }, Shared>) {\n  ctx.shared.score = { value: 820 };\n}\n\nconst ctx = await Flow.create<Shared>()\n  .shared({})\n  .parallel([fetchProfile, fetchWallet, fetchScore], { name: \"enrich\" })\n  .run({ userId: \"u-42\" });\n\nconsole.log(ctx.shared);\n```\n\n### 6. `Flow.runAll` — vários flows, um só “vai”\n\n```ts\nconst userFlow = Flow.create().step(async (ctx) => {\n  /* ... */\n});\nconst billingFlow = Flow.create().step(async (ctx) => {\n  /* ... */\n});\n\nconst results = await Flow.runAll([\n  { name: \"UserService\", run: () => userFlow.run(input) },\n  { name: \"BillingService\", run: () => billingFlow.run(input) },\n]);\n// default \"settle\" — um tropeço não cancela o resto do show\n\n// modo dramático (fail-fast):\nawait Flow.runAll(tasks, { mode: \"all\" });\n```\n\n### 7. Ação reutilizável — escreve uma vez, usa em todo canto\n\n```ts\nexport function createCustomerFlow() {\n  return Flow.create<CustomerShared>()\n    .shared({ user: null, wallet: null })\n    .step(validatePayload)\n    .step(insertUser)\n    .step(createWallet)\n    .step(linkAddress);\n}\n\n// HTTP\nconst ctx = await createCustomerFlow().run(req.body);\nif (ctx.stopped) return res.status(422).json(ctx.result);\n\n// fila / outro contexto — mesma ação, zero copy-paste emocionado\nawait createCustomerFlow().run(payloadFromQueue);\n```\n\n### 8. Soft vs fatal — você no volante, não o destino\n\n```ts\nasync function lockJob(ctx) {\n  if (!(await tryLock(ctx.input.jobId))) {\n    ctx.issue(new FatalIssue(\"job already locked\"));\n    ctx.stop();\n  }\n}\n\nasync function softValidate(ctx) {\n  if (ctx.input.warning) {\n    ctx.issue(new BusinessRuleIssue(\"linha suspeita\")); // segue andando, de olho aberto\n  }\n}\n```\n\n### 9. Quando o inesperado aparece (e você quer saber _onde_)\n\n```ts\nconst ctx = await flow.run(input);\n\nif (ctx.stopped) {\n  const report = ctx.result?.failure ?? buildFailureReport(ctx);\n  // report.location → step / eachIndex\n  // report.error.stack → o vilão\n  // report.trail → o que já tinha dado certo antes do plot twist\n  console.error(report);\n}\n```\n\n---\n\n## Controle do runner (1.1.0)\n\n```ts\nFlow.create<Shared>()\n  .shared({ rows: [], invalid: false })\n  .step(loadRows)\n  .each(\n    (c) => c.shared.rows,\n    [\n      validateRow,\n      { run: stageRow, when: (c) => !c.metadata.get(\"invalid\") },\n    ],\n    { fatalScope: \"item\" }, // FatalIssue/throw só mata o item\n  )\n  .when((c) => !c.stopped, decideCommit)\n  .step(callGateway, {\n    name: \"callGateway\",\n    retry: { attempts: 3, delayMs: 100, backoff: \"exponential\" },\n  })\n  .run({ file: \"clientes.xlsx\" }, { timeoutMs: 30_000, traceId: \"req-42\" });\n\n// ctx.traceId, ctx.stopInfo, ctx.result.trail[].durationMs\n```\n\n`fatalScope: \"flow\"` (default) propaga Fatal do item pro lote. `\"item\"` continua as próximas linhas.\n\n---\n\n## API resumida (cola na parede do time)\n\n```ts\nFlow.create<TShared>()\n  .shared({ ... })\n  .step(fn, { name?, when?, retry? })\n  .step([a, b, { run, name?, when?, retry? }])\n  .when(pred, fn | [steps], { name?, retry? })\n  .each(resolver, steps, { name?, concurrency?, fatalScope?: \"flow\" | \"item\" })\n  .parallel(steps, { name? })\n  .run(input?, { signal?, timeoutMs?, traceId? })\n\nFlow.runAll([{ name, run }], { mode?: \"settle\" | \"all\" })\n\nctx.stop({ reason?, code? })   // → stopInfo\nctx.traceId\nctx.signal                     // AbortSignal do run\nctx.result.trail[].durationMs\nissue.code                     // espelho de metadata.code\n```\n\n`retry`: só em **throw** (não em Issue). Ex.: `{ attempts: 3, delayMs: 50, backoff: \"exponential\" }`.\n\nNome do step: `options.name` → `fn.name` → arquivo → `\"anonymous\"`.\n`\"anonymous\"` é o vilão silencioso do log. Dê nome às suas funções — elas merecem.\n\n---\n\n## Ideias de onde plugar isso\n\n| Cenário                  | Por que encaixa (além do feeling)                       |\n| ------------------------ | ------------------------------------------------------- |\n| Import Excel / CSV / XML | Todas as linhas, Issues, rollback + relatório bonitinho |\n| Cadastro multi-etapa     | Uma ação; falhou no meio → stop + rollback no chamador  |\n| Jobs / filas             | Lock, soft fail vs fatal, status “quase”                |\n| Enriquecer entidade      | `.parallel` em APIs externas sem drama                  |\n| Sync em lote             | `.each` + `concurrency` sem inventar roda               |\n| Orquestrar services      | O Flow vira o caso de uso tipado                        |\n| API com erro rico        | `collectAllIssues` / `buildFailureReport`               |\n| Validação em pipeline    | Steps pequenos no lugar de `if`s nômades                |\n\n---\n\n## O que isso **não** é (combinado?)\n\n- Não aposenta `try/catch` de infraestrutura. Rede caiu? Bug esquisito? Exception ainda existe — o runner localiza e joga em `ctx.result`.\n- Não é BPMN nem aquele workflow eterno que vive num banco esquecido. É esteira **em processo**: leve, tipada, no seu service/job/script.\n- Não esconde o domínio: negócio no `shared`, fofoca do motor no `result`.\n\n---\n\n## Licença\n\nMIT — use, abuse (com carinho) e construa fluxos que você ainda entenda daqui a seis meses.\n\nDivirta-se na esteira. 🚂\n","readmeFilename":"README.pt-BR.md"}