Feature registry — seed list
Not every UI element earns a registry entry. Before adding to the seed, run the candidate against the protocol in contributing/tooltip-protocol. The curated list is short on purpose; entries that don't earn their place dilute the decay signal and make the heuristic worse.
The canonical list lives at:
src/scaffolding.js→FEATURE_REGISTRY_SEED(Helm —.BIKE.SWICKED.HUB)src/seed/feature-registry.ts(legacy Hub copy —KVICK.HUB)
On every cold start, the chassis calls bootstrapFeatureRegistry, which idempotently upserts every entry into the feature_registry table and soft-deletes any orphan row whose feature_key is no longer in the seed.
What goes in the seed
A FeatureSeedEntry:
{
feature_key: string, // `module.snake_case_feature` — lowercase, no spaces
module_key: string, // namespace prefix — lowercase, underscores OK
display_name: string, // operator-readable; appears in admin UI
description: string, // tooltip / hint content; markdown OK
frequency_class: "high" | "medium" | "low" | "rare",
}
feature_key naming
{module}.{snake_case_feature}. Lowercase, underscores. The schema enforces a regex:
/^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/
Examples that work: sales.add_line_item, consignment.evaluate, po.receive_serialized.
Examples that fail: Sales.AddLineItem (uppercase), sales-add-line (hyphens), addLineItem (no module prefix).
Picking a frequency_class
The class determines the chassis default decay thresholds:
| Class | default_decay_uses | default_decay_days | When to use |
|---|---|---|---|
high | 15 | 30 | Touched many times per shift. The till's add_line_item. Service workflow's complete. |
medium | 30 | 60 | Touched daily-ish. PO create / receive. Inventory adjust. |
low | 8 | 120 | Touched weekly-ish. EOD-Z. Margin reports. |
rare | 4 | 180 | Touched a few times a year. Consignment. Tax-rule setup. Adding a team member. |
The intuition for rare: the user will forget how this works between sessions. We want them to see the tooltip every time until they've reached it four times in close succession, and we want the tooltip to come back six months later if they go quiet. The low use-count + high day-window combination encodes that pattern.
If you're not sure, default to medium.
How to add a new feature
- Open
src/seed/feature-registry.ts. - Find the right module section (or add a new one if your feature opens a new module).
- Add an entry with a thoughtful
frequency_classand a cleardescription— the description is what operators read when they open the tooltip. - Land the change. On the next deploy, the chassis boot helper picks it up.
- In the UI surface, wire the feature key to
recordFeatureUse(call it when the primary action fires) and gate the tooltip ongetEffectiveTooltipState.
No migration is needed — the registry is data, not schema. The seed file is the source of truth; the DB row is a denormalized cache for query convenience.
Refining frequencies after the fact
frequency_class (and its derived defaults) can be edited on existing entries. The next bootstrapFeatureRegistry call will refresh the row's default_decay_uses / default_decay_days to match. Existing user_feature_state rows aren't touched; they continue to carry the per-user use_count regardless of how the threshold moves.
If you need to tune for a specific tenant, prefer tenant_tooltip_overrides over editing the seed entry.
When NOT to register a feature
- The feature has no tooltip-worthy primary action (e.g., a passive read view that the operator never has to "learn how to use").
- The feature is a debug or admin probe that operators don't touch.
- The feature is so obvious from the UI that scaffolding would be noise.
It's better to leave a feature unregistered than to register one and live with a stale tooltip. Add when there's a real "first time someone uses this they'll wonder how it works" moment.