03 — Threshold / Label Grouping Pack (Branch B)
title: 03 — Threshold / Label Grouping Pack (Branch B) date: 2026-05-31 status: rehearsed GREEN; COMMIT DEFERRED (RG5)
03 — Threshold / Label Grouping Pack (Branch B)
Goal: long-list grouping fully PG-backed, no frontend thresholds. 50 = MAX ungrouped ceiling, not a target; pagination ≠ semantic grouping.
Schema reality (live)
kg_thresholds(id, dimension, lower_is_better, threshold_green, threshold_yellow, threshold_red, is_active, …)— a green/yellow/red metric table, not a single display ceiling. Reusable as storage (one dimension per species) but semantically awkward (3 threshold cols).taxonomy_facets(… max_labels_per_entity smallint …)— verified values 0–3; this is labels-per-entity, NOT a display ungrouped ceiling. Distinct concept.label_rules(38; has skip_wide_warning boolean),taxonomy(58),species_collection_map(164)— existing classification machinery. Do not rebuild.
Decision: a dedicated display_policy table is the clearest model (single integer per
species, hard ≤50 CHECK). Reuse path noted (kg_thresholds dimension row) for storage-minimal
deployments. Either way the ceiling lives in PG data, never a frontend constant.
Rehearsal (live BEGIN..ROLLBACK, GREEN)
CREATE TABLE display_policy (
species_code text PRIMARY KEY,
max_ungrouped smallint NOT NULL DEFAULT 50,
note text,
CONSTRAINT display_policy_ceiling_ck CHECK (max_ungrouped > 0 AND max_ungrouped <= 50)
);
INSERT INTO display_policy VALUES
('__default__',50,'system default = MAX'), ('dot_tool',30,'smaller ceiling'), ('collection',50,'default');
Result (this run): table created, 3 seeds, CHECK valid; threshold_demo = 160 leaves
evaluated, 28 exceed their PG-resolved ceiling; ROLLBACK → display_policy gone,
idle_in_transaction = 0. The ceiling is resolved per-leaf with no literal 50 in any app:
COALESCE(dp.max_ungrouped, (SELECT max_ungrouped FROM display_policy WHERE species_code='__default__'))
(The 28 uses COALESCE(actual_count, record_count); a record_count-only proxy gives 27 —
the ±1 is the live CAT-023 drift, not a logic difference.)
Label engine (PIV-311 — native now)
entity_labels = 726,864 rows, 23 distinct label_code → PIV-311 (group by label_code)
returns 23 groups, proven against the real pivot_query() engine (Macro 1 doc 04). Top groups:
LBL-105 181,245 · PROV-DOT 180,475 · LBL-031 177,026 · LBL-021 175,245 · then a long
tail (LBL-011 2,018 … LBL-102 13). PIV-311 needs no helper view — it is engine-native.
Data contract fields (for API / UI / Nuxt shell)
A leaf/list node carries, all PG-sourced:
classification_status—classified | CLASSIFICATION_REQUIREDclassification_dimension— facet code (FAC-01..FAC-09 / FAC-PROV)label_ref— label_code(s) when classifiedgrouping_required— boolean =count > resolved_ceilinggrouping_reason— e.g.exceeds display_policy[dot_tool]=30max_ungrouped_threshold— resolved from display_policy (never frontend)suggested_next_grouping— facet to group by (from label_rules priority)classification_workflow_trigger— event when unclassified+over-ceiling (Đ45; doc 05)
Rules: if a list is already classified, use the classification immediately; if unclassified
and over ceiling, mark CLASSIFICATION_REQUIRED (do not silently paginate). Labels/groups
are themselves registry-visible (label_rules/taxonomy) and pivot-countable (PIV-311).
Commit-ready SQL (RG5 — DEFERRED)
CREATE TABLE display_policy … (above) + seeds, additive/reversible (DROP TABLE display_policy). Activates PIV-321-style grouping signals and feeds grouping_required.
No base-table mutation. Commit only on explicit RG5 approval.
Verdict
Threshold/label pack COMPLETE. Mechanism PG-backed, no-hardcode, reuse-first; the live signal (28/160 over ceiling, 23 label groups) is real; commit deferred to RG5.