Files
Stirling-PDF/frontend/editor/src/proprietary/data/classificationLabels.ts
T
EthanHealy01andGitHub 7f01bcdc44 Classifier setup as a processor policy (#7012)
## Overview

Adds a **Classification policy** to the processor's policy catalogue,
set up the same way as the Security policy. This moves classifier
configuration out of the editor (where the labels UI landed in #6898 and
was then removed with the rest of the editor's policy-management surface
in #6932) and into the processor, which is now the single place policies
are configured.

## What it does

- **Classification card** in the processor policy catalogue. Always
shown, but **setup is locked until the backend reports the AI engine is
on** — so admins can see the capability they're missing rather than it
being hidden entirely.
- **Setup wizard** mirrors Security: the workflow step shows the team's
**classification label editor** (reused
`LabelsEditor`/`LabelsEditorModal` — add box, chip grid, per-label icon
picker, import/export, reset) instead of tool toggles, since classify is
a single non-configurable step.
- On enable, the team's label vocabulary is **seeded with the 268
built-in defaults** (clobber-safe: only when the team has none). On
upload the document is classified against the team's labels and tagged;
on SaaS with the engine on, files group by category in the editor
sidebar.

## Reuse & consolidation

- Reuses the existing labels table, `labelsFile` helpers, and default
vocabulary. Labels read/write through the processor's own
`apiClient.local` (not the editor's axios client) so auth/base routing
stays explicit; the wire shape is shared.
- Consolidates policy-category icons into a shared, **id-keyed**
`policyCategoryIcon` util (outline glyphs) used by both the editor and
the processor, replacing the processor's emoji-glyph map (and the stray
`schedule` key that rendered a bare dot).

## Testing

- `task frontend:typecheck:{core,proprietary,portal}`,
`frontend:lint:eslint`, `frontend:test` (156 files / 1305 tests) — all
green.
- Verified in Storybook: the Classification card renders, the setup
wizard shows the label editor (268 defaults), and the full labels editor
opens with icons/import/export/reset. Added an MSW handler for the
app-config + labels endpoints and a `Classification` wizard story.

## Notes for reviewers

- The AI-engine gate reads the public `/api/v1/config/app-config`;
classification labels use `/api/v1/classification/labels` (team-scoped,
team-lead/admin-gated, `policies.enabled`); the classify step hits
`/api/v1/ai/tools/classify-and-label` — all pre-existing backend from
#6898.
- Known parity behavior (matches the editor hook): a transient failure
loading team labels falls back to showing the defaults; not changed here
to avoid diverging the two hooks.
2026-07-15 11:04:30 +00:00

87 lines
3.4 KiB
TypeScript

// Classification labels. The SOURCE OF TRUTH is the co-located static JSON
// (`classificationLabels.json`), imported here and shaped into typed objects —
// edit THAT file, not this one. It's a fixed, built-in vocabulary shared by
// everyone (no per-team customization); it drives the sidebar's grouping, icons,
// and display names.
//
// The backend keeps a SECOND copy of this list
// (`app/proprietary/src/main/resources/classification/classification-labels.json`)
// which it sends to the engine per classify request (the engine holds no
// vocabulary of its own). The two copies must not drift — `classificationLabels.drift.test.ts`
// guards that. When you edit the labels here, update the backend copy too.
//
// `labels` is the flat set: each has a stable `id` (slug — the value on the wire,
// in storage and keyed on) and a human `name` (display, translatable via
// `classification.labels.<id>`); `icon` is presentational only, the engine never
// sees it. `families` are presentational sidebar roll-ups (referencing labels by
// id) the classifier never sees.
import labelsData from "@app/data/classificationLabels.json";
export interface ClassificationLabel {
/** Stable identity (slug) — the value on the wire, stored on the doc, and
* keyed on. Independent of the (translatable) display name. */
id: string;
/** Human display name; the en-US default for `classification.labels.<id>`. */
name: string;
/** Material Symbols icon key (see `labelIcons.ts`). */
icon?: string;
}
export interface LabelFamily {
/** Stable identity for sidebar prefs — never rename once shipped. */
id: string;
/** Group header text shown in the sidebar and the group picker. */
name: string;
/** Material Symbols icon key (see `labelIcons.ts`). */
icon: string;
/** The built-in labels this family rolls up in the sidebar. */
labels: ClassificationLabel[];
}
/** Shape of `classificationLabels.json` — the flat label set plus the
* presentational family grouping (which references labels by id). */
interface LabelsFile {
labels: ClassificationLabel[];
families: { id: string; name: string; icon: string; labelIds: string[] }[];
}
const data = labelsData as LabelsFile;
/** Flat default label set — file order, as the classifier/team-seed sees it. */
export const DEFAULT_CLASSIFICATION_LABELS: ClassificationLabel[] = data.labels;
const LABEL_BY_ID = new Map(data.labels.map((label) => [label.id, label]));
/**
* Built-in families with their labels resolved from the flat set by id. Throws
* at module load if a family references an unknown id, so a bad hand-edit of the
* JSON fails fast rather than silently dropping a label from its group.
*/
export const LABEL_FAMILIES: LabelFamily[] = data.families.map((family) => ({
id: family.id,
name: family.name,
icon: family.icon,
labels: family.labelIds.map((id) => {
const label = LABEL_BY_ID.get(id);
if (!label) {
throw new Error(
`Classification family "${family.id}" references unknown label id "${id}"`,
);
}
return label;
}),
}));
/**
* Stable slug id from a label's canonical (English) name — used to derive an id
* for a NEW custom label the user types. Built-in ids are authored in the JSON;
* this must stay in sync with the slug rule used to generate them.
*/
export function labelId(name: string): string {
return name
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
}