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.

Download on the App Store
Randori-App-Icon, hell: 乱取り, mit Sumi-Tusche auf Washi-Papier gepinselt
Heller Modus
Randori-App-Icon, dunkel: derselbe Pinselstrich in weißer Tusche auf Schwarz
Dunkler Modus
Warum es diese App gibt

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 Session

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.

Randori auf dem iPhone mit laufender Trainingssession: Herzfrequenz, Anstrengung, Session-Dauer
Eine geloggte Trainingsstunde im Feed: das Video, die Herzfrequenz, der Preis.
Randori-Trainingsansicht mit Anstrengungserfassung pro Runde
Der Tab „Training“: einmal antippen — und die Woche bis jetzt.
Randori-Nachbild-Ansicht, die die Session nach dem Training zusammenfasst
Das Nachbild: was die Session gekostet hat, sobald sie vorbei ist.
Aus Ihren Rolls lernen

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.

Session-Notiz

im Armbar aus der Closed Guard erwischt, den Ellbogen nicht rechtzeitig zurückbekommen

Von der Matte

Stacking Armbar Defense

Escape · Closed Guard · 2 Videos

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.

Swift · LearnSemanticIntent
/// 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
}
Randori-Lektionsansicht, die auf eine Session-Notiz hin eine Stacking-Armbar-Defense vorschlägt
Aus einer Notiz wird eine Lektion: auf dem Gerät gematcht, mit Video unterlegt.
Gürtelfortschritt

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.

Weißgurt
Blaugurt
Lilagurt
Braungurt
Schwarzgurt
Die Ränge, wie die App sie zeichnet — dieselbe Gürtelgrafik, die auch die Prognose verwendet.
Randori-Rangansicht mit Prognose des Gürtelfortschritts
Wohin das Training unterwegs ist.
Randori-Bildschirm zur Feier der Graduierung
An dem Tag, an dem es so weit ist, weiß die App, was es gekostet hat.
Das Design

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.

乱取り (randori), mit Sumi-Tusche gepinselt
Torii Hanko Seal Sun Rain Waves Sparrows Ridge
Acht alternative Icons liefert die App gleich mit: Torii, Hanko, Siegel, Sonne, Regen, Wellen, Spatzen, Grat.
Partner & Architektur

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.

iPhone + Apple WatchSwiftUI · HealthKit · Live-Herzfrequenz
CloudKitöffentliche Datenbank · keine Accounts
Meldungen941-Telemetrie · Moderation

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.

Swift · ModerationList
/// 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.
        }
}
Randori-Feed-Ansicht mit den Sessions der Trainingspartner
Der Feed: Ihr Gym, Ihre Partner, sonst nichts.

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:

Swift · SessionTracker
/// 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.