kitfly 0.2.0 → 0.2.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.
Files changed (94) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/VERSION +1 -1
  3. package/dist/_raw/content/reference/slides-authoring-guidelines.md +129 -0
  4. package/dist/_raw/content/reference.md +1 -0
  5. package/dist/content/deployment/preflight.html +4 -4
  6. package/dist/content/deployment/recipes/aws-s3.html +4 -4
  7. package/dist/content/deployment/recipes/cloudflare-pages.html +4 -4
  8. package/dist/content/deployment/recipes/cloudflare-r2.html +4 -4
  9. package/dist/content/deployment/recipes/fly-io.html +4 -4
  10. package/dist/content/deployment/recipes/github-pages.html +4 -4
  11. package/dist/content/deployment/recipes/netlify.html +4 -4
  12. package/dist/content/deployment/recipes/vercel.html +4 -4
  13. package/dist/content/deployment/secrets-and-env-vars.html +4 -4
  14. package/dist/content/deployment.html +4 -4
  15. package/dist/content/guide/approaches.html +4 -4
  16. package/dist/content/guide/features.html +4 -4
  17. package/dist/content/guide/getting-started.html +4 -4
  18. package/dist/content/guide/kitfly-overview.html +4 -4
  19. package/dist/content/reference/configuration.html +4 -4
  20. package/dist/content/reference/design-catalog.html +4 -4
  21. package/dist/content/reference/environment-variables.html +4 -4
  22. package/dist/content/reference/glossary.html +4 -4
  23. package/dist/content/reference/key-concepts.html +4 -4
  24. package/dist/content/reference/plugins.html +4 -4
  25. package/dist/content/reference/slides-authoring-guidelines.html +418 -0
  26. package/dist/content/reference/structure.html +4 -4
  27. package/dist/content/reference.html +5 -4
  28. package/dist/content/templates/crucible.html +4 -4
  29. package/dist/content/templates/handbook.html +4 -4
  30. package/dist/content/templates/minimal.html +4 -4
  31. package/dist/content/templates/overview.html +4 -4
  32. package/dist/content/templates/pipeline.html +4 -4
  33. package/dist/content/templates/productbook.html +4 -4
  34. package/dist/content/templates/runbook.html +4 -4
  35. package/dist/content/templates/servicebook.html +4 -4
  36. package/dist/content-index.json +11 -2
  37. package/dist/docs/decisions/ADR-0001-minimalist-site-code.html +4 -4
  38. package/dist/docs/decisions/ADR-0002-ai-accessibility.html +4 -4
  39. package/dist/docs/decisions/ADR-0003-single-file-bundle.html +4 -4
  40. package/dist/docs/decisions/ADR-0004-bun-runtime.html +4 -4
  41. package/dist/docs/decisions/ADR-0005-plugin-contract-and-distribution.html +4 -4
  42. package/dist/docs/decisions/DDR-0001-viewport-locked-layout.html +4 -4
  43. package/dist/docs/decisions/DDR-0002-theme-system.html +4 -4
  44. package/dist/docs/decisions/DDR-0003-bounded-logo-slot.html +4 -4
  45. package/dist/docs/decisions/DDR-0004-slides-rendering-model.html +4 -4
  46. package/dist/docs/decisions/DDR-0005-deterministic-layout-boundary.html +4 -4
  47. package/dist/docs/userguide/cli/build.html +4 -4
  48. package/dist/docs/userguide/cli/bundle.html +4 -4
  49. package/dist/docs/userguide/cli/dev.html +4 -4
  50. package/dist/docs/userguide/cli/init.html +4 -4
  51. package/dist/docs/userguide/cli/servers.html +4 -4
  52. package/dist/docs/userguide/cli/stop.html +4 -4
  53. package/dist/docs/userguide/cli/update.html +4 -4
  54. package/dist/docs/userguide/cli/version.html +4 -4
  55. package/dist/docs/userguide/cli.html +4 -4
  56. package/dist/docs/userguide/sharing.html +4 -4
  57. package/dist/index.html +4 -4
  58. package/dist/llms.txt +3 -3
  59. package/dist/provenance.json +4 -4
  60. package/dist/schemas/plugin-registry.schema.html +4 -4
  61. package/dist/schemas/plugin-schemas-notes.html +4 -4
  62. package/dist/schemas/plugin.schema.html +4 -4
  63. package/dist/schemas/plugins.schema.html +4 -4
  64. package/dist/schemas/v0/common.schema.html +4 -4
  65. package/dist/schemas/v0/plugin-registry.schema.html +4 -4
  66. package/dist/schemas/v0/plugin.schema.html +4 -4
  67. package/dist/schemas/v0/plugins.schema.html +4 -4
  68. package/dist/schemas/v0/site.schema.html +4 -4
  69. package/dist/schemas/v0/theme.schema.html +4 -4
  70. package/dist/schemas.html +4 -4
  71. package/package.json +1 -1
  72. package/plugins-dist/slides-visuals.css +166 -0
  73. package/plugins-dist/slides-visuals.js +124 -33
  74. package/registry/plugins.yaml +5 -5
  75. package/scripts/build.ts +4 -1
  76. package/scripts/bundle.ts +4 -1
  77. package/scripts/dev.ts +100 -12
  78. package/src/__tests__/build.test.ts +65 -3
  79. package/src/__tests__/dev-plugin-errors.test.ts +20 -0
  80. package/src/__tests__/fixtures/fences/slides-visuals/invalid/flow-branching-no-source.md +5 -0
  81. package/src/__tests__/fixtures/fences/slides-visuals/invalid/flow-converging-no-target.md +6 -0
  82. package/src/__tests__/fixtures/fences/slides-visuals/invalid/staircase-empty-steps.md +3 -0
  83. package/src/__tests__/fixtures/fences/slides-visuals/invalid/timeline-horizontal-no-events.md +2 -0
  84. package/src/__tests__/fixtures/fences/slides-visuals/valid/flow-branching-no-split.md +7 -0
  85. package/src/__tests__/fixtures/fences/slides-visuals/valid/flow-branching.md +8 -0
  86. package/src/__tests__/fixtures/fences/slides-visuals/valid/flow-converging-no-merge.md +7 -0
  87. package/src/__tests__/fixtures/fences/slides-visuals/valid/flow-converging.md +8 -0
  88. package/src/__tests__/fixtures/fences/slides-visuals/valid/staircase-down.md +7 -0
  89. package/src/__tests__/fixtures/fences/slides-visuals/valid/staircase.md +8 -0
  90. package/src/__tests__/fixtures/fences/slides-visuals/valid/timeline-horizontal.md +9 -0
  91. package/src/__tests__/fixtures/fences/slides-visuals/valid/timeline-vertical.md +10 -0
  92. package/src/__tests__/shared.test.ts +23 -0
  93. package/src/__tests__/slides-visuals-runtime-regressions.bun.test.ts +33 -0
  94. package/src/shared.ts +36 -0
package/scripts/dev.ts CHANGED
@@ -18,7 +18,13 @@ import { readdir, readFile, stat } from "node:fs/promises";
18
18
  import { basename, extname, join, resolve } from "node:path";
19
19
  import { marked, Renderer } from "marked";
20
20
  import { ENGINE_ASSETS_DIR, ENGINE_SITE_DIR } from "../src/engine.ts";
21
- import { loadPluginInjections } from "../src/plugin-loader.ts";
21
+ import {
22
+ loadPluginInjections,
23
+ PluginConfigError,
24
+ PluginIntegrityError,
25
+ PluginNetworkError,
26
+ PluginPolicyError,
27
+ } from "../src/plugin-loader.ts";
22
28
  import {
23
29
  buildBreadcrumbsSimple,
24
30
  buildFooter,
@@ -37,6 +43,7 @@ import {
37
43
  envString,
38
44
  // Formatting
39
45
  escapeHtml,
46
+ filterUnknownSlidesVisualsTypeDiagnostics,
40
47
  // Provenance
41
48
  generateProvenance,
42
49
  // YAML/Config parsing
@@ -75,6 +82,60 @@ let daemonLog: {
75
82
  error: (msg: string) => void;
76
83
  } | null = null;
77
84
 
85
+ function isPluginLoaderError(error: unknown): error is Error {
86
+ return (
87
+ error instanceof PluginConfigError ||
88
+ error instanceof PluginIntegrityError ||
89
+ error instanceof PluginPolicyError ||
90
+ error instanceof PluginNetworkError
91
+ );
92
+ }
93
+
94
+ function pluginVersionMismatchHint(message: string): string {
95
+ const m = message.match(/^Plugin ([a-z0-9-]+) version mismatch: ([^ ]+) != ([^ ]+)$/i);
96
+ if (!m) return "";
97
+ const pluginId = m[1];
98
+ const expected = m[3];
99
+ return `Update <code>kitfly.plugins.yaml</code> to <code>${pluginId}@${expected}</code>, then refresh.`;
100
+ }
101
+
102
+ export function buildDevPluginErrorHtml(message: string): string {
103
+ const hint = pluginVersionMismatchHint(message);
104
+ const safeMessage = escapeHtml(message);
105
+ const hintBlock = hint
106
+ ? `<p>${hint}</p>`
107
+ : "<p>Check <code>kitfly.plugins.yaml</code> and <code>registry/plugins.yaml</code>, then refresh.</p>";
108
+ return `<!doctype html>
109
+ <html lang="en">
110
+ <head>
111
+ <meta charset="utf-8">
112
+ <meta name="viewport" content="width=device-width, initial-scale=1">
113
+ <title>Plugin Configuration Error</title>
114
+ <style>
115
+ body { font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 0; background: #0b1020; color: #e8ecf3; }
116
+ main { max-width: 820px; margin: 8vh auto; padding: 1.25rem; }
117
+ .card { background: #131a2e; border: 1px solid #2a3557; border-radius: 12px; padding: 1rem 1.1rem; }
118
+ h1 { margin: 0 0 0.75rem; font-size: 1.25rem; }
119
+ p, li { line-height: 1.5; }
120
+ code { background: #0e1528; padding: 0.08rem 0.3rem; border-radius: 6px; border: 1px solid #2a3557; }
121
+ pre { margin: 0.8rem 0 0; padding: 0.75rem; background: #0e1528; border: 1px solid #2a3557; border-radius: 8px; overflow: auto; }
122
+ .muted { color: #b5bfd2; font-size: 0.92rem; }
123
+ </style>
124
+ </head>
125
+ <body>
126
+ <main>
127
+ <div class="card">
128
+ <h1>Plugin setup error</h1>
129
+ <p>Kitfly could not load one or more plugins for dev preview.</p>
130
+ ${hintBlock}
131
+ <pre><code>${safeMessage}</code></pre>
132
+ <p class="muted">After updating config, refresh this page. No dev server restart required.</p>
133
+ </div>
134
+ </main>
135
+ </body>
136
+ </html>`;
137
+ }
138
+
78
139
  /** Log info — uses structured logger in daemon mode, console.log otherwise */
79
140
  function logInfo(msg: string): void {
80
141
  if (daemonLog) daemonLog.info(msg);
@@ -367,7 +428,9 @@ async function renderSlidesPage(
367
428
  let inner = "";
368
429
  if (slide.kind === "markdown") {
369
430
  if (validateFences) {
370
- const diagnostics = validateSlidesVisualsFences(slide.body);
431
+ const diagnostics = filterUnknownSlidesVisualsTypeDiagnostics(
432
+ validateSlidesVisualsFences(slide.body),
433
+ );
371
434
  if (diagnostics.length) {
372
435
  const msg = diagnostics
373
436
  .slice(0, 12)
@@ -854,19 +917,44 @@ async function main() {
854
917
  return new Response("Not found", { status: 404 });
855
918
  }
856
919
 
857
- // Wrap with request logging middleware when in structured log mode
858
- const fetch = daemonLog
859
- ? async (req: Request) => {
860
- const start = performance.now();
861
- const response = await handleRequest(req);
920
+ // Wrap with request logging + friendly plugin errors.
921
+ const fetch = async (req: Request) => {
922
+ const start = performance.now();
923
+ const url = new URL(req.url);
924
+ try {
925
+ const response = await handleRequest(req);
926
+ if (daemonLog && url.pathname !== "/__reload") {
862
927
  const duration = (performance.now() - start).toFixed(0);
863
- const url = new URL(req.url);
864
- if (url.pathname !== "/__reload") {
865
- daemonLog?.info(`${req.method} ${url.pathname} ${response.status} ${duration}ms`);
928
+ daemonLog.info(`${req.method} ${url.pathname} ${response.status} ${duration}ms`);
929
+ }
930
+ return response;
931
+ } catch (error) {
932
+ const duration = (performance.now() - start).toFixed(0);
933
+ const message = error instanceof Error ? error.message : String(error);
934
+ if (isPluginLoaderError(error)) {
935
+ if (daemonLog && url.pathname !== "/__reload") {
936
+ daemonLog.warn(
937
+ `${req.method} ${url.pathname} 500 ${duration}ms plugin error: ${message}`,
938
+ );
939
+ } else if (!daemonLog) {
940
+ logWarn(`Plugin error: ${message}`);
866
941
  }
867
- return response;
942
+ return new Response(buildDevPluginErrorHtml(message), {
943
+ status: 500,
944
+ headers: { "Content-Type": "text/html; charset=utf-8" },
945
+ });
868
946
  }
869
- : handleRequest;
947
+ if (daemonLog && url.pathname !== "/__reload") {
948
+ daemonLog.error(`${req.method} ${url.pathname} 500 ${duration}ms ${message}`);
949
+ } else if (!daemonLog) {
950
+ console.error(error);
951
+ }
952
+ return new Response("Internal server error", {
953
+ status: 500,
954
+ headers: { "Content-Type": "text/plain; charset=utf-8" },
955
+ });
956
+ }
957
+ };
870
958
 
871
959
  // Create server
872
960
  Bun.serve({
@@ -365,7 +365,7 @@ plugins:
365
365
  slides-visuals:
366
366
  name: "Slides Visuals"
367
367
  description: "Test visuals"
368
- version: "0.2.0"
368
+ version: "0.2.1"
369
369
  contract: "1"
370
370
  kitfly: ">=0.2.0 <1.0.0"
371
371
  license: MIT
@@ -383,15 +383,77 @@ plugins:
383
383
 
384
384
  await writeFile(
385
385
  join(siteDir, "kitfly.plugins.yaml"),
386
- "plugins:\n - slides-visuals@0.2.0\n",
386
+ "plugins:\n - slides-visuals@0.2.1\n",
387
387
  "utf-8",
388
388
  );
389
389
 
390
390
  await build({ folder: siteDir, out: outDir });
391
391
 
392
392
  const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
393
- expect(html).toContain('data-kitfly-plugin="slides-visuals@0.2.0"');
393
+ expect(html).toContain('data-kitfly-plugin="slides-visuals@0.2.1"');
394
394
  expect(html).toContain(css);
395
395
  expect(html).toContain(js);
396
396
  });
397
+
398
+ it("ignores unknown slides-visuals block types while enforcing known contracts", async () => {
399
+ const siteDir = await makeTempDir();
400
+ const outDir = "out";
401
+ await writeSiteYaml(siteDir, { mode: "slides" });
402
+ await writeMd(
403
+ siteDir,
404
+ "docs/deck.md",
405
+ `# Title
406
+
407
+ :::future-thing
408
+ note: this should pass through
409
+ :::
410
+
411
+ :::kpi
412
+ label: Uptime
413
+ value: 99.95%
414
+ :::
415
+ `,
416
+ );
417
+
418
+ const js = "console.log('slides visuals');";
419
+ const css = ".kitfly-visual{border:1px solid red;}";
420
+ await mkdir(join(siteDir, "plugins-dist"), { recursive: true });
421
+ await writeFile(join(siteDir, "plugins-dist", "slides-visuals.js"), js, "utf-8");
422
+ await writeFile(join(siteDir, "plugins-dist", "slides-visuals.css"), css, "utf-8");
423
+ await mkdir(join(siteDir, "registry"), { recursive: true });
424
+ await writeFile(
425
+ join(siteDir, "registry", "plugins.yaml"),
426
+ `version: 1
427
+ updated: "2026-02-15"
428
+ baseUrl: ""
429
+ plugins:
430
+ slides-visuals:
431
+ name: "Slides Visuals"
432
+ description: "Test visuals"
433
+ version: "0.2.1"
434
+ contract: "1"
435
+ kitfly: ">=0.2.0 <1.0.0"
436
+ license: MIT
437
+ verified: true
438
+ modes: ["slides"]
439
+ assets:
440
+ js: "plugins-dist/slides-visuals.js"
441
+ css: "plugins-dist/slides-visuals.css"
442
+ assetSha256:
443
+ js: "sha256:${sha256Hex(js)}"
444
+ css: "sha256:${sha256Hex(css)}"
445
+ `,
446
+ "utf-8",
447
+ );
448
+ await writeFile(
449
+ join(siteDir, "kitfly.plugins.yaml"),
450
+ "plugins:\n - slides-visuals@0.2.1\n",
451
+ "utf-8",
452
+ );
453
+
454
+ await expect(build({ folder: siteDir, out: outDir })).resolves.toBeUndefined();
455
+ const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
456
+ expect(html).toContain('data-kitfly-plugin="slides-visuals@0.2.1"');
457
+ expect(html).toContain("future-thing");
458
+ });
397
459
  });
@@ -0,0 +1,20 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildDevPluginErrorHtml } from "../../scripts/dev.ts";
3
+
4
+ describe("dev plugin error page", () => {
5
+ it("shows actionable update hint for plugin version mismatch", () => {
6
+ const html = buildDevPluginErrorHtml("Plugin slides-visuals version mismatch: 0.2.0 != 0.2.1");
7
+ expect(html).toContain("Plugin setup error");
8
+ expect(html).toContain("kitfly.plugins.yaml");
9
+ expect(html).toContain("slides-visuals@0.2.1");
10
+ });
11
+
12
+ it("escapes error text and falls back to generic guidance", () => {
13
+ const html = buildDevPluginErrorHtml('Invalid plugin ref: bad"@1.0.0 <x>');
14
+ expect(html).toContain(
15
+ "Check <code>kitfly.plugins.yaml</code> and <code>registry/plugins.yaml</code>",
16
+ );
17
+ expect(html).toContain("bad&quot;@1.0.0 &lt;x&gt;");
18
+ expect(html).not.toContain('bad"@1.0.0 <x>');
19
+ });
20
+ });
@@ -0,0 +1,5 @@
1
+ :::flow-branching
2
+ branches:
3
+ - "API Handler"
4
+ - "Static Files"
5
+ :::
@@ -0,0 +1,6 @@
1
+ :::flow-converging
2
+ sources:
3
+ - "Frontend Logs"
4
+ - "API Logs"
5
+ merge: "Aggregator"
6
+ :::
@@ -0,0 +1,7 @@
1
+ :::flow-branching
2
+ source: "Incoming Request"
3
+ branches:
4
+ - "API Handler"
5
+ - "Static Files"
6
+ - "WebSocket"
7
+ :::
@@ -0,0 +1,8 @@
1
+ :::flow-branching
2
+ source: "Incoming Request"
3
+ split: "Route"
4
+ branches:
5
+ - "API Handler"
6
+ - "Static Files"
7
+ - "WebSocket"
8
+ :::
@@ -0,0 +1,7 @@
1
+ :::flow-converging
2
+ sources:
3
+ - "Frontend Logs"
4
+ - "API Logs"
5
+ - "DB Logs"
6
+ target: "Dashboard"
7
+ :::
@@ -0,0 +1,8 @@
1
+ :::flow-converging
2
+ sources:
3
+ - "Frontend Logs"
4
+ - "API Logs"
5
+ - "DB Logs"
6
+ merge: "Aggregator"
7
+ target: "Dashboard"
8
+ :::
@@ -0,0 +1,7 @@
1
+ :::staircase
2
+ direction: down
3
+ steps:
4
+ - "Optimized"
5
+ - "Managed"
6
+ - "Defined"
7
+ :::
@@ -0,0 +1,8 @@
1
+ :::staircase
2
+ steps:
3
+ - "Ad Hoc"
4
+ - "Repeatable"
5
+ - "Defined"
6
+ - "Managed"
7
+ - "Optimized"
8
+ :::
@@ -0,0 +1,9 @@
1
+ :::timeline-horizontal
2
+ events:
3
+ - label: "Kickoff"
4
+ date: "Jan 2026"
5
+ - label: "Alpha"
6
+ date: "Mar 2026"
7
+ - label: "GA Release"
8
+ date: "Jun 2026"
9
+ :::
@@ -0,0 +1,10 @@
1
+ :::timeline-vertical
2
+ events:
3
+ - label: "Research Phase"
4
+ date: "Q1"
5
+ - label: "Prototype"
6
+ - label: "User Testing"
7
+ date: "Q3"
8
+ - label: "Launch"
9
+ date: "Q4"
10
+ :::
@@ -24,6 +24,7 @@ import {
24
24
  envString,
25
25
  escapeHtml,
26
26
  exists,
27
+ filterUnknownSlidesVisualsTypeDiagnostics,
27
28
  formatDate,
28
29
  generateProvenance,
29
30
  getGitInfo,
@@ -42,6 +43,7 @@ import {
42
43
  stripQuotes,
43
44
  toUrlPath,
44
45
  validatePath,
46
+ validateSlidesVisualsFences,
45
47
  } from "../shared.ts";
46
48
 
47
49
  describe("slugify", () => {
@@ -155,6 +157,27 @@ Still one slide`;
155
157
  });
156
158
  });
157
159
 
160
+ describe("slides-visuals diagnostics filtering", () => {
161
+ it("drops unknown-type diagnostics while preserving schema violations", () => {
162
+ const markdown = `:::future-thing
163
+ foo: bar
164
+ :::
165
+
166
+ :::kpi
167
+ label: Missing value
168
+ :::`;
169
+ const diagnostics = validateSlidesVisualsFences(markdown);
170
+ const filtered = filterUnknownSlidesVisualsTypeDiagnostics(diagnostics);
171
+ expect(
172
+ diagnostics.some((d) => d.message.startsWith("Unknown slides-visuals block type:")),
173
+ ).toBe(true);
174
+ expect(filtered.some((d) => d.message.startsWith("Unknown slides-visuals block type:"))).toBe(
175
+ false,
176
+ );
177
+ expect(filtered.some((d) => d.message.includes("Missing required key: value"))).toBe(true);
178
+ });
179
+ });
180
+
158
181
  describe("segmentSlides", () => {
159
182
  it("uses frontmatter title and class when present", () => {
160
183
  const input = `---
@@ -112,3 +112,36 @@ test("slides-visuals: rowCells parses JSON array strings", async () => {
112
112
  const hooks = await loadHooks();
113
113
  expect(hooks.rowCells('["A", "B", "C"]')).toEqual(["A", "B", "C"]);
114
114
  });
115
+
116
+ test("slides-visuals: absorbed scalar marker preserves preceding item (flow-converging)", async () => {
117
+ const { parseBodyNodesWithFirstLines } = await loadHooks();
118
+
119
+ const out = parseBodyNodesWithFirstLines(
120
+ ["sources:"],
121
+ [],
122
+ new FakeElement("UL", "", [
123
+ new FakeElement("LI", "Frontend Logs\ntarget: Dashboard"),
124
+ new FakeElement("LI", "API Logs"),
125
+ ]),
126
+ "flow-converging",
127
+ );
128
+
129
+ expect(out.target).toBe("Dashboard");
130
+ expect(out.sources).toEqual(["Frontend Logs", "API Logs"]);
131
+ });
132
+
133
+ test("slides-visuals: parses object list items for timeline events", async () => {
134
+ const { parseBodyNodesWithFirstLines } = await loadHooks();
135
+
136
+ const out = parseBodyNodesWithFirstLines(
137
+ ["events:"],
138
+ [],
139
+ new FakeElement("UL", "", [
140
+ new FakeElement("LI", "label: Kickoff\ndate: Jan 2026"),
141
+ new FakeElement("LI", "label: Alpha"),
142
+ ]),
143
+ "timeline-horizontal",
144
+ );
145
+
146
+ expect(out.events).toEqual([{ label: "Kickoff", date: "Jan 2026" }, { label: "Alpha" }]);
147
+ });
package/src/shared.ts CHANGED
@@ -506,6 +506,11 @@ const SLIDES_VISUALS_TYPES = new Set([
506
506
  "layer-cake",
507
507
  "pyramid",
508
508
  "funnel",
509
+ "timeline-horizontal",
510
+ "timeline-vertical",
511
+ "flow-branching",
512
+ "flow-converging",
513
+ "staircase",
509
514
  ]);
510
515
 
511
516
  const SLIDES_VISUALS_RULES: Record<
@@ -564,6 +569,31 @@ const SLIDES_VISUALS_RULES: Record<
564
569
  scalars: [],
565
570
  lists: { stages: { kind: "strings" } },
566
571
  },
572
+ "timeline-horizontal": {
573
+ required: ["events"],
574
+ scalars: [],
575
+ lists: { events: { kind: "objects", fields: ["label"], optional: ["date"] } },
576
+ },
577
+ "timeline-vertical": {
578
+ required: ["events"],
579
+ scalars: [],
580
+ lists: { events: { kind: "objects", fields: ["label"], optional: ["date"] } },
581
+ },
582
+ "flow-branching": {
583
+ required: ["source", "branches"],
584
+ scalars: ["source", "split"],
585
+ lists: { branches: { kind: "strings" } },
586
+ },
587
+ "flow-converging": {
588
+ required: ["sources", "target"],
589
+ scalars: ["target", "merge"],
590
+ lists: { sources: { kind: "strings" } },
591
+ },
592
+ staircase: {
593
+ required: ["steps"],
594
+ scalars: ["direction"],
595
+ lists: { steps: { kind: "strings" } },
596
+ },
567
597
  };
568
598
 
569
599
  /**
@@ -754,6 +784,12 @@ export function validateSlidesVisualsFences(markdown: string): SlidesVisualsFenc
754
784
  return diagnostics;
755
785
  }
756
786
 
787
+ export function filterUnknownSlidesVisualsTypeDiagnostics(
788
+ diagnostics: SlidesVisualsFenceDiagnostic[],
789
+ ): SlidesVisualsFenceDiagnostic[] {
790
+ return diagnostics.filter((d) => !d.message.startsWith("Unknown slides-visuals block type:"));
791
+ }
792
+
757
793
  /**
758
794
  * Split markdown content into slide chunks using explicit delimiter.
759
795
  * Delimiter lines inside fenced code blocks are ignored.