pattern - domain knowledge as data

truecopy/pattern

Everything that varies by market, by issuer or by document family should be a value, not a branch. And a pattern off the wire is untrusted input applied to untrusted input.

Everything that varies by market, by issuer or by document family is a value, not a branch.

A value can be served by a backend, refined by whoever reads the documents, versioned, and swapped for another market without a deploy. A branch can only be changed by whoever can change the code — which in practice means a release, a review and a week.

import { compilePatterns, toRawPatterns, NEVER_MATCHES } from 'truecopy/pattern';

// Off the wire, as JSON
const raw = [{ source: 'solde\\s{0,3}(cr[ée]diteur|d[ée]biteur)', flags: 'i' }];

const patterns = compilePatterns(raw);

Regular expressions do not survive JSON, so they travel as { source, flags } and are rebuilt here.

Which is exactly where the danger is

A profile off the wire is untrusted input applied to untrusted input.

A pattern arrives from a backend and is then run, line by line, over a document somebody uploaded. Both ends are outside your control, and a catastrophically backtracking expression there is not slow — it is a tab that never comes back.

So compilePattern guards before it compiles:

  • a source longer than a thousand characters is rejected;
  • a nested quantifier — the shape that makes backtracking explode — is rejected.

And a rejected pattern compiles to NEVER_MATCHES, not to a throw. The feature degrades and the page stays alive. Throwing would take the whole reading down because one entry in a served profile was wrong.

Counting occurrences

import { countMatches } from 'truecopy/pattern';

countMatches(text, pattern); // how many times, bounded

Used by classify for the rule that one occurrence may be a quotation and two rarely are.

What this is not

It is not a place to put your vocabulary. The library never reads your patterns’ meaning — it compiles them, guards them and counts them. Which words belong to a statement and which to an invoice is yours, and it belongs in a profile you can change without asking anyone.