Conversation
Add the OpenSpec change set describing the v1.0.0 rewrite of the nfe-io gem (zero runtime dependencies, Ruby 3.2+, parity-plus with the Node/PHP SDKs): 9 changes covering foundation, HTTP transport, OpenAPI pipeline, client core, entity/invoice/lookup resources, RTC (Reforma Tributaria) emission, and release tooling — plus openspec/project.md context, CHANGESET-OVERVIEW.md, and a multi-perspective adversarial review report (REVIEW-v1-changeset.md). Also stop ignoring openspec/ so the plan is tracked, and ignore the local sibling-repo symlinks (nfeio-docs, client-nodejs, client-php) and build artifacts (Gemfile.lock, coverage/, .env, *.gem, .steep/).
Begin the v1.0.0 rewrite of the nfe-io gem. Replaces the rest-client-based v0.3.2 (preserved on the 0.x-legacy branch); no legacy code is reused. Foundation (add-ruby-foundation): - gemspec nfe-io 1.0.0, required_ruby_version >= 3.2, ZERO runtime dependencies - Nfe::Client.new(api_key:) entry point with 17 lazy resource accessors - Nfe::Configuration multi-base-URL host map; Nfe::FlowStatus terminal helper - RBS signatures + Steep, RuboCop (+rubocop-rspec), RSpec + SimpleCov (>=80%) - GitHub Actions CI matrix (Ruby 3.2/3.3/3.4); removes legacy lib/spec/travis HTTP transport (add-http-transport): - zero-dependency Net::HTTP transport: per-origin pooled connections (mutex), TLS VERIFY_PEER, gzip, timeouts, retry with backoff+jitter + Retry-After - typed Nfe::Error hierarchy + ErrorFactory; duck-typed logger with secret redaction; bodies never logged by default OpenAPI pipeline (add-openapi-pipeline): - dev-only generator (scripts/, stdlib only) emitting immutable Data.define value objects + from_api hydration + RBS from openapi/ specs synced from nfeio-docs; rake generate / generate:check / openapi:sync - generated tree (1100 files, 11 namespaces) committed and marked linguist-generated; CI gate keeps it in sync Tooling: Docker dev/CI harness (Ruby 3.2/3.3/3.4). Verified green on the matrix: rspec (213 examples), rubocop, steep, rbs validate, and generate:check. BREAKING CHANGE: complete rewrite. The v0.3.x global API (Nfe.api_key(...), Nfe::ServiceInvoice) is removed in favor of Nfe::Client.new(api_key:). See MIGRATION.md.
Archive the three implemented-and-verified changes (green on the Ruby 3.2/3.3/3.4 matrix) and apply their deltas to the canonical specs: - add-ruby-foundation -> specs/sdk-foundation (20 requirements) - add-http-transport -> specs/http-transport (20 requirements) - add-openapi-pipeline -> specs/openapi-pipeline (16 requirements) Remaining active changes: add-client-core, add-entity-resources, add-invoice-resources, add-lookup-resources, add-rtc-invoice-emission, add-release-tooling.
…resource stubs Implement the keystone DX layer (add-client-core) wiring the HTTP transport and generated DTOs into the public client. - Nfe::Configuration: multi-base-URL host map (single source of truth) with family aliases, the two-key model (api_key_for), NFE_API_KEY/NFE_DATA_API_KEY env fallback, and ca_file/ca_path/proxy; validates -> Nfe::ConfigurationError. - Nfe::Client: per-family RetryingTransport(NetHttp) memoized under a Mutex; 17 thread-safe lazy resource accessors; internal #request raising the typed Nfe::Error (via ErrorFactory) on non-2xx (202 is a success). - Nfe::Resources::AbstractResource: get/post/put/delete, full_path, hydrate (klass.from_api), binary download, hydrate_list (page/cursor), and the discriminated 202 handle_async_response (Pending/Issued). - Nfe::RequestOptions (per-call/multi-tenant overrides), Nfe::Pending/Issued, Nfe::ListResponse/ListPage, Nfe::IdValidator. - 17 resource stubs (Nfe::Resources::*) declaring their api_family; business methods raise NotImplementedError until the resource changes fill them. - Adds Nfe::ConfigurationError / Nfe::InvoiceProcessingError to the hierarchy. Verified green on the Ruby 3.2/3.3/3.4 matrix: rspec 298/0 (~95.6% coverage), rubocop, steep, rbs validate, generate:check.
…tificate Fill the four :main-host entity resource stubs (add-entity-resources): - Nfe::Webhook (HMAC-SHA1 X-Hub-Signature verify_signature — never raises — and construct_event) + Nfe::WebhookEvent. - Nfe::CertificateValidator / CertificateInfo / CertificateStatus (OpenSSL::PKCS12). - Hand-written DTOs Nfe::Company / LegalPerson / NaturalPerson / WebhookSubscription (Data.define + from_api). - AbstractResource#upload_multipart + #unwrap. - Companies (CRUD + validate/upload/replace_certificate, certificate status, finders), LegalPeople, NaturalPeople, Webhooks (CRUD + test + get_available_events + verify_signature). Verified green on the Ruby 3.2/3.3/3.4 matrix: rspec 404/0 (~95.9% coverage), rubocop, steep, rbs validate, generate:check.
Archive the two verified changes and apply their deltas to the canonical specs: - add-client-core -> specs/client-core (20 requirements) - add-entity-resources -> specs/entity-resources (13) + webhook-signature-verification (8) Remaining active changes: add-invoice-resources, add-lookup-resources, add-rtc-invoice-emission, add-release-tooling.
Fill the five invoice resource stubs against api.nfe.io (service NFS-e, :main) and api.nfse.io (product NF-e, consumer NFC-e, transportation CT-e, inbound NF-e — all :cte): - Discriminated create: HTTP 202 -> *Pending (invoice_id from Location), HTTP 201 -> *Issued (hydrated). idempotency_key: -> Idempotency-Key header; per-call request_options: forwarded without mutating the Client. - service_invoices: create / list (page-style) / retrieve / cancel / send_email / download_pdf|xml (bytes) / get_status (derived, no extra HTTP). - product_invoices: cursor list (+environment required), state-tax create, items/events, correction letter (15-1000 chars), disable/disable_range; download_* return Nfe::NfeFileResource (URI, not bytes). - consumer_invoices: NFC-e parity-plus; downloads return bytes. - transportation_invoices + inbound_product_invoices: inbound/query surface. - Hand-written DTOs under lib/nfe/resources/dto/ (Nfe::ServiceInvoice, ProductInvoice, ConsumerInvoice, NfeFileResource, inbound settings/metadata) + discriminated response VOs under lib/nfe/resources/responses/. - Nfe::ListResponse now includes Enumerable (each -> data), completing the client-core contract the design assumed. Docs aligned to the shipped convention: hand-written DTOs live in lib/nfe/resources/dto/ as top-level Nfe::* (matching entity-resources), not the older lib/nfe/models/ / Nfe::Models:: plan. README documents the discriminated return, the download divergence and the manual polling loop. Verified green on Ruby 3.2/3.3/3.4: rspec 525/0 (~96.9% coverage), rubocop, steep, rbs validate, generate:check. Change archived + specs synced (specs/invoice-resources, 16 requirements). Section 13 (manual sandbox smoke tests) left open: opt-in, outside CI.
Fill the 8 lookup/query resource stubs across five host families
(add-lookup-resources):
- addresses (:addresses) — CEP lookup, $filter search, term lookup.
- legal_entity_lookup (:legal_entity) — basicInfo + 3 state-tax CNPJ lookups.
- natural_person_lookup (:natural_person) — CPF status (DateNormalizer for
the birth date: accepts String/Date/Time/DateTime).
- product_invoice_query (:nfe_query) — NF-e by access key + pdf/xml bytes +
events.
- consumer_invoice_query (:nfe_query) — NFC-e coupon (CFe-SAT) by access key +
xml. Distinct from consumer_invoices (emission).
- tax_calculation (:cte) — /tax-rules/{tenant}/engine/calculate (reuses the
generated CalculoImpostosV1::CalculateResponse).
- tax_codes (:cte) — 4 paginated 1-based code lists.
- state_taxes (:cte) — CRUD with the {stateTax:} envelope, cursor-style list.
Support: Nfe::DateNormalizer (stdlib date/time, ISO round-trip validation);
Nfe::ListResponse already Enumerable; all resources fail fast (no HTTP) on
invalid ids via Nfe::IdValidator (cep/cnpj/cpf/state/access_key).
Adversarial review (4 dimensions vs the openapi/* specs + Node/PHP SDKs)
caught 11 real defects the agent-written specs had masked — fixed here:
- Wrong API keys that were always nil against the live API: Address
neighborhood->district (+nested city{code,name}, numberMin/Max);
ProductInvoiceDetails status->currentStatus (+ dropped bogus top-level
accessKey); LegalEntity statusDate/companySize/openingDate/emails ->
statusOn/size/openedOn/email; TaxCoupon totals/payment/delivery/item/
additionalInformation/buyer remapped to the real CFe-SAT shape.
- LegalEntity state-tax responses given their own shape (StateTaxLegalEntity
with stateTaxes[]) instead of reusing the basicInfo DTO.
- Masking specs rewritten to assert against realistic payloads (real
camelCase keys + enum values) so mismapping can no longer hide.
- tax_calculation tenant_id coerced before strip (typed error, not NoMethodError).
Configuration: documented that :cte intentionally uses api_key (not a data
family), a deliberate divergence from the Node SDK.
Verified green on Ruby 3.2/3.3/3.4: rspec 604/0 (~97.9% coverage), rubocop,
steep, rbs validate, generate:check. Change archived + specs synced
(specs/lookup-resources, 13 requirements). Section 13 (manual sandbox smoke
tests) left open: opt-in, outside CI.
Add the two opt-in Reforma Tributária (RTC) emission resources as paridade-plus addons (Client now exposes 19 accessors: 17 canonical + 2 RTC): - service_invoices_rtc (:main, api.nfe.io/v1) — create (discriminated 202 Pending / 201 Issued), retrieve, cancel, download_cancellation_xml (ADN, post-Cancelled). Reuses the classic Nfe::ServiceInvoice DTO (the service-invoice-rtc spec defines only the NFSeRequest request shape). - product_invoices_rtc (:cte, api.nfse.io/v2) — full classic surface (create, create_with_state_tax, retrieve, cursor list, cancel, items, events, pdf/xml/rejection/epec downloads, correction letter, disable / disable_range), hydrating the generated ProductInvoiceRtcV1 DTOs (InvoiceResource, RequestCancellationResource, DisablementResource, ...). Downloads return Nfe::NfeFileResource (uri), matching the classic resource. Both RTC layouts are selected by the presence of the ibsCbs (NFS-e) / items[].tax.IBSCBS (product) group in the payload — same endpoints, no discriminator header/param. create(data:) takes a Hash with camelCase keys (generated DTOs deserialize only; the request DTO documents the shape). Generated RTC DTOs already emitted (generator auto-globs openapi/*). Adversarial review (3 dims vs the RTC openapi specs + classic resources) confirmed no shipping-code defects; it caught 2 spec-realism issues (fixed): the IBSCBS/ibsCbs fixtures used fabricated keys (cst/classificationCode, operationType "Standard") and omitted the required operationIndicator/classCode — rewritten to the real situationCode/classCode + operationIndicator so the specs are accurate regression guards and copyable usage docs. .rubocop.yml: allow `q` (API search-query param) as a short keyword name. README documents the RTC opt-in, the ibsCbs/IBSCBS layout selector and the manual polling loop. Verified green on Ruby 3.2/3.3/3.4: rspec 647/0 (~98.4% coverage), rubocop, steep, rbs validate, generate:check. Archived + specs synced (specs/rtc-invoice-emission, 16 requirements). Section 12 (create_and_wait/ batch, runtime field validation, future RTC events, tax engine) is the explicit out-of-scope record.
Finalize v1.0.0 for release (add-release-tooling): - nfe-io.gemspec: documentation_uri added; verified zero runtime deps, required_ruby_version >= 3.2, ships lib/**/*.rb + sig/**/*.rbs + README/MIGRATION/CHANGELOG/LICENSE only (no spec/samples/skills/openspec). Built gem: 1248 files, 621 .rbs, 550 generated, runtime_dependencies == []. - Rakefile: require "bundler/gem_tasks" (build/install/release). - .rubocop.yml: exclude samples/** (runnable examples). - CHANGELOG.md: Keep a Changelog + SemVer, full 1.0.0 entry (pt-BR). - MIGRATION.md: exhaustive v0.3.x -> v1.0.0 guide (config, class/method maps, errors, 202 polling, webhook HMAC-SHA1 over raw bytes, downloads, deferred features, vanilla + Rails appendices). Corrected: advanced options (open_timeout/base_url_overrides/ca_file/ca_path/proxy) live on Nfe::Configuration and are injected via configuration:, not on Client.new. - README.md: expanded (badges, requisitos, quickstart, configuração + the Configuration-injection fix, sandbox vs produção, resource map, per-resource examples, errors, downloads, webhooks, versioning, type-checking). - CONTRIBUTING.md: branches, setup, toolchain, generated-files policy, commits. - samples/: 13 runnable examples + config.rb + .env.example + README. - scripts/release.sh: version bump (dotted prerelease form), CHANGELOG rotate, gate, tag + push; --dry-run/--skip-tests/--skip-git; no local gem push. - .github/workflows/release.yml: tag-triggered verify matrix (3.2/3.3/3.4) + publish via RubyGems OIDC trusted publishing (+ GEM_HOST_API_KEY fallback), version-assertion, GitHub Release, prerelease handling. - skills/nfeio-ruby-sdk/: AI skill SKILL.md + 5 reference files. Verified green on Ruby 3.2/3.3/3.4: rspec 647/0 (~98.4% coverage), rubocop, steep, rbs validate, generate:check; gem builds clean with zero runtime deps; release.sh syntax + --help OK; release.yml valid YAML. Archived + specs synced (specs/release-tooling, 15 requirements). DEFERRED TO MAINTAINER (outward-facing / env-gated, kept unchecked in tasks): - §10 update the external nfeio-docs repo page (coordinate with docs team). - §11.6/§11.7 run samples against sandbox + full release.sh --dry-run (needs credentials + a real release env with ruby/gh on master). - §12 cut the actual v1.0.0-rc.1 -> GA release (tag push triggers publish), announce, beta period, GA cutover + README banner removal. This completes the v1.0.0 greenfield change set: all 9 OpenSpec changes implemented, verified, archived (10 capability specs).
The API key does NOT separate production vs. test — that is determined by the account configuration at https://app.nfe.io (server-side). The Client `environment:` symbol (:production/:development) is reserved for future use: today it is validated but has no effect on endpoints, keys, or behavior (confirmed against the Node SDK, where `environment` likewise does not switch prod/test). Fixed the wrong 'separation is done by the API key' claim in README, MIGRATION, the nfeio-ruby-sdk skill, and the Nfe::Configuration doc comment. The separate product/consumer `environment:` String ('Production'/ 'Test', the SEFAZ environment on list/emit) is unchanged. Code behavior unchanged (comment/doc-only); green on Ruby 3.2/3.3/3.4 (647/0).
Add a canonical docs/ tree for the SDK, authored Docusaurus 3 (MDX) ready so it
copy/pastes into nfeio-docs with no rework, plus a YARD API reference themed
with the NFE.io brand identity.
Docs tree (pt-BR, MDX-safe, :::admonitions, frontmatter on every page):
- docs/README.md — landing mirroring bibliotecas/php.md (IntegrationLayout
frontmatter: provider/badge/heroImage/ctaUrl).
- 9 guides: getting-started, configuration, async-and-polling, errors,
webhooks, pagination, downloads, multi-host-routing, rtc-emission.
- docs/resources/ cookbook — one page per resource (17 canonical + an RTC
page), with real method signatures read from lib/nfe/resources/.
- _category_.json files + sidebar_position for the Docusaurus sidebar.
- docs/MAINTAINING.md — internal: the copy/paste sync flow into nfeio-docs
(bibliotecas/ruby/ + static/api/ruby/).
Cross-links verified (0 broken); no bare </{ in prose (MDX-safe).
YARD API reference with NFE.io identity:
- docs/yard-theme/ — a --template-path override: brand CSS (Roboto, #333 text,
120deg gradient header banner with the white logo, brand-colored headings/
links/code/tags), favicon, applied to main + nav pages. Self-contained
(logos as data-URIs) so docs/api/ works after being moved into Docusaurus
static/. Brand values from the official media-kit (#7AEA53 / #5DD0F2).
- .yardopts (ASCII title — an accented title broke YARD's UTF-8 layout),
Rakefile `rake doc` (guarded YARD::Rake::YardocTask), gemspec yard dev-dep,
.gitignore docs/api/, .rubocop.yml excludes docs/.
Production vs. test is documented correctly throughout (account-side at
app.nfe.io; the Client environment: symbol is reserved for future use).
Verified green on Ruby 3.2/3.3/3.4: rspec 647/0, rubocop, steep, rbs,
generate:check; gem builds with zero runtime deps; rake doc -> 122 themed pages.
Make the published URLs explicit and pt-BR via the Docusaurus `slug` frontmatter instead of being derived from the (English) filenames — more predictable and easier to identify in external files. - Add a relative `slug:` (pt-BR) to all 9 guides + 18 cookbook pages, e.g. configuration.md -> `configuracao`, recursos/companies.md -> `empresas` (-> .../recursos/empresas). The landing (README.md) stays the section index. - Rename docs/resources/ -> docs/recursos/ so the directory segment is pt-BR too; fix the README + MAINTAINING references (lib/nfe/resources/ untouched). - recursos/_category_.json gets a generated-index (slug: recursos) so the cookbook folder has a category landing. - Document the slug convention in docs/MAINTAINING.md. Validated: 29 files, frontmatter on every page, 0 broken cross-links, 0 slug collisions, MDX-safe. Filenames stay English; slugs (frontmatter) drive the URLs.
The landing (README.md -> index.md in nfeio-docs) now carries slug: /desenvolvedores/bibliotecas/ruby, mirroring bibliotecas/nodejs/index.md, so the Ruby SDK section owns its canonical URL. Sub-pages keep relative pt-BR slugs.
…andbox)
O smoke test read-only contra o sandbox (samples/smoke_readonly.rb) revelou
três pontos em que o SDK divergia da API real — todos corrigidos e cobertos:
- addresses.lookup_by_postal_code: a API retorna { "address": {...} } (singular),
não { "addresses": [...] } (o openapi/consulta-endereco.yaml está incorreto
para este endpoint). AddressLookupResponse.from_api passa a aceitar AMBOS os
wrappers, normalizando o singular numa lista de um item.
- companies.list: a API exige pageIndex >= 1 (1-based). O page_index público
continua 0-based mas é enviado como page_index + 1 (a tradução do request
estava faltando; a do response já existia em resolve_page_index).
- consumer_invoices.list: a API exige o parâmetro environment ("Production"/
"Test"); agora é keyword obrigatória + require_environment, espelhando
product_invoices.list. Doc do cookbook atualizado.
Adiciona samples/smoke_readonly.rb — smoke test read-only (não grava nada) que
valida auth, roteamento multi-host, o modelo de duas chaves e o mapeamento dos
DTOs contra respostas reais (lê samples/.env, que é gitignored).
Validado contra o sandbox real: addresses/tax_codes/legal_entity/companies.list/
product_invoices.list/consumer_invoices.list/state_taxes.list todos OK.
Verde no Docker 3.2/3.3/3.4: rspec 651/0, rubocop, steep, rbs, generate:check.
Não-bugs observados no sandbox (sem ação no SDK): service_invoices.list devolve
503 (serviço NFS-e instável; SDK mapeia para ServerError corretamente);
companies.retrieve/legal_people.list/natural_people.list falham porque o company
de teste existe no host :cte (api.nfse.io) e não no :main (api.nfe.io) — paths e
erros mapeados corretamente.
- ci.yml: dispara push/PR tambem em 'next', para validar o codigo v1 no GitHub antes da virada next -> master (o gate so rodava no Docker local). - release.yml: adiciona 'rbs validate' ao job verify, igualando-o ao ci.yml (RSpec c/ cobertura, RuboCop, RBS validate, Steep, generate:check).
Silencia o aviso de depreciacao do Node 20 nos runners do GitHub Actions: o checkout@v4 tem como alvo o Node 20 e era forcado a rodar no Node 24.
…QL rb/polynomial-redos) A regex /\A[^\s@]+@[^\s@]+\.[^\s@]+\z/ tinha classes de caractere sobrepostas (o '.' cabe em [^\s@]), causando backtracking polinomial em entradas adversarias. Substituida por uma verificacao estrutural linear e de semantica equivalente: exatamente um '@', sem espacos, e dominio com rotulos nao-vazios antes/depois do ponto. Alerta de alta severidade apontado pelo CodeQL no PR #13.
Adapta o job publish para a OIDC API Key Role criada no RubyGems para o gem nfe-io, passando role-to-assume a rubygems/configure-rubygems-credentials (bump v1.0.0 -> v2.1.0, que suporta o parametro). Dispensa API key de longa duracao; o fallback por RUBYGEMS_API_KEY segue como rede de seguranca.
Adota o mecanismo recomendado (guides.rubygems.org/trusted-publishing): o Trusted Publisher e cadastrado na pagina do gem (repo + workflow release.yml), e a action troca o token OIDC sem role-to-assume. Remove o identificador de role do workflow; fallback por RUBYGEMS_API_KEY segue como rede de seguranca.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Resumo
Reescrita greenfield do SDK Ruby (
nfe-io) da NFE.io: 0.3.2 → 1.0.0.Espelha os SDKs Node.js (v4) e PHP (8.2+) e a nfeio-docs (fonte da verdade),
com zero dependências de runtime (apenas stdlib), Ruby 3.2+ e tipagem
completa via RBS/Steep. Conduzida por specs (OpenSpec).
O que muda
Nfe::Client,Nfe::Configuration, roteamentomulti-host (modelo de duas chaves),
AbstractResourcee transporte HTTPpróprio (retry com backoff + jitter,
Retry-After, redação de segredos).consulta das 5 famílias de nota (serviço, produto, consumidor, transporte,
inbound), empresas, pessoas, inscrições estaduais, webhooks, e os lookups
(CEP, CNPJ, CPF), além de cálculo de impostos.
*Pending/*Issued) com polling manual viaNfe::FlowStatus.terminal?.openapi/*,com
rake generate:checkgarantindo que não há drift.sig/(RBS) + Steep, RuboCop (+rubocop-rspec),RSpec com cobertura ≥ 80%.
YARD com a identidade visual NFE.io.
MIGRATION.mdna raiz.samples/, e automação de release(CI + publish no RubyGems via OIDC).
Verificação
next(matriz Ruby 3.2 / 3.3 / 3.4): RSpec comcobertura, RuboCop,
rbs validate, Steep egenerate:check.roteamento multi-host, o modelo de duas chaves e o mapeamento dos DTOs.
corretamente contra a API real. Emissão de NF-e exercitada até o payload de
tax(dependente do regime tributário do cliente).offline deixou passar (endereço,
pageIndex1-based,environmentobrigatório) — todos corrigidos e cobertos por teste.
Pós-merge (fora deste PR)
RUBYGEMS_API_KEY, e confirmar ownership do gemnfe-io.v1.0.0-rc.1→ GA) via tagv*.Notas
MIGRATION.md.