# Webinar vault dashboard — design

Date: 2026-07-28
Status: approved, ready for implementation planning

## Context

The vault is an LLM-maintained wiki governed by `CLAUDE.md`. It currently holds 12 raw
sources, 9 ingested source summaries, 19 concept pages, and one webinar script
(`raw/sources/Webinar script.md`) that the concepts are supposed to feed.

Two problems motivated this work:

1. **The manual catalog drifts.** `index.md` lists `Ideas for webinar.md` and
   `my theses.md` under `raw/sources/`, but both live in `raw/notes/`. It does not
   mention `Agentic Engineering, explained by a 10x developer.md` at all, which sits
   un-ingested in `raw/sources/`. Nothing detects this.
2. **No view of script coverage.** There is no way to see which concept pages the
   webinar script actually delivers. A manual read shows the script is entirely
   machine-side: all human-side and strategy-side concepts are absent.

## Goals

- Show processed and unprocessed sources, with a one-click ingest on the unprocessed.
- Show every concept and whether the webinar script mentions it, plus which script
  station it lands in.
- Keep coverage status in a separate markdown file, kept synchronized by a rule in
  `CLAUDE.md`.

## Non-goals

- Replacing `index.md` or `log.md`. Both stay exactly as they are.
- A general-purpose Obsidian dashboard framework. This is one vault-specific plugin.
- Publishing the plugin to the community plugin registry.

## Decisions taken

| Decision | Choice | Rationale |
|---|---|---|
| Buttons | Own plugin, not Meta Bind | Needs are narrow and vault-specific; Meta Bind's expensive half (inline CM6 widgets, two-way frontmatter binding) is unused here |
| Coverage status source | Claude judges; user can pin | Sync sets status automatically, but a row marked `Pinned: yes` is never overwritten |
| Ingest mechanism | Headless `claude -p` via `child_process` | One click, fully automatic. Chosen over a queue file with the unsupervised-write trade-off understood |
| Granularity | Status + script station | Turns the table into a pacing map, not just a checklist |
| Layout | Two-pane (Option B) | Sources and coverage both first-class; coverage grouped by station recovers most of the station-board view |

## Architecture

```
.obsidian/plugins/webinar-dash/
  manifest.json
  main.js          # plain CommonJS, no build step
  styles.css       # tesanti tokens scoped to .webinar-dash

dashboard.md               # vault root, beside index.md
wiki/script-coverage.md    # the coverage table
```

`dashboard.md` holds only a config block; the plugin renders everything:

````markdown
# Webinar dashboard

```webinar-dash
script: raw/sources/Webinar script.md
coverage: wiki/script-coverage.md
```
````

Config keys are optional and fall back to those two defaults.

### Where truth lives

| Data | Source of truth | Mechanism |
|---|---|---|
| Which sources are processed | Filesystem, read live | Diff `raw/sources/*.md` against the `**Raw path:**` value in every `wiki/sources/*.md` |
| Concept coverage status | `wiki/script-coverage.md` | Written by Claude on sync, read by the plugin |

Mechanical facts come from the filesystem, judgment comes from the markdown file.
This makes the `index.md` class of drift structurally impossible on the sources half:
the dashboard cannot disagree with the filesystem because it derives from it.

**The plugin never writes to the vault.** It reads files and spawns one subprocess.
Nothing else. It also never reads `index.md` — the catalog is a human-facing artifact,
and treating it as input would reintroduce exactly the drift this design removes.

### Source pipeline derivation

1. List `raw/sources/*.md`.
2. For each `wiki/sources/*.md`, extract the backticked path from the line matching
   `**Raw path:** \`<path>\``. Verified consistent across all 9 existing source pages.
3. A raw file claimed by some source page is **processed**; unclaimed is **unprocessed**.
4. A source page whose raw path no longer exists is reported as **orphaned**.

`raw/notes/` is out of scope — those are notes, not sources.

## Coverage file format

```markdown
# Script coverage

#coverage

## Metadata

- **Script:** `raw/sources/Webinar script.md`
- **Last synced:** 2026-07-28
- **Stations:** Chat box · ReAct · Tools · Memory · Skills · Process · OS

## Coverage

| Concept | Status | Station | Pinned |
|---|---|---|---|
| [[harness]] | covered | Tools | |
| [[agentic-loops]] | partial | Process | |
| [[levels-of-ai-usage]] | partial | all | |
| [[connections-as-moat]] | absent | — | yes |
```

Field rules:

- **Concept** — an Obsidian wikilink to a page in `wiki/concepts/`. The plugin extracts
  the page name from inside the brackets.
- **Status** — exactly one of `covered`, `partial`, `absent`. Any other value renders as
  `invalid` rather than being silently coerced.
- **Station** — one of the seven station names, a comma-separated list of them, `all`,
  or `—` for none.
- **Pinned** — `yes`, or blank. Blank is the default.

The seven stations are the `#` headings of the script that represent technology levels:
Chat box, ReAct, Tools, Memory, Skills, Process, OS. The script's `Intro`,
`Mail from boss`, and `Notes` headings are setup and are not stations.

v1 does not write this file from the plugin. Pinning is a hand-edit of one cell — a pin
toggle button would make the plugin a writer and risk clobbering concurrent user edits,
which is not worth it for a one-word change.

## Rendering — two-pane layout

Left pane (38%):
- **Queue** — unprocessed sources, each row showing filename, byte size, and an
  **Ingest** button. Files missing from `index.md` need no special flag: they appear
  here purely because no source page claims them, which is how
  `Agentic Engineering, explained by a 10x developer.md` surfaces despite being absent
  from the catalog.
- **Ingested** — processed sources as compact rows: date from the source page filename
  prefix, plus title.

Right pane (62%):
- Coverage meter — a stacked bar of covered / partial / absent with a 2px gap between
  segments, plus a counted key.
- Coverage table grouped by station, with `No station` last.

Collapses to a single column below 820px so a narrow Obsidian pane stays usable.

Styling follows the tesanti design system: black / white / red only, Space Grotesk
display, Inter body, JetBrains Mono for eyebrows and data, radii at most 6px, 1px
hairlines instead of shadows, Lucide stroke icons, no emoji. Status colors use the
system's `--ok` / `--warn` / `--danger` tokens and always ship with a text label, never
color alone. Dark mode is derived from the same ink ramp, with the red lifted to
`#ff4a3d` so small text clears contrast on near-black.

## Ingest mechanism

```js
const { spawn } = require("child_process");
spawn("claude", ["-p", `ingest "${file}"`], { cwd: vaultPath, shell: true });
```

`shell: true` is required on Windows to resolve `claude.cmd`, which places the filename
inside a shell string. Before spawning, the filename is rejected if it contains any of:
`"`  `` ` ``  `$`  `&`  `|`  `;`  `<`  `>`  `%`  or a newline. Existing filenames include
Cyrillic, spaces, and `!`, all of which pass. A rejected filename shows an error in its
row and does not spawn.

Vault path comes from `app.vault.adapter.getBasePath()` on `FileSystemAdapter`.

Per-row states: `idle` → `running` with elapsed seconds → `done` or `failed · exit <n>`
with captured stderr in an expandable block. On success the pipeline is re-derived and
the row moves to the ingested list. A second click while running is ignored.

A global **Sync coverage** button spawns `claude -p "sync script coverage"`.

Preflight: if `claude` is not resolvable on PATH, all buttons render disabled with that
reason stated.

### Accepted risk

Headless ingest writes source summaries, concept pages, `index.md`, and `log.md` without
the user watching. This was chosen deliberately over a review checkpoint. Mitigations:
one file per click rather than a batch, captured output retained per row, and visible
per-row status. The writes land before the user reads them; this is understood and
accepted.

## CLAUDE.md changes

1. **Folder convention** — add `wiki/script-coverage.md` to the tree with a note that it
   is machine-maintained.
2. **Tagging rules** — add the row `wiki/script-coverage.md` → `#coverage`. No existing
   page type fits: it is generated tabular data, not prose analysis.
3. **Workflow D: Sync Script Coverage** — re-read the script and every
   `wiki/concepts/*.md`; set `Status` and `Station` for each; never modify a row whose
   `Pinned` is `yes`; add rows for new concept pages; remove rows for deleted ones;
   update `Last synced`; then update `index.md` and append to `log.md`.
4. **Sync triggers** — Workflow D runs at the end of any ingest that creates or modifies
   a concept page, whenever `Webinar script.md` changes, and on the explicit
   `sync script coverage` intent, which is added to Operational Commands.

The coverage baseline is always the raw script at `raw/sources/Webinar script.md`.
Ingesting the script into `wiki/sources/` later does not change the baseline.

## Failure modes

| Condition | Behavior |
|---|---|
| Coverage file missing or table malformed | Sources pane renders normally; coverage pane shows a parse error with the offending line |
| Concept page exists with no table row | Rendered as `unsynced`, so a stale sync is visible rather than silent |
| Table row points at a nonexistent concept page | Rendered as `stale`, kept in place, not auto-removed |
| Source page whose raw path is missing | Listed under `orphaned` in the left pane |
| `child_process` unavailable (mobile) | Buttons render disabled with the reason |
| `claude` not on PATH | Buttons render disabled with the reason |

## Initial coverage assessment

Read manually while designing; the first real sync will regenerate it. 19 concepts:
5 covered, 4 partial, 10 absent.

- **covered** — harness (Tools), skills-as-memory (Skills), solve-first-then-skillify
  (Skills), personal-ai-operating-system (OS), evolution-of-agent-tooling (Tools)
- **partial** — agentic-loops (Process), context-as-scarce-resource (Memory),
  levels-of-ai-usage (all), code-as-throwaway (OS)
- **absent** — connections-as-moat, product-ownership, seniority-and-the-junior-squeeze,
  decoupling-identity-from-profession, network-from-a-standing-start,
  think-wider-not-bigger, make-more-cheap-code, enterprise-ai-reality,
  integration-dead-ends, leave-less-room-for-imagination

Every absent concept is human-side or strategy-side. The `ReAct` station carries no wiki
concept at all.

## Out of scope for v1

- Pin toggle button (hand-edit instead).
- Alternate station-board view toggle. The data file is layout-independent, so this is a
  render change if wanted later.
- Coverage for entities, sources, or queries — concepts only.
- Any view of `raw/notes/`.

## Note on spec location

This file introduces a `docs/` folder at the vault root, which is not part of the
`CLAUDE.md` folder convention and will appear in Obsidian's file explorer. It can be
moved or deleted without affecting the implementation.

The vault is not a git repository, so this spec is not committed.
