The agent never issues a religious ruling. It emits findings about text evidence, and carries no score or confidence figure anywhere.
A number would be a determination with a decimal point in it, and no field in any schema accepts one.
ADR-0001
A triage agent for crowdfunding campaigns. It reads a submitted campaign, assembles the evidence a zakat determination would turn on, names what the text leaves missing or contested, and hands the file to a qualified human reviewer.
Where this came from
The assumptions this is built on
The two constraints it started from
Before any code, one question: what is the worst thing this system could do silently? Two answers came back, and they became constraints, written down first, enforced everywhere.
Two versions, one brain
The problem
Zakat is obligatory almsgiving, and it may only go to the eight categories of recipient named in Surah At-Tawbah (9:60). The word rendered "only" is why the list is read as exhaustive rather than illustrative, so eligibility is a question about which named category a campaign falls under and not about how deserving it looks.
That review load is not spread evenly through the year. A Ramadan giving report cited in section 1.5 of the research brief states that "78% of Zakat donations came inside of Ramadan", so the demand arrives compressed into thirty days, at the one point in the year when reviewer attention is scarcest. A triage system's value sits almost entirely inside that window, and so does its risk.
A wrong determination is a religious harm in both directions, and the failure asymmetry in section 6 of the research brief is what shapes the design. A campaign wrongly badged eligible may leave a donor's obligation undischarged, and that harm is silent. A campaign wrongly denied loses access to the zakat donor pool at the moment that pool is largest, which during Ramadan is effectively a denial for the year, and the harm falls on people who are by construction likely to be poor. Only one of the two generates its own corrective signal, and only one is recoverable.
So the honest default under uncertainty is a question, not a verdict. It costs throughput exactly where throughput is scarcest, which is the real price of this design and is stated rather than hidden.
What it does
Prepares a cited evidence file about a submitted campaign so that a qualified human reviewer can adjudicate it. For a submitted campaign it:
Trust design
The agent prepares the file and a qualified human adjudicates. That boundary is architectural rather than a disclaimer: there is no code path in which a determination is published without a recorded human decision.
The agent never issues a religious ruling. It emits findings about text evidence, and carries no score or confidence figure anywhere.
A number would be a determination with a decimal point in it, and no field in any schema accepts one.
ADR-0001
Citations are verbatim quotes resolved to offsets server side.
A supported finding with no citation is unrepresentable: the union types the citation list as non-empty, so it fails to construct in TypeScript and to parse at runtime.
ADR-0003
Retrieved precedent renders to the reviewer and never enters a prompt.
Retrieval belongs to the render, and src/lib/triage.ts has no way to reach it. A prompt-recording trace test and an import-graph fence hold it.
ADR-0004
The refusal is deterministic code over typed output.
The model cannot talk the pipeline out of an escalation, and escalate: true carries a non-empty reason list, so a bare needs-review flag cannot be constructed.
ADR-0006
Nothing in citation position is model-authored.
Campaign spans are byte-checked against the story, scholarly-difference text is retrieved by id from versioned human-authored data, and model prose is guarded against quotation and citation shapes.
ADR-0007
The only representation of an outcome in the entire schema is a human decision row.
SQL CHECK constraints enforce it, and the suite proves the constraints by inserting past the application-level validation.
ADR-0008
The three statuses
A finding is a statement about the campaign text, never a determination of eligibility.
supported
the story states the qualifying facts the category asks for, and the spans stating them are cited. Hardship, urgency, a sympathetic account and a sum of money are not qualifying facts.
insufficient_evidence
the story engages the category, or gestures at it, and the qualifying facts are missing. The absent fact is named and the question that would obtain it travels with it.
not_supported
the story does not engage the category at all, or engages it and points away.
Evaluation
The gate runs in CI on pull requests into main and on pushes to main, and a missed threshold exits non-zero and fails the build. A missing ANTHROPIC_API_KEY fails the job rather than skipping it, because a gate that passes without running is green for a run nobody performed. The latest green run:
| Gate | Requires | Observed |
|---|---|---|
citation-validity | 100.0% | 100.0% (43 of 43) |
citation-anchoring | 90.0% | 100.0% (14 of 14) |
category-agreement | 80.0% | 97.2% (140 of 144) |
escalation-agreement | 75.0% | 83.3% (15 of 18) |
missing-evidence-coverage | 75.0% | 92.9% (39 of 42) |
judge/responded | at most 2 unjudged | 0 unjudged of 18 |
judge/no-ruling | 0 failures | 0 failures in 18 |
judge/evidence-not-assertion | 85.0% | 94.4% (17 of 18) |
judge/sendable-questions | 85.0% | 100.0% (18 of 18) |
judge/unresolved-only-where-engaged | 66.7% | 66.7% (12 of 18) |
The coverage and engagement floors were lowered after this run, in a commit arguing from two runs on byte-identical code (issue #32); the table shows the floors as they stand, and the run cleared the stricter originals.
The gate went red four times before it went green. Each red caught something real, and none was fixed by lowering a bar without an argument. Every run is in the repository's Actions.