Policy decisions & appeals
Sentiqra returns evidence - signals, scores, and execution states. What your product does with that evidence is your policy. This page ships working code for that boundary so you don't implement it from scratch.
The boundary
Every response separates what the analyzer produced from what you do about it. A high score is evidence about content, not a verdict about a user. Two rules keep that boundary honest:
- Check execution state before acting. A signal only exists when the analyzer actually ran:
statuscompletedwithassessmentsignals_detected. Anot_applicableorinsufficient_contentresult is not a clean bill - route it explicitly. - Actions belong to your policy, not the score. Thresholds, context, and consequences are customer decisions. The same signal can mean "publish" in one product and "queue for review" in another.
Reference architecture
Everything after the response lives in your environment - your dataset, your policy, your appeals log:
POST /v1/analyze --> response (signals + states + receipt)
sentiqra-policy.py evaluate --> decision {action, matched rules, review}
analysis + policy + context your code executes the action
appeal-open / appeal-resolve --> append-only appeals record
The evaluator and appeals tool run locally against saved files. No case content is sent to Sentiqra to produce a decision or process an appeal.
A versioned policy file
Policy is data, not code: a JSON document you own and version. Rules name the signal codes they act on, the score thresholds you chose, and any contextual facts your system supplies:
{
"schema": "sentiqra/policy/v1",
"version": "policy-2026-09",
"rules": [
{
"id": "threat-repeat-offender",
"when": {
"analyzer": "trust-safety",
"signal": "threats_intimidation",
"min_score": 0.9,
"context": {"repeat_offender": {"eq": true}}
},
"then": {"action": "queue_review", "review": "human_required",
"reasons": ["high_confidence_threat", "repeat_offender"]}
},
{
"id": "insufficient-content",
"when": {"analyzer": "trust-safety", "applicability": "insufficient_content"},
"then": {"action": "no_action", "review": "none",
"reasons": ["input_not_analyzable"]}
}
],
"default": {"action": "allow", "review": "none",
"reasons": ["no_rule_matched"]}
}
Rules are evaluated in document order - list most severe first. Signal conditions only evaluate against analyzers that completed with detected signals; a rule that names status, applicability, or assessment targets that execution state instead. Context conditions compare against facts you pass via --context; a missing fact fails the condition rather than being guessed.
The decision record
{
"schema": "sentiqra/decision/v1",
"action": "queue_review",
"policy_version": "policy-2026-09",
"matched_rule_ids": ["threat-repeat-offender"],
"review": "human_required",
"reasons": ["rule:threat-repeat-offender", "high_confidence_threat",
"repeat_offender", "signal:threats_intimidation:threat_direct"],
"evidence": {
"configuration": "<configuration-alias>",
"fingerprint": "fp2_…",
"analyzers": {"trust-safety": {"status": "completed",
"applicability": "analyzable",
"assessment": "signals_detected"}}
}
}
The record answers an auditor's four questions: what happened (action), which rules fired (matched_rule_ids), whether a human must review (review), and which analysis it came from (evidence: the configuration alias and response fingerprint). Evaluation is deterministic - the same response, policy, and context always produce the same decision.
Appeals without rewriting history
An appeal record links the original receipt, the policy version, and the action taken - then records the reviewer's outcome and correction in a separate block. The original is never edited; a SHA-256 record_hash over it lets you prove that:
sentiqra-policy.py appeal-open \
--decision decision.json --receipt receipt.json \
--reason "author disputes classification" --out appeal.json
sentiqra-policy.py appeal-resolve \
--appeal appeal.json --outcome overturned \
--correction correction.json --reviewer rev-7
sentiqra-policy.py appeal-verify --appeal appeal.json # original intact?
Outcomes are upheld, overturned, or modified. A resolved appeal cannot be re-resolved - corrections are append-only.
Appeals correct individual decisions - they do not measure production quality. Appeals and complaints are a self-selected intake channel: cases nobody appeals are not "correct", and a pile of overturned appeals says something about the appealed slice only. For representative measurement, sample across all traffic strata with sentiqra-audit.py (see the Production Guide).
Pair this with receipts and deterministic replay: the receipt reproduces the analysis, the decision record reproduces the policy outcome, and the appeal record shows what changed on review. When you need signed evidence that a specific analysis-and-action pair was bound at a point in time, mint an event record and store its event_id in the appeal's decision block - a repeated fingerprint alone never proves a unique transaction.
What "reasons" explain - and what they don't
A decision's reasons explain the policy: which rules matched and which signals satisfied them. They are not an explanation of the analyzer's internal reasoning. If you surface explanations to end users, describe them as policy reasons ("your message matched our review policy for threatening content"), not as a faithful account of how the model scored the text.
Get the toolkit
The reference evaluator is a single dependency-free Python file:
curl -O https://sentiqra.tech/eval-toolkit/sentiqra-policy.py python3 sentiqra-policy.py sample-policy > policy.json # starter policy python3 sentiqra-policy.py evaluate --analysis resp.json --policy policy.json --context ctx.json
Related: Understanding signals for the fields these rules match on, and the Production Guide for execution states, pinning, and partial results.