Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,8 @@ How most packages work, so the bullets below don't repeat it:
- `vp dlx skills add onmax/comark-ui` installs it from GitHub, the docs serve it under `/.well-known/skills/` through nuxt-agent-discovery, and comark-ui ships it.
- `SKILL.md` installs the plugins, routes each block to its package's reference in `references/`, and holds what several packages share, such as the marks.
- `test/workspace.test.ts` checks that every package has a reference linked from `SKILL.md` that ends with its Register section. A new package adds one, and a change to a package's syntax updates it.
- `skills/before-after-evidence`, `review-document`, `report`, `visual-explanation`, and `teach-lesson` are the skills for a whole document: what goes in it and in what order.
- `skills/before-after-evidence`, `review-document`, `report`, `data-report`, `visual-explanation`, and `teach-lesson` are the skills for a whole document: what goes in it and in what order.
- `data-report` also ships `scripts/build.mjs`, which wraps a report in a page with a top bar, a sidebar, and a theme toggle, and its `references/charts.md` is the chart gallery, built by the same script.
- `vp dlx skills add onmax/comark-ui` lists them beside comark-ui and installs the ones picked.
- Each installs alone, so it carries the syntax of the blocks it uses in `references/blocks.md` and links nothing outside its folder. Its `references/example.md` is a finished document.
- Each has a page in `docs/pages/skills/` that leads with that example rendered.
Expand Down
2 changes: 1 addition & 1 deletion docs/examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const pageOrder = [
"eslint",
"vite-doctor",
];
export const recipeOrder = ["review", "pull-request", "share", "zoom", "carousel"];
export const recipeOrder = ["review", "pull-request", "share", "data-report", "zoom", "carousel"];
// Third-party plugins the preset registers, by slug and npm name. They sit in the rail like a package, but have no
// folder in packages/, so their pages sit in docs/pages/<slug>/.
export const thirdParty: Record<string, string> = { mermaid: "comark-pele" };
Expand Down
165 changes: 165 additions & 0 deletions docs/pages/recipes/data-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
---
title: A data report
description: Headline figures, findings, and a section of charts per question, from one export, in one Markdown file.
keywords: analytics, dashboard, report, KPI, metrics, heatmap, data, BI
tagline: Turn a dataset into a page a reader decides from, with the figures first, then what stands out, then a chart per question.
install: pnpm add comark @comark/html comark-ui
---

::example{label="Data report"}
````md
::stats{tiles}
| Figure | Value |
| --- | --- |
| Questions | 418 |
| Threads | 297 |
| Customers | 8 |
| Answerable today | 78% |
| With action drafts | 87% |
| Median first reply | 1.3 h |
| With a screenshot | 37% |
::

## What stands out

1. **78% of questions** can be answered today from the thread, the docs and code, and product data. Action drafts lift that to 87%. See [Sources](#sources).
2. **Data missing** is the largest type, 103 questions (25%). See [Question types](#question-types).
3. **Contoso** asks 93 questions, and 9 of them need someone from the team. See [Customers](#customers).

## Sources

Each step adds one source an answer can draw on, from what the thread already holds to what only the team knows. What stays below 100% at the last step needs a person.

::chart{type="line" unit="%" title="Share of questions answerable, adding one source at a time" description="All questions, and the two customers who ask the most."}
| step | all | Contoso | Northwind |
| --- | --: | --: | --: |
| thread only | 20 | 20 | 16 |
| + docs and code | 55 | 49 | 55 |
| + product data | 78 | 78 | 76 |
| + action draft | 87 | 90 | 87 |
::

## Customers

::chart{type="stacked" horizontal title="Questions per customer, by the last source a reply needs" description="The first three steps are answerable today; team only needs a person."}
| customer | thread only | docs and code | product data | action draft | team only |
| --- | --: | --: | --: | --: | --: |
| Contoso | 19 | 27 | 27 | 11 | 9 |
| Northwind | 12 | 30 | 16 | 8 | 10 |
| Tailspin | 15 | 17 | 10 | 1 | 9 |
| Fabrikam | 8 | 16 | 13 | 4 | 7 |
| Wingtip | 10 | 15 | 9 | 1 | 8 |
| Litware | 8 | 15 | 5 | 6 | 3 |
| Adventure Works | 7 | 13 | 9 | 3 | 5 |
| Proseware | 4 | 13 | 7 | 4 | 4 |
::

::chart{type="heatmap" title="Questions per customer and type" description="Darker cells hold more questions."}
| customer | Data missing | How it works | Forecast | Purchasing | Bug | Alerts |
| --- | --: | --: | --: | --: | --: | --: |
| Contoso | 20 | 20 | 18 | 13 | 10 | 7 |
| Northwind | 20 | 19 | 8 | 10 | 8 | 1 |
| Tailspin | 11 | 10 | 9 | 7 | 4 | 3 |
| Fabrikam | 13 | 10 | 7 | 8 | 4 | 3 |
| Wingtip | 10 | 13 | 5 | 4 | 1 | 3 |
| Litware | 10 | 6 | 1 | 3 | 6 | 8 |
| Adventure Works | 10 | 4 | 4 | 5 | 4 | 5 |
| Proseware | 9 | 6 | 5 | 3 | 3 | 4 |
::

## Question types

::chart{horizontal title="Questions per type" description="Sorted by volume."}
| type | questions |
| --- | --: |
| Data missing | 103 |
| How it works | 88 |
| Forecast | 57 |
| Purchasing | 53 |
| Bug | 40 |
| Alerts | 34 |
| Feature request | 27 |
| Setup | 16 |
::

::chart{type="heatmap" better="higher" unit="%" title="Share answerable today, per type" description="Setup, feature requests, and bugs need the team most often."}
| type | answerable today |
| --- | --: |
| Data missing | 80 |
| How it works | 86 |
| Forecast | 77 |
| Purchasing | 75 |
| Bug | 65 |
| Alerts | 88 |
| Feature request | 63 |
| Setup | 62 |
::

## Over time

::chart{type="stacked" title="Questions per month, by type" description="The four largest types, and the rest together."}
| month | Data missing | How it works | Forecast | Purchasing | other |
| --- | --: | --: | --: | --: | --: |
| 2026-05 | 9 | 13 | 4 | 6 | 11 |
| 2026-06 | 20 | 13 | 6 | 6 | 13 |
| 2026-07 | 17 | 6 | 11 | 10 | 13 |
| 2026-08 | 19 | 15 | 17 | 10 | 29 |
| 2026-09 | 18 | 15 | 7 | 9 | 25 |
| 2026-10 | 18 | 20 | 10 | 10 | 18 |
::

::chart{type="line" log unit="h" title="Time to first reply" description="Median and 90th percentile per month, on a log scale."}
| month | median | p90 |
| --- | --: | --: |
| 2026-05 | 2.04 | 15.7 |
| 2026-06 | 1.23 | 8.8 |
| 2026-07 | 1.04 | 6.0 |
| 2026-08 | 1.33 | 8.4 |
| 2026-09 | 1.01 | 4.8 |
| 2026-10 | 1.02 | 3.1 |
::

## Team

::chart{type="heatmap" title="Replies per person and type" description="Ana answers most of every type."}
| person | Data missing | How it works | Forecast | Purchasing | Bug | Alerts |
| --- | --: | --: | --: | --: | --: | --: |
| Ana | 52 | 42 | 25 | 26 | 24 | 17 |
| Ben | 26 | 19 | 13 | 15 | 9 | 10 |
| Chen | 16 | 20 | 12 | 9 | 4 | 6 |
| Dara | 9 | 7 | 7 | 3 | 3 | 1 |
::

| Person | Replies | Median first reply |
| --- | --: | --: |
| Ana | 215 | 1.4 h |
| Ben | 101 | 1.1 h |
| Chen | 71 | 1.1 h |
| Dara | 31 | 1.2 h |

## Method

::details{summary="How the numbers were computed"}
The export holds one row per customer message. A question is a message that asks for something; thanks and scheduling are left out. Each question was classified by its type and by the last source a sendable reply needed, in the order the line chart adds them. First reply is the time from the customer's first message to the team's first reply, in calendar hours. Every number on this page comes from one script over the export, `analyze.py`, and nothing was typed by hand.
::
````
::

## The order

A reader opens a data report to learn how much, where, when, and who, so the page keeps one shape:

1. `::stats{tiles}` holds the headline figures in one strip, a name and a value each.
2. **What stands out** lists three to five findings, each with its number in bold and a link to the section whose chart shows it.
3. Each `##` section answers one question with one chart: [bars](/charts#type) for how many, stacked bars for how a total splits, a [heatmap](/charts#heatmap) for two groupings crossed or for scores, a line for a trend, and money with [`unit="$"`](/charts#unit).
4. A `::details` at the end says how every number was computed.

Every chart has a `title` with what it measures and a `description` with what to see. The charts span the column, so a page of them reads like a dashboard.

## Build it with an agent

The [data-report skill](/skills/data-report) has an agent profile the data, compute every number in one script, and write this page. Its `build.mjs` wraps the page in one HTML file with a top bar, a sidebar with the outline and where the data comes from, and a light and dark toggle.

## Check it

Run [comark-linter](/lint) on the file, then open the rendered page and read every chart: each must say what its description says, with no label over another.
44 changes: 44 additions & 0 deletions docs/pages/skills/data-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
title: Data report
description: A dataset turned into an analytics page, with headline figures, findings, and a section of charts per question.
icon: chart-column
keywords: analytics, dashboard, data, export, metrics, KPI, heatmap, chart, report, BI
tagline: Your agent profiles the data, computes every number in one script, and builds a page of charts with a top bar and a sidebar, in light and dark.
---

::skill{name="data-report"}
::

## What it does

The skill turns an export, a database, logs, or eval results into one page with a fixed shape:

- the **headline figures** in one strip
- **what stands out**, three to five findings, each linking to the chart that shows it
- **one section per question**, each with the chart that answers it: bars, stacked bars, lines, heatmaps, log scales, and money as `$22.2k`
- **how the numbers were made**, folded at the end

Every number comes from one script the agent keeps, so the next run starts from the script rather than from the page.

The skill's `build.mjs` wraps the report in a page: a top bar with a link per section, a sidebar with the outline and where the data comes from, and a light and dark toggle. The result is one HTML file that opens from disk or deploys as a static page.

## When to use it

Ask with the data at hand, such as "Build a report of this support export" or "Make a dashboard of these eval runs." To be sure, say "Use the data-report skill."

Choose by what you have:

- Data to explore and summarize: this skill.
- Options to compare, or a plan to approve: [Report](/skills/report).

## It's working if

- The strip at the top answers how much in four to seven figures.
- Each finding's number appears in a chart of the section it links to.
- Every chart has a title with its measure, a description that says what to see, and a unit when its numbers have one.
- A category keeps its color on every chart.
- The page ends with what a row is, what was left out, and the script that made the numbers.

## The chart gallery

The skill's `references/charts.md` maps each question shape to its chart, in the house style the reports follow. The same build script renders it, so it doubles as a gallery of every chart type in light and dark.
1 change: 1 addition & 0 deletions docs/pages/skills/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ tagline: Each skill teaches your agent one kind of document, what goes in it and
- A visible change needs proof on a pull request: [Before/after evidence](/skills/before-after-evidence).
- A review is done, and someone has to decide what to do: [Review document](/skills/review-document).
- Someone has to choose between options or approve a plan, from your research: [Report](/skills/report).
- Someone has a dataset and wants analytics, a dashboard, or a report of it: [Data report](/skills/data-report).
- Someone asked how a system, a flow, or a change works: [Visual explanation](/skills/visual-explanation).
- Someone needs to learn one concept and prove it: [Teach lesson](/skills/teach-lesson).
- Your agent needs a block's syntax, not a whole document: the [comark-ui skill](/guide/agents).
Expand Down
67 changes: 67 additions & 0 deletions e2e/data-report.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { execFileSync } from "node:child_process";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { expect, test } from "@playwright/test";

let folder: string;
let url: string;

test.use({ viewport: { width: 1440, height: 900 } });

test.beforeAll(() => {
folder = mkdtempSync(join(tmpdir(), "comark-data-report-"));
const output = join(folder, "report.html");
execFileSync(
process.execPath,
[
fileURLToPath(new URL("../skills/data-report/scripts/build.mjs", import.meta.url)),
fileURLToPath(new URL("../skills/data-report/references/example.md", import.meta.url)),
output,
],
{ cwd: fileURLToPath(new URL("../packages/ui/", import.meta.url)) },
);
url = pathToFileURL(output).href;
});

test.afterAll(() => rmSync(folder, { recursive: true, force: true }));

test("the generated report tracks its outline in both navigation areas", async ({ page }) => {
await page.goto(url);
const outline = page.locator(".dr-side ol a");
await expect(outline).toHaveCount(7);
for (const link of await outline.all()) {
const target = await link.getAttribute("href");
await expect(page.locator(`.dr-body .cl-heading > ${target}`)).toHaveCount(1);
}
await expect(page.locator(".dr-nav a[aria-current]")).toHaveAttribute("href", "#what-stands-out");
await expect(page.locator(".dr-side ol a[aria-current]")).toHaveAttribute(
"href",
"#what-stands-out",
);

for (const id of ["customers", "sources"]) {
await page.locator(`.dr-body #${id}`).evaluate((heading) => {
window.scrollTo(0, window.scrollY + heading.getBoundingClientRect().top - 80);
});
await expect(page.locator(".dr-nav a[aria-current]")).toHaveAttribute("href", `#${id}`);
await expect(page.locator(".dr-side ol a[aria-current]")).toHaveAttribute("href", `#${id}`);
}
});

test("outline fragments leave wrapped headings below the sticky header", async ({ page }) => {
await page.goto(url);
const headerBottom = await page
.locator(".dr-top")
.evaluate((header) => header.getBoundingClientRect().bottom);
await page.locator('.dr-nav a[href="#customers"]').click();
await expect(page).toHaveURL(/#customers$/);
await expect
.poll(() =>
page
.locator(".dr-body #customers")
.evaluate((heading) => heading.getBoundingClientRect().top),
)
.toBeGreaterThanOrEqual(headerBottom + 15);
});
1 change: 1 addition & 0 deletions packages/ui/test/editor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ describe("the docs examples", async () => {
const examples = await loadExamples(packages);
const skills = [
"before-after-evidence",
"data-report",
"report",
"review-document",
"teach-lesson",
Expand Down
Loading
Loading