ADR 0020: A Core ML text classifier can judge commands, behind a versioned contract, never alone
Date: 2026-09-20. Status: accepted. Extends ADR 0011.
Context
The approval gate judges each command with the rule set plus Apple’s on-device model (ADR 0011). The model classifier is accurate on the labelled set but costs about 1.5 s per command and depends on Apple Intelligence being enabled. An operator may want a classifier that is theirs: trained on their own commands, fast, deterministic, and chosen independently of which model runs the conversation.
Decision
approval.classifierchooses:rules,system-model(the default, unchanged), orcoreml. The olduseModel: falsestill meansrules. The choice is independent ofmodel.CoreMLRiskClassifierruns a Core ML text classifier beside the rules in the same composite, so the rules, the threshold, and the human stay authoritative and the higher level wins.- Contract, version 1. The model takes one string input
text(the command line, trimmed, runs of whitespace collapsed to one space, case kept) and gives one string outputlabelwhose values aresafe,moderate,dangerous. Its creator metadata carrieswisp.classifier.contract = "1"andwisp.classifier.labels = "safe,moderate,dangerous". A model that declares another contract, other labels, or other features is rejected at load. The version string in the model’s metadata is its identity in the audit. - Failure is
moderate. No configured path, a missing asset, a model Core ML cannot load, a contract mismatch, an inference error, no prediction, an unknown label, or a top-label probability belowapproval.coremlMinimumConfidence(default 0.6) all produce amoderateverdict with the reason, and the reason is recorded. Nothing the classifier does can lower a level or grant an approval. - Confidence is recorded only from the model’s own label probabilities, when it gives them (Create ML text classifiers do). It is the model’s estimate, not calibrated; the minimum-confidence threshold is a guard against guessing, not a measurement of accuracy.
- Audit.
classifier.verdictgainsmetadata:coreml.model,coreml.version,coreml.label,coreml.confidence, andcoreml.fallbackwhen the verdict is a fallback, against the same turn and command as every other verdict. scripts/train-risk-classifiertrains a model from atext,labelCSV with Create ML and writes the contract metadata;docs/examples/risk-labels.csvis the eval set;scripts/check evalmeasures a model named byWISP_COREML_MODELwith the same hard requirement as the on-device model: no dangerous command rated safe.
Consequences
- Loading and predicting are cheap (milliseconds); a source
.mlmodelis compiled once per process. - The eval set is 45 commands. A model trained on it proves the pipeline, not its judgement. Measured
on 2026-09-20: trained on all 45 it scores 45/45 on the same rows; trained on the 35 non-held-out
rows it got 5 of the 10 held-out right, and two dangerous commands it called
safe(find … -delete,security find-generic-password) reachedmoderateonly through the confidence guard. That is why the guard exists and why the rules run beside it. Before an operator relies on a model, they evaluate it on commands like theirs; loading and producing valid labels is not evidence of suitability. - General intent classification and model routing are outside this decision.
Amendment, 2026-09-25
ADR 0038 adds contract version 2 (version 1’s features and
labels, with shell punctuation split into tokens), which wisp classifier train writes; version 1
models still load. The classifier now loads its model once, not per verdict. scripts/train-risk-classifier
and docs/examples/risk-labels.csv are removed: they trained on the eval set. The two dangerous
commands this ADR’s measurement saw called safe are revisited there: find … -delete is now caught by
the rules, and a classifier trained on the bundled examples rates neither below moderate.