Randori
Das Jiu-Jitsu-Trainingstagebuch. Gebaut, weil ich trainiere.
Die Fitness-Apps, die ich ausprobiert habe, führen Jiu-Jitsu unter Sonstiges — eine Stunde des härtesten Trainings, das ein Körper leisten kann, erfasst als Kalorienschätzung. Ich wollte nach dem Training drei Dinge wissen: was mich der Abend gekostet hat, was mich erwischt hat und wie ich dafür sorge, dass es mich nicht wieder erwischt. Randori ist genau das: diese drei Fragen, richtig beantwortet.
Ich habe das für mich gebaut — auf der Matte, nach dem Training
Ich mache Jiu-Jitsu. Die meisten Abende enden gleich: Ich sitze am Mattenrand und versuche, mich zu erinnern, was in der letzten Stunde wirklich passiert ist — welche Rolls den Tank geleert haben, in welchen Armbar ich zweimal hineingelaufen bin, was mein Coach in den dreißig Sekunden gesagt hat, in denen ich ihn noch hören konnte.
Die Apps, die ich ausprobiert habe, wollten aus mir einen Läufer machen. Die Notizbücher weichten durch. Also habe ich das Ding gebaut, nach dem ich immer wieder griff und das ich nie fand: ein Tagebuch, das Jiu-Jitsu spricht — Anstrengung, Positionen, Submissions, Notizen in meinen eigenen Worten — und das die Fehler von heute Abend in den Lernplan für morgen verwandelt.
Das ist das gesamte Produktbriefing. Alles in der App beantwortet eine meiner drei Fragen nach dem Training — und was das nicht tat, wurde gestrichen.
Die körperliche Wahrheit eines Trainingsabends
Starten Sie eine Session, und die Watch (oder ein gekoppelter Brustgurt) streamt die Herzfrequenz live, während Sie rollen. Die Anstrengung wird pro Runde geloggt, in den Sekunden zwischen zwei Rolls — ein Daumen genügt, schonungslos ehrlich. Wenn das Training endet, haben Sie die wahre Gestalt des Abends: wie hart, wie lange, wie viel noch im Tank war.
Schluss damit, immer vom Gleichen erwischt zu werden
Für dieses Feature habe ich die App überhaupt erst gebaut. Sie schreiben in Ihren eigenen Worten auf, was passiert ist — eine Notiz, kein Dropdown — und Randori ordnet ihr videogestützte Lektionen zu, von Trainern, von denen sich das Lernen lohnt. Werden Sie in einem Armbar erwischt, sehen Sie die Verteidigung. Und danach die Konter gegen die Konter.
im Armbar aus der Closed Guard erwischt, den Ellbogen nicht rechtzeitig zurückbekommen
Stacking Armbar Defense
Das Matching passiert direkt auf Ihrem iPhone — Ihre Notizen müssen das Gerät nie verlassen, um verstanden zu werden. Die Bibliothek reicht vom Weißgurt bis zum Schwarzgurt; die Antwort holt Sie also auf Ihrem Niveau ab.
Ist Apple Intelligence auf dem iPhone vorhanden, liest ein Sprachmodell direkt auf dem Gerät die Notiz und beantwortet eine einzige, enge Frage: Wurde diese Technik an Ihnen angewendet — oder von Ihnen? Es wählt dabei immer nur zwischen zwei Seiten, die es bereits gibt; eine Fehldeutung ist also ein harmloser Fehlgriff, nie eine Erfindung. Keine Apple Intelligence? Dann übernehmen kalibrierte Heuristiken. So oder so verlässt die Notiz das iPhone nie.
/// The SEMANTIC tier: Apple's on-device foundation model, asked ONE /// narrow question the lexical scorer is structurally bad at. The /// words still never leave the device — the model runs in silicon, /// not a cloud — and every path fails toward the deterministic tier: /// unavailable hardware, a guardrail refusal, and a timeout all /// collapse to nil, never to a wrong answer. The model NEVER /// free-generates a suggestion; it only classifies intent on a /// technique the scorer already found, and the redirect target is /// always a graph edge. enum LearnSemanticIntent { /// Apple Intelligence present, enabled, and ready. On anything /// else (old hardware, disabled, model still downloading) the /// caller falls back to the calibrated heuristics. static var isAvailable: Bool { SystemLanguageModel.default.availability == .available } /// The question the bigram heuristic kept fumbling: was this /// technique done TO the athlete, or BY them? Binary, grounded /// in the exact note and the exact technique — the answer only /// ever chooses between two graph-true pages (the technique or /// its curriculum counter), so a misread is a soft miss, never /// an invention. nil means "no verdict" — heuristic decides. static func happenedToAthlete(note: String, techniqueName: String) async -> Bool? { guard isAvailable else { return nil } let session = LanguageModelSession(instructions: """ You read one Brazilian jiu-jitsu training note and answer one \ question about a technique the note refers to: was that technique \ done TO the athlete who wrote the note (they were caught in it, \ tapped to it, kept getting stuck in it, struggled to defend it), \ or did the athlete perform, drill, practice, teach, or study it \ themselves? Getting caught, tapping out, and being swept or \ submitted all mean it happened to the athlete. Saying they hit \ it, landed it, finished it, drilled it, or attacked with it means \ the athlete performed it — that is NOT done to them, even when \ the technique sounds violent. """) do { let response = try await session.respond( to: "Training note: \"\(note)\"\nTechnique: \(techniqueName)", generating: LearnIntentReading.self, options: GenerationOptions(sampling: .greedy) ) return response.content.happenedToAthlete } catch { return nil } } // A silence-filling catalog-pick tier was BUILT here and KILLED // by the eval the same hour (07-22): asked to match a lexically // silent note against the full catalog, the on-device model // answered 5 positives WRONG (rnc-defense for mount-escape // notes) and false-alarmed on 7 of 17 adversarial negatives // ("took my turtle back to the pet store" → turtle recovery). // The 3B model classifies narrow questions well and free-matches // badly; open-catalog recall belongs to a frontier model behind // a proxy, measured on this same eval, or to nobody. } @Generable struct LearnIntentReading { @Guide(description: """ true when the technique was done TO the athlete — caught in it, \ tapped to it, kept getting stuck in it; false when the athlete \ performed, drilled, or studied it themselves """) var happenedToAthlete: Bool }
Der Gürtel, ehrlich gemacht
Fortschritt im Jiu-Jitsu ist notorisch undurchsichtig — Jahre zwischen den Gürteln, Graduierungen, die kommen, wann sie kommen. Randori zählt, was Sie selbst in der Hand haben: Sessions summieren sich zu einer ehrlichen Prognose, wohin Ihr Training unterwegs ist — damit der lange Weg Kilometersteine hat.
Gezeichnet, nicht gesetzt
Das Icon ist echte Kalligrafie — 乱取り, randori, mit Sumi-Tusche auf Washi-Papier gepinselt. Das helle Icon ist die Tusche; das dunkle ist derselbe Strich in Weiß, das Negativ des Pinsels. Nichts an diesem Zeichen stammt aus einer Schrift.
Der Rest der App hält diese Disziplin. Ein einziges Rot, das Siegel, eingesetzt, wie man einen Hanko einsetzt: selten, und nur, wenn es ernst gemeint ist. Gürtelfarben tragen den Rang, weil sie das im Gym längst tun. Und das Icon gibt es in acht weiteren Handschriften (Torii, Hanko, Siegel, Sonne, Regen, Wellen, Spatzen, Grat), damit Ihr Home-Bildschirm sein eigenes Wetter wählen kann.
Ein Gym besteht aus Menschen. Die App auch.
Sie trainieren nicht allein, und das Tagebuch sollte nicht so tun, als ob. Verbinden Sie sich mit den Menschen, mit denen Sie wirklich rollen, und teilen Sie Sessions — eine soziale Ebene, die auf der öffentlichen CloudKit-Datenbank läuft. Keine Accounts, und Ihre Trainingsdaten bleiben in Ihrer iCloud. Das Einzige, was das Studio je erreicht, sind Missbrauchsmeldungen und anonyme Nutzungszahlen — und keines von beidem enthält Ihr Training.
Community-Funktionen bringen Community-Verantwortung mit sich: Nutzermeldungen waren ab 1.0 an Bord, eingebunden in das hauseigene Moderations-Tooling des Studios.
Moderation auf einer öffentlichen Datenbank ist ein Integritätsproblem, und genau so behandelt die App sie. Jede Sperre wird mit dem Studio-Schlüssel signiert, und jeder Client prüft diese P256-Signatur, bevor er die Aktion umsetzt. Ein gefälschter Eintrag scheitert an der Prüfung und wird ignoriert. Ist die Moderationsliste nicht erreichbar, verhält sich die App fail-open: Der Feed läuft weiter, denn ein Netzwerkschluckauf sollte niemals ein Gym verstummen lassen.
/// The developer's removal power, serverless (guideline 1.2: act on /// reports by removing content and ejecting users). The team /// publishes ModerationActionV1 records to the app's PUBLIC CloudKit /// database — a surface every install can read. Every device fetches /// the list and enforces it: /// /// - a banned POST leaves every reader's stored feed, /// - an ejected USER loses every audience: their posts drop, their /// invitations are refused, and their own app stops publishing. /// /// EVERY ACTION IS SIGNED, and an unverifiable one is ignored. /// CloudKit grants create permission per ROLE, and a server-to-server /// key acts as an authenticated identity (Apple: "as the developer who /// created the key") — so the grant the operator needs is one a /// determined user could also reach with a custom client. The /// signature is what makes the list trustworthy rather than merely /// hard to reach: only the holder of the private half can mint a ban, /// and a forged record dies here. /// /// Fail-open by design: an unreachable public database never blocks /// the app; the last fetched list keeps enforcing from disk. @MainActor @Observable final class ModerationList { /// The moderation signing key's public half (P-256, X9.63). The /// private half lives only in the moderation service. private static let signingPublicKey: P256.Signing.PublicKey? = { guard let data = Data(base64Encoded: "BK+sQ8R8B/PBsA9GuZhVzba4l4NpOn8trxk2tu54/xXqAdBO4xtEc2PZaa+j9drSyfS4TwYu1a/KYofedpdUShc=" ) else { return nil } return try? P256.Signing.PublicKey(x963Representation: data) }() /// A ban counts only if the signature over "kind|targetID" checks /// out against that key. Anything else is somebody else's noise. static func isAuthentic(kind: String, targetID: String, signature: String) -> Bool { guard let key = signingPublicKey, let signatureData = Data(base64Encoded: signature), let parsed = try? P256.Signing.ECDSASignature(derRepresentation: signatureData) else { return false } return key.isValidSignature(parsed, for: Data("\(kind)|\(targetID)".utf8)) } // … refresh() pages through the whole public list; only records // that pass isAuthentic() are ever enforced. When the fetch fails: } catch let error as CKError where error.code == .unknownItem || error.code == .invalidArguments { // The record type does not exist yet in this environment: // an empty list, honestly. First-run schema state, not a // failure worth surfacing. refreshedThisLaunch = true } catch { // Network or account trouble: keep enforcing the last // fetched list. Never louder than that. } }
Kleine, ehrliche Maschinerie
SwiftUI auf iPhone und Watch. Sessions synchronisieren über Ihre private iCloud und landen als echte Workouts in Apple Health. Selbst die anonyme Nutzungstelemetrie definiert eine Session so, wie es ein Trainingstagebuch täte — ein Schub Aktivität, beendet durch eine halbe Stunde Ruhe:
/// Session identity: a burst of activity separated by ≥30 quiet minutes. /// Memory-only by design — no persistence, no cross-launch identity. /// Rotation happens in `noteActivity` (called per tracked event), never in /// `currentID` — reading the id at flush time must not start a new session. enum SessionTracker { static let quietGap: TimeInterval = 30 * 60 private static let lock = NSLock() nonisolated(unsafe) private static var id = UUID().uuidString nonisolated(unsafe) private static var startedAt = Date() nonisolated(unsafe) private static var lastEventAt = Date() /// Session that just closed, reported at rotation so the caller can /// record its end under the OLD id. struct EndedSession: Sendable { let id: String let duration: TimeInterval } /// Called before each tracked event. After a quiet gap the old session /// closes (returned) and a fresh one starts. static func noteActivity(now: Date = Date()) -> EndedSession? { lock.lock() defer { lock.unlock() } var ended: EndedSession? if now.timeIntervalSince(lastEventAt) > quietGap { ended = EndedSession(id: id, duration: lastEventAt.timeIntervalSince(startedAt)) id = UUID().uuidString startedAt = now } lastEventAt = now return ended } } // RandoriTelemetry.swift — the only shape a duration ever leaves in: /// Coarse session-length bucket — raw minutes never leave the device. static func minutesBucket(_ minutes: Int) -> String { switch minutes { case ..<30: "u30" case 30...44: "30_44" case 45...59: "45_59" case 60...89: "60_89" default: "90_plus" } }
Zwölf Sprachen zum Start, von Japanisch bis Arabisch — weil das Vokabular dieses Sports längst global ist und die App ihm dort begegnen sollte.
Ihr Training, endlich gezählt.
Randori 1.0 ist im App Store erhältlich — für iPhone, mit Live-Sessions auf der Apple Watch.