Tutorial 3: Wire a typed Decision Catalog entry
Author a DecisionSpec with required_evidence so the Critic produces a typed, replayable DecisionRecord.
In Tutorial 2 the runtime executed a destructive call. Now we close the audit substrate: every governed action emits a typed DecisionRecord validated against a DecisionSpec in the Decision Catalog. Without typed decisions, audit cannot replay and improvement cannot compound.
What we are building
A spec declares the closed contract for one kind of decision.
{
"decision_key": "support.refund.execute",
"version": "1.0.0",
"owner_role": "support_ops",
"required_evidence": ["order_lookup", "policy.eval", "identity_verified"],
"allowed_outcomes": ["approved", "denied", "escalated"],
"approval_mode": "destructive",
"decision_right": "execute",
"challenge_required": false
}Every field is enforced:
required_evidence— the Critic refuses to commit if any entry is unresolved.allowed_outcomes— the runtime rejects an outcome outside the closed set.approval_mode— must be ≥ the highest mode of any tool the decision may invoke.decision_right—propose/recommend/execute/escalate. The runtime refuses execute on arecommend-only spec.
The decision_binding field on the rule (from Tutorial 2) is what ties the policy decision to the DecisionRecord.
{ "rule_id": "R_HIGH_VALUE_REQUIRES_APPROVAL", "decision_binding": "decision.support.refund.execute" }The Critic uses this link to populate policy_decisions[] on the resulting record.
After the canonical loop completes, the Critic emits a record like this (truncated):
{
"record_id": "dr_2026_05_04_a17",
"decision_key": "support.refund.execute",
"decision_version": "1.0.0",
"status": "DECIDED",
"actor": { "type": "AGENT", "id": "agt_support" },
"outputs": { "refund_amount_inr": 4200, "currency": "INR", "transaction_id": "txn_q9..." },
"evidence_refs": [
"kg:order:ord_881#snapshot_kg_2026_05_03_T0930",
"tool:adp_orders.lookup:tc_117",
"tool:adp_policy.eval:tc_119",
"tool:adp_payments.issue_refund:tc_121"
],
"policy_decisions": [
{ "policy_decision_id": "pol_9900", "rule_ids": ["R_REFUND_REQUIRES_IDV"] },
{ "policy_decision_id": "pol_9901", "rule_ids": ["R_HIGH_VALUE_REQUIRES_APPROVAL"] }
],
"approvals": [{ "gate_id": "GATE_FINANCE_APPROVAL", "approver": "user_finance_lead_77", "approval_mode_effective": "destructive", "evidence_snapshot_hash": "sha256:b2a1..." }],
"controls_active": { "must_refuse": [], "must_escalate": [], "approval_gates_active": ["GATE_FINANCE_APPROVAL"], "redaction_rules_active": ["pan", "credit_card"] },
"lineage": { "pack_version": "ctxpack.support@5.2.0", "snapshot_version": "kg_2026_05_03_T0930" },
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736"
}Notice three properties:
lineagepins both pack and snapshot — this is what makes replay deterministic.evidence_refscovers all three entries fromrequired_evidencein the spec.policy_decisions[]lists every rule that fired, withpolicy_decision_idfor cross-correlation.
The Decision Catalog component re-validates every record on emission. Failures are typed:
{ "error_code": "OUTCOME_OUT_OF_RANGE", "detail": "Outcome 'partial_refund' is not in allowed_outcomes." }
{ "error_code": "EVIDENCE_MISSING", "detail": "required_evidence 'identity_verified' did not appear in evidence_refs." }
{ "error_code": "APPROVAL_MODE_TOO_LOW", "detail": "Spec requires destructive; tool emitted delegated." }These are not exceptions — they are typed verdicts that the Critic surfaces back to the planner under the re-plan budget.
What’s next
Tutorial 4: Close the improvement loop — turn an operator correction into a release-gated StrategyRule.