# Digest

A periodic summary for one person, in sections in a fixed order: what needs them (each row with its link or action), health (each figure with a sparkline and its change against a named period), what was done (by agent or person, as the app names them) and notes in the app's groups. Every noun is the app's: the title, the period, the figures' labels, the groups' names. Empty sections are left out, and a digest with nothing in it says so.

It never makes a trend up: a figure with fewer points than `minPoints` says "Not enough data yet" and how many it has, not a flat line, and its change waits with it; a change always names its period, and one without a period isn't drawn; a figure that isn't known says "Not known", never 0. Notes and titles from an agent or a tool are drawn as text, and a note is named as its writer's. For the same digest by email, render `DigestEmail` with the same data.

- Registry name: `digest` (`@candor/digest`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/digest

## Install

```bash
npx candor-ui@3 add digest
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/digest`.
It brings in: agent-avatar, bidi, chat, heading-level, heading-scope, link, locale, sparkline, tokens, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import { Digest, type DigestData } from "@/components/candor/digest"

const planner = { kind: "agent" as const, agent: { id: "planner", name: "Planning agent", role: "planner" } }
const reviewer = { kind: "agent" as const, agent: { id: "reviewer", name: "Review agent", role: "reviewer" } }

const digest: DigestData = {
  title: "This week",
  period: "6 to 12 November",
  needsYou: [
    { id: "n1", title: "Approve the supplier import", detail: "Waiting since Tuesday", href: "#approve" },
    { id: "n2", title: "Question from the Planning agent", detail: "Should the March dates move too?" },
  ],
  health: [
    { id: "h1", label: "Runs finished", value: 128, points: [96, 104, 99, 115, 121, 128], change: { value: 0.12, format: "percent", period: "since last week", good: "up" } },
    { id: "h2", label: "Time to review", value: 3.2, valueText: "3.2 h", points: [4.1, 3.9, 3.6, 3.2], change: { value: 0.4, text: "0.4 h", period: "since last week", good: "down" } },
    { id: "h3", label: "Reruns", value: 5, points: [7, 5], change: { value: -2, period: "since last week", good: "down" } },
    { id: "h4", label: "Hours saved", value: null, missing: "Not measured until the timer is on.", points: [] },
  ],
  done: [
    { id: "d1", title: "Drafted the quarterly summary", by: planner, href: "#summary" },
    { id: "d2", title: "Moved four dates after the delay", by: planner },
    { id: "d3", title: "Checked 212 sources", by: reviewer },
    { id: "d4", title: "Signed off the plan", by: { kind: "person", name: "Dana Reyes" } },
  ],
  notes: [
    { id: "g1", name: "Northwind", notes: [{ id: "x1", text: "The supplier's file arrived a day late; nothing downstream moved.", by: planner }] },
    { id: "g2", name: "Harbour", notes: [{ id: "x2", text: "Kick-off is on Thursday.", by: { kind: "person", name: "Dana Reyes" } }] },
  ],
}

export default function Example() {
  return <Digest className="max-w-lg" digest={digest} onOpen={() => {}} />
}
```

## <Digest>

Props:

- `digest: DigestData`: What the digest shows: its title and period, and the rows of each section.
- `minPoints?: number | undefined` (default `defaultMinPoints`): The fewest numbers a health row's points need before its trend and change are drawn, at least 2. Defaults to 4.
- `onOpen?: ((id: string) => void) | undefined`: Called with a needs-you row's id when it has no `href` and the person opens it.
- `headingLevel?: HeadingLevel | undefined`: The level of the digest's title. Defaults to one below the nearest `HeadingScope`, else 2; the sections sit one below it.
- `className?: string | undefined`: Classes for the digest, merged with Candor's.

## Strings (`digest`)

The fixed words, English by default. Override any of them under `digest` in `<LocaleProvider strings={{ digest: { … } }}>`; the table is exported as `digestStrings`.

- `needsYou`: Needs you
- `health`: Health
- `done`: What was done
- `notes`: Notes
- `nothing`: Nothing to report for this period.
- `notKnown`: Not known
- `notEnoughData`: { one: "Not enough data yet: {count} point", other: "Not enough data yet: {count} points" }
- `up`: Up {amount} {period}
- `down`: Down {amount} {period}
- `same`: No change {period}
- `trend`: {label} trend
- `things`: { one: "{count} thing", other: "{count} things" }
- `wrote`: Written by {name}

## Types

### DigestActor

Who did something or wrote a note in a `Digest`: an agent, by the identity the app gives it (its name is the app's, never the model's), or a person, by the name the app gives them.

```ts
{ kind: "agent"; agent: AgentIdentity } | { kind: "person"; name: string }
```

### DigestNeed

One thing in a `Digest` that waits on the person.

```ts
{
  id: string;
  /** What waits, in the app's words or a tool's: drawn as text. */
  title: string;
  /** A second line: what it is about, how long it has waited. */
  detail?: string | undefined;
  /** Where to deal with it. Without it, the row is a button that calls `Digest`'s `onOpen`, or text when there is none. */
  href?: string | undefined;
}
```

### DigestChange

A change in a health row's figure against a named period, as the app measured it. Candor never works one out.

```ts
{
  /** How much the figure moved: above 0 is up, below 0 down, 0 no change. A fraction when `format` is "percent" (0.12 is 12 %). Not a finite number (NaN, Infinity), no change is drawn. */
  value: number;
  /** The period it is measured against, in the app's words as they follow the amount: "since last week". */
  period: string;
  /** How the amount is written: "number" (the default) or "percent". */
  format?: "number" | "percent" | undefined;
  /** The amount as the app writes it, without its sign, taking the formatted number's place: "$1,200", "3 h". */
  text?: string | undefined;
  /** Which way is good news, to colour the change: "up" or "down". Left out, the change is drawn without a tone. */
  good?: "up" | "down" | undefined;
}
```

### DigestHealthRow

One figure in a `Digest`'s health section, with its trend.

```ts
{
  id: string;
  /** What the figure is: "Runs finished", "Time to review". */
  label: string;
  /** The latest figure, or `null` when it isn't known (it says so, never 0). */
  value: number | null;
  /** The figure as the app writes it, taking the formatted number's place: money in its currency, "2.4 h". */
  valueText?: string | undefined;
  /** Why the figure isn't known, shown after "Not known" when `value` is null. */
  missing?: string | undefined;
  /**
   * The figure's past values in order, oldest first; `null` leaves a gap. With fewer numbers than the digest's
   * `minPoints`, the row says "Not enough data yet" with how many there are, in place of the line and the change.
   */
  points: (number | null)[];
  /** The change against a named period. Drawn only with enough points, a known figure and a period. */
  change?: DigestChange | undefined;
}
```

### DigestDone

One thing that was done in the period, and who did it.

```ts
{
  id: string;
  /** What was done, in the app's words or a tool's: drawn as text. */
  title: string;
  /** Who did it. */
  by: DigestActor;
  /** Where to see it. */
  href?: string | undefined;
}
```

### DigestNote

A note in one of a `Digest`'s groups. Its text is drawn as text, never as HTML or a link, and named as its writer's.

```ts
{
  id: string;
  /** What the note says. An agent's note is what the agent wrote, believed, not checked. */
  text: string;
  /** Who wrote it. */
  by: DigestActor;
}
```

### DigestNoteGroup

One of the app's groups of notes, by its own name.

```ts
{
  id: string;
  /** The group's name, the app's word for it. */
  name: string;
  /** Its notes; a group with none is left out. */
  notes: DigestNote[];
}
```

### DigestData

What a `Digest` (and `DigestEmail`) shows. Every section is optional, and an empty one is left out.

```ts
{
  /** The digest's title, in the app's words: "This week". */
  title: string;
  /** The period it covers, in the app's words: "6 to 12 November". */
  period?: string | undefined;
  /** What waits on the person, first. */
  needsYou?: DigestNeed[] | undefined;
  /** Figures and their trends. */
  health?: DigestHealthRow[] | undefined;
  /** What was done, and by whom. */
  done?: DigestDone[] | undefined;
  /** Notes, in the app's groups. */
  notes?: DigestNoteGroup[] | undefined;
}
```

## Types from other items

The props above take these. Each is defined by another item; import it from the path shown.

### HeadingLevel

Import: `import type { HeadingLevel } from "@/lib/candor/heading-level"` (item `heading-level`, https://candor.themethodic.com/docs/heading-level.md)

A heading's level in the page's outline: `1` is `<h1>`, `6` is `<h6>`. Every Candor item that draws a heading takes its `headingLevel` as this type; the level is where the heading sits in the outline, never how large it looks.

```ts
1 | 2 | 3 | 4 | 5 | 6
```

### AgentIdentity

Import: `import type { AgentIdentity } from "@/lib/candor/chat"` (item `chat`, https://candor.themethodic.com/docs/chat.md)

```ts
{
  id: string;
  /** The displayed name, e.g. "Architect agent". Never derived from model output. */
  name: string;
  role: AgentRole;
  specialty?: string | undefined;
}
```

### AgentRole

Import: `import type { AgentRole } from "@/lib/candor/chat"` (item `chat`, https://candor.themethodic.com/docs/chat.md)

What an agent does: "assistant", "coder", "reviewer"… The app registers each role's glyph and hue (AgentRolesProvider).

```ts
string
```

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider`: its fixed strings in `strings`, its nouns in `nouns`; entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
