comark-ui 0.10.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,305 @@
1
+ ---
2
+ title: "Migrate wizard: start with Env and Resend, then Auth and Content"
3
+ status: Proposal
4
+ date: 2026-11-07
5
+ tags: [migrate wizard, codemods]
6
+ ---
7
+
8
+ ## The answer
9
+
10
+ Build the wizard shell with the Env codemod, then add Resend, Auth and Content. Auth and Content hold most of the users, but each needs ViteHub work first. Env and Resend are cheap and prove the wizard end to end.
11
+
12
+ > [!IMPORTANT]
13
+ > **Your call:** hold `@nuxtjs/i18n`, the largest audience at 977k a week, until #3564 merges and i18n has URL routing?
14
+
15
+ > [!WARNING]
16
+ > Every row needs Nuxt 5. Most source apps run Nuxt 4 and the codemods don't upgrade Nuxt, so the wizard checks the version first.
17
+
18
+ ## Value against cost
19
+
20
+ Up and to the right is worth more and costs more. Bubble size is weekly npm downloads. Auth and Content are the two big bubbles. The cheap wins sit at difficulty 2.
21
+
22
+ ::chart{title="Shortlist: value and difficulty (1 to 5), bubble size = npm downloads a week"}
23
+ | module | difficulty | value | downloads |
24
+ | --- | --: | --: | --: |
25
+ | nuxt-auth-utils | 4 | 5 | 177926 |
26
+ | @nuxt/content | 4 | 4 | 240394 |
27
+ | Docus | 3 | 4 | 15800 |
28
+ | Resend | 2 | 4 | 1251 |
29
+ | nuxt-security limiter | 3 | 3 | 220740 |
30
+ | t3-env | 2 | 3 | 6072 |
31
+ | sidebase | 4.5 | 3 | 46448 |
32
+ | nuxt-cron | 2 | 3 | 3123 |
33
+
34
+ ```vega-lite
35
+ {
36
+ "width": 620,
37
+ "height": 320,
38
+ "encoding": {
39
+ "x": { "field": "difficulty", "type": "quantitative", "scale": { "domain": [1.5, 5] }, "title": "Difficulty (5 = hardest)" },
40
+ "y": { "field": "value", "type": "quantitative", "scale": { "domain": [2.5, 5.5] }, "title": "Value (5 = most)" }
41
+ },
42
+ "layer": [
43
+ {
44
+ "mark": { "type": "circle", "opacity": 0.55 },
45
+ "encoding": { "size": { "field": "downloads", "type": "quantitative", "scale": { "range": [80, 3000] }, "legend": null } }
46
+ },
47
+ {
48
+ "transform": [{ "filter": "indexof(['t3-env', 'Docus', 'Resend'], datum.module) < 0 && datum.module !== 'nuxt-cron'" }],
49
+ "mark": { "type": "text", "dy": -26, "fontSize": 12 },
50
+ "encoding": { "text": { "field": "module" } }
51
+ },
52
+ {
53
+ "transform": [{ "filter": "indexof(['t3-env', 'Docus', 'Resend'], datum.module) >= 0" }],
54
+ "mark": { "type": "text", "align": "right", "dx": -12, "fontSize": 12 },
55
+ "encoding": { "text": { "field": "module" } }
56
+ },
57
+ {
58
+ "transform": [{ "filter": "datum.module === 'nuxt-cron'" }],
59
+ "mark": { "type": "text", "dy": 16, "fontSize": 12 },
60
+ "encoding": { "text": { "field": "module" } }
61
+ }
62
+ ]
63
+ }
64
+ ```
65
+ ::
66
+
67
+ ## The order
68
+
69
+ Boxes are codemods, numbered in build order. Hexagons are ViteHub work that has to land first; a dashed arrow means it should land first but doesn't have to.
70
+
71
+ ```mermaid
72
+ flowchart LR
73
+ shell["1 · Wizard shell + Env"]
74
+ shell --> resend["2 · Resend"]
75
+ shell --> auth["3 · Auth"]
76
+ shell --> content["4 · Content"] --> docus["Docus, then nuxt-studio"]
77
+ shell --> rl["5 · Rate limit"]
78
+ shell --> cron["6 · nuxt-cron"]
79
+ shell --> i18n["7 · i18n"]
80
+ ssr{{"SSR session + page middleware"}} --> auth
81
+ upstash{{"Upstash driver"}} -.-> rl
82
+ pr{{"#3564 + URL routing"}} --> i18n
83
+ ```
84
+
85
+ | Step | Module → primitive | npm/week | Fit |
86
+ | --- | --- | --: | --- |
87
+ | 1 | `@t3-oss/env-nuxt` → Env | 6,072 | :badge[High]{color="success"} |
88
+ | 2 | `resend`, `nuxt-resend` → Email | 1,251 | :badge[High]{color="success"} |
89
+ | 3 | `nuxt-auth-utils` → Auth | 177,926 | :badge[Partial]{color="warning"} |
90
+ | 3 | `@sidebase/nuxt-auth` → Auth | 46,448 | :badge[Partial]{color="warning"} |
91
+ | 4 | `@nuxt/content` v3 → Content | 240,394 | :badge[Partial]{color="warning"} |
92
+ | 4 | Docus → `@vite-hub/docs` | ~15,800 | :badge[High]{color="success"} |
93
+ | 5 | `nuxt-security` limiter → Rate Limit | 220,740 | :badge[Partial]{color="warning"} |
94
+ | 6 | `nuxt-cron` → Schedule | 3,123 | :badge[High]{color="success"} |
95
+
96
+ Resend's count is `nuxt-resend` alone; 274 more apps call `resend` directly from server routes. Inside step 3, `@nuxtjs/better-auth` (2,101 a week) goes first: it runs Better Auth on both sides, so users, hashes and sessions keep their format.
97
+
98
+ ## What ViteHub has to add
99
+
100
+ Each gap below blocks a codemod or a group of users. i18n routing unblocks the most by far.
101
+
102
+ ::chart{type="bar" horizontal labels title="npm downloads a week each missing feature would unblock"}
103
+ | Missing in ViteHub | downloads |
104
+ | --- | --: |
105
+ | i18n: URL routing | 985785 |
106
+ | Auth: SSR session + page middleware | 226475 |
107
+ | MCP: app-level server | 42096 |
108
+ | Email: SMTP driver | 4620 |
109
+ | Rate Limit: Upstash driver | 2044 |
110
+ ::
111
+
112
+ ::details{summary="Which modules each gap unblocks"}
113
+ | Gap | Unblocks |
114
+ | --- | --- |
115
+ | i18n: URL routing | `@nuxtjs/i18n` 977,490, `nuxt-i18n-micro` 8,295 |
116
+ | Auth: SSR session + page middleware | `nuxt-auth-utils` 177,926, sidebase 46,448, `@nuxtjs/better-auth` 2,101 |
117
+ | MCP: app-level server | `@nuxtjs/mcp-toolkit` 42,096 |
118
+ | Email: SMTP driver | `nuxt-nodemailer` 3,376, `nuxt-mail` 1,244, plus 609 apps on raw `nodemailer` |
119
+ | Rate Limit: Upstash driver, windows past 10s and 1m on Cloudflare | `nuxt-api-shield` 2,044, `@upstash/ratelimit`, and the nuxt-security default of 5 minutes on Cloudflare |
120
+ ::
121
+
122
+ ## Each codemod
123
+
124
+ Every item has the same three parts: what the codemod rewrites, what stays manual, and what has to land first.
125
+
126
+ ::accordion
127
+ :::accordion-item{label="1 · @t3-oss/env-nuxt → Env"}
128
+ | t3-env | ViteHub Env |
129
+ | --- | --- |
130
+ | `createEnv({ server, client })` | the existing converter in `nuxt-safe-runtime-config/schema.ts`, as it is |
131
+
132
+ - **Manual:** nothing found.
133
+ - **Bonus:** `@arkenv/nuxt` (202 a week) comes almost free, since the converter already reads ArkType.
134
+ :::
135
+
136
+ :::accordion-item{label="2 · Resend → Email"}
137
+ | Resend | ViteHub Email |
138
+ | --- | --- |
139
+ | `new Resend(key).emails.send(msg)`, `useResend()` | `email.send(msg)`, same message fields |
140
+ | `const { data, error }` | `const [error, data]` |
141
+ | (nothing) | `vitehub.email = { driver: 'resend', options: { apiKey: env(...) } }` |
142
+
143
+ - **Manual:** batch, contacts, audiences, broadcasts, `react` payloads, Resend templates.
144
+ - **Gain:** a dev outbox, Console preview, a path to Cloudflare Email.
145
+ - **The wizard's report must say:** in dev, sends go to the outbox unless `remote: true`.
146
+ :::
147
+
148
+ :::accordion-item{label="3 · nuxt-auth-utils → Auth"}
149
+ | nuxt-auth-utils | ViteHub Auth |
150
+ | --- | --- |
151
+ | `'nuxt-auth-utils'` in `modules` | `'vite-hub/nuxt'` |
152
+ | `runtimeConfig.oauth.<p>`, `NUXT_OAUTH_<P>_*` | `socialProviders.<p>` in `server/auth.ts`, with Env |
153
+ | `useUserSession()` `fetch`, `clear` | `refresh`, `signOut` (`loggedIn`, `user`, `ready` match) |
154
+ | `/auth/github` links | `signIn.social({ provider: 'github' })` |
155
+ | `getUserSession`, `requireUserSession` | `auth.api.getSession`, `withAuth` |
156
+
157
+ - **Providers:** about 25 of 52 are in Better Auth core; auth0, keycloak, okta and others go through `genericOAuth`; six, such as steam, mastodon and atproto, have no equivalent.
158
+ - **Manual:** `onSuccess` upserts move to `databaseHooks`; custom `setUserSession` data; user shape and IDs change (`user.login` breaks); everyone signs in again and each provider needs the `/api/auth/callback/<p>` URL; Adonis scrypt hashes need a custom `password.verify`; WebAuthn moves to the passkey table.
159
+ - **First:** a server-rendered session (`useUserSession()` stays pending during SSR, `packages/auth/README.md:317`) and page middleware. Without them the migrated app flashes signed-out on every load.
160
+ - **Gain:** a real users store, the Console, Auth events, admin, 2FA and organizations.
161
+ :::
162
+
163
+ :::accordion-item{label="3 · @sidebase/nuxt-auth → Auth"}
164
+ | sidebase (authjs) | ViteHub Auth |
165
+ | --- | --- |
166
+ | `NuxtAuthHandler({ providers })` | `socialProviders` |
167
+ | `useAuth()`, `status === 'authenticated'` | `useUserSession()`, `loggedIn` |
168
+ | `getServerSession` | `auth.api.getSession` |
169
+
170
+ - **Refuse:** the `local` provider, which sends bearer tokens to an external backend.
171
+ - **Manual:** the `jwt` and `session` callbacks.
172
+ :::
173
+
174
+ :::accordion-item{label="4 · @nuxt/content v3 → Content"}
175
+ | @nuxt/content | ViteHub Content |
176
+ | --- | --- |
177
+ | `content.config.ts` `defineCollection({ source })` | `server/content.ts` `defineContent({ sources })` with `glob()` |
178
+ | `queryCollection(c).path(p).first()` | `content.get(p)` |
179
+ | `queryCollectionNavigation` | `content.navigation` |
180
+ | `<ContentRenderer>`, `<MDC>` | `<Markdown>` |
181
+
182
+ - **Manual, reported with `file:line`:** every `.where/.order/.limit/.select/.count` chain (no query builder), the `database` adapters, remote `repository` sources, `queryCollectionItemSurroundings`.
183
+ - **Gain:** drafts and publishing, Console editing, Agent access, no SQL dump at build.
184
+ - **Won't move:** apps that query content heavily.
185
+ :::
186
+
187
+ :::accordion-item{label="4 · Docus → @vite-hub/docs"}
188
+ | Docus | @vite-hub/docs |
189
+ | --- | --- |
190
+ | `extends: ['docus']` + `app.config.ts` `seo`, `header`, `github`, `socials` | `modules: ['@vite-hub/docs/nuxt']` with `docs: { site, github, socials }` |
191
+ | `content/**` | `content/docs/<section>/` |
192
+ | `AppHeader`, `DocsAsideLeftTop` overrides | same names |
193
+
194
+ - **Manual:** Docus i18n, which has no target.
195
+ - **Not verified:** a Comark UI equivalent for every Nuxt UI prose component.
196
+ - **Why it ranks high:** the whole site moves, vitehub.dev is the oracle, and it reuses the Content codemod.
197
+ :::
198
+
199
+ :::accordion-item{label="5 · nuxt-security limiter → Rate Limit"}
200
+ :progress{value=293 max=785 label="nuxt-security configs that mention rateLimiter"}
201
+
202
+ The limiter is on by default: 150 requests per 300,000 ms per IP and route.
203
+
204
+ | nuxt-security | ViteHub Rate Limit |
205
+ | --- | --- |
206
+ | `security.rateLimiter` | `false`; the module stays for headers and CSP |
207
+ | (nothing) | `server/middleware/rate-limit.ts` with `requireRateLimit()`, `vitehub.rateLimit: true` |
208
+
209
+ - **Manual:** per-route `routeRules` overrides, `whiteList`, `ipHeader`, Redis storage.
210
+ - **Wall:** only memory and Cloudflare drivers, and Cloudflare allows only 10s or 1m windows, so the module's 5m default fails the build there.
211
+ :::
212
+
213
+ :::accordion-item{label="6 · nuxt-cron → Schedule"}
214
+ | nuxt-cron | ViteHub Schedule |
215
+ | --- | --- |
216
+ | `server/cron/<n>.ts` `defineCronHandler(time, cb)` | `server/schedules/<n>.ts` `defineSchedule({ cron, handler })` |
217
+ | presets | 5-field cron |
218
+
219
+ - **Manual:** `everySecond`, `timeZone`.
220
+ - **Gain:** a node-only cron runs on Cloudflare, Vercel and Netlify, with run history.
221
+ :::
222
+ ::
223
+
224
+ ## Left out
225
+
226
+ The chart shows what we leave on the table, 10k a week and up. i18n is the one big loss. The rest have no ViteHub target or wait on a codemod above.
227
+
228
+ ::chart{type="bar" horizontal labels title="Left out: npm downloads a week (10k and up)"}
229
+ | module | downloads |
230
+ | --- | --: |
231
+ | @nuxtjs/i18n | 977490 |
232
+ | nuxt-csurf | 223523 |
233
+ | @nuxtjs/supabase | 94526 |
234
+ | @nuxtjs/turnstile | 46735 |
235
+ | @nuxtjs/mcp-toolkit | 42096 |
236
+ | @clerk/nuxt | 40184 |
237
+ | nitro-cloudflare-dev | 29560 |
238
+ | nuxt-vuefire | 15428 |
239
+ | nuxt-studio | 14956 |
240
+ | nuxt-multi-cache | 14125 |
241
+ | nuxt-oidc-auth | 10859 |
242
+ :::annotation{at="@nuxtjs/i18n" mark="box" color="amber" side="right"}
243
+ Your call: hold until #3564 has URL routing.
244
+ :::
245
+ ::
246
+
247
+ ::accordion
248
+ :::accordion-item{label="Waits on ViteHub work (8)"}
249
+ | Module | npm/week | Waits on |
250
+ | --- | --: | --- |
251
+ | `@nuxtjs/i18n` | 977,490 | #3564 and URL routing (`useLocalePath`, `NuxtLinkLocale`, hreflang) |
252
+ | `nuxt-i18n-micro` | 8,295 | the same |
253
+ | `nuxt-studio` | 14,956 | the Content codemod |
254
+ | `nuxt-oidc-auth` | 10,859 | the Auth codemod; small audience, `h3` 1 |
255
+ | `nuxt-nodemailer`, `nuxt-mail` | 3,376 + 1,244 | an SMTP driver; the largest email segment |
256
+ | `nuxt-api-shield`, `@upstash/ratelimit` | 2,044 | an Upstash or Redis driver |
257
+ | `nitro-cloudflare-dev` | 29,560 | proof that ViteHub provides every binding in dev (only KV checked, #3480) |
258
+ | `nuxt-file-storage` | 845 | nothing; a cheap shim over `blob` after step 6 |
259
+ :::
260
+
261
+ :::accordion-item{label="No ViteHub primitive (5)"}
262
+ | Module | npm/week | Missing |
263
+ | --- | --: | --- |
264
+ | `nuxt-csurf` | 223,523 | CSRF |
265
+ | `@nuxtjs/turnstile` | 46,735 | captcha |
266
+ | `@nuxtjs/mcp-toolkit` | 42,096 | an app-level MCP server |
267
+ | `nuxt-multi-cache` | 14,125 | component and route caching, CDN tags |
268
+ | `nuxt-processor` (BullMQ) | 1,183 | priorities, repeats and flows in Queue |
269
+ :::
270
+
271
+ :::accordion-item{label="Data lives in a hosted service (4)"}
272
+ | Module | npm/week |
273
+ | --- | --: |
274
+ | `@nuxtjs/supabase` | 94,526 |
275
+ | `@clerk/nuxt` | 40,184 |
276
+ | `nuxt-vuefire` | 15,428 |
277
+ | Strapi, Sanity, Storyblok, Directus, Prismic | various |
278
+
279
+ Users, hashes and content stay in the service, so a codemod has nothing to move. At most a skeleton and a link to Better Auth's guide.
280
+ :::
281
+
282
+ :::accordion-item{label="Nothing worth a codemod (6)"}
283
+ | Module | Why |
284
+ | --- | --- |
285
+ | `nuxt-authorization` (7,188) | keep it; rewrite its two resolvers inside the Auth codemod |
286
+ | `nuxt-email-renderer` (5,291) | templates keep working through `email.send` |
287
+ | `@workflow/nuxt` (2.8M, transitive) | Workflow DevKit is already portable |
288
+ | `@prisma/nuxt` (896) | deprecated, and no Prisma support |
289
+ | Nitro `scheduledTasks` (294 configs) | Nitro 3 already runs them; a Vite Doctor hint fits better |
290
+ | Nitro `useStorage` with Redis (270 configs) | every call site changes and there's no ioredis driver; only `nitro.storage.cache` → `vitehub.cache` is cheap |
291
+ :::
292
+ ::
293
+
294
+ ## How I checked
295
+
296
+ > [!CAUTION]
297
+ > **Not verified:** any source module on Nuxt 5; sessions surviving the `@nuxtjs/better-auth` move; how many nuxt-security users want only the limiter; the Docus count (one npm call); Comark covering every MDC feature, such as named slots and `{{ $doc }}`.
298
+
299
+ ::details{summary="Sources and method"}
300
+ - **The field:** all 334 modules in the `api.nuxt.com/modules` registry (2026-11-07), 375 npm search hits across 45 primitive terms, and about 120 named packages the registry misses, such as nuxt-auth-utils.
301
+ - **Downloads:** `api.npmjs.org/downloads/point/last-week`, 2026-10-30 to 2026-11-05.
302
+ - **Stars and last push:** the GitHub API.
303
+ - **Module APIs:** the npm tarballs. **ViteHub APIs:** `docs/content/docs/*` and the package READMEs on main (`c845af3ae`).
304
+ - **Code counts:** GitHub's legacy code search; rough and not limited to Nuxt.
305
+ ::
@@ -0,0 +1,74 @@
1
+ // Renders a comark Markdown report to HTML and screenshots it at reading width.
2
+ // Usage: node render.mjs report.md [outDir]
3
+ // Run it from a folder where comark, @comark/html, comark-ui and playwright are installed.
4
+ import { createRequire } from "node:module";
5
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { basename, join, resolve } from "node:path";
8
+ import { pathToFileURL } from "node:url";
9
+
10
+ const [input, outDir = "render"] = process.argv.slice(2);
11
+ if (!input) throw new Error("Usage: node render.mjs report.md [outDir]");
12
+ const require = createRequire(join(process.cwd(), "noop.js"));
13
+ const load = (name) => import(pathToFileURL(require.resolve(name)).href);
14
+
15
+ const { createHtmlRenderer } = await load("@comark/html");
16
+ const { default: comarkUi } = await load("comark-ui/html");
17
+ const playwright = await load("playwright");
18
+ const { chromium } = playwright.chromium ? playwright : playwright.default;
19
+
20
+ mkdirSync(outDir, { recursive: true });
21
+ const name = basename(input).replace(/\.md$/, "");
22
+ const source = readFileSync(input, "utf8");
23
+ const rendered = await createHtmlRenderer(comarkUi())(source);
24
+ // A host such as Drop shows the frontmatter title above the page; the renderer doesn't.
25
+ const title = source.match(/^---\n[\s\S]*?^title:\s*"?(.*?)"?\s*$/m)?.[1] ?? "";
26
+ const escape = (text) =>
27
+ text.replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
28
+ const htmlPath = resolve(outDir, `${name}.html`);
29
+ writeFileSync(
30
+ htmlPath,
31
+ `<!doctype html><meta charset="utf-8"><style>body{max-width:760px;margin:2rem auto;padding:0 1rem;font-family:system-ui}</style>${title ? `<h1>${escape(title)}</h1>` : ""}${typeof rendered === "string" ? rendered : rendered.html}`,
32
+ );
33
+
34
+ // Playwright's own browser may be missing; fall back to the newest one in its cache.
35
+ const cache = join(homedir(), ".cache/ms-playwright");
36
+ const fallback = existsSync(cache)
37
+ ? readdirSync(cache)
38
+ .filter((d) => /^chromium-\d+$/.test(d))
39
+ .sort((a, b) => Number(b.slice(9)) - Number(a.slice(9)))
40
+ .map((d) => join(cache, d, "chrome-linux64/chrome"))
41
+ .find((path) => existsSync(path))
42
+ : undefined;
43
+ let browser;
44
+ try {
45
+ browser = await chromium.launch();
46
+ } catch {
47
+ browser = await chromium.launch({ executablePath: fallback });
48
+ }
49
+
50
+ const page = await browser.newPage({ viewport: { width: 900, height: 1600 } });
51
+ await page.goto(pathToFileURL(htmlPath).href);
52
+ await page.waitForTimeout(1000);
53
+ const shots = [];
54
+ for (const state of ["closed", "open"]) {
55
+ if (state === "open")
56
+ await page.evaluate(() => document.querySelectorAll("details").forEach((d) => (d.open = true)));
57
+ // Folds animate open, so wait until the page stops growing before measuring it.
58
+ let height = 0;
59
+ for (let last = -1; height !== last; await page.waitForTimeout(400)) {
60
+ last = height;
61
+ height = await page.evaluate(() => document.documentElement.scrollHeight);
62
+ }
63
+ for (let y = 0, i = 0; y < height; y += 1600, i++) {
64
+ const path = resolve(outDir, `${name}-${state}-${i}.png`);
65
+ await page.screenshot({
66
+ path,
67
+ fullPage: true,
68
+ clip: { x: 0, y, width: 900, height: Math.min(1600, height - y) },
69
+ });
70
+ shots.push(path);
71
+ }
72
+ }
73
+ await browser.close();
74
+ console.log([htmlPath, ...shots].join("\n"));
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: review-document
3
- description: Turns a review's findings into one short Markdown document a reader can act on, with the verdict first and each finding ranked, located, backed by evidence, and paired with its fix. Use when writing up a code review, a design or QA review, an audit, or any report whose reader must decide what to do next.
3
+ description: Turns a review's findings into one short Markdown document a reader can act on, with the verdict first and each finding ranked, located, backed by evidence, and paired with its fix. Use when writing up a code review, a design or QA review, or an audit, where the reader must decide what to do about what you found.
4
4
  license: MIT
5
5
  ---
6
6
 
package/dist/skills.mjs CHANGED
@@ -46,7 +46,7 @@ const skills = [
46
46
  "files": [
47
47
  {
48
48
  "path": "SKILL.md",
49
- "text": "---\nname: comark-ui\ndescription: Writes PR evidence, explanations of how something works, and docs pages in Markdown with comark-ui. Use when a document needs an annotated screenshot, a before/after, a chart, a diagram, a code change, or another comark block, or when setting up comark-ui or a comark-* package.\n---\n\n# comark-ui\n\nYou write Markdown, and the comark plugins render it as static HTML. The reader sees only what you measured and wrote down, so measure every mark from the page, take every number from the data, and check the render before you hand it over.\n\n## Install\n\nInstall `comark-ui` by default. It holds every plugin and registers them in the order they need:\n\n```sh\npnpm add comark @comark/html comark-ui\n```\n\nRun the comark CLIs with the [global Vite+ CLI](https://viteplus.dev/guide/global-cli): `vp dlx` for a package the project doesn't install, `vp exec` for one it does. Without Vite+, `pnpm dlx` and `pnpm exec` take the same arguments.\n\nIf pnpm reports `ERR_PNPM_TRUST_DOWNGRADE`, read [pnpm trust errors](https://comark-ui.onmax.me/getting-started/#pnpm-trust-errors) for the publishing history and exact-version exceptions. Apply an exception only when the user's instructions authorize the policy change.\n\n```ts\nimport { createHtmlRenderer } from \"@comark/html\";\nimport comarkUi from \"comark-ui/html\";\n\nconst render = createHtmlRenderer(comarkUi());\n```\n\nEvery plugin is on except `include`. The [comark-ui reference](references/ui.md) covers its options, the plugins that read files or use the network, and its Vue, React, and Svelte entries.\n\nTo use only some plugins, install those packages and register each plugin with its components:\n\n```sh\npnpm add comark @comark/html comark-annotate\n```\n\n```ts\nimport { createHtmlRenderer } from \"@comark/html\";\nimport annotate, { components } from \"comark-annotate/html\";\n\nconst render = createHtmlRenderer({ plugins: [annotate()], components });\n```\n\nEach reference ends with a Register section that says how its package registers alone and which options it needs.\n\nEither way, the HTML renderer inlines the styles once per document, and the Vue, React, and Svelte components add them on mount. On a server-rendered page, import each package's `style.css`, such as `comark-cards/style.css`, so the page doesn't shift.\n\nThe `--comark-*` tokens theme every plugin at once; https://comark-ui.onmax.me/guide/theming/ lists them. In a Nuxt UI app the plugins follow its `--ui-*` variables with no setup. For a page rendered outside the app, such as review evidence, put the CSS from `nuxtUiTheme({ colors: appConfig.ui?.colors, css: mainCss })` (from `comark-ui/nuxt-ui`) in a `<style>`, so the page matches the app.\n\nTo move a project to Comark, start with `migrate-to-comark`, which finds its Markdown libraries and runs the migrator for each; see the [migrate-to-comark reference](references/migrate-to-comark.md). A project that renders Markdown with remark moves to Comark with `remark-to-comark`; the [remark-to-comark reference](references/remark-to-comark.md) walks through it. One that renders with markdown-it moves with `markdown-it-to-comark`; see the [markdown-it-to-comark reference](references/markdown-it-to-comark.md). A project on marked moves with `marked-to-comark`; see the [marked-to-comark reference](references/marked-to-comark.md).\n\nProblems land in `meta.issues` after `parseMarkdown`, one sentence each, and a host can refuse a document while any remain. For untrusted input, add Comark's `security` plugin with `blockedTags` such as `script`, `style`, and `iframe`; it cleans the Markdown inside blocks too.\n\n## Shape the document\n\nThis skill teaches the blocks. Four sibling skills teach a whole document, what goes in it and in what order: `before-after-evidence`, `review-document`, `visual-explanation`, and `teach-lesson`. When the user asks for one of those and it isn't installed, suggest `vp dlx skills add onmax/comark-ui` and picking it from the list.\n\n## Pick the blocks\n\nRead the reference for each block you use before you write it. Mix the blocks freely with ordinary Markdown, such as headings, tables, and lists.\n\n### Review blocks\n\n| To show | Write | Reference |\n| --- | --- | --- |\n| One state of a screen, with marks, numbered notes, or arrows | `::screenshot` | [annotate](references/annotate.md) |\n| A change on screen, before and after | `::compare` | [annotate](references/annotate.md) |\n| A command's output, such as a test run or a build, as colored text | `::cast` | [cast](references/cast.md) |\n| Numbers, such as sizes, timings, before/after metrics, a trend, or a timeline | `::chart` | [charts](references/charts.md) |\n| A flow, a sequence, an architecture, or a schema as a diagram | a `mermaid`, `d2`, `dot`, `plantuml`, `dbml`, `goat`, or `svgbob` fence | [diagrams](references/diagrams.md) |\n| Links and `@mentions` with the site's favicon or the person's avatar | links, `@user`, `:chip` | [chips](references/chips.md) |\n| Colors, such as design tokens before and after, or a palette | a color in inline code, `::palette` | [colors](references/colors.md) |\n| A suggested change to one file, before and after | `::diff` | [diff](references/diff.md) |\n| Suggested edits and comment threads on prose, such as a copy review, each note in a card in the margin | `:suggestion`, `:comment` | [comments](references/comments.md) |\n| A video, a design file, or a live reproduction from its link | `::embed` | [embed](references/embed.md) |\n| A screen recording or a sound that a screenshot can't show | `![alt](clip.mp4)` | [media](references/media.md) |\n| A number before and after, a table of befores and afters, or progress | `:delta`, `::stats`, `:progress` | [metrics](references/metrics.md) |\n| Data from a CSV, TSV, or JSON fence or file, as a table that copies and sorts | `::table`, a `csv` fence | [table](references/table.md) |\n| A test run's passed, failed, and skipped counts and its failures, from a JUnit report | `::tests` | [tests](references/tests.md) |\n| A note, tip, or warning box, or long output folded away, as on GitHub | `> [!NOTE]`, `<details>` | [github](references/github.md) |\n| The review's status, pull request, author, date, and tags in a box | frontmatter, `::properties` | [properties](references/properties.md) |\n| When something happened or is due, as \"2 days ago\" or a date, with the full date on hover | `:time[2026-11-02]` | [time](references/time.md) |\n| Claims backed by links, with numbered citations | `:cite`, `::sources` | [sources](references/sources.md) |\n| An icon in a sentence or a table cell, such as a logo or a status | `:icon` | [icon](references/icon.md) |\n| A preview link a reviewer can scan with a phone | `:qr` | [qr](references/qr.md) |\n| Why a line of code is there, as numbered notes under it | `// <1>` and a numbered list | [code-notes](references/code-notes.md) |\n\n### Page behavior\n\n| To show | Write | Reference |\n| --- | --- | --- |\n| A stable link to a section, or a link that opens a new tab | `{#id}`, `{target=\"_blank\"}` | [links](references/links.md) |\n| A table, code block, or diagram too small to read in place | `::zoom` | [zoom](references/zoom.md) |\n| A sequence to page through in one place, such as the screens of a flow | `::carousel` | [carousel](references/carousel.md) |\n| Versions of one thing where each reader needs only one, such as an OS or a package manager | `::tabs`, `::code-group`, `pm` | [tabs](references/tabs.md) |\n| Code with lines marked added, removed, or highlighted, or a pasted `git diff` | `ins={4}` after a fence's language, `diff lang=\"ts\"` | [code](references/code.md) |\n\n### Docs blocks\n\n| To show | Write | Reference |\n| --- | --- | --- |\n| Content a reader may skip, such as a long log, behind a summary | `::details`, `::accordion`, `::collapsible` | [details](references/details.md) |\n| Folders and files, with what a change added, removed, or changed | `::file-tree` | [file-tree](references/file-tree.md) |\n| A component's props or a function's options, with types and defaults | `::field` | [fields](references/fields.md) |\n| A version, status, or deprecation tag in a heading or a sentence | `:badge` | [badge](references/badge.md) |\n| Links to the next pages as cards, or blocks side by side | `::card-group`, `::columns` | [cards](references/cards.md) |\n| Instructions the reader follows in order, such as repro or setup steps | `::steps` | [steps](references/steps.md) |\n| Code that changes over several steps, such as a refactor told one move at a time | `::magic-move` | [magic-move](references/magic-move.md) |\n| An interactive lesson question, poll, or recall card | `::question`, `::poll`, `::recall` | [quiz](references/quiz.md) |\n\n### Markdown syntax\n\n| To show | Write | Reference |\n| --- | --- | --- |\n| Bold or italics in Chinese, Japanese, or Korean text | `**…**`, `*…*` | [cjk](references/cjk.md) |\n| An image with a caption under it | `![alt](src \"Caption\")` | [figure](references/figure.md) |\n| A highlighted phrase or a superscript | `==text==`, `^2^` | [mark](references/mark.md) |\n| A suggested edit or a comment inside the text | `{++ ++}`, `{-- --}`, `{~~ ~> ~~}`, `{== ==}`, `{>> <<}` | [critic](references/critic.md) |\n| Terms and their definitions, such as a glossary | `Term`, then `: definition` | [deflist](references/deflist.md) |\n| A numbered note at the end of the page, such as a source or a caveat | `[^label]`, `^[note]` | [footnotes](references/footnotes.md) |\n| A formula, such as a complexity bound or how a number was computed | `$…$`, `$$…$$` | [math](references/math.md) |\n| A keyboard shortcut | `++ctrl+k++` | [keys](references/keys.md) |\n\n### Authoring tools\n\n| To show | Write | Reference |\n| --- | --- | --- |\n| A shared section, or real source lines, pasted from a file | `:include{src}` | [include](references/include.md) |\n| A table of contents of the page's headings | `::toc` | [outline](references/outline.md) |\n| Figures and tables numbered and referred to by number | `{#fig-name}`, `:ref{#fig-name}` | [xref](references/xref.md) |\n| Word count and reading time, for the page's layout | nothing; it writes `meta.readingTime` | [reading-time](references/reading-time.md) |\n\n### Chat\n\n| To show | Use | Reference |\n| --- | --- | --- |\n| An AI SDK chat message, with its reasoning, tool calls, sources, and files | `MessageParts` from `comark-chat` | [chat](references/chat.md) |\n\n## Marks\n\nScreenshots and charts draw the same marks. Each `::annotation` is one mark and its note, written in Markdown.\n\n- `mark` is `box` (default), `circle`, `underline`, `line`, `highlight`, `bracket`, `cross`, `check`, or `none`, which draws nothing, so its note needs a `side` and the arrow alone points.\n- `color` is `violet` (default) to point something out, `red` for a problem, `amber` for a warning, `green` for a fix, `blue` for a second voice, `gray` for neutral, or a 3- or 6-digit hex color.\n- `side` is `top`, `bottom`, `left`, or `right`, and puts the note in that margin with an arrow to its mark. Notes without one are numbered in a list under the figure.\n\nMark only what your text talks about. Write each note in the words the screen or table shows, saying what changed or what is wrong, so the reader can check it against the figure. Leave out a value the figure doesn't show.\n\n## Check the result\n\n1. Lint the file with `vp exec comark-lint report.md`, in a project with `comark-linter` and the plugins installed (`comark-ui` counts as every plugin). It prints one line per problem with its line, its rule, and the fix, and exits with 1 while any remain. Fix every problem and run it again until it exits with 0. The [lint reference](references/lint.md) covers the flags, the config file, the library, ESLint, and Vite Doctor.\n2. Render the document as the block's reference says, open it in a browser, and take a screenshot. Each mark must sit on its target, each number must match its note, and each chart must say what your text says. Name anything you couldn't verify when you hand the document over.\n"
49
+ "text": "---\nname: comark-ui\ndescription: Writes PR evidence, explanations of how something works, and docs pages in Markdown with comark-ui. Use when a document needs an annotated screenshot, a before/after, a chart, a diagram, a code change, or another comark block, or when setting up comark-ui or a comark-* package.\n---\n\n# comark-ui\n\nYou write Markdown, and the comark plugins render it as static HTML. The reader sees only what you measured and wrote down, so measure every mark from the page, take every number from the data, and check the render before you hand it over.\n\n## Install\n\nInstall `comark-ui` by default. It holds every plugin and registers them in the order they need:\n\n```sh\npnpm add comark @comark/html comark-ui\n```\n\nRun the comark CLIs with the [global Vite+ CLI](https://viteplus.dev/guide/global-cli): `vp dlx` for a package the project doesn't install, `vp exec` for one it does. Without Vite+, `pnpm dlx` and `pnpm exec` take the same arguments.\n\nIf pnpm reports `ERR_PNPM_TRUST_DOWNGRADE`, read [pnpm trust errors](https://comark-ui.onmax.me/getting-started/#pnpm-trust-errors) for the publishing history and exact-version exceptions. Apply an exception only when the user's instructions authorize the policy change.\n\n```ts\nimport { createHtmlRenderer } from \"@comark/html\";\nimport comarkUi from \"comark-ui/html\";\n\nconst render = createHtmlRenderer(comarkUi());\n```\n\nEvery plugin is on except `include`. The [comark-ui reference](references/ui.md) covers its options, the plugins that read files or use the network, and its Vue, React, and Svelte entries.\n\nTo use only some plugins, install those packages and register each plugin with its components:\n\n```sh\npnpm add comark @comark/html comark-annotate\n```\n\n```ts\nimport { createHtmlRenderer } from \"@comark/html\";\nimport annotate, { components } from \"comark-annotate/html\";\n\nconst render = createHtmlRenderer({ plugins: [annotate()], components });\n```\n\nEach reference ends with a Register section that says how its package registers alone and which options it needs.\n\nEither way, the HTML renderer inlines the styles once per document, and the Vue, React, and Svelte components add them on mount. On a server-rendered page, import each package's `style.css`, such as `comark-cards/style.css`, so the page doesn't shift.\n\nThe `--comark-*` tokens theme every plugin at once; https://comark-ui.onmax.me/guide/theming/ lists them. In a Nuxt UI app the plugins follow its `--ui-*` variables with no setup. For a page rendered outside the app, such as review evidence, put the CSS from `nuxtUiTheme({ colors: appConfig.ui?.colors, css: mainCss })` (from `comark-ui/nuxt-ui`) in a `<style>`, so the page matches the app.\n\nTo move a project to Comark, start with `migrate-to-comark`, which finds its Markdown libraries and runs the migrator for each; see the [migrate-to-comark reference](references/migrate-to-comark.md). A project that renders Markdown with remark moves to Comark with `remark-to-comark`; the [remark-to-comark reference](references/remark-to-comark.md) walks through it. One that renders with markdown-it moves with `markdown-it-to-comark`; see the [markdown-it-to-comark reference](references/markdown-it-to-comark.md). A project on marked moves with `marked-to-comark`; see the [marked-to-comark reference](references/marked-to-comark.md).\n\nProblems land in `meta.issues` after `parseMarkdown`, one sentence each, and a host can refuse a document while any remain. For untrusted input, add Comark's `security` plugin with `blockedTags` such as `script`, `style`, and `iframe`; it cleans the Markdown inside blocks too.\n\n## Shape the document\n\nThis skill teaches the blocks. Five sibling skills teach a whole document, what goes in it and in what order: `before-after-evidence`, `review-document`, `report`, `visual-explanation`, and `teach-lesson`. When the user asks for one of those and it isn't installed, suggest `vp dlx skills add onmax/comark-ui` and picking it from the list.\n\n## Pick the blocks\n\nRead the reference for each block you use before you write it. Mix the blocks freely with ordinary Markdown, such as headings, tables, and lists.\n\n### Review blocks\n\n| To show | Write | Reference |\n| --- | --- | --- |\n| One state of a screen, with marks, numbered notes, or arrows | `::screenshot` | [annotate](references/annotate.md) |\n| A change on screen, before and after | `::compare` | [annotate](references/annotate.md) |\n| A command's output, such as a test run or a build, as colored text | `::cast` | [cast](references/cast.md) |\n| Numbers, such as sizes, timings, before/after metrics, a trend, or a timeline | `::chart` | [charts](references/charts.md) |\n| A flow, a sequence, an architecture, or a schema as a diagram | a `mermaid`, `d2`, `dot`, `plantuml`, `dbml`, `goat`, or `svgbob` fence | [diagrams](references/diagrams.md) |\n| Links and `@mentions` with the site's favicon or the person's avatar | links, `@user`, `:chip` | [chips](references/chips.md) |\n| Colors, such as design tokens before and after, or a palette | a color in inline code, `::palette` | [colors](references/colors.md) |\n| A suggested change to one file, before and after | `::diff` | [diff](references/diff.md) |\n| Suggested edits and comment threads on prose, such as a copy review, each note in a card in the margin | `:suggestion`, `:comment` | [comments](references/comments.md) |\n| A video, a design file, or a live reproduction from its link | `::embed` | [embed](references/embed.md) |\n| A screen recording or a sound that a screenshot can't show | `![alt](clip.mp4)` | [media](references/media.md) |\n| A number before and after, a table of befores and afters, or progress | `:delta`, `::stats`, `:progress` | [metrics](references/metrics.md) |\n| Data from a CSV, TSV, or JSON fence or file, as a table that copies and sorts | `::table`, a `csv` fence | [table](references/table.md) |\n| A test run's passed, failed, and skipped counts and its failures, from a JUnit report | `::tests` | [tests](references/tests.md) |\n| A note, tip, or warning box, or long output folded away, as on GitHub | `> [!NOTE]`, `<details>` | [github](references/github.md) |\n| The review's status, pull request, author, date, and tags in a box | frontmatter, `::properties` | [properties](references/properties.md) |\n| When something happened or is due, as \"2 days ago\" or a date, with the full date on hover | `:time[2026-11-02]` | [time](references/time.md) |\n| Claims backed by links, with numbered citations | `:cite`, `::sources` | [sources](references/sources.md) |\n| An icon in a sentence or a table cell, such as a logo or a status | `:icon` | [icon](references/icon.md) |\n| A preview link a reviewer can scan with a phone | `:qr` | [qr](references/qr.md) |\n| Why a line of code is there, as numbered notes under it | `// <1>` and a numbered list | [code-notes](references/code-notes.md) |\n\n### Page behavior\n\n| To show | Write | Reference |\n| --- | --- | --- |\n| A stable link to a section, or a link that opens a new tab | `{#id}`, `{target=\"_blank\"}` | [links](references/links.md) |\n| A table, code block, or diagram too small to read in place | `::zoom` | [zoom](references/zoom.md) |\n| A sequence to page through in one place, such as the screens of a flow | `::carousel` | [carousel](references/carousel.md) |\n| Versions of one thing where each reader needs only one, such as an OS or a package manager | `::tabs`, `::code-group`, `pm` | [tabs](references/tabs.md) |\n| Code with lines marked added, removed, or highlighted, or a pasted `git diff` | `ins={4}` after a fence's language, `diff lang=\"ts\"` | [code](references/code.md) |\n\n### Docs blocks\n\n| To show | Write | Reference |\n| --- | --- | --- |\n| Content a reader may skip, such as a long log, behind a summary | `::details`, `::accordion`, `::collapsible` | [details](references/details.md) |\n| Folders and files, with what a change added, removed, or changed | `::file-tree` | [file-tree](references/file-tree.md) |\n| A component's props or a function's options, with types and defaults | `::field` | [fields](references/fields.md) |\n| A version, status, or deprecation tag in a heading or a sentence | `:badge` | [badge](references/badge.md) |\n| Links to the next pages as cards, or blocks side by side | `::card-group`, `::columns` | [cards](references/cards.md) |\n| Instructions the reader follows in order, such as repro or setup steps | `::steps` | [steps](references/steps.md) |\n| Code that changes over several steps, such as a refactor told one move at a time | `::magic-move` | [magic-move](references/magic-move.md) |\n| An interactive lesson question, poll, or recall card | `::question`, `::poll`, `::recall` | [quiz](references/quiz.md) |\n\n### Markdown syntax\n\n| To show | Write | Reference |\n| --- | --- | --- |\n| Bold or italics in Chinese, Japanese, or Korean text | `**…**`, `*…*` | [cjk](references/cjk.md) |\n| An image with a caption under it | `![alt](src \"Caption\")` | [figure](references/figure.md) |\n| A highlighted phrase or a superscript | `==text==`, `^2^` | [mark](references/mark.md) |\n| A suggested edit or a comment inside the text | `{++ ++}`, `{-- --}`, `{~~ ~> ~~}`, `{== ==}`, `{>> <<}` | [critic](references/critic.md) |\n| Terms and their definitions, such as a glossary | `Term`, then `: definition` | [deflist](references/deflist.md) |\n| A numbered note at the end of the page, such as a source or a caveat | `[^label]`, `^[note]` | [footnotes](references/footnotes.md) |\n| A formula, such as a complexity bound or how a number was computed | `$…$`, `$$…$$` | [math](references/math.md) |\n| A keyboard shortcut | `++ctrl+k++` | [keys](references/keys.md) |\n\n### Authoring tools\n\n| To show | Write | Reference |\n| --- | --- | --- |\n| A shared section, or real source lines, pasted from a file | `:include{src}` | [include](references/include.md) |\n| A table of contents of the page's headings | `::toc` | [outline](references/outline.md) |\n| Figures and tables numbered and referred to by number | `{#fig-name}`, `:ref{#fig-name}` | [xref](references/xref.md) |\n| Word count and reading time, for the page's layout | nothing; it writes `meta.readingTime` | [reading-time](references/reading-time.md) |\n\n### Chat\n\n| To show | Use | Reference |\n| --- | --- | --- |\n| An AI SDK chat message, with its reasoning, tool calls, sources, and files | `MessageParts` from `comark-chat` | [chat](references/chat.md) |\n\n## Marks\n\nScreenshots and charts draw the same marks. Each `::annotation` is one mark and its note, written in Markdown.\n\n- `mark` is `box` (default), `circle`, `underline`, `line`, `highlight`, `bracket`, `cross`, `check`, or `none`, which draws nothing, so its note needs a `side` and the arrow alone points.\n- `color` is `violet` (default) to point something out, `red` for a problem, `amber` for a warning, `green` for a fix, `blue` for a second voice, `gray` for neutral, or a 3- or 6-digit hex color.\n- `side` is `top`, `bottom`, `left`, or `right`, and puts the note in that margin with an arrow to its mark. Notes without one are numbered in a list under the figure.\n\nMark only what your text talks about. Write each note in the words the screen or table shows, saying what changed or what is wrong, so the reader can check it against the figure. Leave out a value the figure doesn't show.\n\n## Check the result\n\n1. Lint the file with `vp exec comark-lint report.md`, in a project with `comark-linter` and the plugins installed (`comark-ui` counts as every plugin). It prints one line per problem with its line, its rule, and the fix, and exits with 1 while any remain. Fix every problem and run it again until it exits with 0. The [lint reference](references/lint.md) covers the flags, the config file, the library, ESLint, and Vite Doctor.\n2. Render the document as the block's reference says, open it in a browser, and take a screenshot. Each mark must sit on its target, each number must match its note, and each chart must say what your text says. Name anything you couldn't verify when you hand the document over.\n"
50
50
  },
51
51
  {
52
52
  "path": "agents/openai.yaml",
@@ -262,18 +262,49 @@ const skills = [
262
262
  }
263
263
  ]
264
264
  },
265
+ {
266
+ "name": "report",
267
+ "description": "Writes research, a ranked shortlist, a comparison, or a plan as one Markdown page someone decides from, with the answer and decisions first, the numbers in charts, and the detail folded. Use when the reader has to choose between options or approve a plan.",
268
+ "frontmatter": {
269
+ "name": "report",
270
+ "description": "Writes research, a ranked shortlist, a comparison, or a plan as one Markdown page someone decides from, with the answer and decisions first, the numbers in charts, and the detail folded. Use when the reader has to choose between options or approve a plan.",
271
+ "license": "MIT"
272
+ },
273
+ "files": [
274
+ {
275
+ "path": "SKILL.md",
276
+ "text": "---\nname: report\ndescription: Writes research, a ranked shortlist, a comparison, or a plan as one Markdown page someone decides from, with the answer and decisions first, the numbers in charts, and the detail folded. Use when the reader has to choose between options or approve a plan.\nlicense: MIT\n---\n\n# Report\n\nThe reader scans a report before reading it. They want the answer, the decisions they have to make, and the numbers behind both, and they check the rest only when they doubt something. So the page has a fixed shape: the answer on top, one picture per section, short table cells, and detail folded underneath.\n\nFor findings about something that already exists, such as a code review or an audit, use `review-document`. For how something works, use `visual-explanation`. This skill shapes a report you already have the material for; gathering the material is the research itself.\n\n## 1. Write the answer and the decisions\n\nWrite the answer in two sentences: what you recommend and the main reason. For a plan, that's the first step and the goal it serves. Under it, write each decision the reader has to make as a question they can answer yes or no. Up to three go in the callout as a numbered list. Past three, ask one question (\"Take my recommendation on all seven?\") and give the decisions their own table right after the answer.\n\nDone when someone who reads only the answer and the callout knows what you recommend and what they have to decide.\n\n## 2. Give every fact one home\n\nList what the report holds: the items, their numbers, any order, the blockers, the findings, what you left out, how you checked. Give each one exactly one section. When the items have both a rank and an order to do them in, keep the order the reader acts on and put the rank's inputs, if it has any, in the picture. Another section refers to a fact by its name or number (\"change 3\", \"D2\") and leaves its value where it lives. Reuse the source's numbering; when you have to add IDs, give each kind a prefix no other kind starts with. A chart's table is already under it in a Data fold, so it gets no second copy.\n\nDone when no value, list, or order is written out in two sections.\n\n## 3. Pick one picture per section\n\nA section opens with one sentence that says what to look at, then its picture. A second picture fits only when it answers the question the first one raises, such as the table of steps under the flowchart of their order. Numbers live in charts and table columns, and a sentence names only the number that matters.\n\n| The section answers | Picture |\n| --- | --- |\n| Which items are worth it, by two scores and a size | Bubble chart: `::chart` with a `vega-lite` spec |\n| How big each item is, or what each blocker would unlock | `::chart{type=\"bar\" horizontal labels}` |\n| How each item's size splits into parts | `::chart{type=\"stacked\" horizontal}` |\n| In what order, and what has to land first | `mermaid` flowchart, blockers as `{{hexagons}}` |\n| How a mechanism moves between states | `mermaid` `stateDiagram-v2` |\n| A number before and after | `::stats` or `:delta` |\n| A share of a whole | `:progress` |\n| Findings, options, or decisions compared across the same columns | Table of at most five columns, `:badge` for categories and verdicts |\n| Detail per item | `::accordion`, one item per row, the same parts inside every item |\n| What you left out | Bar chart of the largest, then `::accordion` grouped by reason |\n| How you checked | `> [!CAUTION]` for what you didn't verify, kept open; sources in `::details` |\n\nA table cell holds a value, a badge, or a few words. The reasoning behind a cell goes in that item's accordion. A column whose cells are all the same says one thing, so say it in the section's sentence and drop the column. Two charts that compare the same unit share one scale, so draw them as one chart, or fix the axis domain in both.\n\n[references/blocks.md](references/blocks.md) has the syntax for each, with the gotchas the renderer doesn't report. The `comark-ui` skill documents every block in full.\n\nDone when every section opens with its sentence and its picture, every number the answer rests on sits in a chart or a table column, and no table cell runs past a line or two at reading width.\n\n## 4. Assemble the page\n\nThe two ends are fixed, and the middle holds the sections your material has, in the order the reader acts on them:\n\n```text\nFrontmatter title = the answer in under 70 characters; status, date, tags\nThe answer two sentences, the decisions in > [!IMPORTANT], at most one > [!WARNING]\nDecisions the decision table, only when there are more than three\n middle from step 3, such as: the picture the answer rests on, findings,\n the order, blockers, before and after, each item, left out\nHow I checked > [!CAUTION] open, sources folded\n```\n\nKeep open what the reader decides on, and fold what they would only check: each decision and your recommendation stay open in the callout or the decision table, and the arguments behind them fold. When each item fits one table row, the table is the item list and needs no accordion. Past about a dozen items, the table is the list, and only the items that need reasoning get an accordion item. Lists stay one level deep; a second level becomes a table. Cards are for links to the next pages only.\n\n[references/example.md](references/example.md) is a full report in this shape: the answer and decision on top, a bubble chart, one order shown as a flowchart and a table, blockers sized by what they unlock, one accordion item per module with the same parts, the left-out chart with its groups, and the open \"Not verified\" callout.\n\nDone when the page follows this order and nothing the reader decides on is folded.\n\n## 5. Check the render\n\n1. Lint it, and fix until it reports no problems:\n\n ```sh\n vp dlx --package comark-linter --package comark-ui comark-lint report.md\n ```\n\n These commands use the [global Vite+ CLI](https://viteplus.dev/guide/global-cli). Without it, `pnpm dlx` takes the same arguments.\n\n2. Render it as [references/blocks.md](references/blocks.md#render-it-locally) shows, and read every screenshot, closed and open. Labels must not overlap, each chart must say what its sentence says, table cells must stay short, and diagram text must read at the size of the body text. A wide flowchart renders small: switch it to `TB`, shorten its labels, or split it past about ten nodes.\n3. Hand it over where the task says. When it goes to a host such as Drop, publish it as the host's skill describes and give the link.\n\nDone when lint reports no problems and you've read every screenshot of the render.\n"
277
+ },
278
+ {
279
+ "path": "agents/openai.yaml",
280
+ "text": "interface:\n display_name: \"Report\"\n short_description: \"Answer and decisions first, numbers in charts, detail folded\"\n"
281
+ },
282
+ {
283
+ "path": "references/blocks.md",
284
+ "text": "# Blocks for a report\n\nEach block below renders with the full `comark-ui` preset. The `comark-ui` skill documents every block in full; read its reference for a block before you change more than the values here.\n\n## The answer: callouts\n\n```md\n> [!IMPORTANT]\n> **Your calls:**\n>\n> 1. Hold `@nuxtjs/i18n` until #3564 has URL routing?\n> 2. Ship the Env and Resend codemods before Auth's SSR gap closes?\n\n> [!WARNING]\n> Every row needs Nuxt 5, and the codemods don't upgrade Nuxt.\n```\n\nFrontmatter `status`, `date`, and `tags` render as a properties box at the top with no extra markup.\n\n## Bubble chart: two scores and a size\n\n````md\n::chart{title=\"Shortlist: value and difficulty (1 to 5), bubble size = npm downloads a week\"}\n| module | difficulty | value | downloads |\n| --- | --: | --: | --: |\n| nuxt-auth-utils | 4 | 5 | 177926 |\n| Resend | 2 | 4 | 1251 |\n| t3-env | 2 | 3 | 6072 |\n| nuxt-cron | 2 | 3 | 3123 |\n\n```vega-lite\n{\n \"width\": 620,\n \"height\": 320,\n \"encoding\": {\n \"x\": { \"field\": \"difficulty\", \"type\": \"quantitative\", \"scale\": { \"domain\": [1.5, 5] }, \"title\": \"Difficulty (5 = hardest)\" },\n \"y\": { \"field\": \"value\", \"type\": \"quantitative\", \"scale\": { \"domain\": [2.5, 5.5] }, \"title\": \"Value (5 = most)\" }\n },\n \"layer\": [\n {\n \"mark\": { \"type\": \"circle\", \"opacity\": 0.55 },\n \"encoding\": { \"size\": { \"field\": \"downloads\", \"type\": \"quantitative\", \"scale\": { \"range\": [80, 3000] }, \"legend\": null } }\n },\n {\n \"transform\": [{ \"filter\": \"indexof(['Resend', 't3-env', 'nuxt-cron'], datum.module) < 0\" }],\n \"mark\": { \"type\": \"text\", \"dy\": -26, \"fontSize\": 12 },\n \"encoding\": { \"text\": { \"field\": \"module\" } }\n },\n {\n \"transform\": [{ \"filter\": \"indexof(['Resend', 't3-env'], datum.module) >= 0\" }],\n \"mark\": { \"type\": \"text\", \"align\": \"right\", \"dx\": -12, \"fontSize\": 12 },\n \"encoding\": { \"text\": { \"field\": \"module\" } }\n },\n {\n \"transform\": [{ \"filter\": \"datum.module === 'nuxt-cron'\" }],\n \"mark\": { \"type\": \"text\", \"dy\": 16, \"fontSize\": 12 },\n \"encoding\": { \"text\": { \"field\": \"module\" } }\n }\n ]\n}\n```\n::\n````\n\n- With a `vega-lite` spec, `::chart` takes only `title`; `type`, `x`, and `y` are lint errors. Name the columns in `encoding`.\n- Set `width` in pixels. `\"container\"` renders about 230px wide.\n- Labels overlap when two points share a spot or sit close. Give each label layer a `filter` and move those points' labels left (`align: right`, `dx: -12`) or below (`dy: 16`). Look at the render; the lint can't see overlap.\n- Write a range such as 4–5 as its middle, 4.5, and say so in the item's detail.\n\n## Bar chart: sizes, with one mark\n\n```md\n::chart{type=\"bar\" horizontal labels title=\"Left out: npm downloads a week (10k and up)\"}\n| module | downloads |\n| --- | --: |\n| @nuxtjs/i18n | 977490 |\n| nuxt-csurf | 223523 |\n:::annotation{at=\"@nuxtjs/i18n\" mark=\"box\" color=\"amber\" side=\"right\"}\nYour call: hold until #3564 has URL routing.\n:::\n::\n```\n\nRows draw in table order, so sort them largest first. Mark the bar a decision is about, at most one; a chart no decision points at gets no mark.\n\nFor sizes that split into parts, use `type=\"stacked\"`, one column per part:\n\n```md\n::chart{type=\"stacked\" horizontal title=\"Description characters per skill\"}\n| skill | sentence | trigger list |\n| --- | --: | --: |\n| emil-mobile-native | 760 | 551 |\n| emil-prep-for-prod | 865 | 386 |\n::\n```\n\nEach chart scales its own axis. When two charts compare the same unit, put the rows in one chart, or write both in `vega-lite` with the same `\"scale\": { \"domain\": [0, 1400] }` on the value axis, so equal values draw equal bars.\n\n## Order and blockers: a flowchart\n\n````md\n```mermaid\nflowchart LR\n shell[\"1 · Wizard shell + Env\"]\n shell --> resend[\"2 · Resend\"]\n shell --> auth[\"3 · Auth\"]\n ssr{{\"SSR session + page middleware\"}} --> auth\n upstash{{\"Upstash driver\"}} -.-> rl[\"5 · Rate limit\"]\n```\n````\n\nUse `LR` for up to about five steps side by side and `TB` for a longer chain or long labels; the diagram scales down to fit the page width, so a wide one renders small. Boxes are steps, numbered in the order the reader acts. Hexagons are work that has to land first; a dashed arrow means it should land first but doesn't have to. Say both in the sentence above the diagram.\n\n## The decision table: past three decisions\n\n```md\n| # | Decision | Opus | Sol | Recommend |\n| --- | --- | --- | --- | --- |\n| D1 | Import family for apps | `vite-hub/*` | `@vite-hub/*` | :badge[vite-hub/*]{color=\"success\"} |\n| D2 | Sandbox Task option key | `sandboxTask` | `sandbox.tasks` | :badge[sandboxTask]{color=\"success\"} |\n```\n\nEach cell is the position, not the argument. The arguments go in the decision's accordion item, labeled with its number.\n\n## The table: at most five columns\n\n```md\n| Step | Module → primitive | npm/week | Fit |\n| --- | --- | --: | --- |\n| 1 | `@t3-oss/env-nuxt` → Env | 6,072 | :badge[High]{color=\"success\"} |\n| 3 | `nuxt-auth-utils` → Auth | 177,926 | :badge[Partial]{color=\"warning\"} |\n```\n\nBadge colors: `success` good, `warning` partial or at risk, `error` blocked, `neutral` unknown. Scores that are already in a chart stay out of the table. A cell holds a value, a badge, or a few words; a column of sentences turns the table into a wall of text, so move it into the accordion.\n\nWrite file names and domains in backticks, such as `AGENTS.md` and `devtools.fm`. Bare, they render as link chips with an icon.\n\n## Each item: an accordion\n\n````md\n::accordion\n:::accordion-item{label=\"2 · Resend → Email\"}\n| Resend | ViteHub Email |\n| --- | --- |\n| `new Resend(key).emails.send(msg)` | `email.send(msg)` |\n\n- **Manual:** batch, contacts, audiences, broadcasts.\n- **First:** nothing.\n- **Gain:** a dev outbox and Console preview.\n:::\n::\n````\n\nEvery item has the same parts in the same order, with bold labels, a line or two each. A part that needs more becomes a table inside the item. The label starts with the item's step number from the table. Icons come from `lucide:`; pick one per kind of item.\n\n## Before and after: stats and delta\n\n```md\n::stats{better=\"lower\"}\n| Measure | Before | After |\n| --- | --: | --: |\n| Description characters loaded | 23,515 | 10,981 |\n| Model-invoked skills | 49 | 41 |\n::\n\nTrimming the lists saves :delta{from=23515 to=10981 unit=\"chars\" better=\"lower\"} every session.\n```\n\n`::stats` holds one table of exactly three columns: a name, before, after. Each cell is a number with an optional unit, the same unit in both cells, and the block adds the percent itself. A value with a qualifier, such as \"about 65\" or \"6 of 11\", belongs in a plain table, or keeps its number in the cell and moves the qualifier into the row's name. `better` applies to every row, so rows that are better higher go in a second block.\n\n## A share: progress\n\n```md\n:progress{value=293 max=785 label=\"nuxt-security configs that mention rateLimiter\"}\n```\n\n## How I checked\n\n````md\n> [!CAUTION]\n> **Not verified:** any source module on Nuxt 5; the Docus count (one npm call).\n\n::details{summary=\"Sources and method\"}\n- **Downloads:** `api.npmjs.org/downloads/point/last-week`, 2026-10-30 to 2026-11-05.\n::\n````\n\n## Render it locally\n\nA host renders the report only after you publish it, and a private page may need the user's sign-in, so check the render before you hand it over. Copy this skill's [scripts/render.mjs](../scripts/render.mjs) next to the report, then:\n\n```sh\nmkdir -p tmp/report-render && cd tmp/report-render\necho '{ \"private\": true, \"type\": \"module\" }' > package.json\npnpm add --ignore-workspace comark @comark/html comark-ui playwright\nnode ../../render.mjs ../../report.md out\n```\n\nWithout its own `package.json`, pnpm walks up to the repository's and can fail on its catalog. The script writes `out/report.html` and screenshots at 900px wide, 1600px tall each: `report-closed-*.png` as the reader first sees it, and `report-open-*.png` with every fold open. When Playwright has no browser downloaded, it uses the newest Chromium in `~/.cache/ms-playwright`.\n"
285
+ },
286
+ {
287
+ "path": "references/example.md",
288
+ "text": "---\ntitle: \"Migrate wizard: start with Env and Resend, then Auth and Content\"\nstatus: Proposal\ndate: 2026-11-07\ntags: [migrate wizard, codemods]\n---\n\n## The answer\n\nBuild the wizard shell with the Env codemod, then add Resend, Auth and Content. Auth and Content hold most of the users, but each needs ViteHub work first. Env and Resend are cheap and prove the wizard end to end.\n\n> [!IMPORTANT]\n> **Your call:** hold `@nuxtjs/i18n`, the largest audience at 977k a week, until #3564 merges and i18n has URL routing?\n\n> [!WARNING]\n> Every row needs Nuxt 5. Most source apps run Nuxt 4 and the codemods don't upgrade Nuxt, so the wizard checks the version first.\n\n## Value against cost\n\nUp and to the right is worth more and costs more. Bubble size is weekly npm downloads. Auth and Content are the two big bubbles. The cheap wins sit at difficulty 2.\n\n::chart{title=\"Shortlist: value and difficulty (1 to 5), bubble size = npm downloads a week\"}\n| module | difficulty | value | downloads |\n| --- | --: | --: | --: |\n| nuxt-auth-utils | 4 | 5 | 177926 |\n| @nuxt/content | 4 | 4 | 240394 |\n| Docus | 3 | 4 | 15800 |\n| Resend | 2 | 4 | 1251 |\n| nuxt-security limiter | 3 | 3 | 220740 |\n| t3-env | 2 | 3 | 6072 |\n| sidebase | 4.5 | 3 | 46448 |\n| nuxt-cron | 2 | 3 | 3123 |\n\n```vega-lite\n{\n \"width\": 620,\n \"height\": 320,\n \"encoding\": {\n \"x\": { \"field\": \"difficulty\", \"type\": \"quantitative\", \"scale\": { \"domain\": [1.5, 5] }, \"title\": \"Difficulty (5 = hardest)\" },\n \"y\": { \"field\": \"value\", \"type\": \"quantitative\", \"scale\": { \"domain\": [2.5, 5.5] }, \"title\": \"Value (5 = most)\" }\n },\n \"layer\": [\n {\n \"mark\": { \"type\": \"circle\", \"opacity\": 0.55 },\n \"encoding\": { \"size\": { \"field\": \"downloads\", \"type\": \"quantitative\", \"scale\": { \"range\": [80, 3000] }, \"legend\": null } }\n },\n {\n \"transform\": [{ \"filter\": \"indexof(['t3-env', 'Docus', 'Resend'], datum.module) < 0 && datum.module !== 'nuxt-cron'\" }],\n \"mark\": { \"type\": \"text\", \"dy\": -26, \"fontSize\": 12 },\n \"encoding\": { \"text\": { \"field\": \"module\" } }\n },\n {\n \"transform\": [{ \"filter\": \"indexof(['t3-env', 'Docus', 'Resend'], datum.module) >= 0\" }],\n \"mark\": { \"type\": \"text\", \"align\": \"right\", \"dx\": -12, \"fontSize\": 12 },\n \"encoding\": { \"text\": { \"field\": \"module\" } }\n },\n {\n \"transform\": [{ \"filter\": \"datum.module === 'nuxt-cron'\" }],\n \"mark\": { \"type\": \"text\", \"dy\": 16, \"fontSize\": 12 },\n \"encoding\": { \"text\": { \"field\": \"module\" } }\n }\n ]\n}\n```\n::\n\n## The order\n\nBoxes are codemods, numbered in build order. Hexagons are ViteHub work that has to land first; a dashed arrow means it should land first but doesn't have to.\n\n```mermaid\nflowchart LR\n shell[\"1 · Wizard shell + Env\"]\n shell --> resend[\"2 · Resend\"]\n shell --> auth[\"3 · Auth\"]\n shell --> content[\"4 · Content\"] --> docus[\"Docus, then nuxt-studio\"]\n shell --> rl[\"5 · Rate limit\"]\n shell --> cron[\"6 · nuxt-cron\"]\n shell --> i18n[\"7 · i18n\"]\n ssr{{\"SSR session + page middleware\"}} --> auth\n upstash{{\"Upstash driver\"}} -.-> rl\n pr{{\"#3564 + URL routing\"}} --> i18n\n```\n\n| Step | Module → primitive | npm/week | Fit |\n| --- | --- | --: | --- |\n| 1 | `@t3-oss/env-nuxt` → Env | 6,072 | :badge[High]{color=\"success\"} |\n| 2 | `resend`, `nuxt-resend` → Email | 1,251 | :badge[High]{color=\"success\"} |\n| 3 | `nuxt-auth-utils` → Auth | 177,926 | :badge[Partial]{color=\"warning\"} |\n| 3 | `@sidebase/nuxt-auth` → Auth | 46,448 | :badge[Partial]{color=\"warning\"} |\n| 4 | `@nuxt/content` v3 → Content | 240,394 | :badge[Partial]{color=\"warning\"} |\n| 4 | Docus → `@vite-hub/docs` | ~15,800 | :badge[High]{color=\"success\"} |\n| 5 | `nuxt-security` limiter → Rate Limit | 220,740 | :badge[Partial]{color=\"warning\"} |\n| 6 | `nuxt-cron` → Schedule | 3,123 | :badge[High]{color=\"success\"} |\n\nResend's count is `nuxt-resend` alone; 274 more apps call `resend` directly from server routes. Inside step 3, `@nuxtjs/better-auth` (2,101 a week) goes first: it runs Better Auth on both sides, so users, hashes and sessions keep their format.\n\n## What ViteHub has to add\n\nEach gap below blocks a codemod or a group of users. i18n routing unblocks the most by far.\n\n::chart{type=\"bar\" horizontal labels title=\"npm downloads a week each missing feature would unblock\"}\n| Missing in ViteHub | downloads |\n| --- | --: |\n| i18n: URL routing | 985785 |\n| Auth: SSR session + page middleware | 226475 |\n| MCP: app-level server | 42096 |\n| Email: SMTP driver | 4620 |\n| Rate Limit: Upstash driver | 2044 |\n::\n\n::details{summary=\"Which modules each gap unblocks\"}\n| Gap | Unblocks |\n| --- | --- |\n| i18n: URL routing | `@nuxtjs/i18n` 977,490, `nuxt-i18n-micro` 8,295 |\n| Auth: SSR session + page middleware | `nuxt-auth-utils` 177,926, sidebase 46,448, `@nuxtjs/better-auth` 2,101 |\n| MCP: app-level server | `@nuxtjs/mcp-toolkit` 42,096 |\n| Email: SMTP driver | `nuxt-nodemailer` 3,376, `nuxt-mail` 1,244, plus 609 apps on raw `nodemailer` |\n| Rate Limit: Upstash driver, windows past 10s and 1m on Cloudflare | `nuxt-api-shield` 2,044, `@upstash/ratelimit`, and the nuxt-security default of 5 minutes on Cloudflare |\n::\n\n## Each codemod\n\nEvery item has the same three parts: what the codemod rewrites, what stays manual, and what has to land first.\n\n::accordion\n:::accordion-item{label=\"1 · @t3-oss/env-nuxt → Env\"}\n| t3-env | ViteHub Env |\n| --- | --- |\n| `createEnv({ server, client })` | the existing converter in `nuxt-safe-runtime-config/schema.ts`, as it is |\n\n- **Manual:** nothing found.\n- **Bonus:** `@arkenv/nuxt` (202 a week) comes almost free, since the converter already reads ArkType.\n:::\n\n:::accordion-item{label=\"2 · Resend → Email\"}\n| Resend | ViteHub Email |\n| --- | --- |\n| `new Resend(key).emails.send(msg)`, `useResend()` | `email.send(msg)`, same message fields |\n| `const { data, error }` | `const [error, data]` |\n| (nothing) | `vitehub.email = { driver: 'resend', options: { apiKey: env(...) } }` |\n\n- **Manual:** batch, contacts, audiences, broadcasts, `react` payloads, Resend templates.\n- **Gain:** a dev outbox, Console preview, a path to Cloudflare Email.\n- **The wizard's report must say:** in dev, sends go to the outbox unless `remote: true`.\n:::\n\n:::accordion-item{label=\"3 · nuxt-auth-utils → Auth\"}\n| nuxt-auth-utils | ViteHub Auth |\n| --- | --- |\n| `'nuxt-auth-utils'` in `modules` | `'vite-hub/nuxt'` |\n| `runtimeConfig.oauth.<p>`, `NUXT_OAUTH_<P>_*` | `socialProviders.<p>` in `server/auth.ts`, with Env |\n| `useUserSession()` `fetch`, `clear` | `refresh`, `signOut` (`loggedIn`, `user`, `ready` match) |\n| `/auth/github` links | `signIn.social({ provider: 'github' })` |\n| `getUserSession`, `requireUserSession` | `auth.api.getSession`, `withAuth` |\n\n- **Providers:** about 25 of 52 are in Better Auth core; auth0, keycloak, okta and others go through `genericOAuth`; six, such as steam, mastodon and atproto, have no equivalent.\n- **Manual:** `onSuccess` upserts move to `databaseHooks`; custom `setUserSession` data; user shape and IDs change (`user.login` breaks); everyone signs in again and each provider needs the `/api/auth/callback/<p>` URL; Adonis scrypt hashes need a custom `password.verify`; WebAuthn moves to the passkey table.\n- **First:** a server-rendered session (`useUserSession()` stays pending during SSR, `packages/auth/README.md:317`) and page middleware. Without them the migrated app flashes signed-out on every load.\n- **Gain:** a real users store, the Console, Auth events, admin, 2FA and organizations.\n:::\n\n:::accordion-item{label=\"3 · @sidebase/nuxt-auth → Auth\"}\n| sidebase (authjs) | ViteHub Auth |\n| --- | --- |\n| `NuxtAuthHandler({ providers })` | `socialProviders` |\n| `useAuth()`, `status === 'authenticated'` | `useUserSession()`, `loggedIn` |\n| `getServerSession` | `auth.api.getSession` |\n\n- **Refuse:** the `local` provider, which sends bearer tokens to an external backend.\n- **Manual:** the `jwt` and `session` callbacks.\n:::\n\n:::accordion-item{label=\"4 · @nuxt/content v3 → Content\"}\n| @nuxt/content | ViteHub Content |\n| --- | --- |\n| `content.config.ts` `defineCollection({ source })` | `server/content.ts` `defineContent({ sources })` with `glob()` |\n| `queryCollection(c).path(p).first()` | `content.get(p)` |\n| `queryCollectionNavigation` | `content.navigation` |\n| `<ContentRenderer>`, `<MDC>` | `<Markdown>` |\n\n- **Manual, reported with `file:line`:** every `.where/.order/.limit/.select/.count` chain (no query builder), the `database` adapters, remote `repository` sources, `queryCollectionItemSurroundings`.\n- **Gain:** drafts and publishing, Console editing, Agent access, no SQL dump at build.\n- **Won't move:** apps that query content heavily.\n:::\n\n:::accordion-item{label=\"4 · Docus → @vite-hub/docs\"}\n| Docus | @vite-hub/docs |\n| --- | --- |\n| `extends: ['docus']` + `app.config.ts` `seo`, `header`, `github`, `socials` | `modules: ['@vite-hub/docs/nuxt']` with `docs: { site, github, socials }` |\n| `content/**` | `content/docs/<section>/` |\n| `AppHeader`, `DocsAsideLeftTop` overrides | same names |\n\n- **Manual:** Docus i18n, which has no target.\n- **Not verified:** a Comark UI equivalent for every Nuxt UI prose component.\n- **Why it ranks high:** the whole site moves, vitehub.dev is the oracle, and it reuses the Content codemod.\n:::\n\n:::accordion-item{label=\"5 · nuxt-security limiter → Rate Limit\"}\n:progress{value=293 max=785 label=\"nuxt-security configs that mention rateLimiter\"}\n\nThe limiter is on by default: 150 requests per 300,000 ms per IP and route.\n\n| nuxt-security | ViteHub Rate Limit |\n| --- | --- |\n| `security.rateLimiter` | `false`; the module stays for headers and CSP |\n| (nothing) | `server/middleware/rate-limit.ts` with `requireRateLimit()`, `vitehub.rateLimit: true` |\n\n- **Manual:** per-route `routeRules` overrides, `whiteList`, `ipHeader`, Redis storage.\n- **Wall:** only memory and Cloudflare drivers, and Cloudflare allows only 10s or 1m windows, so the module's 5m default fails the build there.\n:::\n\n:::accordion-item{label=\"6 · nuxt-cron → Schedule\"}\n| nuxt-cron | ViteHub Schedule |\n| --- | --- |\n| `server/cron/<n>.ts` `defineCronHandler(time, cb)` | `server/schedules/<n>.ts` `defineSchedule({ cron, handler })` |\n| presets | 5-field cron |\n\n- **Manual:** `everySecond`, `timeZone`.\n- **Gain:** a node-only cron runs on Cloudflare, Vercel and Netlify, with run history.\n:::\n::\n\n## Left out\n\nThe chart shows what we leave on the table, 10k a week and up. i18n is the one big loss. The rest have no ViteHub target or wait on a codemod above.\n\n::chart{type=\"bar\" horizontal labels title=\"Left out: npm downloads a week (10k and up)\"}\n| module | downloads |\n| --- | --: |\n| @nuxtjs/i18n | 977490 |\n| nuxt-csurf | 223523 |\n| @nuxtjs/supabase | 94526 |\n| @nuxtjs/turnstile | 46735 |\n| @nuxtjs/mcp-toolkit | 42096 |\n| @clerk/nuxt | 40184 |\n| nitro-cloudflare-dev | 29560 |\n| nuxt-vuefire | 15428 |\n| nuxt-studio | 14956 |\n| nuxt-multi-cache | 14125 |\n| nuxt-oidc-auth | 10859 |\n:::annotation{at=\"@nuxtjs/i18n\" mark=\"box\" color=\"amber\" side=\"right\"}\nYour call: hold until #3564 has URL routing.\n:::\n::\n\n::accordion\n:::accordion-item{label=\"Waits on ViteHub work (8)\"}\n| Module | npm/week | Waits on |\n| --- | --: | --- |\n| `@nuxtjs/i18n` | 977,490 | #3564 and URL routing (`useLocalePath`, `NuxtLinkLocale`, hreflang) |\n| `nuxt-i18n-micro` | 8,295 | the same |\n| `nuxt-studio` | 14,956 | the Content codemod |\n| `nuxt-oidc-auth` | 10,859 | the Auth codemod; small audience, `h3` 1 |\n| `nuxt-nodemailer`, `nuxt-mail` | 3,376 + 1,244 | an SMTP driver; the largest email segment |\n| `nuxt-api-shield`, `@upstash/ratelimit` | 2,044 | an Upstash or Redis driver |\n| `nitro-cloudflare-dev` | 29,560 | proof that ViteHub provides every binding in dev (only KV checked, #3480) |\n| `nuxt-file-storage` | 845 | nothing; a cheap shim over `blob` after step 6 |\n:::\n\n:::accordion-item{label=\"No ViteHub primitive (5)\"}\n| Module | npm/week | Missing |\n| --- | --: | --- |\n| `nuxt-csurf` | 223,523 | CSRF |\n| `@nuxtjs/turnstile` | 46,735 | captcha |\n| `@nuxtjs/mcp-toolkit` | 42,096 | an app-level MCP server |\n| `nuxt-multi-cache` | 14,125 | component and route caching, CDN tags |\n| `nuxt-processor` (BullMQ) | 1,183 | priorities, repeats and flows in Queue |\n:::\n\n:::accordion-item{label=\"Data lives in a hosted service (4)\"}\n| Module | npm/week |\n| --- | --: |\n| `@nuxtjs/supabase` | 94,526 |\n| `@clerk/nuxt` | 40,184 |\n| `nuxt-vuefire` | 15,428 |\n| Strapi, Sanity, Storyblok, Directus, Prismic | various |\n\nUsers, hashes and content stay in the service, so a codemod has nothing to move. At most a skeleton and a link to Better Auth's guide.\n:::\n\n:::accordion-item{label=\"Nothing worth a codemod (6)\"}\n| Module | Why |\n| --- | --- |\n| `nuxt-authorization` (7,188) | keep it; rewrite its two resolvers inside the Auth codemod |\n| `nuxt-email-renderer` (5,291) | templates keep working through `email.send` |\n| `@workflow/nuxt` (2.8M, transitive) | Workflow DevKit is already portable |\n| `@prisma/nuxt` (896) | deprecated, and no Prisma support |\n| Nitro `scheduledTasks` (294 configs) | Nitro 3 already runs them; a Vite Doctor hint fits better |\n| Nitro `useStorage` with Redis (270 configs) | every call site changes and there's no ioredis driver; only `nitro.storage.cache` → `vitehub.cache` is cheap |\n:::\n::\n\n## How I checked\n\n> [!CAUTION]\n> **Not verified:** any source module on Nuxt 5; sessions surviving the `@nuxtjs/better-auth` move; how many nuxt-security users want only the limiter; the Docus count (one npm call); Comark covering every MDC feature, such as named slots and `{{ $doc }}`.\n\n::details{summary=\"Sources and method\"}\n- **The field:** all 334 modules in the `api.nuxt.com/modules` registry (2026-11-07), 375 npm search hits across 45 primitive terms, and about 120 named packages the registry misses, such as nuxt-auth-utils.\n- **Downloads:** `api.npmjs.org/downloads/point/last-week`, 2026-10-30 to 2026-11-05.\n- **Stars and last push:** the GitHub API.\n- **Module APIs:** the npm tarballs. **ViteHub APIs:** `docs/content/docs/*` and the package READMEs on main (`c845af3ae`).\n- **Code counts:** GitHub's legacy code search; rough and not limited to Nuxt.\n::\n"
289
+ },
290
+ {
291
+ "path": "scripts/render.mjs",
292
+ "text": "// Renders a comark Markdown report to HTML and screenshots it at reading width.\n// Usage: node render.mjs report.md [outDir]\n// Run it from a folder where comark, @comark/html, comark-ui and playwright are installed.\nimport { createRequire } from \"node:module\";\nimport { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { basename, join, resolve } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\n\nconst [input, outDir = \"render\"] = process.argv.slice(2);\nif (!input) throw new Error(\"Usage: node render.mjs report.md [outDir]\");\nconst require = createRequire(join(process.cwd(), \"noop.js\"));\nconst load = (name) => import(pathToFileURL(require.resolve(name)).href);\n\nconst { createHtmlRenderer } = await load(\"@comark/html\");\nconst { default: comarkUi } = await load(\"comark-ui/html\");\nconst playwright = await load(\"playwright\");\nconst { chromium } = playwright.chromium ? playwright : playwright.default;\n\nmkdirSync(outDir, { recursive: true });\nconst name = basename(input).replace(/\\.md$/, \"\");\nconst source = readFileSync(input, \"utf8\");\nconst rendered = await createHtmlRenderer(comarkUi())(source);\n// A host such as Drop shows the frontmatter title above the page; the renderer doesn't.\nconst title = source.match(/^---\\n[\\s\\S]*?^title:\\s*\"?(.*?)\"?\\s*$/m)?.[1] ?? \"\";\nconst escape = (text) =>\n text.replace(/[&<>]/g, (c) => ({ \"&\": \"&amp;\", \"<\": \"&lt;\", \">\": \"&gt;\" })[c]);\nconst htmlPath = resolve(outDir, `${name}.html`);\nwriteFileSync(\n htmlPath,\n `<!doctype html><meta charset=\"utf-8\"><style>body{max-width:760px;margin:2rem auto;padding:0 1rem;font-family:system-ui}</style>${title ? `<h1>${escape(title)}</h1>` : \"\"}${typeof rendered === \"string\" ? rendered : rendered.html}`,\n);\n\n// Playwright's own browser may be missing; fall back to the newest one in its cache.\nconst cache = join(homedir(), \".cache/ms-playwright\");\nconst fallback = existsSync(cache)\n ? readdirSync(cache)\n .filter((d) => /^chromium-\\d+$/.test(d))\n .sort((a, b) => Number(b.slice(9)) - Number(a.slice(9)))\n .map((d) => join(cache, d, \"chrome-linux64/chrome\"))\n .find((path) => existsSync(path))\n : undefined;\nlet browser;\ntry {\n browser = await chromium.launch();\n} catch {\n browser = await chromium.launch({ executablePath: fallback });\n}\n\nconst page = await browser.newPage({ viewport: { width: 900, height: 1600 } });\nawait page.goto(pathToFileURL(htmlPath).href);\nawait page.waitForTimeout(1000);\nconst shots = [];\nfor (const state of [\"closed\", \"open\"]) {\n if (state === \"open\")\n await page.evaluate(() => document.querySelectorAll(\"details\").forEach((d) => (d.open = true)));\n // Folds animate open, so wait until the page stops growing before measuring it.\n let height = 0;\n for (let last = -1; height !== last; await page.waitForTimeout(400)) {\n last = height;\n height = await page.evaluate(() => document.documentElement.scrollHeight);\n }\n for (let y = 0, i = 0; y < height; y += 1600, i++) {\n const path = resolve(outDir, `${name}-${state}-${i}.png`);\n await page.screenshot({\n path,\n fullPage: true,\n clip: { x: 0, y, width: 900, height: Math.min(1600, height - y) },\n });\n shots.push(path);\n }\n}\nawait browser.close();\nconsole.log([htmlPath, ...shots].join(\"\\n\"));\n"
293
+ }
294
+ ]
295
+ },
265
296
  {
266
297
  "name": "review-document",
267
- "description": "Turns a review's findings into one short Markdown document a reader can act on, with the verdict first and each finding ranked, located, backed by evidence, and paired with its fix. Use when writing up a code review, a design or QA review, an audit, or any report whose reader must decide what to do next.",
298
+ "description": "Turns a review's findings into one short Markdown document a reader can act on, with the verdict first and each finding ranked, located, backed by evidence, and paired with its fix. Use when writing up a code review, a design or QA review, or an audit, where the reader must decide what to do about what you found.",
268
299
  "frontmatter": {
269
300
  "name": "review-document",
270
- "description": "Turns a review's findings into one short Markdown document a reader can act on, with the verdict first and each finding ranked, located, backed by evidence, and paired with its fix. Use when writing up a code review, a design or QA review, an audit, or any report whose reader must decide what to do next.",
301
+ "description": "Turns a review's findings into one short Markdown document a reader can act on, with the verdict first and each finding ranked, located, backed by evidence, and paired with its fix. Use when writing up a code review, a design or QA review, or an audit, where the reader must decide what to do about what you found.",
271
302
  "license": "MIT"
272
303
  },
273
304
  "files": [
274
305
  {
275
306
  "path": "SKILL.md",
276
- "text": "---\nname: review-document\ndescription: Turns a review's findings into one short Markdown document a reader can act on, with the verdict first and each finding ranked, located, backed by evidence, and paired with its fix. Use when writing up a code review, a design or QA review, an audit, or any report whose reader must decide what to do next.\nlicense: MIT\n---\n\n# Review document\n\nA review's reader has one question: what do I do now? Answer it in the first two lines, then give each finding just enough evidence that the reader trusts it without redoing your work. This skill organizes findings you already have; finding them is the review itself.\n\nLength is the usual failure. A reader who has to dig through praise, process, and restated findings misses the one that blocks the merge. Every line below the verdict either supports a finding or says what you didn't check.\n\n## 1. Decide the verdict\n\nPick one: **approve**, **approve with changes**, **request changes**, or **blocked** on a named thing. Under it, write the reason in one sentence that names the finding behind it.\n\nDone when someone who reads only the verdict knows what to do.\n\n## 2. Keep the findings that change the decision\n\n- Keep a finding when fixing it, or not, changes what the reader should do.\n- Merge findings with one root cause into one, and list every place it appears.\n- Rank each one: **blocking** (wrong results, lost data, a security hole, broken for users), **should fix** (a real cost that can wait), or **nit**, only when the reader asked for them.\n\nOrder them blocking first. Done when every finding has a rank and no two share a cause.\n\n## 3. Back each finding\n\nEach finding is four parts, in this order:\n\n1. **A title that states the consequence**, for whom: \"Small avatars load the 512px image\", not \"Cache key issue\".\n2. **Where**: the file and line, linked when there's a URL.\n3. **Evidence**, the strongest you have: a failing test or command you ran (`::tests`, a fenced block of output), a screenshot of the wrong state (measured as the before-after-evidence skill describes), the lines that cause it (a fence with the line highlighted), then reasoning. Say which it is: a finding you reproduced reads differently from one you read in the code.\n4. **The fix**: a `::diff` of the smallest change, or one sentence when the fix is a decision for the reader.\n\nDone when every blocking finding has evidence you ran or captured, or says plainly that it comes from reading the code.\n\n## 4. Say what you checked\n\nEnd with two short lists: what you checked and how, and what you didn't check. A review that implies more coverage than it had loses the reader's trust in the parts that were real.\n\n## 5. Write it\n\nFollow [references/example.md](references/example.md):\n\n- The verdict in a callout: `> [!CAUTION]` for request changes or blocked, `> [!WARNING]` for approve with changes, `> [!TIP]` for approve.\n- One `##` heading per finding, numbered, with its rank in the heading. About a dozen lines each, evidence included.\n- No summary that repeats the findings, no section for an empty rank, no list of what's good unless the reader asked.\n\n[references/blocks.md](references/blocks.md) has the syntax for the callout, `::tests`, `::diff`, and highlighted lines.\n\n## 6. Check the render\n\n1. Lint it, and fix until it reports no problems:\n\n ```sh\n vp dlx --package comark-linter --package comark-ui comark-lint review.md\n ```\n\n These commands use the [global Vite+ CLI](https://viteplus.dev/guide/global-cli). Without it, `pnpm dlx` takes the same arguments.\n\n2. Open the rendered page where it will be read and read it top to bottom as the reader would. Each finding's evidence should be visible without expanding anything except a long log, and each `::diff` should start at the line its finding names.\n\nDone when the page renders without problems and you've read it once as the reader.\n"
307
+ "text": "---\nname: review-document\ndescription: Turns a review's findings into one short Markdown document a reader can act on, with the verdict first and each finding ranked, located, backed by evidence, and paired with its fix. Use when writing up a code review, a design or QA review, or an audit, where the reader must decide what to do about what you found.\nlicense: MIT\n---\n\n# Review document\n\nA review's reader has one question: what do I do now? Answer it in the first two lines, then give each finding just enough evidence that the reader trusts it without redoing your work. This skill organizes findings you already have; finding them is the review itself.\n\nLength is the usual failure. A reader who has to dig through praise, process, and restated findings misses the one that blocks the merge. Every line below the verdict either supports a finding or says what you didn't check.\n\n## 1. Decide the verdict\n\nPick one: **approve**, **approve with changes**, **request changes**, or **blocked** on a named thing. Under it, write the reason in one sentence that names the finding behind it.\n\nDone when someone who reads only the verdict knows what to do.\n\n## 2. Keep the findings that change the decision\n\n- Keep a finding when fixing it, or not, changes what the reader should do.\n- Merge findings with one root cause into one, and list every place it appears.\n- Rank each one: **blocking** (wrong results, lost data, a security hole, broken for users), **should fix** (a real cost that can wait), or **nit**, only when the reader asked for them.\n\nOrder them blocking first. Done when every finding has a rank and no two share a cause.\n\n## 3. Back each finding\n\nEach finding is four parts, in this order:\n\n1. **A title that states the consequence**, for whom: \"Small avatars load the 512px image\", not \"Cache key issue\".\n2. **Where**: the file and line, linked when there's a URL.\n3. **Evidence**, the strongest you have: a failing test or command you ran (`::tests`, a fenced block of output), a screenshot of the wrong state (measured as the before-after-evidence skill describes), the lines that cause it (a fence with the line highlighted), then reasoning. Say which it is: a finding you reproduced reads differently from one you read in the code.\n4. **The fix**: a `::diff` of the smallest change, or one sentence when the fix is a decision for the reader.\n\nDone when every blocking finding has evidence you ran or captured, or says plainly that it comes from reading the code.\n\n## 4. Say what you checked\n\nEnd with two short lists: what you checked and how, and what you didn't check. A review that implies more coverage than it had loses the reader's trust in the parts that were real.\n\n## 5. Write it\n\nFollow [references/example.md](references/example.md):\n\n- The verdict in a callout: `> [!CAUTION]` for request changes or blocked, `> [!WARNING]` for approve with changes, `> [!TIP]` for approve.\n- One `##` heading per finding, numbered, with its rank in the heading. About a dozen lines each, evidence included.\n- No summary that repeats the findings, no section for an empty rank, no list of what's good unless the reader asked.\n\n[references/blocks.md](references/blocks.md) has the syntax for the callout, `::tests`, `::diff`, and highlighted lines.\n\n## 6. Check the render\n\n1. Lint it, and fix until it reports no problems:\n\n ```sh\n vp dlx --package comark-linter --package comark-ui comark-lint review.md\n ```\n\n These commands use the [global Vite+ CLI](https://viteplus.dev/guide/global-cli). Without it, `pnpm dlx` takes the same arguments.\n\n2. Open the rendered page where it will be read and read it top to bottom as the reader would. Each finding's evidence should be visible without expanding anything except a long log, and each `::diff` should start at the line its finding names.\n\nDone when the page renders without problems and you've read it once as the reader.\n"
277
308
  },
278
309
  {
279
310
  "path": "agents/openai.yaml",
@@ -345,6 +376,6 @@ const skills = [
345
376
  }
346
377
  ];
347
378
  /** The comark-ui version the skills come from, for a host's cache key or a digest. */
348
- const version = "0.10.0";
379
+ const version = "0.10.1";
349
380
  //#endregion
350
381
  export { skills, version };
@@ -10,7 +10,7 @@
10
10
  onMount(() => startMermaid());
11
11
  </script>
12
12
 
13
- <div class="cmm" data-source={content}>
13
+ <div class="cmm" data-comark-frame="" data-source={content}>
14
14
  <Pele {content} {...rest} />
15
15
  <div class="cmm-bar">{@html mermaidBar()}</div>
16
16
  </div>