{ // Example configuration for the Archivist pipeline. // Keys in English; extracted values and rendered file names are in // French, as they appear in the source documents. // ========================================================================== // fields: declaration of every field handled by the rules. // A missing mandatory field at the end of the pipeline marks the document // invalid and routes it to pending/ for human review. // "template" is a regular field: rules select the naming template with it. // ========================================================================== "fields": [ { "name": "emittingCompany", "type": "text", "mandatory": true }, { "name": "documentType", "type": "enum", "mandatory": true }, { "name": "emissionDate", "type": "date", "mandatory": true }, { "name": "template", "type": "text", "mandatory": false }, { "name": "bankAccount", "type": "enum", "mandatory": false }, { "name": "address", "type": "text", "mandatory": false }, { "name": "reference", "type": "text", "mandatory": false } ], // ========================================================================== // rules: an ordered tree evaluated against the document pages // (pages array, pages[0] = page 1) and the fields already set or deleted // by earlier rules. // // Each rule contains four keys: // when (required): expr-lang expression; the rest of the rule runs only // if it evaluates to true. String literals inside // expressions are written as raw strings with backticks. // set : list of assignments // { "field": "x", "value": } constant // { "field": "x", "eval": "" } evaluated in the // same context // delete: list of field names to unset // rules : child rules, evaluated in order only if the parent "when" // matched; nesting is recursive // // Built-in helpers: // firstMatch(regex, texts?) returns the first match of the regex // in the document text (first capture group when present); texts? is an // expression evaluated to the text(s) to search: pages[0] searches // the first page, pages[0:3] searches pages 1 to 3 (expr slice // syntax, end exclusive), any list of strings works; omitted = the // whole document. // frDate(text) parses a French natural-language date ("le 19 janvier // 2026"; the day is optional and "1er" / abbreviated months are // accepted) into a native date, for documents whose date is not // written in a Go layout format. // period(value, unit, shift, layout?) renders a period instead of a // raw date: unit = `month`|`quarter`|`half`|`year`, shift = -1/0/+1 // for the previous / containing / next period (rent receipt: 0, // end-of-quarter charges call: +1, bank statement: -1). Returns a // French string (`Janvier 2026`, `T2 2026`, `S1 2026`, `2026`); the // optional layout is a Go reference-date layout applied to the // period's first day with French month names; ignored for // quarter/half (fixed formats). // Strings are parsed into native dates with expr's standard date(value, // layout) builtin (Go layout, e.g. `02/01/2006` for 15/01/2026); the // format is carried by the rule so it can differ per document type. // ========================================================================== "rules": [ // --- Generic extraction, runs for every document ------------------------- // Emission date, e.g. "Paris, le 15/01/2026" -> "15/01/2026" (parsed // into a native date; named emissionDate to avoid clashing with the // expr date() builtin). // Every format probe is guarded by its own "when": an unguarded // date(firstMatch(...)) on a document that does not carry the format // is a rule error, and a single rule error routes a perfectly // classifiable document to pending/ (US-15). { "when": "firstMatch(`[0-9]{2}/[0-9]{2}/[0-9]{4}`, pages[0]) != ``", "set": [ { "field": "emissionDate", "eval": "date(firstMatch(`[0-9]{2}/[0-9]{2}/[0-9]{4}`, pages[0]), `02/01/2006`)" } ] }, // Fallback: French natural-language date, e.g. "le 19 janvier 2026", // when the page carries no numeric date. { "when": "firstMatch(`[0-9]{2}/[0-9]{2}/[0-9]{4}`, pages[0]) == `` && firstMatch(`(?:le )?[0-9]{1,2} [A-Za-zÀ-ÿ]{3,} [0-9]{4}`, pages[0]) != ``", "set": [ { "field": "emissionDate", "eval": "frDate(firstMatch(`(?:le )?[0-9]{1,2} [A-Za-zÀ-ÿ]{3,} [0-9]{4}`, pages[0]))" } ] }, // --- Bank statements (relevés de compte) ---------------------------------- { "when": "pages[0] contains `Relevé de compte`", "set": [ { "field": "documentType", "value": "Relevé de compte" }, { "field": "template", "value": "bank_statement" } ], "rules": [ // Issuing bank { "when": "pages[0] contains `BRED`", "set": [ { "field": "emittingCompany", "value": "BRED" } ] }, { "when": "pages[0] contains `Fortunéo`", "set": [ { "field": "emittingCompany", "value": "Fortunéo" } ] }, { "when": "pages[0] contains `Crédit Mutuel`", "set": [ { "field": "emittingCompany", "value": "Crédit Mutuel" } ] }, // Personal accounts, matched on the account number printed on page 1 { "when": "pages[0] contains `12345678901`", "set": [ { "field": "bankAccount", "value": "Compte Thomas" } ] }, { "when": "pages[0] contains `98765432109`", "set": [ { "field": "bankAccount", "value": "Compte joint" } ] } ] }, // --- Taxe d'habitation, one child rule per address ------------------------ { "when": "pages[0] contains `Taxe d'habitation`", "set": [ { "field": "documentType", "value": "Taxe d'habitation" }, { "field": "emittingCompany", "value": "DGFiP" }, { "field": "template", "value": "tax_notice_housing" } ], "rules": [ { "when": "pages[0] contains `12 rue des Lilas`", "set": [ { "field": "address", "value": "12 rue des Lilas, Paris" } ] }, { "when": "pages[0] contains `34 avenue Jean Jaurès`", "set": [ { "field": "address", "value": "34 avenue Jean Jaurès, Bordeaux" } ] } ] }, // --- Avis d'impôt sur le revenu -------------------------------------------- { "when": "pages[0] contains `Impôt sur le revenu` && pages[0] contains `avis d'imposition`", "set": [ { "field": "documentType", "value": "Avis d'impôt sur le revenu" }, { "field": "emittingCompany", "value": "DGFiP" }, { "field": "template", "value": "tax_notice_income" } ] }, // --- Generic invoices (e-commerce and others) ----------------------------- { "when": "pages[0] contains `Facture` && pages[0] contains `TVA`", "set": [ { "field": "documentType", "value": "Facture" }, { "field": "template", "value": "invoice" }, // Invoice number, e.g. "Facture n° F-2026-0123" -> "F-2026-0123" { "field": "reference", "eval": "firstMatch(`[Ff]acture n° ?([A-Za-z0-9-]+)`)" } ], // Fields that do not apply to invoices, in case an earlier rule // set them "delete": [ "bankAccount", "address" ], "rules": [ { "when": "pages[0] contains `Amazon`", "set": [ { "field": "emittingCompany", "value": "Amazon" } ] }, { "when": "pages[0] contains `LDLC`", "set": [ { "field": "emittingCompany", "value": "LDLC" } ] }, { "when": "pages[0] contains `Cdiscount`", "set": [ { "field": "emittingCompany", "value": "Cdiscount" } ] } ] } ], // ========================================================================== // templates: named naming templates, selected by the "template" field. // A template is a literal string containing {expr} placeholders: the // inside of each placeholder is an expr-lang/expr expression evaluated in // the document context (fields, helpers, builtins) and replaced by its // result, e.g. {emissionDate.Format(`2006-01-02`)} or // {upper(emittingCompany)}. Unset optional fields evaluate to an empty // string. The rendered template is the full object path under files/. // If no rule sets "template", the "default" template is used. // ========================================================================== "templates": { // A bank statement covers the month before its emission date. "bank_statement": "{emittingCompany}/{period(emissionDate, `month`, -1)}_{documentType}_{bankAccount}.pdf", "tax_notice_housing": "Impots/{emissionDate.Format(`2006-01-02`)}_{documentType}_{address}.pdf", "tax_notice_income": "Impots/{emissionDate.Format(`2006-01-02`)}_{documentType}.pdf", "invoice": "Factures/{emissionDate.Format(`2006-01-02`)}_{emittingCompany}_{reference}.pdf", "default": "Divers/{documentType}.pdf" } }