The YazSes core contract — one behaviour, many implementations¶
Status: shipped and enforced in CI (2026-08-07) · normative decision in ADR-MOB-008 Implements: contract/ — six units live (clean_text, disfluency, voice_punctuation, spacing, vocabulary, grammar; 191 cases), guarded by tests/test_contract_vectors.py. The hold detector and the VAD gate land with the implementations that need them. Last updated: 2026-08-09
It earned its keep on day one. The first 54 cases surfaced three real bugs in shipped code:
"that is likely correct"became"that is ly correct",basically_fnbecame_fn, and a URL silently lost a path segment — a filler regex was missing a trailing word boundary and the code-identifier guard was inspecting the matched filler instead of the token containing it. A year of use had not caught them. Writing down what should happen, and then reading what does, is the entire mechanism.
The Android app shares no code with the Python desktop, yet must be the same product. This document specifies the mechanism that makes that checkable rather than aspirational: a language-neutral contract of golden test vectors that every implementation runs.
If you are here to contribute a Kotlin module, the short version is: your module is correct when contract/vectors/<your-file>.json is green. You do not need to read Python.
1. Layout¶
contract/
README.md how to run and extend the contract
VERSION semver, e.g. 4.0.0
schema/
config.schema.json every config section, key, type, default
features.schema.json capability-registry shape
transcript.schema.json Transcript / segment / token-confidence shape
vectors/ PARITY — generated from the shipped implementation
clean_text.json
disfluency.json
voice_punctuation.json
spacing.json
vocabulary.json
grammar_classify.json
hold_detector.json
vad_gate.json
semantic/ MEANING — hand-authored, never regenerated (§8)
dimensions.json conserved dimensions + extraction patterns
invariants.json invariants + minimal pairs
audio/ short licence-clean clips for end-to-end fixtures
2. Vector format¶
Every vector file is a JSON object with a unit, the contract version it was generated under, and a list of cases:
{
"unit": "postprocess.clean_text",
"contract_version": "4.0.0",
"source": "src/yazses/postprocess/cleaner.py::clean_text",
"cases": [
{
"id": "strips-blank-audio-marker",
"description": "Whisper emits [BLANK_AUDIO] on silence; it must never be delivered",
"options": {},
"input": "[BLANK_AUDIO]",
"expected": ""
},
{
"id": "strips-leading-punctuation",
"description": "a burst starting with a comma is a decode artefact",
"options": {},
"input": ", hello world",
"expected": "hello world"
}
]
}
Rules:
idis stable and kebab-case; renaming an id is a breaking change to the vector file.optionscarries only the config keys the unit actually reads, using the shared key names fromschema/config.schema.json.input/expectedare JSON values, not necessarily strings —hold_detectortakes an event trace,grammar_classifyreturns an intent object.- Every unit MUST include the ugly cases: empty, whitespace-only, punctuation-only, unicode and RTL (Persian is a first-class test language for this project), very long input, repeated-word input, and the adversarial sequences the disfluency filter exists for ("no wait, scratch that").
3. Generation and the drift guard¶
scripts/gen-contract-vectors.py runs the shipped Python implementation, writes vectors/
tests/test_contract_vectors.py re-runs every vector against Python; fails on mismatch
This is the same generate-and-guard pattern the repo already uses for the CLI reference (scripts/gen-docs.py + tests/test_gen_docs.py).
The consequence is the point: a desktop PR that changes shared behaviour fails CI until the author regenerates the vectors, which turns silent cross-platform drift into a visible diff and a contract-version bump. Reviewers must read that diff — a regenerated vector file is a behaviour change, not a formatting change.
Case inputs are hand-written and reviewed (they encode intent); only expected outputs are generated. A generator that invents its own inputs would happily bless a bug.
4. Consumption¶
| Implementation | How |
|---|---|
| Python desktop | tests/test_contract_vectors.py (pytest, parameterised over every case) |
| Android | :core:contract-test reads the same files from the repo; JUnit parameterised tests |
| iOS (later) | XCTest over the same files, contract version recorded in About |
No implementation may ship a "known-failing vectors" list. A vector that a platform genuinely cannot satisfy is a contract bug: either the vector is wrong, or the behaviour needs a platform-conditional expectation added explicitly to the case.
5. Versioning¶
contract/VERSION is semver:
- patch — new cases for existing behaviour, no expectation changes;
- minor — a new unit / vector file;
- major — any changed expectation, i.e. a deliberate behaviour change. Requires the justification a behaviour change deserves, and every implementation must be updated or explicitly recorded as satisfying the older version.
Each implementation reports the contract version it satisfies (the Android About screen shows it next to the app version), so "is my phone behaving like my laptop?" has an answer a user can read off the screen.
6. What the contract deliberately does not cover¶
- Model output. Vectors start after speech recognition, from recognised text. Engines differ and are not deterministic across platforms; acoustic quality is
:bench's job. - Platform surfaces. Injection, IME behaviour, lifecycle, permissions — no shared semantics exist, so pretending otherwise would be noise.
- APIs. The contract constrains behaviour, never class names or module structure. Each platform stays idiomatic.
7. The semantic layer — the half the desktop cannot generate¶
Everything above pins parity: every implementation delivers the same string. That is necessary and not sufficient, and the gap is structural rather than accidental.
Because §3 generates every expectation by running the shipped Python, one commit can change the implementation, the generator and the golden data together. CI stays green, the vectors stay green, and a cleanup rule that erased a real distinction is now the cross-platform consensus — inherited by Android and iOS as the specification.
contract/semantic/ closes that hole with expectations no generator can write (ADR-MOB-008 §8, proposed by @YossiMH in #98). Each case records the delivered text a human says the user must receive, plus the dimensions that must survive:
Post-processing may simplify form, but it must not silently erase or invert a distinction that changes what a downstream reader should understand or do.
{
"id": "polarity-medication-not-taken",
"input": "I did not take the medication",
"expected": "I did not take the medication",
"must_preserve": {"polarity": "negative", "actor": ["speaker"]},
"minimal_pair": {"partner": "polarity-medication-taken", "differs_in": "polarity"}
}
Minimal pairs are the sharp edge. Two utterances differing in exactly one consequential dimension must not collapse into the same delivered text — a check no per-case assertion can make, because each output looks entirely plausible alone:
she is stable -> "she is stable"
she is sort of stable -> "she is stable" <- identical: the hedge is gone
On its first run the layer found five shipped behaviours that destroy meaning (#146) — including that dose is not right delivered as that dose is not — while all 191 parity vectors were green. Those cases are recorded as known-gap and are strict-xfail: green while the gap is documented, red the moment the code is fixed, so the case must be promoted rather than forgotten.
For a phone or a watch implementation this changes the promise. "Match Python" is a weaker guarantee than "preserve these meanings" — and only the second one survives the desktop being wrong.
8. Why this is worth the extra step in desktop PRs¶
- It converts "port this module" into a weekend-sized task with an unambiguous definition of done — the single biggest lever on contributor throughput.
- It gives the desktop a regression net over behaviour that is currently specified only implicitly by its own tests.
- It makes the iOS wave cheap: the spec already exists and is executable.
- It means a user can be told, truthfully, that the phone and the laptop clean, filter, punctuate and classify their speech identically.