enquadra 0.1.0 → 0.1.2

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.
Files changed (3) hide show
  1. package/dist/main.mjs +112 -4
  2. package/package.json +1 -1
  3. package/README.md +0 -186
package/dist/main.mjs CHANGED
@@ -2733,6 +2733,11 @@ const GENERAL_USE_CHAPTERS = [
2733
2733
  "85",
2734
2734
  "94"
2735
2735
  ];
2736
+ /**
2737
+ * Itens de NCM de uso geral cujo nome já identifica o produto de saúde: o código 4014.10.00 é só
2738
+ * de preservativos, então "preservativo" na descrição basta, sem palavra de contexto hospitalar.
2739
+ */
2740
+ const SELF_EVIDENT = /* @__PURE__ */ new Set(["IV.91"]);
2736
2741
  /** Primeiro trecho do item até a vírgula ou ponto e vírgula, em no máximo 80 caracteres. */
2737
2742
  const shorten = (text) => {
2738
2743
  const head = text.split(/[;,]/)[0] ?? text;
@@ -2749,7 +2754,7 @@ const build$1 = (spec) => {
2749
2754
  no: [],
2750
2755
  minMatches: spec.origin === "IX" || yes.length < 3 ? 1 : 2,
2751
2756
  ...spec.requirePositive ? { requirePositive: true } : {},
2752
- ...spec.context ? { context: spec.context } : include.some((p) => GENERAL_USE_CHAPTERS.some((c) => p.startsWith(c))) ? { context: HEALTH_CONTEXT } : {}
2757
+ ...spec.context ? { context: spec.context } : include.some((p) => GENERAL_USE_CHAPTERS.some((c) => p.startsWith(c))) && !SELF_EVIDENT.has(`${spec.origin}.${number}`) ? { context: HEALTH_CONTEXT } : {}
2753
2758
  };
2754
2759
  return {
2755
2760
  id: `${spec.origin}.${number}`,
@@ -3189,6 +3194,24 @@ const BOOK = {
3189
3194
  /\bbrinde/
3190
3195
  ]
3191
3196
  };
3197
+ /**
3198
+ * Uso veterinário: os arts. 133 e 146 tratam de medicamento registrado na Anvisa (uso humano);
3199
+ * produto veterinário é registrado no MAPA e segue o Anexo IX ou a regra geral.
3200
+ */
3201
+ const VETERINARY = [
3202
+ /\bveterinari/,
3203
+ /\bvet\b/,
3204
+ /\bcaes?\b/,
3205
+ /\bcachorro/,
3206
+ /\bgatos?\b/,
3207
+ /\bpet\b/,
3208
+ /\bbovin/,
3209
+ /\bsuin/,
3210
+ /\bovin/,
3211
+ /\bequin/,
3212
+ /\baves\b/,
3213
+ /\bgado\b/
3214
+ ];
3192
3215
  const PHONOGRAM = {
3193
3216
  id: "phonogram",
3194
3217
  question: "O item é fonograma musical gravado (CD, DVD musical ou disco de vinil)?",
@@ -3541,6 +3564,32 @@ const MANUAL_RULES = [
3541
3564
  text: "São também reduzidas a zero as alíquotas do IBS e da CBS sobre o fornecimento de medicamentos registrados na Anvisa quando: III - classificados como soros ou vacinas, conforme regulamentação sanitária específica. (NCM/SH 3002.41, vacinas para medicina humana)",
3542
3565
  include: ["300241"],
3543
3566
  registration: "Anvisa"
3567
+ },
3568
+ {
3569
+ id: "art133",
3570
+ origin: "art133",
3571
+ cClassTrib: cClassTrib("200032"),
3572
+ provision: "LC 214/2025, art. 133, caput",
3573
+ label: "Medicamentos registrados na Anvisa (art. 133), redução de 60%",
3574
+ text: "Ficam reduzidas em 60% (sessenta por cento) as alíquotas do IBS e da CBS incidentes sobre o fornecimento dos medicamentos registrados na Anvisa ou produzidos por farmácias de manipulação, ressalvados os medicamentos sujeitos à alíquota zero de que trata o art. 146 desta Lei Complementar.",
3575
+ include: ["3003", "3004"],
3576
+ derivedInclusion: "A lei não cita NCM para medicamento: as posições 30.03 e 30.04 da NCM/SH são as de medicamentos (Capítulo 30, Nota 1 e textos das posições). A API oficial aceita qualquer NCM neste código.",
3577
+ veto: VETERINARY,
3578
+ registration: "Anvisa",
3579
+ unconfirmed: "O medicamento é registrado na Anvisa e fica fora da lista de alíquota zero do art. 146?"
3580
+ },
3581
+ {
3582
+ id: "art146.caput",
3583
+ origin: "art146",
3584
+ cClassTrib: cClassTrib("200009"),
3585
+ provision: "LC 214/2025, art. 146, caput",
3586
+ label: "Medicamentos do art. 146 (doenças raras, oncologia, diabetes...), alíquota zero",
3587
+ text: "São reduzidas a zero as alíquotas do IBS e da CBS sobre o fornecimento dos medicamentos registrados na Anvisa, desde que destinados, de acordo com o registro sanitário, a: I - doenças raras; II - doenças negligenciadas; III - oncologia; IV - diabetes; V - HIV/aids e outras infecções sexualmente transmissíveis (IST); VI - doenças cardiovasculares; e VII - Programa Farmácia Popular do Brasil ou equivalente.",
3588
+ include: ["3003", "3004"],
3589
+ derivedInclusion: "A lei não cita NCM para medicamento: as posições 30.03 e 30.04 da NCM/SH são as de medicamentos (Capítulo 30, Nota 1 e textos das posições). A API oficial aceita qualquer NCM neste código.",
3590
+ veto: VETERINARY,
3591
+ registration: "Anvisa",
3592
+ unconfirmed: "O medicamento está na lista de alíquota zero do ato conjunto (art. 146, § 3º): doenças raras, negligenciadas, oncologia, diabetes, HIV/IST, cardiovasculares ou Farmácia Popular?"
3544
3593
  }
3545
3594
  ];
3546
3595
  /** Destino animal na descrição: o alimento dos Anexos I, VII e XV é para consumo humano (arts. 125, 135, 148). */
@@ -3616,7 +3665,7 @@ const evaluateRule = (rule, text) => {
3616
3665
  if (rule.veto?.some((r) => r.test(text))) return {
3617
3666
  rule,
3618
3667
  by: "condition",
3619
- reason: "a descrição indica ração ou uso animal, fora de alimento humano"
3668
+ reason: "a descrição indica ração ou uso animal, fora do consumo humano"
3620
3669
  };
3621
3670
  const evaluation = rule.condition ? evaluate(rule.condition, text) : "meets";
3622
3671
  if (evaluation === "fails") return {
@@ -3660,6 +3709,56 @@ const collectHypotheses = (code, description, rules = PRODUCT_RULES) => {
3660
3709
  discarded
3661
3710
  };
3662
3711
  };
3712
+ /**
3713
+ * Cadastro com NCM trocado é comum e passa calado: "ARROZ" com NCM de roupa sai como tributação
3714
+ * integral. A primeira palavra da descrição costuma ser o produto; para os produtos de benefício
3715
+ * mais vendidos, ela indica em que capítulos da NCM o item pode estar. Fora deles, o motor avisa.
3716
+ * Não decide nada: só aponta o cadastro suspeito.
3717
+ */
3718
+ const CHAPTERS_BY_HEAD_WORD = {
3719
+ arroz: [
3720
+ "10",
3721
+ "11",
3722
+ "19",
3723
+ "20",
3724
+ "21"
3725
+ ],
3726
+ feijao: [
3727
+ "07",
3728
+ "20",
3729
+ "21"
3730
+ ],
3731
+ acucar: ["17"],
3732
+ cafe: ["09", "21"],
3733
+ sal: ["25"],
3734
+ carne: ["02", "16"],
3735
+ ovos: ["04"],
3736
+ fralda: [
3737
+ "48",
3738
+ "56",
3739
+ "63",
3740
+ "96"
3741
+ ],
3742
+ absorvente: [
3743
+ "48",
3744
+ "56",
3745
+ "96"
3746
+ ],
3747
+ sabonete: ["34"],
3748
+ preservativo: ["40"]
3749
+ };
3750
+ /** A palavra que não combina com o capítulo do NCM, ou `undefined` quando combina ou não se sabe. */
3751
+ const descriptionNcmMismatch = (description, code) => {
3752
+ const head = normalizeDescription(description).split(" ")[0] ?? "";
3753
+ const word = [
3754
+ head,
3755
+ head.replace(/oes$/, "ao"),
3756
+ head.replace(/s$/, "")
3757
+ ].find((w) => w in CHAPTERS_BY_HEAD_WORD);
3758
+ const chapters = word ? CHAPTERS_BY_HEAD_WORD[word] : void 0;
3759
+ if (!word || !chapters || chapters.includes(code.slice(0, 2))) return void 0;
3760
+ return word;
3761
+ };
3663
3762
  const EXPORT = {
3664
3763
  kind: "own",
3665
3764
  cClassTrib: cClassTrib("410004"),
@@ -3794,6 +3893,15 @@ const classify = async (item, deps) => {
3794
3893
  return build(item, r, deps, date, "not_checked");
3795
3894
  }
3796
3895
  r.trail.push(`NCM ${formatNcm(item.ncm)}: ${ncmName}`);
3896
+ const mismatch = descriptionNcmMismatch(item.description, item.ncm);
3897
+ if (mismatch) {
3898
+ r.alerts.push({
3899
+ code: "ncm_description_mismatch",
3900
+ severity: "warning",
3901
+ message: `A descrição começa com "${mismatch}", que não costuma ser do capítulo ${item.ncm.slice(0, 2)} da NCM: confira o NCM do cadastro antes de usar o código.`
3902
+ });
3903
+ r.trail.push(`Descrição ("${mismatch}") e NCM ${formatNcm(item.ncm)} não combinam.`);
3904
+ }
3797
3905
  if (item.manualDecision) decideManual(item, r, deps);
3798
3906
  else if (decideOperation(item, r)) await decideProduct(item, ncmName, r, deps);
3799
3907
  else if (operationByCfop(item.cfop).kind === "question") await addProductOptions(item, ncmName, r, deps);
@@ -4183,7 +4291,7 @@ const createOfficialValidator = (options = {}) => {
4183
4291
  };
4184
4292
  /**
4185
4293
  * Tabela oficial congelada em disco, uma pasta por versão do banco da Calculadora RTC
4186
- * (`data/sources/V0059/`). Nada é atualizado no lugar: versão nova é pasta nova.
4294
+ * (`data/sources/V0066/`). Nada é atualizado no lugar: versão nova é pasta nova.
4187
4295
  * O formato de cada arquivo é validado com zod ao carregar; mudança de contrato na API
4188
4296
  * vira erro alto, não dado errado em silêncio.
4189
4297
  */
@@ -4241,7 +4349,7 @@ const loadTable = async (folder) => {
4241
4349
  };
4242
4350
  };
4243
4351
  /** Pasta da tabela versionada que acompanha o repositório. */
4244
- const DEFAULT_TABLE = fileURLToPath(new URL("../../../data/sources/V0059", import.meta.url));
4352
+ const DEFAULT_TABLE = fileURLToPath(new URL("../../../data/sources/V0066", import.meta.url));
4245
4353
  //#endregion
4246
4354
  //#region src/labels.ts
4247
4355
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "enquadra",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Classifies a catalog or NF-e XML into IBS/CBS CST and cClassTrib, with the legal basis.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
package/README.md DELETED
@@ -1,186 +0,0 @@
1
- # Enquadra
2
-
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).