@cfbender/cesium 0.3.6 → 0.5.0

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +28 -14
  3. package/assets/styleguide.html +149 -0
  4. package/package.json +1 -1
  5. package/src/cli/commands/serve.ts +3 -0
  6. package/src/index.ts +4 -1
  7. package/src/prompt/field-reference.ts +94 -0
  8. package/src/prompt/system-fragment.md +56 -65
  9. package/src/render/blocks/catalog.ts +39 -0
  10. package/src/render/blocks/escape.ts +27 -0
  11. package/src/render/blocks/index.ts +6 -0
  12. package/src/render/blocks/markdown.ts +217 -0
  13. package/src/render/blocks/render.ts +96 -0
  14. package/src/render/blocks/renderers/callout.ts +38 -0
  15. package/src/render/blocks/renderers/code.ts +44 -0
  16. package/src/render/blocks/renderers/compare-table.ts +56 -0
  17. package/src/render/blocks/renderers/diagram.ts +48 -0
  18. package/src/render/blocks/renderers/divider.ts +31 -0
  19. package/src/render/blocks/renderers/hero.ts +66 -0
  20. package/src/render/blocks/renderers/kv.ts +45 -0
  21. package/src/render/blocks/renderers/list.ts +51 -0
  22. package/src/render/blocks/renderers/pill-row.ts +45 -0
  23. package/src/render/blocks/renderers/prose.ts +29 -0
  24. package/src/render/blocks/renderers/raw-html.ts +32 -0
  25. package/src/render/blocks/renderers/risk-table.ts +76 -0
  26. package/src/render/blocks/renderers/section.ts +95 -0
  27. package/src/render/blocks/renderers/timeline.ts +58 -0
  28. package/src/render/blocks/renderers/tldr.ts +30 -0
  29. package/src/render/blocks/types.ts +127 -0
  30. package/src/render/blocks/validate-block.ts +202 -0
  31. package/src/render/critique.ts +410 -10
  32. package/src/render/fallback.ts +18 -0
  33. package/src/render/theme.ts +235 -0
  34. package/src/render/validate.ts +282 -17
  35. package/src/render/wrap.ts +7 -7
  36. package/src/server/lifecycle.ts +7 -1
  37. package/src/storage/assets.ts +66 -0
  38. package/src/storage/index-cache.ts +1 -0
  39. package/src/storage/index-gen.ts +13 -14
  40. package/src/tools/ask.ts +5 -3
  41. package/src/tools/critique.ts +41 -6
  42. package/src/tools/publish.ts +39 -12
  43. package/src/tools/styleguide.ts +109 -9
@@ -0,0 +1,66 @@
1
+ // Materializes /theme.css in the state directory, atomically and idempotently.
2
+
3
+ import { createHash } from "node:crypto";
4
+ import { join } from "node:path";
5
+ import {
6
+ frameworkRulesCss,
7
+ themeTokensCss,
8
+ defaultTheme,
9
+ type ThemeTokens,
10
+ } from "../render/theme.ts";
11
+ import { atomicWrite } from "./write.ts";
12
+ import { readFile } from "node:fs/promises";
13
+
14
+ /** Per-theme CSS cache: built CSS string keyed by theme content hash. */
15
+ const cssCache = new Map<string, string>();
16
+
17
+ /** Returns a stable cache key for a theme (hash of its JSON representation). */
18
+ function themeKey(theme: ThemeTokens): string {
19
+ return createHash("sha256").update(JSON.stringify(theme)).digest("hex");
20
+ }
21
+
22
+ /** Build the full theme.css string for a given theme (tokens + framework rules). */
23
+ function buildCss(theme: ThemeTokens): string {
24
+ const key = themeKey(theme);
25
+ const cached = cssCache.get(key);
26
+ if (cached !== undefined) return cached;
27
+ const css = themeTokensCss(theme) + "\n" + frameworkRulesCss();
28
+ cssCache.set(key, css);
29
+ return css;
30
+ }
31
+
32
+ /** Returns the absolute path to theme.css in stateDir. */
33
+ export function themeCssAssetPath(stateDir: string): string {
34
+ return join(stateDir, "theme.css");
35
+ }
36
+
37
+ /**
38
+ * Writes <stateDir>/theme.css with the full framework CSS (tokens + rules)
39
+ * for the given theme, iff the on-disk file is missing or its content hash
40
+ * differs from the expected content. Idempotent and self-healing on plugin
41
+ * upgrade or theme change.
42
+ *
43
+ * When called without a theme argument, falls back to defaultTheme() so
44
+ * existing call sites remain valid.
45
+ */
46
+ export async function ensureThemeCss(
47
+ stateDir: string,
48
+ theme: ThemeTokens = defaultTheme(),
49
+ ): Promise<void> {
50
+ const dest = themeCssAssetPath(stateDir);
51
+ const bundledCss = buildCss(theme);
52
+ const bundledHash = createHash("sha256").update(bundledCss).digest("hex");
53
+
54
+ // Fast path: compare hash of existing file to expected hash.
55
+ try {
56
+ const existing = await readFile(dest, "utf8");
57
+ const existingHash = createHash("sha256").update(existing).digest("hex");
58
+ if (existingHash === bundledHash) {
59
+ return; // already up-to-date
60
+ }
61
+ } catch {
62
+ // ENOENT or unreadable — fall through to write
63
+ }
64
+
65
+ await atomicWrite(dest, bundledCss);
66
+ }
@@ -19,6 +19,7 @@ export interface IndexEntry {
19
19
  projectSlug: string;
20
20
  projectName: string;
21
21
  bodyText: string;
22
+ inputMode?: "html" | "blocks";
22
23
  }
23
24
 
24
25
  export async function loadIndex(jsonPath: string): Promise<IndexEntry[]> {
@@ -2,8 +2,8 @@
2
2
 
3
3
  import type { IndexEntry } from "./index-cache.ts";
4
4
  import type { ThemeTokens } from "../render/theme.ts";
5
- import { frameworkRulesCss, themeTokensCss } from "../render/theme.ts";
6
5
  import { faviconLinkTag, faviconEmblemSvg } from "../render/favicon.ts";
6
+ import { fallbackCss } from "../render/fallback.ts";
7
7
 
8
8
  export interface RenderProjectIndexArgs {
9
9
  projectSlug: string;
@@ -236,6 +236,9 @@ function indexJs(): string {
236
236
  function renderEntryCard(entry: IndexEntry): string {
237
237
  const isSuperseded = entry.supersededBy !== null ? "1" : "0";
238
238
  const kindPill = `<span class="pill">${esc(entry.kind)}</span>`;
239
+ const inputModeBadge = entry.inputMode !== undefined
240
+ ? ` <span class="tag">${esc(entry.inputMode)}</span>`
241
+ : "";
239
242
  const dateStr = `<span class="card-date">${esc(formatDate(entry.createdAt))}</span>`;
240
243
  const supersededBadge =
241
244
  entry.supersedes !== null
@@ -255,7 +258,7 @@ function renderEntryCard(entry: IndexEntry): string {
255
258
  : "";
256
259
 
257
260
  return `<div class="entry-card" data-card data-kind="${esc(entry.kind)}" data-title-lower="${esc(entry.title.toLowerCase())}" data-body-text="${esc(entry.bodyText.toLowerCase())}" data-superseded="${isSuperseded}">
258
- <div class="card-top">${kindPill}${supersededBadge}${supersededByBadge}${dateStr}</div>
261
+ <div class="card-top">${kindPill}${inputModeBadge}${supersededBadge}${supersededByBadge}${dateStr}</div>
259
262
  <div class="card-title"><a href="artifacts/${esc(entry.filename)}">${esc(entry.title)}</a></div>
260
263
  ${summaryHtml}${tagsHtml}
261
264
  <div class="card-footer"><a class="open-link" href="artifacts/${esc(entry.filename)}">Open →</a></div>
@@ -265,7 +268,7 @@ function renderEntryCard(entry: IndexEntry): string {
265
268
  // ─── renderProjectIndex ──────────────────────────────────────────────────────
266
269
 
267
270
  export function renderProjectIndex(args: RenderProjectIndexArgs): string {
268
- const { projectSlug, projectName, entries, theme } = args;
271
+ const { projectSlug, projectName, entries } = args;
269
272
  const href =
270
273
  args.themeCssHref === undefined
271
274
  ? "../../theme.css"
@@ -274,8 +277,7 @@ export function renderProjectIndex(args: RenderProjectIndexArgs): string {
274
277
  : args.themeCssHref;
275
278
  const suppressLink = args.themeCssHref === null;
276
279
 
277
- const rules = frameworkRulesCss();
278
- const tokens = themeTokensCss(theme);
280
+ const fallback = fallbackCss();
279
281
  const iCss = indexCss();
280
282
  const iJs = indexJs();
281
283
 
@@ -358,9 +360,8 @@ ${cardsHtml}
358
360
  <meta charset="utf-8">
359
361
  <meta name="viewport" content="width=device-width, initial-scale=1">
360
362
  <title>${esc(projectName)} · cesium</title>
361
- <style>${rules}
362
- /* fallback theme tokens — used when theme.css is missing or unreachable */
363
- ${tokens}${iCss}</style>${linkTag}${faviconTag}
363
+ <style>/* fallback — standalone-readable; full styles served from /theme.css */
364
+ ${fallback}${iCss}</style>${linkTag}${faviconTag}
364
365
  </head>
365
366
  <body>
366
367
  <div class="page">
@@ -381,7 +382,7 @@ ${tokens}${iCss}</style>${linkTag}${faviconTag}
381
382
  // ─── renderGlobalIndex ───────────────────────────────────────────────────────
382
383
 
383
384
  export function renderGlobalIndex(args: RenderGlobalIndexArgs): string {
384
- const { projects, theme } = args;
385
+ const { projects } = args;
385
386
  const href =
386
387
  args.themeCssHref === undefined
387
388
  ? "theme.css"
@@ -390,8 +391,7 @@ export function renderGlobalIndex(args: RenderGlobalIndexArgs): string {
390
391
  : args.themeCssHref;
391
392
  const suppressLink = args.themeCssHref === null;
392
393
 
393
- const rules = frameworkRulesCss();
394
- const tokens = themeTokensCss(theme);
394
+ const fallback = fallbackCss();
395
395
  const iCss = indexCss();
396
396
 
397
397
  const linkTag = suppressLink ? "" : `\n <link rel="stylesheet" href="${href}">`;
@@ -448,9 +448,8 @@ export function renderGlobalIndex(args: RenderGlobalIndexArgs): string {
448
448
  <meta charset="utf-8">
449
449
  <meta name="viewport" content="width=device-width, initial-scale=1">
450
450
  <title>All projects · cesium</title>
451
- <style>${rules}
452
- /* fallback theme tokens — used when theme.css is missing or unreachable */
453
- ${tokens}${iCss}</style>${linkTag}${faviconTag}
451
+ <style>/* fallback — standalone-readable; full styles served from /theme.css */
452
+ ${fallback}${iCss}</style>${linkTag}${faviconTag}
454
453
  </head>
455
454
  <body>
456
455
  <div class="page">
package/src/tools/ask.ts CHANGED
@@ -13,7 +13,7 @@ import { wrapDocument, type ArtifactMeta } from "../render/wrap.ts";
13
13
  import type { InteractiveData } from "../render/validate.ts";
14
14
  import { deriveProjectIdentity, artifactFilename, pathsFor } from "../storage/paths.ts";
15
15
  import { atomicWrite } from "../storage/write.ts";
16
- import { writeThemeCss } from "../storage/theme-write.ts";
16
+ import { ensureThemeCss } from "../storage/assets.ts";
17
17
  import { writeFaviconSvg } from "../storage/favicon-write.ts";
18
18
  import { loadIndex, writeIndex, appendEntry, type IndexEntry } from "../storage/index-cache.ts";
19
19
  import { withLock } from "../storage/lock.ts";
@@ -204,6 +204,7 @@ export function createAskTool(
204
204
  supersedes: null,
205
205
  supersededBy: null,
206
206
  contentSha256,
207
+ inputMode: "html",
207
208
  };
208
209
 
209
210
  // 12. Build interactive data
@@ -228,8 +229,8 @@ export function createAskTool(
228
229
  // 14. Build theme + wrap document
229
230
  const theme = mergeTheme(themeFromPreset(config.themePreset), config.theme);
230
231
 
231
- // 14a. Write theme.css + favicon.svg (idempotent, outside index lock — separate files)
232
- await writeThemeCss(config.stateDir, theme);
232
+ // 14a. Ensure theme.css + favicon.svg (idempotent, outside index lock — separate files)
233
+ await ensureThemeCss(config.stateDir, theme);
233
234
  await writeFaviconSvg(config.stateDir);
234
235
 
235
236
  const fullHtml = wrapDocument({
@@ -309,6 +310,7 @@ export function createAskTool(
309
310
  portMax: config.portMax,
310
311
  idleTimeoutMs: config.idleTimeoutMs,
311
312
  hostname: config.hostname,
313
+ theme,
312
314
  });
313
315
  if (maybeInfo !== null) {
314
316
  serverInfo = maybeInfo;
@@ -1,8 +1,15 @@
1
- // Tool handler for cesium_critique — runs the body analyzer and returns a human-readable report.
1
+ // Tool handler for cesium_critique — mode-aware body analyzer.
2
+ // Accepts either { html: string } (html mode) or { blocks: Block[] } (blocks mode). Exactly one required.
2
3
 
3
4
  import { tool } from "@opencode-ai/plugin";
4
5
  import type { PluginInput } from "@opencode-ai/plugin";
5
- import { critique, type CritiqueResult, type CritiqueSeverity } from "../render/critique.ts";
6
+ import {
7
+ critiqueHtml,
8
+ critiqueBlocks,
9
+ type CritiqueResult,
10
+ type CritiqueSeverity,
11
+ } from "../render/critique.ts";
12
+ import type { Block } from "../render/blocks/types.ts";
6
13
 
7
14
  const TOOL_DESCRIPTION = `Analyze a draft HTML body for adherence to the cesium design
8
15
  system before publishing. Returns a 0-100 score and findings (warn/suggest/info).
@@ -18,6 +25,7 @@ HTML only, no <!doctype>/<html>/<head>/<body> wrappers.`;
18
25
  * Format a CritiqueResult into a concise human-readable string the agent can parse.
19
26
  * Format:
20
27
  * score: 87/100
28
+ * mode: html
21
29
  *
22
30
  * warn:
23
31
  * - [external-resource] External resource will be stripped...
@@ -29,7 +37,7 @@ HTML only, no <!doctype>/<html>/<head>/<body> wrappers.`;
29
37
  * - [code-without-highlights] Code blocks render without...
30
38
  */
31
39
  export function formatCritiqueForAgent(result: CritiqueResult): string {
32
- const lines: string[] = [`score: ${result.score}/100`];
40
+ const lines: string[] = [`score: ${result.score}/100`, `mode: ${result.mode}`];
33
41
 
34
42
  const bySeverity: Record<CritiqueSeverity, typeof result.findings> = {
35
43
  warn: [],
@@ -47,7 +55,8 @@ export function formatCritiqueForAgent(result: CritiqueResult): string {
47
55
  lines.push("");
48
56
  lines.push(`${sev}:`);
49
57
  for (const f of group) {
50
- lines.push(`- [${f.code}] ${f.message}`);
58
+ const pathSuffix = f.path !== undefined ? ` (${f.path})` : "";
59
+ lines.push(`- [${f.code}] ${f.message}${pathSuffix}`);
51
60
  }
52
61
  }
53
62
 
@@ -57,9 +66,35 @@ export function formatCritiqueForAgent(result: CritiqueResult): string {
57
66
  export function createCritiqueTool(_ctx: PluginInput): ReturnType<typeof tool> {
58
67
  return tool({
59
68
  description: TOOL_DESCRIPTION,
60
- args: { html: tool.schema.string() },
69
+ args: {
70
+ html: tool.schema.string().optional(),
71
+ blocks: tool.schema.any().optional(),
72
+ },
61
73
  async execute(args) {
62
- const result = critique(args.html);
74
+ const hasHtml = args.html !== undefined && args.html !== null;
75
+ const hasBlocks = args.blocks !== undefined && args.blocks !== null;
76
+
77
+ if (hasHtml && hasBlocks) {
78
+ return "error: provide exactly one of html or blocks, not both";
79
+ }
80
+ if (!hasHtml && !hasBlocks) {
81
+ return "error: provide exactly one of html or blocks";
82
+ }
83
+
84
+ let result: CritiqueResult;
85
+
86
+ if (hasHtml) {
87
+ if (typeof args.html !== "string") {
88
+ return "error: html must be a string";
89
+ }
90
+ result = critiqueHtml(args.html);
91
+ } else {
92
+ if (!Array.isArray(args.blocks)) {
93
+ return "error: blocks must be an array";
94
+ }
95
+ result = critiqueBlocks(args.blocks as Block[]);
96
+ }
97
+
63
98
  return formatCritiqueForAgent(result);
64
99
  },
65
100
  });
@@ -10,10 +10,11 @@ import { scrub } from "../render/scrub.ts";
10
10
  import { extractTextContent } from "../render/extract.ts";
11
11
  import { themeFromPreset, mergeTheme } from "../render/theme.ts";
12
12
  import { validatePublishInput, htmlBodyWarnings, PUBLISH_KINDS } from "../render/validate.ts";
13
+ import { renderBlocks } from "../render/blocks/render.ts";
13
14
  import { wrapDocument, type ArtifactMeta } from "../render/wrap.ts";
14
15
  import { deriveProjectIdentity, artifactFilename, pathsFor } from "../storage/paths.ts";
15
16
  import { atomicWrite, patchEmbeddedMetadata } from "../storage/write.ts";
16
- import { writeThemeCss } from "../storage/theme-write.ts";
17
+ import { ensureThemeCss } from "../storage/assets.ts";
17
18
  import { writeFaviconSvg } from "../storage/favicon-write.ts";
18
19
  import {
19
20
  loadIndex,
@@ -112,7 +113,18 @@ export function createPublishTool(
112
113
  args: {
113
114
  title: tool.schema.string(),
114
115
  kind: tool.schema.enum([...PUBLISH_KINDS] as [string, ...string[]]),
115
- html: tool.schema.string(),
116
+ html: tool.schema
117
+ .string()
118
+ .optional()
119
+ .describe(
120
+ "Body HTML — escape valve / legacy mode. Provide exactly one of html or blocks.",
121
+ ),
122
+ blocks: tool.schema
123
+ .array(tool.schema.any())
124
+ .optional()
125
+ .describe(
126
+ "Structured block array — preferred for token efficiency. Provide exactly one of html or blocks.",
127
+ ),
116
128
  summary: tool.schema.string().optional(),
117
129
  tags: tool.schema.array(tool.schema.string()).optional(),
118
130
  supersedes: tool.schema.string().optional(),
@@ -174,11 +186,23 @@ export function createPublishTool(
174
186
  // 6. Timestamps
175
187
  const createdAt = now();
176
188
 
177
- // 7. Scrub
178
- const scrubbed = scrub(input.html);
189
+ // 7. Render body (blocks path or html path)
190
+ let bodyHtml: string;
191
+ let scrubRemovedCount = 0;
192
+ const inputMode: "html" | "blocks" = input.blocks !== undefined ? "blocks" : "html";
193
+
194
+ if (input.blocks !== undefined) {
195
+ // Blocks path: render structured blocks → trusted HTML
196
+ bodyHtml = renderBlocks(input.blocks);
197
+ } else {
198
+ // HTML path: scrub agent-supplied HTML
199
+ const scrubbed = scrub(input.html);
200
+ bodyHtml = scrubbed.html;
201
+ scrubRemovedCount = scrubbed.removed.length;
202
+ }
179
203
 
180
204
  // 7a. Extract body text for full-text search
181
- const bodyText = extractTextContent(scrubbed.html);
205
+ const bodyText = extractTextContent(bodyHtml);
182
206
 
183
207
  // 8. Compute filename + paths
184
208
  const filename = artifactFilename({ title: input.title, id, createdAt });
@@ -189,7 +213,7 @@ export function createPublishTool(
189
213
  });
190
214
 
191
215
  // 9. Content SHA-256
192
- const contentSha256 = createHash("sha256").update(scrubbed.html).digest("hex");
216
+ const contentSha256 = createHash("sha256").update(bodyHtml).digest("hex");
193
217
 
194
218
  // 10. Build ArtifactMeta
195
219
  const meta: ArtifactMeta = {
@@ -210,14 +234,15 @@ export function createPublishTool(
210
234
  supersedes: input.supersedes ?? null,
211
235
  supersededBy: null,
212
236
  contentSha256,
237
+ inputMode,
213
238
  };
214
239
 
215
240
  // 11. Build warnings
216
241
  const warnings: string[] = [];
217
- if (scrubbed.removed.length > 0) {
218
- warnings.push(`Removed ${scrubbed.removed.length} external resource(s) during scrub.`);
242
+ if (scrubRemovedCount > 0) {
243
+ warnings.push(`Removed ${scrubRemovedCount} external resource(s) during scrub.`);
219
244
  }
220
- const bodyWarnings = htmlBodyWarnings(scrubbed.html);
245
+ const bodyWarnings = input.html !== undefined ? htmlBodyWarnings(bodyHtml) : [];
221
246
  for (const w of bodyWarnings) {
222
247
  warnings.push(w);
223
248
  }
@@ -225,12 +250,12 @@ export function createPublishTool(
225
250
  // 12. Build theme + wrap document
226
251
  const theme = mergeTheme(themeFromPreset(config.themePreset), config.theme);
227
252
 
228
- // 12a. Write theme.css + favicon.svg (idempotent, outside index lock — separate files)
229
- await writeThemeCss(config.stateDir, theme);
253
+ // 12a. Ensure theme.css + favicon.svg (idempotent, outside index lock — separate files)
254
+ await ensureThemeCss(config.stateDir, theme);
230
255
  await writeFaviconSvg(config.stateDir);
231
256
 
232
257
  const fullHtml = wrapDocument({
233
- body: scrubbed.html,
258
+ body: bodyHtml,
234
259
  meta,
235
260
  theme,
236
261
  warnings,
@@ -257,6 +282,7 @@ export function createPublishTool(
257
282
  projectSlug: identity.slug,
258
283
  projectName: identity.name,
259
284
  bodyText,
285
+ inputMode,
260
286
  };
261
287
 
262
288
  const lockPath = join(config.stateDir, ".index.lock");
@@ -336,6 +362,7 @@ export function createPublishTool(
336
362
  portMax: config.portMax,
337
363
  idleTimeoutMs: config.idleTimeoutMs,
338
364
  hostname: config.hostname,
365
+ theme,
339
366
  });
340
367
  if (maybeInfo !== null) {
341
368
  serverInfo = maybeInfo;
@@ -1,15 +1,115 @@
1
- // Tool handler for cesium_styleguide — returns the full CSS reference page as a string.
1
+ // Tool handler for cesium_styleguide — returns a markdown reference generated from the block catalog.
2
2
 
3
- import { readFile } from "node:fs/promises";
4
- import { dirname, join } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
3
  import { tool } from "@opencode-ai/plugin";
7
4
  import type { PluginInput } from "@opencode-ai/plugin";
5
+ import { blockCatalog, blockTypes } from "../render/blocks/catalog.ts";
6
+ import { renderBlock } from "../render/blocks/render.ts";
7
+ import type { RenderCtx, SectionCounter } from "../render/blocks/render.ts";
8
8
 
9
- const STYLEGUIDE_PATH = join(
10
- dirname(fileURLToPath(import.meta.url)),
11
- "../../assets/styleguide.html",
12
- );
9
+ function makeCtx(): RenderCtx {
10
+ const counter: SectionCounter = { value: 1 };
11
+ return { sectionCounter: counter, depth: 0, path: "blocks[0]" };
12
+ }
13
+
14
+ /** Escape a string for safe insertion inside a markdown fenced code block. */
15
+ function escapeForCodeFence(s: string): string {
16
+ // Prevent accidental fence closing — replace ``` with ` `` ` (rare in practice)
17
+ return s.replace(/```/g, "` `` `");
18
+ }
19
+
20
+ /** Generate the full markdown reference from the catalog. Deterministic — same catalog → same output. */
21
+ export function generateStyleguideMarkdown(): string {
22
+ const lines: string[] = [];
23
+
24
+ lines.push("# Cesium publishing reference");
25
+ lines.push("");
26
+ lines.push("## Two input modes");
27
+ lines.push("");
28
+ lines.push(
29
+ "`cesium_publish` accepts either `blocks: Block[]` (preferred) or `html: string`" +
30
+ " (escape valve). Provide exactly one.",
31
+ );
32
+ lines.push("");
33
+ lines.push(
34
+ "Prefer `blocks` for plans, reviews, reports, explainers, comparisons, audits, design docs." +
35
+ " Use `html` only for whole-document bespoke layouts (custom hero, non-standard grid," +
36
+ " experimental visual essay). For isolated bespoke regions, stay in `blocks` and use" +
37
+ " `raw_html` or `diagram`.",
38
+ );
39
+ lines.push("");
40
+ lines.push("## Block reference");
41
+ lines.push("");
42
+
43
+ for (const blockType of blockTypes) {
44
+ const entry = blockCatalog[blockType];
45
+
46
+ lines.push(`### \`${entry.type}\``);
47
+ lines.push("");
48
+ lines.push(entry.description);
49
+ lines.push("");
50
+
51
+ // Schema as JSON
52
+ lines.push("```json");
53
+ lines.push(escapeForCodeFence(JSON.stringify(entry.schema, null, 2)));
54
+ lines.push("```");
55
+ lines.push("");
56
+
57
+ // Canonical example
58
+ lines.push("Example:");
59
+ lines.push("");
60
+ lines.push("```json");
61
+ lines.push(escapeForCodeFence(JSON.stringify(entry.example, null, 2)));
62
+ lines.push("```");
63
+ lines.push("");
64
+
65
+ // Rendered HTML
66
+ const rendered = entry.renderedExample ?? (() => {
67
+ try {
68
+ return renderBlock(entry.example, makeCtx());
69
+ } catch {
70
+ return "";
71
+ }
72
+ })();
73
+
74
+ if (rendered !== "") {
75
+ lines.push("Renders to:");
76
+ lines.push("");
77
+ lines.push("```html");
78
+ lines.push(escapeForCodeFence(rendered));
79
+ lines.push("```");
80
+ lines.push("");
81
+ }
82
+ }
83
+
84
+ lines.push(
85
+ "## Markdown subset (inside `prose`, `tldr`, `callout.markdown`, list items, table cells)",
86
+ );
87
+ lines.push("");
88
+ lines.push(
89
+ "- Block: paragraph, `-` lists, `1.` lists, `>` blockquote, `---` rule, hard break (two-space EOL).",
90
+ );
91
+ lines.push(
92
+ "- Inline: `**bold**`, `*italic*`, `` `code` ``, `[text](href)` (relative or anchor only).",
93
+ );
94
+ lines.push(
95
+ "- HTML safelist: `<kbd>`, `<span class=\"pill\">`, `<span class=\"tag\">`. Anything else is escaped.",
96
+ );
97
+ lines.push("");
98
+ lines.push("## When to reach for raw_html / diagram");
99
+ lines.push("");
100
+ lines.push(
101
+ "- `diagram` — inline SVG visualizations or bespoke composed HTML diagrams.",
102
+ );
103
+ lines.push(
104
+ "- `raw_html` — anything genuinely creative that doesn't fit a known block type." +
105
+ " Include a `purpose` string describing what you're building.",
106
+ );
107
+ lines.push(
108
+ "- Critique flags raw_html overuse (>2 blocks or >30% of body characters).",
109
+ );
110
+
111
+ return lines.join("\n");
112
+ }
13
113
 
14
114
  export function createStyleguideTool(_ctx: PluginInput): ReturnType<typeof tool> {
15
115
  return tool({
@@ -17,7 +117,7 @@ export function createStyleguideTool(_ctx: PluginInput): ReturnType<typeof tool>
17
117
  "Returns the cesium HTML design system reference page (CSS classes with example usage). Call this once at the start of writing a complex artifact to internalize the available components.",
18
118
  args: {},
19
119
  async execute(_args, _context) {
20
- return await readFile(STYLEGUIDE_PATH, "utf8");
120
+ return generateStyleguideMarkdown();
21
121
  },
22
122
  });
23
123
  }