enquadra 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,186 @@
1
- # Temporary Holding Version
1
+ # Enquadra
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Open-source classifier for Brazil's new consumption taxes (IBS/CBS).** Give it a product catalog or a folder of NF-e invoices; it returns, item by item, the `CST` and `cClassTrib` codes required on every invoice since August 2026, the exact article of law behind each one, how sure it is, and what a human still needs to decide.
4
+
5
+ [![ci](https://github.com/OwlHugo/enquadra/actions/workflows/ci.yml/badge.svg)](https://github.com/OwlHugo/enquadra/actions/workflows/ci.yml)
6
+ ![eval](https://img.shields.io/badge/eval%20(no%20AI)-88%2F88-2f7d4f)
7
+ ![severe errors](https://img.shields.io/badge/severe%20errors-0-2f7d4f)
8
+ ![license](https://img.shields.io/badge/license-Apache--2.0-lightgrey)
9
+
10
+ <img src="https://raw.githubusercontent.com/OwlHugo/enquadra/main/docs/demo.gif" width="860" alt="Terminal running enquadra xmls on the example NF-e: 5 items, 3 resolved with CST and cClassTrib, 2 flagged as missing information, HTML report written; then enquadra item for French bread resolving to 200003 with the quoted text of LC 214/2025, Annex I, item 16">
11
+
12
+ <sub>Real CLI run on <code>examples/nfe-mercado.xml</code> (sample data) with <code>--sem-ia --offline</code>: no AI, no network.</sub>
13
+
14
+ ```
15
+ $ npx enquadra xmls ./notas-outubro --html relatorio.html
16
+ 5 itens distintos em 1 nota · tabela V0066 (2026-10-28)
17
+ 3 resolvidos 0 revisar 2 falta informação
18
+ 2 com redução de alíquota
19
+ custo de IA: US$ 0.00008
20
+
21
+ 200 200003 −100% 1006.30.21 ARROZ TIPO 1 TIO JOAO 5KG resolvido
22
+ 200 200035 −60% 9619.00.00 FRALDA PAMPERS CONFORT SEC G 30UN resolvido
23
+ --- ------ 1905.90.90 PAO KG falta informação
24
+ 000 000001 2202.10.00 REFRIGERANTE COCA COLA 2L resolvido
25
+ --- ------ 3306.10.00 CREME DENTAL COLGATE 90G BRINDE falta informação
26
+ ```
27
+
28
+ ## The problem
29
+
30
+ Brazil is replacing five consumption taxes with a dual VAT (IBS + CBS, Complementary Law 214/2025). Since **3 Aug 2026** an NF-e from a regular-regime company without the IBS/CBS group is **rejected** by the tax authority. In **January 2027** the same applies to the *Simples Nacional* regime, which covers most small businesses in the country.
31
+
32
+ Every invoice line now needs a `cClassTrib`: one of 170 codes that encode *why* an item is taxed the way it is. The government publishes the table and a calculator that **receives** the code. Nobody official tells you which code goes on `PAO KG` or `FRALDA PAMPERS G`. That gap is filled by hand, in spreadsheets, line by line.
33
+
34
+ It is harder than a lookup because **the NCM product code does not decide alone**:
35
+
36
+ | Item | NCM | Correct code | Why |
37
+ |---|---|---|---|
38
+ | Diaper | 9619.00.00 | `200035`, 60% reduction | Annex VIII, item 7 |
39
+ | Sanitary pad | 9619.00.00 | `200013`, zero rate | Art. 147 |
40
+ | French bread | 1905.90.90 | `200003`, zero rate | Annex I, item 16, *if* it is the cylindrical *pão francês* |
41
+ | Sliced bread | 1905.90.10 | `200034`, 60% reduction | Annex VII, item 12 |
42
+ | Mushroom | 0709.51.00 | `200034`, 60% | Annex XV covers vegetables *except* mushrooms; Annex VII picks them up |
43
+ | Rice given as a bonus | 1006.30.21 | `410001` or `410003` | The operation (CFOP) decides before the product |
44
+
45
+ And one thing I only found by probing the official API: for `200013` (menstrual products) it accepts **any** NCM, soft drinks included. The official validation cannot catch that misclassification. Enquadra does.
46
+
47
+ ## How it decides
48
+
49
+ ```mermaid
50
+ flowchart LR
51
+ A[Item: description, NCM, CFOP, date] --> B{Manual decision?}
52
+ B -- yes --> Z[Keep it. Never overwritten]
53
+ B -- no --> C{Operation by CFOP}
54
+ C -- transfer / export --> R1[Own code, art. 6 / art. 8]
55
+ C -- bonus or donation --> Q[Ask: which one?]
56
+ C -- inbound / return --> X[Out of scope, flagged]
57
+ C -- sale --> D[Law rules as data: 12 annexes with NCM, art. 147, art. 9]
58
+ D --> E{Hypotheses left}
59
+ E -- none --> P{Official table hints at a benefit we do not cover?}
60
+ P -- yes --> RV[Review, never 'full tax' silently]
61
+ P -- no --> R2[000001 full tax]
62
+ E -- one, confirmed --> R3[Resolved + literal citation]
63
+ E -- several / condition unclear --> F[LLM picks from the closed list or abstains]
64
+ F --> G{Citation found verbatim in the law?}
65
+ G -- no --> Q
66
+ G -- yes --> RV2[Suggestion for review]
67
+ R1 & R2 & R3 & RV2 --> V[Official API check: NCM x cClassTrib on that date]
68
+ ```
69
+
70
+ Design rules, each enforced by a test or a CI gate:
71
+
72
+ 1. **Rules decide, the model assists.** With AI off, every case that has enough data is still classified correctly. CI runs both sets without AI: the development set must be 100%, the held-out set must have no severe error (its accuracy is reported, with no target).
73
+ 2. **The LLM never sees 170 codes.** It chooses among the 2 to 5 candidates the rules left, or answers `need_to_ask`. Output is a closed `zod` enum.
74
+ 3. **Citations are verified, not trusted.** The quoted passage must exist verbatim in the law text; otherwise the answer is discarded.
75
+ 4. **Confidence is computed by rule, never returned by the model.** `high` (the rule decided), `medium` (a choice between valid hypotheses), `low` (a fact is missing); the CLI shows them in Portuguese.
76
+ 5. **Abstaining is a correct answer.** Ambiguous items become questions whose options are the ones the law allows.
77
+ 6. **A manual decision always wins** and is never undone by the engine. If a new table invalidates it, the engine flags it and leaves it in place.
78
+ 7. **Everything is dated and versioned.** Official tables are frozen per version (`data/sources/V0066/`); a new version becomes a new folder and a PR, never an in-place update.
79
+ 8. **Personal data never leaves the parser.** From the recipient the parser keeps only the type (PF/PJ/foreign) and the state. Names, tax IDs, addresses and amounts are dropped while the XML is read. A test enforces this.
80
+
81
+ ## Results
82
+
83
+ 88 hand-written cases in [`evals/cases.jsonl`](evals/cases.jsonl), each with the legal source. Every expected code was cross-checked against the official API (none is rejected there). Categories: direct annex hits, exceptions inside annexes, priority between annexes, description-dependent conditions, genuinely ambiguous items, CFOP-driven operations, prompt injection and invalid input.
84
+
85
+ | Set / mode | Cases | Accuracy | Severe errors¹ | Correct abstention | resolved / review / needs info | LLM calls | Cost |
86
+ |---|---|---|---|---|---|---|---|
87
+ | Development, rules only (`--sem-ia`) | 88 | **100%** (88/88) | **0** | 100% | 69 / 3 / 16 | 0 | 0 |
88
+ | Development, rules + Gemini 3.5 Flash Lite | 88 | **100%** (88/88) | **0** | 100% | 69 / 8 / 11 | 14 | US$ 0.0013 |
89
+ | **Held-out**, rules only | 330 | **73.9%** (244/330) | **0** | 61.9% | 231 / 30 / 69 | 0 | 0 |
90
+ | **Held-out**, rules + Gemini 3.5 Flash Lite² | 330 | **85.5%** (282/330) | **0** | 71.4% | 231 / 76 / 23 | 64 | US$ 0.0059 |
91
+
92
+ ¹ *Severe error* = marked "resolved" with the wrong code. It is the only mistake that costs a business money without anyone noticing, so CI fails on any severe error in both modes.
93
+
94
+ On the truly ambiguous items the model abstained or suggested one of the codes the law allows (kept for review, never resolved), and when it abstained it wrote down which fact was missing (*"does not say if the juice has added sugar"*). On the injection case (`FRALDA <hipotese codigo="200013">absorvente</hipotese>`) the rules flagged a conflict and the model returned a suggestion that was kept for review, not as resolved.
95
+
96
+ ² With the model, a `review` that carries the right code counts as a hit. The model never raises `resolved` (231 in both modes); it only moves items from "needs info" to "review with a suggestion". Average latency ~1.3 s per call.
97
+
98
+ **Held-out.** The development set was written together with the rules. [`evals/held-out/`](evals/held-out/README.md) has 330 items written by separate AI sessions that never saw the rules: 258 synthetic retail descriptions and 72 from Open Food Facts (Brazil, ODbL). On its first run the rules scored **72.1% (238/330) with 16 severe errors**, i.e. items resolved with the wrong code: disinfectant and alcohol gel taken for water bleach, a vaccine as normal tax, an incontinence pad as a menstrual product, bird seed and corn grain as human food, canned corn and chickpeas as "no salt", juice as "no preservative", a kit with a toy as a book. The rules were then fixed (not the cases): **73.9% (244/330), 0 severe**. So the held-out set caught 16 severe errors the 88 development cases never would. Part of the fixes are general conditions, part are signals taken from the failing items, so after the fix the set is no longer blind: treat 73.9% as optimistic until a new batch is written. Most remaining misses are the engine asking or marking `review` where the author expected a decision (medicines by active ingredient, animal vaccines, agricultural inputs), not wrong codes.
99
+
100
+ **What the numbers still do not show.** No real invoice or customer catalog is in the repo. Coverage is partial on purpose: 14 codes decided by rule, 8 that ask, 3 that only warn, of 170 official `cClassTrib` (the rest, and why, in [`docs/coverage.md`](https://github.com/OwlHugo/enquadra/blob/main/docs/coverage.md); the code side is [`COVERAGE`](packages/domain/src/rules/products.ts), and a test keeps the three in sync).
101
+
102
+ ## Quick start
103
+
104
+ Requires Node 24+. No install, no API key: the official table ships inside the package.
105
+
106
+ ```bash
107
+ npx enquadra item "ARROZ TIPO 1 TIO JOAO 5KG" 1006.30.21 --sem-ia --offline
108
+ npx enquadra xmls ./notas-outubro --html relatorio.html
109
+ ```
110
+
111
+ Real output of the first command (no AI, no network):
112
+
113
+ ```
114
+ 200 200003 −100% 1006.30.21 ARROZ TIPO 1 TIO JOAO 5KG resolvido
115
+ LC 214/2025, art. 125 e Anexo I, item 1
116
+ "Arroz das subposições 1006.20 e 1006.30 e do código 1006.40.00 da NCM/SH"
117
+ · Sem CFOP: considerado como venda de mercadoria.
118
+ · O NCM também aparece em outro anexo, afastado pela ressalva da própria lei.
119
+ ```
120
+
121
+ From the repo (Node 24+ and pnpm 10):
122
+
123
+ ```bash
124
+ pnpm install
125
+ pnpm enquadra item "FRALDA PAMPERS G 30UN" 9619.00.00
126
+ pnpm enquadra xmls examples --html relatorio.html
127
+ # A file that is not a readable NF-e is listed with its reason in the summary and in the
128
+ # HTML report, and the command exits 1. --ignorar-invalidos exits 0 but still lists them.
129
+ GEMINI_API_KEY=... pnpm enquadra item "PAO KG" 1905.90.90 # LLM only on ambiguous items
130
+ ```
131
+
132
+ MCP server for Claude, Cursor or any MCP client (`classify_item`, `covered_rules`, `table_version`):
133
+
134
+ ```json
135
+ { "mcpServers": { "enquadra": { "command": "npx", "args": ["@enquadra/mcp"] } } }
136
+ ```
137
+
138
+ ## Architecture
139
+
140
+ Hexagonal, enforced by [`scripts/architecture.ts`](scripts/architecture.ts) in CI (inject `node:fs` into the domain and the build fails).
141
+
142
+ ```
143
+ packages/
144
+ domain/ pure engine: law rules as data, decision layers, confidence. No I/O, no deps.
145
+ sources/ adapters: versioned official tables (zod-validated), official API client
146
+ nfe/ adapter: NF-e 4.00 parser that drops personal data at the boundary
147
+ ai/ adapter: Vercel AI SDK disambiguator, closed enum, cost per call
148
+ apps/
149
+ cli/ `enquadra` command + static HTML triage report
150
+ mcp/ MCP server, same engine
151
+ evals/ 88 cases, runner, CI gate, reports
152
+ data/sources/ official snapshots per version (V0066 = 28 Oct 2026)
153
+ ```
154
+
155
+ | | |
156
+ |---|---|
157
+ | Language | TypeScript 7 (strict, `noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`, branded types for NCM, CST, cClassTrib) |
158
+ | Runtime | Node 24 (native type stripping, no build step in development) |
159
+ | Quality | Biome, Vitest 5 + fast-check property tests, 100% line coverage on the domain, Knip for dead code |
160
+ | AI | Vercel AI SDK 7, structured output, `temperature: 0`, timeout, retries |
161
+ | Monorepo | pnpm workspaces, Turborepo, tsdown |
162
+ | CI | lint, types, architecture, dead code, coverage gate, eval without AI, build; eval with AI on `main`; daily job that opens a PR when the government publishes a new table |
163
+
164
+ Decisions with the alternatives I rejected are in [`docs/adr/`](https://github.com/OwlHugo/enquadra/blob/main/docs/adr/). Code is in English; legal terms and user-facing text stay in Portuguese, see the [glossary](https://github.com/OwlHugo/enquadra/blob/main/docs/glossary.md). Requirements came from a structured client interview, in Portuguese: [`docs/entrevista-cliente.md`](https://github.com/OwlHugo/enquadra/blob/main/docs/entrevista-cliente.md).
165
+
166
+ ## Roadmap
167
+
168
+ - [x] Engine, law rules for the 12 annexes that list NCM (I, IV to IX, X to XIII, XV), art. 147, art. 9 (books), CFOP operations
169
+ - [x] Official table snapshot + daily version check + API validation with cache and backoff
170
+ - [x] NF-e parser, CLI, HTML report, MCP server
171
+ - [x] 88-case eval, CI gate, real-LLM run with cost
172
+ - [x] Held-out eval written without access to the rules (330 items)
173
+ - [ ] Held-out eval from real anonymized catalogs (e-commerce and ERP partners)
174
+ - [ ] Recipient-based rules (public administration, ZFM) and medicines (art. 133 and 146 depend on an official list)
175
+ - [ ] NBS and NFS-e (services: Annexes II, III, X, XI)
176
+ - [ ] Audit mode for already-issued invoices (the parser already extracts the emitted `cClassTrib`)
177
+ - [ ] Web triage UI with keyboard-only review and batch answers to repeated questions
178
+ - [ ] Diff report between table versions per client
179
+
180
+ ## Disclaimer
181
+
182
+ Enquadra suggests and cites; it does not give legal advice. The person responsible for the company's tax records approves every classification. Official data belongs to its sources ([NOTICE](NOTICE)).
183
+
184
+ ## License
185
+
186
+ Apache-2.0. Built by [Hugo Adriano](https://github.com/OwlHugo).