> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hebbianrobotics.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Metric glossary for Quality Indexing fields

> Definitions and derivations for every field published by the Quality Indexing API, from frame counts and share denominators to opaque version tokens.

Field-by-field types and nullability live in the API reference. This page is
the meaning behind them: what a number counts, what it is a count *of*, and
which readings of it are wrong.

Derivations are given wherever knowing one changes how you should read the
number. Calibration is not: the cutoffs, models, tooling and agreement rules
behind a measurement are ours, and are represented on the wire only by opaque
version tokens. Where that boundary bites, it says so.

## Populations and denominators

These names recur across every endpoint, and getting them wrong is the easiest
way to compute a confident wrong answer.

| Field                        | What it counts                                                                                      |
| ---------------------------- | --------------------------------------------------------------------------------------------------- |
| `clips_analyzed`             | The corpus as read: every clip the answering run covered, whatever became of it.                    |
| `clips_measured`             | Clips that reached the frame measurements — the cleanliness population.                             |
| `clips_without_measurements` | Analysed clips with nothing in the measurement run, so absent from `frames_measured` entirely.      |
| `clips_without_identity`     | Older artifacts that recorded no stable source identity and therefore cannot be joined to anything. |
| `frames_measured`            | The denominator of the three cleanliness shares.                                                    |
| `segments_unassessed`        | Spans of footage no answer was obtained for.                                                        |
| `segments_unusable`          | Spans an answer was obtained for, but not a usable one.                                             |
| `measured_seconds`           | Successfully answered footage, in seconds — the denominator of every hand-activity share.           |

On the cleanliness response these reconcile exactly, and it is worth asserting:

```
clips_analyzed == clips_measured + clips_without_measurements + clips_without_identity
```

`clips_analyzed` and `clips_measured` are different populations, and the second
is not a rounding error away from the first. Anything computed over one must
not be divided by the other.

`clips_analyzed` is also **per endpoint**, because each names the run that
answered it. Vocabulary and cleanliness resolve their runs independently, so
their `clips_analyzed` are two facts about two runs and need not agree. Compare
them only when the echoed run tokens say they came from the same measurement.

`segments_unassessed` and `segments_unusable` are published rather than
absorbed because they are the honest edge of the answer. A corpus with a large
count in either is one where the shares describe a smaller slice of the footage
than the clip count suggests.

## Vocabulary

The corpus described in three sections: `environments` (where the footage was
shot), `objects` (what is handled), `actions` (what is done).

### Terms

A **term** is a published concept, not a raw string. Independent passes over
the footage produce wordings that mean the same thing; those are merged, and
`spellings` lists every variant that folded into the published term. This is
why term matching in `GET /api/quality/clips` is evidence-based: any spelling
that was merged finds the term, and a spelling nothing ever produced finds
nothing.

`kind` is the coarser category the term groups under, absent when the term is
its own kind. It is what makes `distinct_kinds` auditable rather than asserted
— you can re-derive the kind count from the term table.

`clip_count` and `occurrences` both report how many distinct clips carry the
term, counted once however many passes agreed on it.

<Note>
  How many independent passes had to agree before a term was published is not
  on the wire. That describes the filter, and the filter is ours. What you get
  is the surviving vocabulary and the evidence behind each of its members.
</Note>

### Counting a vocabulary

Four counts, and they answer four different questions.

**`distinct_terms`** — published terms that carry a tier. The plain count of
what is in the corpus. It rises with how much footage was read, so it is not
comparable across datasets.

**`effective_distinct_terms`** — the repetition-corrected count. Formally
`exp(H)` over the occurrence distribution, where `H` is Shannon entropy: a
Hill number of order one, the "effective number of species" of ecology, the
perplexity of the distribution.

Read it as a count, because it is measured in terms and behaves like one. Ten
evenly-used terms give back exactly `10.0`. Two hundred kitchen clips plus one
glimpse of a garage give back `1.06`, not `2`. A corpus naming 134 objects
where four of them account for nearly every clip is not as rich as one naming
134 evenly, and this is the number that can tell them apart.

**`rarefied_distinct_terms`** — `distinct_terms` interpolated down to a fixed
common depth, stamped in `comparable_depth_clips`. **This is the only count
here that may be set beside another dataset's.** A raw term count keeps rising
with how much footage was read and that curve never flattens, so comparing two
raw counts compares sampling effort as much as content. It is `null` when the
corpus is thinner than the common depth, because rarefaction interpolates and
must not extrapolate — an estimate for a depth never reached would be a guess
wearing a number's clothes.

**`distinct_kinds`** — how many coarser categories the published terms fall
into. Answers "how varied is this corpus" where the term count answers "what is
in it".

### Tiers

Each section's terms are partitioned into **tiers**. For objects and actions
these are ordinal — they rank how much dexterity handling or performing the
term demands, so a higher tier is a harder thing and not merely a different
one. For environments they are not: the tiers there are collection settings,
which are categories with no order between them. One wire shape, two meanings,
and only the first can be summed into anything resembling a difficulty.

The response enumerates every tier, **including ones no term landed in**, with
its stable wire name, its display label, its share of the section's
occurrences, and its most frequent members. Reading the labels and the examples
from the response is the intended way to learn the scheme; it means the tier
set cannot drift away from this page.

`untiered_terms` lists published terms that could not be placed. They are in
neither `distinct_terms` nor the shares, which is why the term table can be
longer than the counts imply. Every published term is either counted in
`distinct_terms` or named in `untiered_terms` — that identity is worth
asserting in your own pipeline.

## Cleanliness

An exhaustive partition of the decoded frames into three categories, plus the
denominators behind it. The endpoint decodes nothing and calls no model; it
reduces measurements already stored.

| Category    | Means                                                                              |
| ----------- | ---------------------------------------------------------------------------------- |
| `usable`    | Free of both conditions below.                                                     |
| `distorted` | The frame is hard to read on its own terms — out of focus, blown out, or too dark. |
| `jerky`     | The frame is individually legible but differs sharply from the one before it.      |

`usable_frames + distorted_frames + jerky_frames == frames_measured`, exactly.
The three `_share` fields are derived from that same denominator and are absent
— never three zeros — when nothing was measured.

Two derivations that change how the counts read:

* **Distortion takes precedence.** A frame that is both unreadable and abrupt
  is counted `distorted`, never `jerky`, so the two categories never
  double-count and `jerky` means "legible but abrupt".
* **A frame that could not be measured is `distorted`, not `usable`.** A frame
  with no measurable focus or brightness carries no image information, and the
  conservative direction is to flag it rather than let it inflate the usable
  share. The first frame of a clip has no predecessor and so is never `jerky`,
  though it is still judged on its own terms.

<Warning>
  **`jerky` cannot distinguish camera shake from fast subject motion.** It
  responds to both, and on egocentric footage the wearer's own head movement
  produces it constantly. Treat it as a signal to weigh against what you are
  training, not as a rejection verdict.
</Warning>

**`classification_version`** is an opaque token naming the rule that produced
the partition. Two partitions carrying the same token were produced by the same
rule and can be compared directly. Two carrying different tokens cannot, and a
corpus should be re-read rather than re-interpreted when the token moves.

The token exists precisely so that comparability is available without the
cutoffs being published — which is the only thing the cutoffs would have told
you, and publishing them would let a reader re-partition a corpus into whatever
answer suited them.

## Hand activity

Footage-weighted shares over `measured_seconds`. The measure is defined in
terms of the camera wearer, so it says something only about egocentric footage.

| Field                       | Means                                                                          |
| --------------------------- | ------------------------------------------------------------------------------ |
| `hands_visible_share`       | Share of measured footage with at least one camera-wearer hand visible.        |
| `two_hands_visible_share`   | Share with both camera-wearer hands visible.                                   |
| `active_manipulation_share` | Share showing a hand actually acting on something, rather than merely present. |

"Camera-wearer" is load-bearing. Another person's hands in frame are not the
wearer's and do not count, nothing is inferred from a hand that is out of frame
or behind an object, and any amount of visibility counts — fingertips included.
Visibility is presence, not use.

`two_hands_visible` is a strict subset of `hands_visible`: one-hand and
two-hand stretches are recorded as disjoint spans and summed. Manipulation is
observed independently of visibility, so no containment holds between them in
either direction, and you should not expect
`active_manipulation ≤ hands_visible` to be true of every corpus.

Every share is `null` rather than `0.0` when `measured_seconds` is zero.

## Clips

One row per clip: **counts and seconds throughout, never shares**. A share
commits you to our denominator; counts let you re-aggregate over any subset —
one operator, one site, one week — which is the reason this endpoint exists
beside the corpus-level ones.

**`clip`** is the clip's identity: the source file and the span within it. No
renumbering changes it, so it is the join key to use between pages, between
endpoints, and against your own records.

**`analysed_secs`** is a duration of footage assessed, not an offset into the
source.

**`episode_index`** is the position the clip had when analysed, and is
**absent when two passes disagreed about it**. A corpus renumbered between
passes means either number would address somebody else's footage, so neither is
published. Join on `clip`.

**`frame_quality`** and **`hand_activity`** are opt-in, via
`frame_quality=true` and `hand_activity=true`. They are booleans rather than
addresses because they add columns to rows the vocabulary has already chosen.

<Warning>
  **An absent object is not a zeroed one.** A clip carrying no `frame_quality`
  was never decoded. A clip whose `frame_quality.active` is `0` was decoded and
  found to contain no usable frames. Those are opposite facts, and treating the
  first as the second silently drags every average down. Check for the field
  before you aggregate it — nothing is filled in with zeros to make the rows
  uniform.
</Warning>

## Reading a run token

Every metric response echoes the run that answered it — `vocabulary`,
`analysis`, `frame_quality`. You need not supply one: name a dataset and you
get its most recent measurements.

The tokens are opaque and exist for exactly one purpose: telling two answers
apart. The same token in two responses a week apart means the corpus was not
re-measured between them and the numbers are directly comparable. A different
token means it was, and any drift you see is real change plus re-measurement,
not one or the other.

Do not pin a token. It addresses one stored run, and a re-measurement will
leave a pinned integration reading an answer that no longer reflects the
corpus.
