Record AI decisions about people
A signed, hash-chained record of every automated step your app takes about a person, and of the human decisions after it. It holds hashes, your own ids and a keyed reference for each person, never their name, contact details or profile. From it you can explain to one person, in plain words, what was recorded about them, and export a signed log to keep.
It records and explains. It does not score, rank or screen anyone, and it does not make a tool compliant with any law.
Checked 29 Sep 2026. Full eval. Open source (Apache-2.0); nothing is sent to Decosa.
Try it
A sample decision log for a synthetic [TEST] requisition, made by the kit: screens, a score, a ranking, messages and two human decisions about two synthetic people.
How it works
- Your app calls record() at each automated step: which model or rule ran, the sha256 of what went in and what came out, a small structured result (a score, a band, which requirements were met) and what the step did to the person's path (a signal, a ranking, not shortlisted, a message sent).
- A person is named only by a keyed reference: an HMAC of your own candidate id with a secret you keep. The kit refuses entries that hold names, emails, URLs, phone numbers, free text or floats, before anything is written.
- Each entry names the hash of the one before it, and the chain is signed with your own Ed25519 key at regular checkpoints and at every export. Change, delete or reorder one entry and verification names the entry.
- explain(ref) turns one person's entries into a plain explanation: the steps in order, the outcome, how much was automated, the kinds of data used and how to ask for a correction or a person's review. It comes with a proof that discloses only that person's entries.
- export(range) seals a signed snapshot of the whole chain and gives flat rows (JSON or CSV) for a date range, for the log you keep.
What a record holds
| Holds | The step, the model or rule and its version, the sha256 of the input and output, a small result, the effect, the reviewer's user id and decision, the time, and your own ids for the organisation and the requisition. |
| Never holds | Names, emails, phone numbers, profile or resume text, links, free-text reasoning about the person, or floats (probabilities are stored as integers per mille). |
| Where it lives | In your own database, through a small store interface (a Postgres table, a file, or memory). Nothing is sent to Decosa or anyone else. |
| Who can check it | Anyone with the export and your public key: the kit's own verifier, decosa-api's Python verifier, or POST /record/verify on Decosa's API. |
Use it
import { DecisionLog, Signer, candidateRef } from "@decosa/decision-record";
const log = new DecisionLog({
signer: Signer.fromString(process.env.DECISION_RECORD_SIGNING_KEY),
store, // your DecisionStore: a Postgres table, a file, memory
app: "your-app", chain: `your-app:req:${reqId}`,
subject: { type: "requisition", id: reqId },
});
const ref = candidateRef(process.env.DECISION_RECORD_REF_KEY, candidateId);
await log.record({
step: "score", candidateRef: ref, model: { id: "your-model", provider: "you" },
input: prompt, output: modelOutput, // only their sha256 is kept
result: { score, band }, // integers, booleans, short labels
effect: "signal",
});
await log.record({ kind: "human", candidateRef: ref, reviewer: userId, decision: "rejected" });
const { text, proof } = await log.explain(ref, { employer: "Acme", contact: "reply to this email" });
const { record, rows } = await log.export({ fromMs, toMs });| record(step) | Append one automated step ({step, model, input, output, result, effect}) or a human one ({kind: "human", reviewer, decision}). Throws on personal data or an invalid step. |
| explain(ref, {employer, contact}) | The plain explanation for one person, plus a proof bundle that discloses only their entries. |
| export({fromMs, toMs}) | A sealed decosa.record.v1 snapshot of the whole chain and flat rows for the range; rowsToCsv() makes a spreadsheet-safe CSV. |
| verifyDecisionRecord(record) | Checks the chain, signature, checkpoints and the decision-log rules, and names the first bad entry. |
| POST /record/verify | Decosa's public verifier accepts an export's record unchanged (no key needed; it reports that it did not sign it). |
How it was checked
- Python and TypeScript build byte-identical records, with the same signature, from the same inputs.
- Each kind of tampering in the eval (a changed result, a deleted or reordered entry, a rewired link, a forged checkpoint or signature, free text or a raw id slipped in) fails on both sides at the same entry.
- Both sides give the expected verdict on every case in the shared personal-data guard set.
- A larger synthetic log built on both sides is identical, verifies across languages and contains none of the synthetic ids or names it was built from.
The measured figures are in the eval write-up, generated by the eval script. Synthetic data only.
Who uses it
AutoTalentin review
Decosa's sister recruiting app, on a branch: scoring, the search-result screen, the talent-pool ranking, the AI filter, the must-have check and the outreach and follow-up senders each write a record, and each requisition gets a decisions export. Not live until its owner reviews it.
Any app that decides about peopleopen
Hiring, lending, tenant screening, benefits: the same kit, with your own steps and your own store.
Why keep these records
Laws on automated employment decisions ask employers for notices, explanations and records. These are the ones the format was built around. Not legal advice.
- Colorado SB 26-189: Signed 14 May 2026, in force 1 Jan 2027: notice, a plain explanation within 30 days of an adverse decision, and a way to ask for human review.
- California automated-decision rules (employment): In force 1 Oct 2025; related records kept for four years. Secondary summary; not checked against the regulation text.
- New York City Local Law 144: Bias audits and notices for automated employment decision tools (State Comptroller's audit of its enforcement, 2 Dec 2025).
Licences
| @decosa/decision-record | Apache-2.0 (TypeScript, no runtime dependencies). Not yet published to npm: apps vendor a pinned copy. |
| Record format and Python verifier | Apache-2.0 (decosa-api attest.py, hashchain.py, decision_record.py), an exception to decosa-api's AGPL. |
What it does not do
- It does not score, rank, screen or decide anything about anyone. Your app does that; the kit records it.
- It does not make a tool or a process compliant with any law. It keeps the record that notices, explanations and retention draw on; what a law requires of you is for you and your counsel.
- It does not prove a model ran or that its output was right. The signature is your attestation that the recorded hashes and results were not changed after signing.
- It does not catch every kind of personal data: the guard stops common mistakes, and a short label can still identify someone if you put it there.
- It is not published to npm yet.