# Task 1819 — Top-Level Label Allowlist Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Make the two failing top-level-label tests pass by resolving the real disagreement between the derived allowlist and the client mirror, and register `CashEntry` as a top-level label across every registry that gates one.

**Architecture:** Two independent tasks. Task 1 is the 1819 correctness fix: one label into `STATIC_TOP_LEVEL_LABELS` plus the doc comment that would have prevented the drift. Task 2 registers `CashEntry` across six surfaces in the shared `graph-style` lib and the two UI label sets. A reviewer can accept Task 1 and reject Task 2.

**Tech Stack:** TypeScript, vitest, Node 22.22.0 via nvm.

## Global Constraints

- Run everything under Node 22.22.0 (`source ~/.nvm/nvm.sh && nvm use 22.22.0`). The repo's jsdom vitest env requires `require(esm)`, which needs Node >= 20.19 / >= 22.12.
- Working directory is the worktree: `/Users/neo/getmaxy/maxy-code/.claude/worktrees/task-1819-top-level-label-allowlist`. All paths below are relative to it.
- Never edit or rebuild through `maxy-code/platform/lib/graph-style/dist` while it is a symlink to the parent checkout. Task 2 Step 1 replaces it with a local build.
- Do not edit either test's assertions. The tests are correct; the sets are wrong.
- Every commit carries the trailers:
  ```
  Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  Session: 693bcfd0-f04e-49ed-8ebe-593e59be24c2
  ```

---

### Task 1: Add `InboundInvoice` to the static allowlist

`InboundInvoice` is a genuine operator-entry top-level label. Commit `7b225f814` moved its row from `schema-construction.md`'s parsed `## Top-level node types` table into `schema-base.md`'s `## Node Types` table, which the derivation deliberately never parses because it mixes in child types. `STATIC_TOP_LEVEL_LABELS` is the compartment for exactly that case.

**Files:**
- Modify: `maxy-code/platform/ui/server/lib/top-level-labels.ts:25-69`
- Test (existing, do not edit): `maxy-code/platform/ui/server/lib/top-level-labels.test.ts`
- Test (existing, do not edit): `maxy-code/platform/ui/server/routes/__tests__/graph-default-topLevel.test.ts`

**Interfaces:**
- Consumes: nothing from other tasks.
- Produces: `STATIC_TOP_LEVEL_LABELS` gains the member `'InboundInvoice'`. Task 2 adds `'CashEntry'` to this same set.

- [ ] **Step 1: Run the two tests to confirm they fail**

```bash
cd maxy-code/platform/ui
source ~/.nvm/nvm.sh && nvm use 22.22.0
npx vitest run server/lib/top-level-labels.test.ts server/routes/__tests__/graph-default-topLevel.test.ts
```

Expected: `Test Files 2 failed (2)`, `Tests 2 failed | 12 passed (14)`. Both failures are the same set diff, with `InboundInvoice` present in expected and absent from received.

- [ ] **Step 2: Add the label**

In `maxy-code/platform/ui/server/lib/top-level-labels.ts`, inside `STATIC_TOP_LEVEL_LABELS`, add `'InboundInvoice'` immediately after `'Invoice',` (line 51) so the two invoice labels read together:

```ts
    'Invoice',
    // Supplier invoice. Declared in schema-base.md's `## Node Types` table
    // (promoted there from schema-construction.md's top-level table by
    // 7b225f814), which this module never parses — so it is static, not
    // derived. Top-level because it is operator-entry with a natural key
    // (accountId, supplier, confirmationNumber), unlike its InvoiceLine /
    // InvoicePayment / Credit children.
    'InboundInvoice',
```

- [ ] **Step 3: Document the reverse migration**

The existing doc comment on `STATIC_TOP_LEVEL_LABELS` covers only the forward direction. Replace the comment at lines 25-29 with:

```ts
/**
 * Top-level labels not (yet) sourced from a vertical `## Top-level node types`
 * section. Membership moves in both directions, and both are load-bearing:
 *
 *   - A vertical GAINS a top-level section: its labels move from here into
 *     the derived union, and the STATIC copy is removed in the same edit.
 *   - A label is PROMOTED INTO `schema-base.md`: it must be ADDED here in the
 *     same edit. `schema-base.md` has no top-level section and is never
 *     parsed (its `## Node Types` table mixes in child types), so a promotion
 *     silently deletes the label from the derived set. This is what happened
 *     to `InboundInvoice` in 7b225f814 and it is what Task 1819 fixed.
 */
```

- [ ] **Step 4: Run the two tests to verify they pass**

```bash
cd maxy-code/platform/ui
source ~/.nvm/nvm.sh && nvm use 22.22.0
npx vitest run server/lib/top-level-labels.test.ts server/routes/__tests__/graph-default-topLevel.test.ts
```

Expected: `Test Files 2 passed (2)`, `Tests 14 passed (14)`.

The `keeps STATIC_TOP_LEVEL_LABELS disjoint from the table-derived labels` test in the same file must still pass — it proves `InboundInvoice` is genuinely absent from every parsed table, confirming static is the right home.

- [ ] **Step 5: Commit**

```bash
git add maxy-code/platform/ui/server/lib/top-level-labels.ts
git commit -m "fix: restore InboundInvoice to the top-level label allowlist (1819)

Commit 7b225f814 promoted the InboundInvoice row out of
schema-construction.md's parsed top-level table into schema-base.md's
Node Types table. That table is never parsed — it mixes child types —
so the derived allowlist silently lost a real operator-entry label and
the deep-equals guard against the client mirror went red.

The client mirror was right. STATIC_TOP_LEVEL_LABELS is the compartment
for base-schema labels with no top-level section, and InboundInvoice had
never been added to it.

Also documents the reverse migration on STATIC_TOP_LEVEL_LABELS. The
comment covered only 'vertical gains a section, label leaves static' and
was silent on 'label promoted into base, label enters static' — the
direction that caused this.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Session: 693bcfd0-f04e-49ed-8ebe-593e59be24c2"
```

---

### Task 2: Register `CashEntry` as a top-level label

`CashEntry` was added by the same commit and is absent from every registry, so it fails no test but has no `/graph` chip. It is money "not attached to any invoice" with no parent label, so unlike a child label it cannot be reached by drilling down — without a chip it is unreachable except by search.

**Files:**
- Modify: `maxy-code/platform/lib/graph-style/src/index.ts` — colour table, icon table, `pickShortLabel`, `pickDisplayName`
- Modify: `maxy-code/platform/ui/app/lib/graph-labels.ts:139-191` — `FILTER_TOP_LEVEL_LABELS`
- Modify: `maxy-code/platform/ui/server/lib/top-level-labels.ts` — `STATIC_TOP_LEVEL_LABELS`
- Test (existing): `maxy-code/platform/lib/graph-style/src/__tests__/icons.test.ts`, `parity.test.ts`
- Test (existing): `maxy-code/platform/ui/app/lib/__tests__/graph-labels.test.ts`

**Interfaces:**
- Consumes: `STATIC_TOP_LEVEL_LABELS` from Task 1.
- Produces: `GRAPH_LABEL_COLOURS.CashEntry === '#7D5A2B'`; `iconForLabel('CashEntry')` returns a non-null SVG string; `CashEntry` is a member of both `FILTER_TOP_LEVEL_LABELS` and `STATIC_TOP_LEVEL_LABELS`.

- [ ] **Step 1: Replace the dist symlink with a local build**

`dist` currently points at the parent checkout. Building through it would write to the shared checkout other sessions use.

```bash
cd /Users/neo/getmaxy/maxy-code/.claude/worktrees/task-1819-top-level-label-allowlist
ls -ld maxy-code/platform/lib/graph-style/dist        # confirm it is a symlink (leading 'l')
rm -f maxy-code/platform/lib/graph-style/dist
source ~/.nvm/nvm.sh && nvm use 22.22.0
cd maxy-code/platform
./ui/node_modules/.bin/tsc -p lib/graph-style/tsconfig.json
ls -ld lib/graph-style/dist                            # now a real directory ('d')
```

Expected: `tsc` exits 0 and `dist` is a real directory. Use `ui/node_modules/.bin/tsc` explicitly — the libs have no `package.json`, so a bare `npx tsc` resolves to an unrelated package.

- [ ] **Step 2: Write the failing test**

Add to `maxy-code/platform/lib/graph-style/src/__tests__/parity.test.ts`, inside the `palette invariants` describe block:

```ts
  it("registers CashEntry as its own hue, distinct from the invoice labels", () => {
    expect(GRAPH_LABEL_COLOURS.CashEntry).toBe("#7D5A2B");
    expect(GRAPH_LABEL_COLOURS.CashEntry).not.toBe(GRAPH_LABEL_COLOURS.Invoice);
    expect(GRAPH_LABEL_COLOURS.CashEntry).not.toBe(GRAPH_LABEL_COLOURS.InboundInvoice);
  });
```

- [ ] **Step 3: Run it to verify it fails**

```bash
cd maxy-code/platform/lib/graph-style
source ~/.nvm/nvm.sh && nvm use 22.22.0
../../ui/node_modules/.bin/vitest run
```

Expected: FAIL on the new test with `expected undefined to be '#7D5A2B'`. The `icons.test.ts` coverage test still passes at this point, because `CashEntry` is not yet in the colour table.

- [ ] **Step 4: Add the colour**

In `maxy-code/platform/lib/graph-style/src/index.ts`, append to the END of the `GRAPH_LABEL_COLOURS` object, after `Hypothesis: '#9A8C3E',` (line 147):

```ts

  // Ledger cash movement (schema-base.md, added by 7b225f814). One movement
  // of money attached to no invoice — operator-entry with no parent to drill
  // down from, so it is a top-level chip. Burnished olive-gold sits in the
  // money register beside Invoice's brown without colliding with it.
  //
  // Deliberately appended at the END rather than beside the ledger family:
  // SHAPE_BY_LABEL assigns shapes by index position in ALL_GRAPH_LABELS
  // (this object's key order), so a mid-table insert would silently change
  // the rendered shape of every label after it.
  CashEntry: '#7D5A2B',
```

- [ ] **Step 5: Run the tests to see the icon gate fail**

```bash
cd maxy-code/platform/lib/graph-style
source ~/.nvm/nvm.sh && nvm use 22.22.0
../../ui/node_modules/.bin/vitest run
```

Expected: the new palette test now PASSES, and `icons.test.ts` now FAILS with `CashEntry` listed in the missing-glyph array. This is the coverage gate doing its job.

- [ ] **Step 6: Add the icon**

In the same file, append to the END of the `LABEL_ICONS` object (the Lucide `banknote` glyph):

```ts
  CashEntry: '<rect width="20" height="12" x="2" y="6" rx="2"></rect><circle cx="12" cy="12" r="2"></circle><path d="M6 12h.01"></path><path d="M18 12h.01"></path>',
```

- [ ] **Step 7: Add the caption branches**

`CashEntry`'s properties are `accountId`, `entryId`, `direction`, `amount`, `date`, `description`. The generic caption fallthrough only checks `name`, `title`, `summary`, `givenName`, `subject`, `text`, so without a branch every node renders as the literal string `CashEntry`. Both caption functions carry per-label branches and both need one.

In `pickShortLabel`, after the `WhatsAppGroup` branch and before the `Message` branch:

```ts
  } else if (primaryLabel === 'CashEntry') {
    const desc = typeof props.description === 'string' ? props.description : ''
    if (desc.length > 0) {
      return desc.length > 24 ? desc.slice(0, 24) + '…' : desc
    }
```

In `pickDisplayName`, add the matching untruncated branch in the same relative position:

```ts
  } else if (primaryLabel === 'CashEntry') {
    const desc = typeof props.description === 'string' ? props.description : ''
    if (desc.length > 0) return desc
```

- [ ] **Step 8: Run the graph-style suite to verify it passes**

```bash
cd maxy-code/platform/lib/graph-style
source ~/.nvm/nvm.sh && nvm use 22.22.0
../../ui/node_modules/.bin/vitest run
```

Expected: all tests pass, including the icon-coverage test and the hex-format invariant.

- [ ] **Step 9: Rebuild dist so the UI sees the new symbols**

```bash
cd maxy-code/platform
source ~/.nvm/nvm.sh && nvm use 22.22.0
./ui/node_modules/.bin/tsc -p lib/graph-style/tsconfig.json
```

Expected: exit 0.

- [ ] **Step 10: Add `CashEntry` to both label sets**

In `maxy-code/platform/ui/app/lib/graph-labels.ts`, inside `FILTER_TOP_LEVEL_LABELS`, after `'InboundInvoice',` (line 166):

```ts
    'CashEntry',
```

In `maxy-code/platform/ui/server/lib/top-level-labels.ts`, inside `STATIC_TOP_LEVEL_LABELS`, after the `'InboundInvoice',` entry added in Task 1:

```ts
    // Ledger cash movement, declared in schema-base.md. Operator-entry with
    // a natural key (accountId, entryId) and no parent label, so it is
    // reachable only as a chip.
    'CashEntry',
```

Both sets must change together — the deep-equals test from Task 1 fails if only one does. That failure is the guard working.

- [ ] **Step 11: Run the full UI graph-label surface**

```bash
cd maxy-code/platform/ui
source ~/.nvm/nvm.sh && nvm use 22.22.0
npx vitest run server/lib/top-level-labels.test.ts \
  server/routes/__tests__/graph-default-topLevel.test.ts \
  app/lib/__tests__/graph-labels.test.ts \
  app/data/__tests__/dynamic-chips.test.ts
```

Expected: all four files pass. The hex-duplicate-rejection test in `graph-labels.test.ts` is the one that proves `#7D5A2B` collides with nothing.

- [ ] **Step 12: Commit**

```bash
git add maxy-code/platform/lib/graph-style/src/index.ts \
        maxy-code/platform/lib/graph-style/src/__tests__/parity.test.ts \
        maxy-code/platform/ui/app/lib/graph-labels.ts \
        maxy-code/platform/ui/server/lib/top-level-labels.ts
git commit -m "feat(graph): register CashEntry as a top-level label

CashEntry arrived in schema-base.md with 7b225f814 and was registered
nowhere, so /graph offered no chip for it. It is money attached to no
invoice and has no parent label, so unlike a child label it cannot be
reached by drilldown — without a chip it is unreachable except by search.

Six surfaces: colour, icon, both caption functions, the client chip
mirror, and the server static allowlist.

The colour is appended at the end of GRAPH_LABEL_COLOURS rather than
beside the ledger family on purpose. SHAPE_BY_LABEL assigns shapes by
index position in that object's key order, so a mid-table insert would
have silently changed the rendered shape of ~40 other labels.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Session: 693bcfd0-f04e-49ed-8ebe-593e59be24c2"
```

---

## Verification (after both tasks)

```bash
cd maxy-code/platform/ui
source ~/.nvm/nvm.sh && nvm use 22.22.0
npx tsc --noEmit
npx vitest run
```

Expected: `tsc` exits 0. The full suite has no failures attributable to these files. Compare any residual red against the parent checkout on `main` before treating it as caused by this diff.
