Shoddy · Fettler

Screening

Stop a regulated document leaving a tree by accident. The first tier needs nothing installed and one line of configuration.

This page assumes Fettler is installed and turned on in a project. That is installing it and in a project, in that order. The screen is the last thing you switch on, and most projects never switch it on at all.

Read step 1 before you read anything else. The screen refuses whole responses. In a tree it does not belong in, it will refuse a great deal of ordinary work, and the way people get out of that is by turning it off — which leaves you worse off than never having switched it on. Point it at the tree that holds the records, and at nothing else.
# 1. decide whether you need it at all       <- most trees do not
# 2. add "screen" to the tree in .fettler.json     <- works at once
#    restart the assistant, and test a refusal
fettle roots                                 # confirm what is screened
fettle read one-of-the-records.pdf           # confirm it refuses

That is the whole feature for most people who want it. Names, addresses, conditions and free text need a model, which is a second program and a download you fetch yourself — installing the models is that half, and nothing on this page depends on it.

Do you need this?

Switch it on when a tree holds records about people, and a person outside your team could read the transcript. Fettler reads PDF, Word and Excel — the formats regulated data actually lives in. What it returns goes into an assistant's context, then into a transcript, a log, and whatever ships those onward. The screen sits between the file and that chain.

Your tree holdsSwitch it on?
Patient notes, discharge summaries, lab resultsYes. This is the case the screen was built for.
Customer records with names, addresses, card or account numbersYes, with identifiers at least — which catches the card and account numbers. The names and addresses need a model; see the categories.
Signed contracts, credit files, student recordsProbably. Read the caveat in step 11 first — two of those three regimes are not covered categories.
Source code, build scripts, documentationNo. The email-address detector alone will refuse a great deal of it. See the warning below.
Anonymised or synthetic dataNo — but if it sits inside a screened tree, give it a scope that turns the screen off. Step 2 shows how.

Note Do not put "screen": true on a source tree to see what happens. One of the patterns matches any email address, and source trees are full of them — in comments, in test fixtures, in package metadata. Every read that touches one will refuse. That is the detector working correctly, in a tree nobody meant to screen.

What gets screened

The two commands that hand content back: read and search. Nothing else returns file content, so nothing else is screened. find returns paths, roots returns your own configuration, and extract writes files inside the tree rather than sending them out.

It judges what is being sent, not what is in the file. A document may hold a patient's details on page 40. Read page 2, and page 2 is sent, because page 2 discloses nothing. Judging the whole file would lock every useful tree, and the way out of that is to turn the screen off.

A search is judged one file at a time, as the matched lines plus whatever context brings with them. The screen sees exactly what you would have seen.

Switch on the first tier

This needs nothing installed and works the moment you restart the assistant. Add "screen" to the tree that holds the records, in .fettler.json:

{
  "trees": {
    "work": { "path": "." },
    "records": {
      "path": "../records",
      "can": ["list", "read"],
      "screen": ["identifiers", "clinical"]
    }
  }
}

Three ways to write it, and one that is refused:

You writeIt screens
"screen": trueAll four categories. Forgetting to name one cannot go wrong this way.
"screen": ["identifiers", "clinical"]Only those two. A bare list includes.
"screen": ["-scientific"]Everything except scientific. A list of - words excludes.
"screen": falseNothing. This is also what a tree that says nothing gets.
"screen": ["identifiers", "-scientific"]Refused when the file is read. It reads either as "identifiers only" or as "everything but scientific", and those differ by two whole categories. Nothing here guesses which you meant.

A misspelt category is refused too. "lgal" would quietly subtract a screen, and you would not find out until data left a tree you believed was covered.

Note Three regulation-shaped words are refused outright, and the refusal says what to write instead. phi, pii and sci name a regulation rather than what actually runs, so each promises coverage that only arrives once a model is installed while reading as though it were there from the start. They are refused rather than quietly translated, because a translation would keep them alive in configurations indefinitely, still over-promising.

RefusedWrite instead
phiidentifiers for the labelled patterns, clinical for model screening of clinical text — it does duty for both
piiidentifiers
sciscientific

The four categories

WordConcernWhat carries itWhich model
identifiersStructured identifiers, whoever they belong toSix patterns, running inside fettle. The only category that works with nothing installed.None, ever. It never crosses the sidecar's pipe and never asks for a model.
clinicalClinical text — the HIPAA concernA model you install, and nothing until you do.A de-identification fine-tune from the BioBERT or ClinicalBERT family. The manifest names the exact checkpoint and revision, and fettle roots reports it back to you.
legalContract and consumer-report contentA model you install, and nothing until you do.A contract-clause model trained on CUAD, or LegalBERT. Named in the manifest, reported by roots.
scientificResearch and scientific dataA model you install, and nothing until you do.A SciBERT entity-recognition fine-tune. Named in the manifest, reported by roots.
clinical, legal and scientific screen nothing until you install a model. None of the three has a pattern behind it. A tree that names one of them and has no models directory is screened in name only. fettle roots says so in as many words, and step 6 is the act that changes it.
The mirror of that, and the more important half: out of the box the screen catches structured identifiers. A social security number, a Luhn-valid card, a formatted phone number, an email address, a labelled record number and a labelled date of birth. Names, addresses, conditions and free text are caught only by a model you install — and the manifest plus roots name exactly which model that is. A tree screening identifiers alone is protected against the first list and nothing beyond it.

What the patterns catch

These run inside fettle itself. There is no model, no second program, and no new download.

DetectorCategoryIt matches
ssnidentifiersA US social security number, written with hyphens, in a range the Social Security Administration actually issues.
credit-cardidentifiers13 to 19 digits, written in groups the way cards are printed, that pass the Luhn check digit.
phoneidentifiersA number with a leading +, or one written in separated groups.
emailidentifiersAn email address.
medical-record-numberidentifiersAn identifier introduced by its own label — MRN, patient id, medical record no.
date-of-birthidentifiersA date introduced by its own label — DOB, date of birth, born on.

All six are one category, and that is the point of the name. They are all identifiers with a shape somebody fixed or a label announcing them. None of them reads clinical language, and a refusal from any of them reports as identifiers — never as a judgement about what kind of document it came from.

They are narrow on purpose, and two of them are deliberately label-driven. No two hospitals agree on the shape of a record number, so the only honest thing to match is the label that introduces it. Matching bare identifier-shaped text would refuse every part number in every spreadsheet — and a check that cries wolf is a check somebody switches off.

Nine bare digits are an order number far more often than a social security number, so nine bare digits do not match. Neither do ten bare digits as a phone number, nor sixteen bare digits as a card.

Carving out a folder that does not need it

A scope's screen replaces the tree's, exactly as its can does. That is what lets a scope take screening away:

{
  "trees": {
    "records": {
      "path": "../records",
      "can": ["list", "read"],
      "screen": true,
      "scopes": {
        "anonymised": { "can": ["list", "read"], "screen": false },
        "exports":    { "can": ["list", "read"], "screen": ["identifiers"] }
      }
    }
  }
}

Check it, then prove it

Restart the assistant first. The server reads .fettler.json when it launches and does not re-read it, so a screen you just added is not on yet. Then ask for the roots:

$ fettle roots
work          /work/shoddy  (default)
              can: list read create update rename delete
records       /work/records
              can: list read
              screen: identifiers clinical
              anonymised  can: list read  screen: nothing

no "models" directory is declared, so clinical, legal, scientific screen nothing here - only the identifier patterns run

declared by /work/shoddy/.fettler.json

The screen line appears only where a screen is on, and a scope shows one only where the scope itself states one. A scope with no screen line is inheriting.

Read that last line before you believe the one above it. The tree says it screens clinical, and clinical is judged by a model that is not installed yet, so right now it screens nothing at all. roots says so rather than letting the word imply otherwise — and once you install one it names the checkpoint instead. That is step 6.

Now read a file you know holds an identifier:

$ fettle read ward-round.docx
refused: this response would disclose regulated data: 2 in identifiers. What was found is
deliberately not quoted back, because a refusal that named it would put it in the
log and the transcript, which is the disclosure being refused. Read a narrower
range, or take the screen off this scope if the content is not what it looks like.
$ echo $LASTEXITCODE
13

A category and a count, and never the text itself. A refusal that named what it found would write it into the log and the transcript — the exact harm the screen exists to prevent, delivered by the screen. Every screening refusal answers 13.

The two findings here are the labelled MRN: line and the labelled DOB: line, which is why the count is two and the category is identifiers. The clinical prose around them was not read by anything — no model is installed.

If steps 1 and 2 are all you need, stop here. Tier one is real protection with no new dependency, no download and no second process. It is also the whole of what the screen catches: structured identifiers, and nothing else. The rest of this page adds the named-entity models, which are the only way clinical, legal and scientific ever screen anything.
The other three categories need a model, and a model is a separate download. clinical, legal and scientific screen nothing at all until one is installed, and fettle roots says so rather than letting the category name imply otherwise. Installing the models →

Prove it refuses

The configuration being right and the screen holding are two different claims, and only the second matters. Ask the assistant to do each of these, and watch what comes back.

Ask the assistant to…What a working screen does
ask for the rootsthe screened tree carries a screen line naming the categories, and your carve-out scope shows screen: nothing
read a file holding a social security number or a card numberrefused, exit 13, naming a count in identifiers and never the number
read a file carrying a date of birth under its own label — the literal word DOB, a colon, and a daterefused, a count in identifiers
read the same file from the unscreened scopeserved — a scope's screen replaces the tree's
search the tree for a common word that appears near an identifierrefused, because the matched lines and their context are what would have been sent
read an image in a screened scoperefused — nothing here reads pixels, and a photographed record is exactly what this is for
read an ordinary file with no identifiers in itserved. If this one refuses, the screen is on a tree it does not belong on.

Driving burler by hand

When the model tier is the part in doubt, run burler yourself. It reads one request per line and answers one line:

$ burler --models C:/models/screening
{"op":"screen","categories":["clinical"],"payload":"the weather was fine"}
{"ok":true,"findings":[]}

Type the request line, press return, and read the answer.

What comes backWhat it tells you
{"ok":true,"findings":[]}Everything works. It loaded the model, ran it, and found nothing in that sentence.
{"ok":true,"findings":[{"category":"clinical","count":1,…It found something. Try a payload with a name or a date in it to see this.
{"ok":false,"error":"'clinical' is screened and there is no model for it…"}The models directory has no clinical folder. The message names the path it looked at.
{"ok":false,"error":"the model at … would not load…"}The file is there and is not a model burler can run. Usually a bad export, or a file that did not finish downloading.
Nothing at all, and it exitsCheck the message on the error stream. Started with no --models, it says burler needs --models DIRECTORY, naming where the screening models are. and exits 2.

An empty findings array means it checked and found nothing. A missing one is a fault. The two are never spelled the same way, on either side of the pipe.

When it refuses and you did not expect it

Every refusal names its own cause. Read the first few words: they say whether something was found, or whether the screen could not run. Those are different problems.

It saysDo this
this response would disclose regulated data: Working as intended. Read a narrower range, or take the screen off that scope if the content is not what it looks like. The count tells you how many entities, and the category tells you which kind.
this response could not be screened, so it is not being served: The screen broke. What follows is one of the rows below. Nothing was checked, so nothing is served.
… the screening sidecar is not installed burler is not beside fettle. The message says where it looked. Redo step 3, or take "screen" off the scope.
… would not start: The file is there and will not launch. Usually the wrong platform archive, or no permission to execute it. On macOS, the quarantine attribute.
… refused: and then burler's own words burler declined and said why — a category with no directory, a model that would not load, a manifest missing a field. Its message names the file. Drive it by hand to see the whole thing.
… did not answer within 30 seconds It is alive and stuck. The child is killed, so the next read pays a fresh start rather than reusing a process nobody can account for. A payload far larger than a page can do this on a slow machine.
… failed: and then it said: It started and then died, and whatever it printed on the way out is quoted. That is usually the real answer.
… answered something that is not JSON, or four similar The pipe carried something that is not the protocol. Almost always a fettle and a burler from different releases. Unpack matching versions.
this scope is screened, and this is an image Expected. Nothing here reads pixels. Move the image to an unscreened scope if it genuinely holds nothing.

The reference page lists every message the screen can produce, with what each one means.

Why every one of these refuses

Serving content because the check broke is the one outcome this must never produce. A sidecar that will not start, a model that will not load, an answer nobody can parse and an inference that outran its clock all refuse and say which. There is no path through it that fails any other way — including a fault nobody anticipated, which becomes a refusal too.

It reads as an outage, and that is the intended trade. A screen that quietly stopped screening would be worse in exactly the way that matters.

What it costs

WhenWhat happens
Every screened read, alwaysThe patterns run in fettle itself. Each has a one-second ceiling, and in practice they are not measurable.
The first screened read after a quiet spellburler starts and loads a model. Seconds, and the wait is allowed up to 90 of them, because a timeout here would deny a disclosure that was going to be served.
Every read after thatTens of milliseconds. The child is held warm. Thirty seconds is the point at which something is wrong rather than slow.
Five minutes with nothing to screenThe child is killed and its memory released. A tool sitting in an editor all day should not hold hundreds of megabytes for a document somebody read at breakfast.
The same content twiceAnswered from memory. The last 256 verdicts are kept, keyed on the content and on the manifests of the models that judged it.

What it does not do

This is a safety net, not a boundary, and the difference is not a quibble. Named-entity models miss entities. Patterns miss anything written a way they do not expect — a number split across a line break defeats every one of them. A clean verdict is evidence of absence and never a certificate of it. Anyone who believes a screened tree cannot leak has been misled, and that belief is the actual hazard.

What it stops is the ordinary accident: a regulated document read into a transcript by something that was only trying to help. It is not built to withstand anybody trying.

It does notBecause
Cover FCRA or FERPAConsumer-report and education-record data is largely structured identifiers, and those are caught under identifiers by the patterns. That is all. Nothing here is "FERPA compliance", and reading it that way is the misunderstanding this sentence exists to prevent.
Offer a confidence settingAny detection refuses. A dial gets turned until the check is off.
Read imagesNothing here reads pixels, so an image in a screened scope is refused rather than served unchecked.
Redact, mask or editIt refuses the whole response. Handing back a document with holes in it would look like a safe copy of the original, and would not be one.
Screen what you write into a treeThat is the credential net, which judges what a write adds. The refusals section covers it.
Stop anything outside FettlerA shell command, an allowed built-in reader, or a person with the file open reads it as normal. The screen sits on Fettler's answers, and only there. fettle doctor is what finds the ways round.

Where the rest of it is written down

You wantPage
Installing the models the second tier needsScreening: the models
Declaring trees, scopes and tasks in the first placeIn a project
Every exit code, including 13The reference
What Fettler defends against, and what it does notThe reference