# Graph Caption Resolver Implementation Plan

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

**Goal:** Make every top-level operator-entry graph label resolve to a human-readable caption instead of its bare type, and add a build-time gate that fails when a new styled label ships without a caption decision.

**Architecture:** Add per-label caption branches to `pickShortLabel` and `pickDisplayName` in `platform/lib/graph-style/src/index.ts` for the first-class labels that currently fall through to `return primaryLabel`. Add a `caption-coverage.test.ts` that reconciles every `GRAPH_LABEL_COLOURS` key against a `CAPTION_COVERAGE` fixture, so registering a styled colour without deciding its caption is a red build.

**Tech Stack:** TypeScript, vitest 4 (run via `platform/ui/node_modules/.bin/vitest`), no new dependencies.

## Global Constraints

- Do not touch `GRAPH_LABEL_COLOURS` or `LABEL_ICONS` — every label already has both.
- `pickShortLabel` truncates to 24 chars (`slice(0, 24) + '…'`); `pickDisplayName` returns the untruncated value. New branches follow that per-function convention.
- Caption source is a scalar property already written on the node itself; never an id that points at another node, never a composed value that needs a second node.
- The UI parity test `platform/ui/app/graph/__tests__/display-helpers.test.ts` imports graph-style's **built** `dist/index.js`, so graph-style must be rebuilt (`tsc -p tsconfig.json`) before that test is rerun.
- `SHAPE_BY_LABEL` assigns shapes by key order in `GRAPH_LABEL_COLOURS`; do not reorder or insert keys.

---

## Label classification (the crux)

The resolver captions a node when: (1) its primary label has an explicit branch, or (2) the node carries one of the generic-fallback keys `name, title, summary, givenName, subject, text`, or (3) it is the `Conversation` special-case. Otherwise it returns the bare type.

**Already captioned by an existing branch** (unchanged): `ToolCall, WorkflowRun, WorkflowStep, Person, Agent, Job, Quote, InboundInvoice, WhatsAppGroup, CashEntry, Message`, plus the `Conversation` `sessionId` special-case.

**Already captioned by the generic fallback** (they declare a fallback key): `LocalBusiness, Service, Organization, Customer, Site, Asset, Part, PpmContract, DigitalDocument, Question, FAQPage, DefinedTerm, Task, Event, ImageObject` (via `name`); `CreativeWork, ConversationArchive, Objective, KeyResult, Decision, Source` (via `title`); `Note` (via `text`).

**First-class fall-throughs that need a NEW branch** (top-level operator-entry per schema, no fallback key, no existing branch):

| Label | Caption field(s) | Schema evidence |
|-------|------------------|-----------------|
| `Property` | `address` → `slug` | schema-estate-agent top-level; required `slug`, `address` |
| `Listing` | `displayName` → `slug` | schema-estate-agent top-level; required `slug`, `displayName` |
| `Viewing` | `date` | schema-estate-agent top-level; local scalar `date` |
| `Offer` | `status` then `price` composed as `status £price` when both present, else whichever is present | schema-estate-agent top-level; local scalars `status`, `price` |
| `Visit` | `purpose` → `status` | schema-construction top-level; local scalars `purpose`, `status` |
| `PurchaseOrder` | `poNumber` | schema-construction top-level; natural key `poNumber` |
| `Invoice` | `confirmationNumber` | schema-base first-class financial; `confirmationNumber` (mirror of the `InboundInvoice` branch's identity) |
| `Risk` | `statement` | schema-knowledge-work; local `statement` |
| `Finding` | `statement` | schema-knowledge-work; local `statement` |
| `Hypothesis` | `statement` | schema-knowledge-work; local `statement` |

**Grey allowlist — render as bare type by design** (child/specification/platform-plumbing labels, not first-class viewer entities): `PriceSpecification, OpeningHoursSpecification, Section, Chunk, Review, Preference, UserProfile, AdminUser, AccessGrant, StepResult`.

**Empirically finalised during implementation** (their bucket depends on their actual write shape, decided by building a synthetic node from the label's real properties and observing `pickShortLabel`): `KnowledgeDocument, Project, Workflow, Email, EmailAccount, UserMessage, AssistantMessage, AdminConversation, PublicConversation`. Rule: if the label declares a fallback key (`name/title/summary/subject/text`) it goes in the captioned bucket with that field as `expected`; otherwise it is grey. Every one of these must land in exactly one bucket before Task 2 passes — none may be left undeclared.

---

## Task 1: Caption-coverage gate (Part B) — written first, RED

**Files:**
- Create: `platform/lib/graph-style/src/__tests__/caption-coverage.test.ts`

**Interfaces:**
- Consumes: `GRAPH_LABEL_COLOURS`, `pickShortLabel` from `../index`.
- Produces: nothing consumed by later tasks; it is the verifier for Task 2.

- [ ] **Step 1: Write the coverage test with the full fixture**

The test owns `CAPTION_COVERAGE: Record<string, { node: GraphNodeLike; expected: string } | { fallbackGrey: true }>`, one entry per key of `GRAPH_LABEL_COLOURS`. Captioned entries carry a synthetic node built from the label's identity fields and the `expected` caption. Grey entries are `{ fallbackGrey: true }`. Seed from the classification table above.

Assertions:
1. **Completeness.** `Object.keys(GRAPH_LABEL_COLOURS)` and `Object.keys(CAPTION_COVERAGE)` are set-equal — every styled label has an entry, and no entry names an unstyled label.
2. **Correctness.** For each non-grey entry, `pickShortLabel(entry.node) === entry.expected` and `pickShortLabel(entry.node) !== label`.

- [ ] **Step 2: Run it — expect RED**

Run: `cd platform/lib/graph-style && ./node_modules/.bin/vitest run src/__tests__/caption-coverage.test.ts`
Expected: FAIL — the correctness assertion fails for `Property, Listing, Viewing, Offer, Visit, PurchaseOrder, Invoice, Risk, Finding, Hypothesis` (each still returns its bare type).

- [ ] **Step 3: Commit the RED test**

```bash
git add platform/lib/graph-style/src/__tests__/caption-coverage.test.ts
git commit -m "test(graph-style): caption-coverage gate (red)"
```

## Task 2: Caption branches (Part A) — turn it GREEN

**Files:**
- Modify: `platform/lib/graph-style/src/index.ts` — `pickShortLabel` (before the generic-fallback loop, ~line 494) and `pickDisplayName` (before its generic-fallback loop, ~line 560).

**Interfaces:**
- Consumes: the fixture from Task 1 as its pass condition.
- Produces: caption branches for the ten fall-through labels; both functions keep their existing signatures.

- [ ] **Step 1: Add the ten branches to `pickShortLabel`**

Each branch reads the label's caption field(s) as `string`, returns the 24-char-truncated value when non-empty, and otherwise falls through to the next fallback. `Offer` composes `status` and `£price`. Placed in the existing `else if` chain before the `for (const k of [...])` loop.

- [ ] **Step 2: Add the same ten branches to `pickDisplayName`, untruncated**

Identical field logic, returning the full value (no `slice`), matching that function's convention.

- [ ] **Step 3: Finalise the empirical bucket for the nine ambiguous labels**

For `KnowledgeDocument, Project, Workflow, Email, EmailAccount, UserMessage, AssistantMessage, AdminConversation, PublicConversation`, build a synthetic node from the label's real write shape, run `pickShortLabel`, and place each in the captioned or grey bucket of `CAPTION_COVERAGE` per the rule. Add a branch only if the label is a first-class entity that falls through (none expected; if one appears, it joins Part A).

- [ ] **Step 4: Run the coverage test — expect GREEN**

Run: `cd platform/lib/graph-style && ./node_modules/.bin/vitest run`
Expected: PASS — all graph-style tests including `caption-coverage.test.ts`.

- [ ] **Step 5: Rebuild dist and run the UI parity test — expect GREEN**

Run: `cd platform/lib/graph-style && ./node_modules/.bin/tsc -p tsconfig.json && cd ../../ui && ./node_modules/.bin/vitest run app/graph/__tests__/display-helpers.test.ts`
Expected: PASS (77 tests) — parity between the canvas renderer and the live page holds.

- [ ] **Step 6: Commit Part A**

```bash
git add platform/lib/graph-style/src/index.ts platform/lib/graph-style/src/__tests__/caption-coverage.test.ts
git commit -m "fix(graph-style): caption branches for top-level fall-through labels"
```

---

## Self-review

- **Spec coverage.** Task file Part A (Property/Listing branches) → Task 2 Steps 1-2, expanded to all fall-throughs per the approved scope decision. Task file Part B (coverage gate) → Task 1. Verification section (reproduction check, parity, display-helpers) → Task 2 Steps 4-5.
- **Scope deviation from the task file.** The task named only Property and Listing for Part A; the coverage seed surfaced eight more first-class fall-throughs (`Viewing, Offer, Visit, PurchaseOrder, Invoice, Risk, Finding, Hypothesis`), and the operator chose to branch all of them now. This is a deliberate, approved expansion, recorded in Plan Conformity at land time.
- **Correction to the task file.** The task listed `Note` as a grey example; `Note` declares `text`, a generic-fallback key, so it captions via `text` and is in the captioned bucket, not grey.
- **No placeholders.** The nine empirically-finalised labels have an explicit decision rule and a hard requirement to land in one bucket; they are not left "TBD".
