Structured tagging primitives for human-in-the-loop annotation workflows.
@tag-kit/core (zero runtime deps) · @tag-kit/ui (headless React) · extracted from a real moderation app (inertial).
Install
$ pnpm add @tag-kit/core @tag-kit/uiWhy it exists.
Bring your own taxonomy, bring your own UI, bring your own scoring loop — tag-kit ships the substrate.
Most “tagging” features in HITL tools are unstructured strings — you type a label, it gets stored next to the decision, and then nobody can aggregate or score across them later.
Extracted from a real moderation app (inertial).
01
The unstructured-string problem
Most "tagging" features in human-in-the-loop tools are unstructured strings: an annotator types a label, it gets stored next to the decision, and nobody can aggregate or score across them later. That's fine for one-off review, and useless the moment you want measurement.
Real annotation is scoped. An annotator needs to say "the audio at 0:12–0:24 is harassment; the video is fine" — not render one whole-asset verdict. And real quality measurement is agreement: when two reviewers tag overlapping segments with the same label, that's agreement; when they tag disjoint ones, it isn't. Neither works on strings.
tag-kit is what falls out when you take the tag layer out of a real moderation app — inertial — and ask: what's the smallest reusable shape for this? Bring your own taxonomy, bring your own UI, bring your own scoring loop; tag-kit ships the substrate.
02
Conservative by construction
The scoring core is deliberately cautious. Scope overlap follows conservative rules: different modality never matches, different asset never matches, and ranges must genuinely overlap — half-open intervals, so segments that merely touch don't count. Each expected tag is consumed at most once when computing precision and recall, which prevents double-counting a single ground-truth item against multiple reviewer tags. Per-tag precision, recall, and F1 aggregate across entities from there.
Tag IDs are stable by convention — once a tagId ships in any catalog it never gets renamed, because it's the foreign key from persisted reviewer tags to your taxonomy. That's what lets tags survive UI rewrites and feed longitudinal scoring.
The portability test for every API decision: does this still work for medical chart annotation, legal document review, and ML training-data labeling — not just content moderation? If a change only makes sense for one domain, it doesn't land.
03
Zero dependencies, zero styles
@tag-kit/core has zero runtime dependencies — no Zod, no lodash, nothing. It is a TypeScript-only set of wire shapes and pure functions, which means it drops into any stack, any bundler, any runtime, with no version-conflict surface at all.
@tag-kit/ui is headless React: a catalog-browsing TagPicker and a TagChip, shipping zero CSS. Styling hooks are data-tag-kit-* attributes; markup override is a children render-prop. Your design system stays yours — the primitives only bring behavior.
Signature features.
4 featuresPer-modality scoping
The audio at 0:12–0:24 is harassment; the video is fine.
Scope-aware agreement scoring
Agreement scored per scope, not flattened across the whole item.
Stable taxonomy IDs
Stable taxonomy IDs so tags aggregate and score across decisions.
Domain portability
Moderation, medical chart annotation, legal review, ML labeling.
Packages.
2 on npmExplore further.
The source lives on GitHub. Copy, paste, own — no fork, no vendor lock-in.