06 — Datenmodell
Aufbau eines Datensatzes
Alles, was Prometheus speichert und veröffentlicht, ist eine Zeile derselben Form: welches Projekt, welche Version, welche Kohorte, welches Zeitfenster — und ein Wert. Aus den Melder-Modi A bis C wird dieser Wert als Band ausgewiesen statt als Zahl; nur Modus D führt Zahlen aus öffentlichen Registries im Klartext. Das Schema hat keine Felder für Personen, Hosts oder Sitzungen — sie sind nicht etwa leer, es gibt sie schlicht nicht.
Schlüssel
Der Aggregat-Key
(project_id, version_id, cohort_id, window_start) — mehr braucht
es nicht, um ein Aggregat zu identifizieren. Was darin nicht vorkommt, ist
genauso Teil der Aussage wie das, was darin steht.
So entsteht eine Zeile: Aus Roh-Events, die nur im Arbeitsspeicher liegen, werden vier Schlüsselspalten gesetzt, der gemessene Wert wird auf ein Band gemappt, und erst nach der k-Prüfung verlässt das Ergebnis die Maschine.
- project_id pkg:npm/example-lib
- version_id 1.4.2
- cohort_id rt-runtime-mins
- window_start 2026-05-26T12:00:00Z
k_effective unter 5, würde dieselbe Zeile
lokal nach __small_cohorts umgeschrieben — vor dem Senden, nicht
danach.
project_id — die PURL
Immer eine Package-URL, nie freier Text: pkg:npm/example-lib,
pkg:pypi/sample-pkg, pkg:maven/<group>/<artifact>,
pkg:cargo/…, pkg:golang/…. Damit ist
pkg:npm/foo sauber von pkg:pypi/foo getrennt.
version_id — die gemessene Version
Die Version, auf die sich die Messung bezieht. Erst dadurch wird die Frage beantwortbar, ob eine Migration in der Breite ankommt oder ob eine alte Major-Version weiterläuft.
cohort_id — die Kategorie
Eine vordefinierte Kategorie, kein frei gebildeter Schlüssel — etwa
rt-runtime-mins, install-count oder
dep-edges-from-public-manifests. Neue Kohorten werden im
Federation-Standard publiziert, nicht ad hoc erfunden.
window_start — das Zeitfenster
Beginn eines Tumbling-Fensters, UTC-ausgerichtet. Feinere Zeitstempel als die Fenstergranularität sind im Schema verboten — sie wären ein Verhaltensprofil.
Was kein Teil des Keys ist: Es gibt kein tenant_id —
die Plattform kennt keine Mandanten, weil sie keine Kunden hat. Und
source_mode (A, B, C, D) ist eine eigene Spalte, kein
Schlüsselbestandteil: dieselbe Kohorte aus dem SDK und aus dem Registry-Scraper
steht bewusst nebeneinander, statt sich gegenseitig zu überschreiben. Der Key
gruppiert also, er erzwingt keine Einzigkeit — zwei Zeilen mit gleichem Key und
verschiedenem source_mode sind zwei Messungen derselben Sache aus
zwei Quellen.
Wire-Format
Aufbau einer Submission
Eine Submission ist ein kanonisch serialisiertes JSON-Dokument mit einem Fenster, einer Melder-Identität, einer Signatur und einer Liste von Aggregaten. Was hier nicht drinsteht, bekommt ein Mirror auch nicht vom Melder — seine eigenen technischen Metadaten und Bewertungen setzt er beim Eingang selbst dazu.
{
"schema_version": "1.0",
"submitter_id": "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH",
"signature": "ed25519:5K3PqL…",
"window_start": "2026-05-26T12:00:00Z",
"window_end": "2026-05-26T13:00:00Z",
"aggregates": [
{
"project_id": "pkg:npm/example-lib",
"version_id": "1.4.2",
"cohort_id": "rt-runtime-mins",
"metric": "runtime_minutes_band",
"value": "30-60", // Band, keine Zahl
"k_effective": 23, // tatsächliche Kohortengröße
"k_min": 5, // geforderte Untergrenze
"source_mode": "A" // Erfassungsmodus, Spalte statt Key
}
],
"attestation": {
"kind": "declarative",
"claims": ["k>=5", "no-pii-fields", "window-monotonic"]
}
}
Das Beispiel ist eine Lese-Ansicht der Struktur, nicht die signierte Byte-Folge:
Kanonisches JSON kennt keine //-Kommentare, und JCS sortiert die Keys
lexikografisch. Signiert wird das Ergebnis dieser Kanonisierung, nicht das, was
hier steht.
Kanonische Bytes — RFC 8785
Keys sortiert, keine Whitespace-Varianten, UTF-8. Dieselbe Eingabe ergibt in jeder SDK-Sprache byte-identisch dieselbe Ausgabe — sonst wäre keine Signatur sprachübergreifend prüfbar.
Signatur — ed25519 ohne Prehash
Signiert wird direkt über die kanonischen Body-Bytes. Die Melder-Identität
ist ein did:key: ein selbstbeschreibender Schlüssel, keine
Registrierung bei irgendeiner Stelle.
submission_hash — Idempotenz
sha256 über denselben kanonischen Body, aber getrennt von der
Signatur und kein Signatur-Input. Er ist der Schlüssel im öffentlichen
Audit-Log: dieselbe Submission zweimal eingereicht erzeugt keinen zweiten
Eintrag.
attestation — die Zusicherung
Der Melder erklärt, welche Bedingungen er lokal eingehalten hat. In Phase 1 ist die Zusicherung deklarativ; ab Phase 2 tritt eine kryptografische k-Anon-Attestation an ihre Stelle.
Felder
Die Felder im Einzelnen
Die obere Gruppe kommt in den Modi A, B und C vom Melder und ist Teil der signierten Bytes. Bei Modus D gibt es keinen Melder: Dort füllt der Scraper des Mirrors dieselben Spalten mirror-intern und ohne Signatur-Roundtrip. Die untere Gruppe setzt der Mirror in jedem Fall beim Eingang — sie ist Auswertung, nicht Aussage des Melders, und deshalb bewusst nicht signiert.
| Feld | Beispiel | Bedeutung | Gesetzt von |
|---|---|---|---|
| Vom Melder — bei Modus D vom Scraper des Mirrors | |||
| project_id | pkg:npm/example-lib |
Gemessenes Projekt als PURL. Key-Bestandteil. | Melder (A–C) · Scraper (D) |
| version_id | 1.4.2 |
Gemessene Version. Key-Bestandteil. | Melder (A–C) · Scraper (D) |
| cohort_id | rt-runtime-mins |
Vordefinierte Kategorie. Key-Bestandteil. | Melder (A–C) · Scraper (D) |
| window_start / window_end | 2026-05-26T12:00:00Z |
Tumbling-Fenster, UTC-ausgerichtet. window_start ist Key-Bestandteil. |
Melder (A–C) · Scraper (D) |
| metric | runtime_minutes_band |
Welche Größe gemessen wurde. Bestimmt, welche Bänder für value gelten. |
Melder (A–C) · Scraper (D) |
| value | 30-60 |
Band-Label, keine Klartext-Zahl — Ausnahme ist Modus D auf öffentlichen Quellen. | Melder · bei D der Scraper |
| k_effective | 23 |
Tatsächliche Kohortengröße hinter dem Wert. Bei Modus D leer, weil eine öffentliche Registry keine Kohorte hat. | Melder · für D nicht anwendbar |
| k_min | 5 |
Geforderte Untergrenze. Default 5; in den endnutzernahen Modi B und C mindestens 25. | Melder · für D nicht anwendbar |
| submitter_did Wire-Feld: submitter_id |
did:key:z6Mk… |
Identität des Melders als selbstbeschreibender Schlüssel — eine Instanz, keine Person. | Melder · für D nicht anwendbar |
| source_mode | A |
Erfassungsmodus A, B, C oder D. Spalte, nicht Key-Bestandteil. | Melder · bei D der Scraper |
| Vom Mirror beim Eingang gesetzt | |||
| submission_hash | 244be0…3df0 |
64 Hex-Zeichen über die kanonischen Bytes. Idempotenz-Schlüssel und Verweis ins Audit-Log. | Mirror |
| received_at | 2026-05-26T13:04:11Z |
Eingangszeit am Mirror. Bewusst getrennt vom Messfenster. | Mirror |
| trust_tier | new → established |
Neue Melder durchlaufen einen siebentägigen Burn-In, bevor ihre Daten als etabliert gelten. | Mirror |
| plausibility | ok |
Auffälligkeits-Marker (track-a-spike, track-d-missing). Nichts wird gelöscht, es wird annotiert. |
Mirror |
| provenance | registry-scraper@mirror-eu-1 |
Herkunft bei Modus D: welcher Scraper bei welchem Mirror die Zeile erzeugt hat. | Mirror |
| source_schema_version | 1.0 |
Major-Version des Submission-Schemas, aus dem die Zeile stammt. | Mirror |
Die vier Annotations-Spalten — trust_tier, plausibility,
provenance und source_schema_version — sind Auswertungs-Output und im Wire-Format schlicht nicht
vorgesehen: Ein Melder kann sich weder selbst als vertrauenswürdig einstufen noch
seine eigene Herkunft umschreiben.
Fenster & Bänder
Banding statt exakter Zahlen
Die Auflösung ist bewusst so grob, dass niemand darin auftaucht. Zwei Stellschrauben entscheiden darüber, ob aus Aggregaten ein Profil rekonstruierbar wird: wie fein die Zeit aufgelöst ist und wie genau der Wert beziffert wird. Beide sind bewusst grob gewählt.
| Fenster | Verwendung | Sichtbar |
|---|---|---|
| 1 min | nur lokal im Aggregator, zum Debuggen | nie in einer Submission |
| 1 h | Standard für das Self-Instrumentation-SDK (Modus A) | Submission, API, Snapshot |
| 1 d | Standard für das Registry-Scraping (Modus D) | mirror-interner Datensatz, API, Snapshot |
| Kohorte / Metrik | Bänder |
|---|---|
rt-runtime-minsruntime_minutes_band |
0-5 · 5-15 · 15-30 · 30-60 ·
60-180 · 180-1440 · 1440+ |
install-countinstall_count_band |
1 · 2-5 · 5-25 · 25-100 ·
100-1000 · 1000+ |
dep-edges-from-public-manifestsobserved_dependency_count |
Klartext-Zahl zulässig — Modus D wertet ausschließlich öffentliche Manifeste aus, hier gibt es nichts zu schützen, was nicht ohnehin öffentlich wäre. |
So sieht dieselbe Struktur aus, wenn sie aus dem Registry-Scraping kommt: kein
k_effective, weil es keine Kohorte gibt, dafür eine Herkunftsangabe
und ein Tagesfenster.
{
"project_id": "pkg:pypi/sample-pkg",
"version_id": "2.0.0",
"cohort_id": "dep-edges-from-public-manifests",
"metric": "observed_dependency_count",
"value": "1247",
"k_effective": null,
"k_min": null,
"window_start":"2026-05-26T00:00:00Z",
"window_end": "2026-05-27T00:00:00Z",
"source_mode": "D",
"provenance": "registry-scraper@mirror-eu-1"
}
Leerstellen
Was das Schema nicht kennt
Die folgenden Angaben sind nicht optional, nicht abschaltbar und nicht „nur intern“ — für sie existiert schlicht kein Feld. Ein SDK-Fehler, der so etwas mitschicken wollte, produziert eine abgelehnte Submission, keinen Datensatz.
Zwei Gates, nicht eines
Der lokale Aggregator prüft vor dem Verlassen der Maschine, der Mirror prüft beim Eingang noch einmal. Beide Prüfungen sind identisch streng — die zweite existiert für den Fall, dass die erste umgangen wurde.
__small_cohorts
Bleibt eine Kohorte unter k_min, wird sie nicht verworfen und
auch nicht knapp aufgefüllt, sondern in einen Sammeltopf umgeschrieben. Der
Wert geht in die Summe ein, die Kohorte verschwindet.
Ablehnung ohne Spur
Verletzt eine Submission Signatur, Schema, k-Grenze oder Fenster-Logik, antwortet der Mirror mit einem Fehlercode — und persistiert nichts. Auch nicht zur Fehlersuche.
Fenster-Plausibilität
window_start < window_end, und das Fenster darf nicht in der
Zukunft liegen; toleriert werden fünf Minuten Uhren-Drift. Das schließt
vorgezogene Einreichungen aus. Eine untere Zeitgrenze kennt die Prüfung
bislang nicht — ein altes, korrekt geordnetes Fenster erfüllt sie.
Snapshots
Veröffentlichung als Open Data
Was ein Mirror akzeptiert, wird veröffentlicht — ohne Login, ohne Paywall, ohne eine zweite, reichhaltigere Fassung für zahlende Dritte. Es gibt nur diesen einen Datenbestand.
| Eigenschaft | Wert |
|---|---|
| Zeitplan | täglich 00:00 UTC |
| Formate | Parquet als primäres Format, dazu CSV.gz — auswertbar ohne Pandas oder sonstiges Tooling |
| Varianten | raw sind die Roh-Aggregate und die kanonische Fassung fürs
Audit; resolved fasst Projekt-Aliase zusammen. Beide
entstehen täglich; __small_cohorts wird auch in
resolved nie namentlich aufgelöst. |
| Integrität | Beide Varianten tragen denselben SHA256-Manifest-Eintrag des
zugrundeliegenden Datenstandes plus den Hash der verwendeten
Alias-Tabelle — auch die raw-Variante, damit von der
Audit-Seite aus belegbar bleibt, wogegen aufgelöst wurde. |
| Lizenz | CC-BY-4.0 als Vorab-Beschluss; solange die Rechtsfreigabe aussteht, trägt jeder Snapshot eine Provisional-Klausel im Manifest |
| Retention | unbegrenzt — ältere Snapshots werden archiviert, nicht gelöscht |
Neben den Snapshots steht dieselbe Struktur live über die Aggregat-API und den Ereignis-Stream zur Verfügung, ebenfalls ohne Login. Wer prüfen will, ob zwei Mirrors dasselbe sagen, vergleicht die veröffentlichten Audit-Log-Köpfe.