Two kinds of contribution matter here, and the first one matters more.
This is the contribution the project needs. The format library is only worth its sample sizes, and right now most of them are zero.
You do not need to have used this repo's renderer for the format to count — but you do need to be able to attribute a published video to a specific variant.
# 1. Publish something rendered from this repo, and record its variant id
npx kw variant ranking-suspense
# ranking-suspense@1.0.0+7101b32c10da
# 2. One line in measure/mapping.csv, written at upload time
# platform,external_id,variant_id,published_at,content_axis
# 3. Wait 72 hours. Early numbers are platform push, not format quality.
# 4. Export analytics CSVs into measure/inbox/ (see references/measuring.md)
npx kw measure
npx kw measure apply
# 5. Open a PR
git checkout -b measure/<your-handle>
git add formats/*/data.yml measure/mapping.csv
git commit -m "measure: 6 samples across ranking-*"If you would rather not clone anything: Submit a measurement →
The form asks for exactly the fields below, and refuses to submit without the ones that make a number checkable.
| Field | Why it is required |
|---|---|
variant_id |
Without it the number is attached to nothing. not-in-library is a valid answer for a format that is not in this repo — it will count toward a content axis and never toward a format. |
platform + external_id |
So the row can be audited against the export it came from. |
published_at |
Age drives everything early. A day-one number and a week-one number are different measurements. |
measured_at |
Views and retention both move. A figure with no reading date cannot be compared with anything, and it is the field people skip. Two readings of the same video days apart are two observations, not a contradiction — both are kept. |
n |
Videos, not views. A retention figure without a sample size is a vibe. |
source |
csv or manual. Hand-entered numbers are kept and rendered distinctly; neither is better, the difference just has to be visible. |
| confounds | Follower band, posting time, whether topics differed. Every number here is confounded by something. |
Anything your export does not contain is left blank. Never estimated, never interpolated, never rounded to something tidier. A null is more useful than a guess.
| Platform | Export | Retention curve? |
|---|---|---|
| YouTube Studio | Analytics → Content → Advanced mode → Export → CSV | Yes, per video: Engagement → Audience retention → Export |
| TikTok | Analytics → Content → Download data | Not in the export in most regions — read it by hand and mark manual |
| No usable export — read the reel dashboard by hand | No |
Instagram has no ingest path. kw measure reads YouTube and TikTok exports
only. A reel measurement is welcome, but it is entered by hand into
measure/manual.yml and marked source: manual.
A YouTube table export carries no retention column, so hook_3s comes from
the separate audience-retention export or stays blank. Save that one as
measure/inbox/retention/<video_id>.csv — the filename is how it gets matched.
Delimiter and decimal separator are sniffed per file, so a semicolon-delimited
export with comma decimals reads correctly with no configuration. A genuinely
ambiguous number (1,234 — is that 1234 or 1.234?) returns null rather than a
guess.
- The
data.ymldiffs, written bykw measure apply— not by hand. - The
mapping.csvrows, so the join is auditable. - Which board the result belongs to. A
formatresult is a claim about scene structure; acontent_axisresult is a claim about subject matter. They live in separate blocks and are never averaged. If your videos carried acontent_axislabel, say whether each axis ran across more than one format — an axis carried by a single format is that format's result wearing a second label. - A note in the PR describing the confound: your follower count band, the platform, roughly when you posted, and whether the topics differed between variants. Every number here is confounded by something; saying which one is the difference between data and decoration.
- Numbers with no
n. A retention figure without a sample size is a vibe. - Hand-edited
data.yml. Ifkw measure applydid not write it, it does not go in. This is not about trusting you; it is about a reader six months from now being able to tell which numbers came from a CSV. - Aggregates with no
mapping.csv. "My ranking videos average 62%" is not attributable to a variant. - Samples under 3 submitted as a result. Send them anyway — they will be
merged and shown as
n: 2, which is honest — just do not describe them as a finding. - A blended format/axis number. "My personal-body countdowns hit 68%" is one number for two claims. Report the two separately, or report it as a single confounded observation and say so.
Small n is welcome. Overclaiming is not.
The bar is deliberately high, because a library of near-duplicates cannot be ranked meaningfully.
A new format must:
- Have a hypothesis you could lose. "This looks nice" is not one. Good hypotheses name a mechanism and a cost — a format that is better at everything is usually a format nobody measured properly.
- Not be a re-skin. A different accent colour is a theme. If two formats would produce the same retention curve, they are one format.
- Generalise past one video. If it only works for your topic, it is a
datablock, not a format.
npx kw new my-format --from=cold-open-line
npx kw preview my-format
npx kw render my-format --check-determinism
npx kw checkYour meta.yml must declare sampleContent: placeholder (obvious filler — the
default and usually the right call) or sampleContent: sourced with a sources:
list giving a URL and the exact claim each source backs. kw check fails without
it, and CI runs kw check. A library about honest measurement does not ship
unattributed numbers in its own demo copy.
Full guidance: references/authoring.md.
data.yml ships as n: 0 · untested. That is correct and the gallery shows it
as such. Do not pre-fill it.
The one rule: determinism. The composition must paint a frame as a pure
function of its number. Never introduce into engine/composition/:
Date.now(),performance.now(),Math.random()requestAnimationFrame,setTimeout,setInterval- CSS
transitionoranimation - anything that measures layout inside
seek()— measure once ininit()
Before opening a PR:
npx kw check # every format validates
npx kw render stat-counter-rise --check-determinism
npx tsc --noEmit
npm test # the measurement readerIf you touched anything under measure/src/ that reads a date or a number,
run the tests under a non-UTC timezone too:
TZ=Europe/Istanbul npm testCI does this across four zones. It is not ceremony: the one bug this project has actually shipped was a date that came out right in UTC and a day early everywhere else, and a UTC-only test run went green on it.
If you add an element type, it needs: a type in engine/shared/spec.ts, a
build() branch and a paint() branch in comp.ts, validation in
format.ts, and an entry in references/spec.md. The paint() branch must
write unconditionally, even when the element is invisible — skipping the write
would leave the DOM holding a previous frame's state, which makes the output a
function of seek history rather than of time.
Be straightforward and assume good faith. Disagreements about what the numbers mean are the entire point of the project; disagreements about people are not.
By contributing you agree your work is released under the MIT licence.