# Single-source the `/graph` top-level label allowlist from the vertical ontology — Design

Task: `.tasks/1624-single-source-graph-labels-top-level-from-vertical-ontology.md`.

## Problem

A businessType's top-level operator-entry node types live in the `## Top-level node
types` table of each vertical's `schema-<vertical>.md` (established by 1622). A third
encoding of "top-level entity" still lives apart: `platform/ui/app/lib/graph-labels.ts`
`FILTER_TOP_LEVEL_LABELS` is a hand-maintained global union of every brand's top-level
labels — the `/graph` filter-popover chip allowlist. It drifts from the ontology by
construction: a top-level label added to a vertical schema does not become a chip until
someone also edits this TypeScript set.

## Goal

Derive the `/graph` chip allowlist from the same per-vertical ontology source so a
top-level label added to a vertical schema surfaces as a chip with no second edit, while
preserving today's exact chip visibility as the acceptance baseline.

## Blast radius (evidence)

`FILTER_TOP_LEVEL_LABELS` has four real code consumers:

1. `server/routes/admin/graph-labels-in-graph.ts` — popover chip allowlist (the route the
   task names). Passes the set as the `$allowed` cypher param; the cypher count-gates.
2. `server/routes/admin/graph-subgraph.ts:280` — default-view label set.
3. `server/routes/admin/graph-default-view.ts:158` — default-view eligibility check.
4. `app/data/searchHelpers.ts:46` — client-side `/data` search chip pre-filter, a
   build-time static import that cannot read files.

Scope decision (operator-confirmed): **the three server routes switch to the
ontology-derived source. The client `searchHelpers.ts` keeps the static set**, with a
follow-up task filed to close that last second source.

## Facts that shape the mechanism

- Only `schema-construction.md` and `schema-estate-agent.md` currently carry a
  `## Top-level node types` section. Every other vertical (and 1626) adds theirs later.
- `schema-base.md` has no top-level section; its `## Node Types` table mixes top-level and
  child types (Message, Section, Chunk). Base labels therefore **cannot** be derived — they
  stay an explicit list. This resolves the task's open base-list decision.
- The construction top-level table includes `WhatsAppGroup` typed `cdm:Channel`. The 1622
  Python file-bucket generator excludes non-`schema:` rows (a channel gets no file bucket),
  but `WhatsAppGroup` **is** a current graph chip. So the graph derivation takes **every**
  Neo4j Label row and applies **no** namespace filter.
- The construction table has 5 columns, estate-agent 4. The Neo4j Label column is resolved
  by header name, never by index.

## Architecture

New server-only module `platform/ui/server/lib/top-level-labels.ts`:

```
getTopLevelLabelAllowlist(opts?: { referencesDir?: string }): ReadonlySet<string>
```

- Resolves `referencesDir` to `${MAXY_PLATFORM_ROOT}/plugins/memory/references` (same
  env/`__dirname` fallback the UI server already uses), or the injected override.
- Reads every `schema-*.md`. For each, locates the `## Top-level node types` heading by
  case-insensitive prefix, reads the first pipe-table beneath it (before the next `##`),
  and extracts the `Neo4j Label` column by header name, stripping backticks.
- Unions parsed labels with the module const `STATIC_TOP_LEVEL_LABELS` — the base-schema
  infra plus any vertical whose schema has no top-level section yet (knowledge-work today).
  Documented to shrink as 1626 adds sections.
- Caches the result at module level (schema files are static per install). An injected
  `referencesDir` bypasses the cache so tests are isolated.
- Emits one observability line on computation:
  `op=top-level-labels source=ontology count=<n> files=<...> derived=<d> static=<s>`.

The three server routes replace `FILTER_TOP_LEVEL_LABELS` with
`getTopLevelLabelAllowlist()` (a `ReadonlySet<string>`, so `.has()` and spread are
preserved). Cypher count-gating is unchanged: a non-resident label surfaces no chip.

Unchanged: `FILTER_TOP_LEVEL_LABELS` (client), `HIDDEN_BY_DEFAULT_LABELS`,
`AGENT_ACTION_LABELS`, `EXCLUDED_EDGE_TYPES`.

## Byte-identical guarantee

Derived union = construction table (11) ∪ estate table (4) ∪ `STATIC_TOP_LEVEL_LABELS`
(33) = the current 48-member `FILTER_TOP_LEVEL_LABELS`. Because the sets are equal, the
existing route tests (which assert membership against `FILTER_TOP_LEVEL_LABELS`) keep
passing unchanged.

`STATIC_TOP_LEVEL_LABELS` (top-level labels not yet sourced from a vertical section):

- Base-schema infra: `LocalBusiness`, `Service`, `PriceSpecification`,
  `OpeningHoursSpecification`, `Organization`, `Person`, `UserProfile`, `Preference`,
  `AdminUser`, `AccessGrant`, `KnowledgeDocument`, `DigitalDocument`, `CreativeWork`,
  `Question`, `FAQPage`, `DefinedTerm`, `Review`, `ImageObject`, `Invoice`, `Task`,
  `Project`, `Event`, `Workflow`, `Email`, `EmailAccount`, `Agent`.
- Knowledge-work vertical (no top-level section yet; 1626): `Objective`, `KeyResult`,
  `Decision`, `Risk`, `Source`, `Finding`, `Hypothesis`.

## Testing

Committed, colocated `platform/ui/server/lib/top-level-labels.test.ts`:

1. `getTopLevelLabelAllowlist()` against the real references dir deep-equals
   `FILTER_TOP_LEVEL_LABELS` (regression baseline).
2. A fixture references dir with an added top-level row surfaces that label with no change
   to any TypeScript set.
3. A `cdm:`-namespaced row (e.g. `WhatsAppGroup`) is included (no namespace filter).

The pre-existing `labels-in-graph` and `graph-default-topLevel` tests must still pass
after the swap (count-gating and eligibility unchanged).

## Observability

`op=top-level-labels source=ontology count=<n>` on the server log, emitted when the
allowlist is resolved, so an operator can confirm the chips come from the ontology, not a
stale hardcoded list.

## Out of scope

- Client `searchHelpers.ts` static set — filed as a follow-up task.
- `HIDDEN_BY_DEFAULT_LABELS`, `AGENT_ACTION_LABELS`, `EXCLUDED_EDGE_TYPES`.
- Adding `## Top-level node types` sections to verticals that lack one (1626).
- The file-schema generator and quotation reification (landed in 1622).
