---
name: docujoint-dashboard-authoring
description: Author dj dashboard.yaml files — preset widgets with binds, indicators, the where query language, navigation with facet filters, grouped inventory views and interactive graph views. Use when building or evolving analytics/dashboards over a docujoint vault.
---

# Authoring dashboard.yaml

Every number on a dj dashboard is a **query over the parsed vault** —
nothing is hand-maintained, and every visual encoding must carry its meaning
(legend/caption/callout) or the page renders a visible ⚠ marker instead.
Evaluate with `dj report --dashboard`, render the interactive app with
`dj dashboard`.

## The where-language

`field == value` · `field != value` · `field in [a, b]` ·
`field startswith 'prefix'` · `field contains 'sub'` · bare `field` = truthiness ·
`&& || ! ( )`. Bare words on the right-hand side are string literals.

`contains` is a substring test, for when a cell HOLDS a value rather than
being it — a link to a document (`owner contains 'Team/Ada.md'`), a
`;`-separated list, a sentence citing an id.

Contexts you query:
- **rows** (`select: rows, from: <block>`): every authored column by lowercase
  name (e.g. `status`, `kind`) plus `id`, `state` (derived), one boolean
  `cites_<scheme>` per declared scheme (e.g. `cites_test`), and the
  owning concept's `type`, `path`, `domain`. `from:` accepts a list to span
  blocks: `from: [open-questions, anomalies]`.
- **concepts**: `type`, `path`, `domain`, `title`, every flag the type
  declares by its own name (e.g. `stub`, `canonical` — only if declared), plus
  every declared frontmatter field (`app`, `database`, `status`, …).
- **artifacts** (needs `--inventory`): `uri`, `scheme`, `kind`, `namespace`,
  `name`, `in_scope`, `exempt`, `referenced`.

## Presets — predefined indicator sets, bound to YOUR vocabulary

The three presets live on the generic views/widgets path — compose them into
a view like any other widget. (The pre-0.7.0 `use: [{ pack: … }]` spelling
still evaluates but emits a `dashboard-pack-deprecated` warning per use; same
binds, moved onto the widget path.)

```yaml
views:
  - id: overview
    label: Overview
    widgets:
      - widget: stat-row          # two tiles: total + open
        preset: traceability
        bind:
          rows: features
          label: Features
          complete: "state == built"
          complete_label: built
          open: "state in [partial, missing, drift, unspecified]"
          open_label: Open features
          open_caption: partial, missing, drifted or blocked on a question
      - widget: coverage          # one bar per concept type
        bind:
          rows: features
          types: [page, module, service]
          built: "state == built"
          tested: "cites_test"    # the second number is a QUERY, like built
          legend:                 # REQUIRED meaning: every bar segment in words
            built: built — implemented, no gap recorded
            partial: partial — implemented with a recorded gap
      - widget: reverse-gap       # inventory-only; degrades to "unavailable"
        bind: { group_by: kind }
```

## Custom indicators

```yaml
indicators:
  - id: unhappy-untested          # make the damning number visible
    label: Untested unhappy paths
    select: rows
    from: scenarios
    where: "kind == unhappy && state == untested"
    metric: count
    caption: failure paths documented but not proven by any test
  - id: test-coverage
    select: rows
    from: features
    metric: { pct_where: "cites_test" }   # % of selected rows matching
    caption: features carry test evidence
```

## Navigation — every entry is a query

```yaml
navigation:
  - group: Product
    items:
      - { label: Pages, select: concepts, where: "type == page",
          filter: { by: app, label: App } }            # facet segmented-control
      - { label: Modules, select: concepts, where: "type == module",
          filter: { by: app, label: App, empty: Shared } }
```

`theme:` values are validated before they reach the stylesheet: a conservative
value grammar (colours, lengths, font stacks, `var(--token)`) that rejects
anything able to close a declaration or fetch a resource. Rejected values are
DROPPED with a finding, never escaped — the same applies to `shared.colors` in
format.yaml. Authored CSS is untrusted input the moment someone other than the
author views the page.

`filter:` renders All + one option per declared-metadata value with counts —
it narrows query members, never hides DOM, and cannot drift because facet
values come from the documents.

The same `filter:` works on a `select: rows` view, where `by:` names a COLUMN
of the selected block(s) instead of a frontmatter field (matched
case-insensitively, faceted on the cell's rendered text). `default:` opens the
view pre-filtered while every bucket stays one click away — the triage shape:

```yaml
      # the whole doubt channel — questions and anomalies — opening on the open ones
      - { label: Doubts, select: rows, from: [open-questions, anomalies],
          filter: { by: Status, label: Status, default: open } }
```

**Power search.** One facet answers one question. A rows view often needs
several at once — assigned to X, of this kind, still lacking evidence — so it
can also declare a **multi-property search**: tokens of *property · operator ·
value* that compose (every token must hold), each removable on its own.

```yaml
      - { label: Work, select: rows, from: features,
          search: { by: [ { property: Owner, label: Assigned to, from_concepts: person },
                          Kind, state, type, document, Gap ] } }
```

A property is a declared COLUMN of the selected block(s), or one of the three
derived ones every row has: `state`, `document`, `type`. Its vocabulary is
whatever the format already knows — a column's `enum:`, the titles of a concept
type (`from_concepts:`), an explicit `values:` list, or, failing all of those,
the values the documents actually hold. A vocabulary offers `is` / `is not` /
`is any of`; anything with too many distinct values to list falls back to text
(`contains` / `is` / `is empty` / `is not empty`). The engine supplies the
operators and nothing else: every property, label and value comes from the
definition or the vault, so a filter can never mean something the format did
not say.

`search:` and `filter:` compose — the segmented control narrows first, the
tokens narrow further.

**Trees.** Where documents name a parent in frontmatter, a concepts view can
render that hierarchy instead of a flat list:

```yaml
      - { label: Flows, select: concepts, where: "type == flow",
          tree: { by: parent_flow } }
```

`by:` is any self-referencing field — its value is matched against another
selected document's title, then its path. A document whose parent is empty,
unmatched, or would close a cycle becomes a root, so a hierarchy the documents
got wrong still renders, just flat.

**Grouped inventory view** (a backend-inventory tab — needs `--inventory`):

```yaml
- label: Backend
  grouped:
    where: "scheme == api && kind != apigroup"
    sections: { by: kind }
    groups: { by: namespace, empty: "(ungrouped)" }
    legend: { on: documented — cited by a requirement, off: not referenced }
    callout: Counts as documented when a row cites it ({documented} of {count}).
```

**Graph view** (interactive D3 — select nodes to explore neighborhoods,
type toggles, drag/zoom, double-click opens the doc):

```yaml
- label: System graph
  graph:
    nodes: { where: "type in [app, page, service, database, table, flow]" }
    edges: [links, "meta:app", "meta:database"]
```

Edge sources: `links` = resolved doc-to-doc links; `meta:<field>` joins a
concept to the concept whose title (or `meta.name`) equals the field's value —
so give join targets a short `name:` key.

## Theming

The rendered app's design tokens are declarative too — override any of them
per light/dark mode with `theme:` (unknown token names warn, values are raw
CSS). Value→colour semantics stay in format.yaml's `shared.colors`; `theme:`
is pure branding:

```yaml
theme:
  light: { brand: "#0f766e", bg: "#f8fafc" }
  dark:  { brand: "#2dd4bf" }
```

Tokens: accent(-ink) · bg · surface(-2,-3) · text(-2,-3) · border(-2) ·
success/warning/error/info(+-bg) · brand · shadow-low/med · radius-el/ct/full ·
font · mono.

## Rules that keep dashboards honest

1. Meaning slots are mandatory for encodings: coverage `legend`, grouped
   `legend` + `callout`. Missing meaning renders ⚠, never a naked encoding.
2. Inventory-dependent pieces must degrade to "unavailable", never zeros.
3. Prefer indicators that name a risk ("untested unhappy paths", "open
   questions") over vanity counts.
4. Anything you want to filter or join on must be DECLARED frontmatter in
   format.yaml — add the field there first, then query it here. An undeclared
   frontmatter key never reaches the query context at all, so a `where:` over
   one counts nothing; the engine now reports it as `dashboard-unknown-field`
   rather than rendering a confident zero.
5. Every name in a query is CHECKED against the format. A `from:` naming a
   block the format does not declare is refused outright, and a field that is
   neither a declared column, nor declared frontmatter, nor one of the reserved
   names (`id` · `state` · `type` · `path` · `domain` · `cites_<scheme>`) is
   reported. Spelling `stattus` where you meant `status` used to render
   `value: 0` and exit 0 — that is the class of bug these checks exist for.
6. A chart series that sums a column (`y: Estimate`) wants that column declared
   `type: number` in format.yaml. Without it, a cell that does not parse is
   silently dropped from the total and `dashboard-metric-type` says so, naming
   how many were lost. Declaring the type moves the guarantee to the record,
   where a bad cell is refused instead of quietly vanishing from a number
   somebody is about to trust.
7. A chart binding `history:` reads the **operational event log** instead of
   the rows, and draws `burndown · burnup · flow · cycle-time` over the state
   groups the FORMAT declares (`of:` names an enum column; `open:` / `done:` /
   `cancelled:` name values of it). Nothing is truncated — a window past the
   caps is refused by name — and cycle time excludes rather than guesses,
   counting every record it left out in a footnote under the chart. See the
   view-composition skill for the whole binding.
