kitfly 0.2.3 → 0.2.4

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 (107) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +13 -11
  3. package/VERSION +1 -1
  4. package/dist/_raw/content/reference/gantt-widget.md +468 -0
  5. package/dist/_raw/content/reference/plugins.md +157 -2
  6. package/dist/content/deployment/preflight.html +5 -6
  7. package/dist/content/deployment/recipes/aws-s3.html +5 -6
  8. package/dist/content/deployment/recipes/cloudflare-pages.html +5 -6
  9. package/dist/content/deployment/recipes/cloudflare-r2.html +5 -6
  10. package/dist/content/deployment/recipes/fly-io.html +5 -6
  11. package/dist/content/deployment/recipes/github-pages.html +5 -6
  12. package/dist/content/deployment/recipes/netlify.html +5 -6
  13. package/dist/content/deployment/recipes/vercel.html +5 -6
  14. package/dist/content/deployment/secrets-and-env-vars.html +5 -6
  15. package/dist/content/deployment.html +5 -6
  16. package/dist/content/guide/approaches.html +5 -6
  17. package/dist/content/guide/branding.html +5 -6
  18. package/dist/content/guide/data-driven-content.html +5 -6
  19. package/dist/content/guide/features.html +5 -6
  20. package/dist/content/guide/getting-started.html +5 -6
  21. package/dist/content/guide/kitfly-overview.html +5 -6
  22. package/dist/content/reference/configuration.html +5 -6
  23. package/dist/content/reference/design-catalog.html +5 -6
  24. package/dist/content/reference/environment-variables.html +5 -6
  25. package/dist/content/reference/gantt-widget.html +899 -0
  26. package/dist/content/reference/glossary.html +5 -6
  27. package/dist/content/reference/key-concepts.html +5 -6
  28. package/dist/content/reference/plugins.html +245 -9
  29. package/dist/content/reference/slides-authoring-guidelines.html +5 -6
  30. package/dist/content/reference/structure.html +5 -6
  31. package/dist/content/reference.html +5 -6
  32. package/dist/content/templates/crucible.html +5 -6
  33. package/dist/content/templates/handbook.html +5 -6
  34. package/dist/content/templates/minimal.html +5 -6
  35. package/dist/content/templates/overview.html +5 -6
  36. package/dist/content/templates/pipeline.html +5 -6
  37. package/dist/content/templates/productbook.html +5 -6
  38. package/dist/content/templates/runbook.html +5 -6
  39. package/dist/content/templates/servicebook.html +5 -6
  40. package/dist/content-index.json +10 -2
  41. package/dist/docs/decisions/ADR-0001-minimalist-site-code.html +5 -6
  42. package/dist/docs/decisions/ADR-0002-ai-accessibility.html +5 -6
  43. package/dist/docs/decisions/ADR-0003-single-file-bundle.html +5 -6
  44. package/dist/docs/decisions/ADR-0004-bun-runtime.html +5 -6
  45. package/dist/docs/decisions/ADR-0005-plugin-contract-and-distribution.html +5 -6
  46. package/dist/docs/decisions/ADR-0006-data-driven-content.html +5 -6
  47. package/dist/docs/decisions/DDR-0001-viewport-locked-layout.html +5 -6
  48. package/dist/docs/decisions/DDR-0002-theme-system.html +5 -6
  49. package/dist/docs/decisions/DDR-0003-bounded-logo-slot.html +5 -6
  50. package/dist/docs/decisions/DDR-0004-slides-rendering-model.html +5 -6
  51. package/dist/docs/decisions/DDR-0005-deterministic-layout-boundary.html +5 -6
  52. package/dist/docs/userguide/cli/build.html +5 -6
  53. package/dist/docs/userguide/cli/bundle.html +5 -6
  54. package/dist/docs/userguide/cli/dev.html +5 -6
  55. package/dist/docs/userguide/cli/init.html +5 -6
  56. package/dist/docs/userguide/cli/servers.html +5 -6
  57. package/dist/docs/userguide/cli/stop.html +5 -6
  58. package/dist/docs/userguide/cli/update.html +5 -6
  59. package/dist/docs/userguide/cli/version.html +5 -6
  60. package/dist/docs/userguide/cli.html +5 -6
  61. package/dist/docs/userguide/sharing.html +5 -6
  62. package/dist/index.html +5 -6
  63. package/dist/llms.txt +3 -3
  64. package/dist/provenance.json +4 -5
  65. package/dist/reports/license-inventory.csv +199 -0
  66. package/dist/schemas/plugin-registry.schema.html +5 -6
  67. package/dist/schemas/plugin-schemas-notes.html +5 -6
  68. package/dist/schemas/plugin.schema.html +5 -6
  69. package/dist/schemas/plugins.schema.html +5 -6
  70. package/dist/schemas/v0/common.schema.html +5 -6
  71. package/dist/schemas/v0/plugin-registry.schema.html +5 -6
  72. package/dist/schemas/v0/plugin.schema.html +5 -6
  73. package/dist/schemas/v0/plugins.schema.html +5 -6
  74. package/dist/schemas/v0/site.schema.html +5 -6
  75. package/dist/schemas/v0/theme.schema.html +5 -6
  76. package/dist/schemas.html +5 -6
  77. package/package.json +1 -1
  78. package/plugins-dist/planning-visuals.css +261 -0
  79. package/plugins-dist/planning-visuals.js +669 -0
  80. package/registry/plugins.yaml +15 -1
  81. package/scripts/build-all.ts +5 -0
  82. package/scripts/build.ts +73 -11
  83. package/scripts/bundle.ts +73 -10
  84. package/scripts/dev.ts +49 -5
  85. package/scripts/embed-docs.ts +119 -0
  86. package/src/__tests__/build.test.ts +124 -0
  87. package/src/__tests__/bundle.test.ts +61 -0
  88. package/src/__tests__/docs.test.ts +117 -0
  89. package/src/__tests__/fixtures/fences/planning-visuals/invalid/bad-month-format.md +10 -0
  90. package/src/__tests__/fixtures/fences/planning-visuals/invalid/marker-format-mismatch.md +13 -0
  91. package/src/__tests__/fixtures/fences/planning-visuals/invalid/milestone-format-mismatch.md +13 -0
  92. package/src/__tests__/fixtures/fences/planning-visuals/invalid/missing-tracks.md +5 -0
  93. package/src/__tests__/fixtures/fences/planning-visuals/invalid/track-reversed.md +10 -0
  94. package/src/__tests__/fixtures/fences/planning-visuals/valid/markers-basic.md +15 -0
  95. package/src/__tests__/fixtures/fences/planning-visuals/valid/markers-no-milestones.md +13 -0
  96. package/src/__tests__/fixtures/fences/planning-visuals/valid/month-basic.md +16 -0
  97. package/src/__tests__/fixtures/fences/planning-visuals/valid/no-milestones.md +10 -0
  98. package/src/__tests__/fixtures/fences/planning-visuals/valid/week-basic.md +20 -0
  99. package/src/__tests__/planning-visuals-fence-contract.test.ts +28 -0
  100. package/src/__tests__/planning-visuals-runtime-regressions.bun.test.ts +68 -0
  101. package/src/__tests__/planning-visuals-runtime.bun.test.ts +192 -0
  102. package/src/__tests__/shared.test.ts +121 -0
  103. package/src/cli.ts +113 -18
  104. package/src/commands/docs.ts +71 -0
  105. package/src/generated/embedded-docs.ts +2384 -0
  106. package/src/server-registry.ts +50 -10
  107. package/src/shared.ts +449 -25
@@ -70,6 +70,11 @@ function getVersion(): string {
70
70
  function main(): void {
71
71
  const version = getVersion();
72
72
 
73
+ // Generate embedded docs before compiling binaries
74
+ console.log("Generating embedded documentation...");
75
+ execSync("bun scripts/embed-docs.ts", { stdio: "inherit" });
76
+ console.log();
77
+
73
78
  console.log(`Building ${TARGETS.length} binaries for '${BINARY_NAME}' v${version}`);
74
79
  console.log(` Entry point: ${ENTRY_POINT}`);
75
80
  console.log(` Output dir: ${OUT_DIR}`);
package/scripts/build.ts CHANGED
@@ -29,6 +29,7 @@ import {
29
29
  type ContentFile,
30
30
  collectFiles,
31
31
  // Navigation/template building
32
+ collectPlanningVisualsContainmentWarnings,
32
33
  collectSlides,
33
34
  envBool,
34
35
  // Config helpers
@@ -38,6 +39,7 @@ import {
38
39
  // File utilities
39
40
  exists,
40
41
  filterByProfile,
42
+ filterUnknownPlanningVisualsTypeDiagnostics,
41
43
  filterUnknownSlidesVisualsTypeDiagnostics,
42
44
  // Provenance
43
45
  generateProvenance,
@@ -59,6 +61,7 @@ import {
59
61
  type SiteConfig,
60
62
  slugify,
61
63
  validatePath,
64
+ validatePlanningVisualsFences,
62
65
  validateSlidesVisualsFences,
63
66
  } from "../src/shared.ts";
64
67
  import { generateThemeCSS, getPrismUrls, loadTheme, type Theme } from "../src/theme.ts";
@@ -154,6 +157,27 @@ async function resolveSiteAssetsDir(siteRoot: string): Promise<string | null> {
154
157
  return null;
155
158
  }
156
159
 
160
+ type FenceValidationFlags = {
161
+ slidesVisuals: boolean;
162
+ planningVisuals: boolean;
163
+ };
164
+
165
+ async function getFenceValidationFlags(root: string): Promise<FenceValidationFlags> {
166
+ try {
167
+ const raw = await readFile(join(root, "kitfly.plugins.yaml"), "utf-8");
168
+ const parsed = parseYaml(raw) as unknown as Record<string, unknown>;
169
+ const enabled = Array.isArray(parsed?.plugins) ? (parsed.plugins as unknown[]) : [];
170
+ return {
171
+ slidesVisuals: enabled.some((p) => typeof p === "string" && p.startsWith("slides-visuals@")),
172
+ planningVisuals: enabled.some(
173
+ (p) => typeof p === "string" && p.startsWith("planning-visuals@"),
174
+ ),
175
+ };
176
+ } catch {
177
+ return { slidesVisuals: false, planningVisuals: false };
178
+ }
179
+ }
180
+
157
181
  function computePathPrefix(urlKey: string): string {
158
182
  const clean = urlKey.replace(/^\/+/, "").replace(/\.html$/, "");
159
183
  if (!clean) return "./";
@@ -217,6 +241,7 @@ async function renderFile(
217
241
  config: SiteConfig,
218
242
  theme: Theme,
219
243
  plugins: PluginInjections,
244
+ fenceValidation: FenceValidationFlags,
220
245
  ): Promise<string> {
221
246
  const uiVersion = provenance.version ? `v${provenance.version}` : "unversioned";
222
247
  const content = await readFile(filePath, "utf-8");
@@ -242,6 +267,22 @@ async function renderFile(
242
267
  title = frontmatter.title as string;
243
268
  }
244
269
  pageMeta = buildPageMeta(frontmatter);
270
+ if (fenceValidation.planningVisuals) {
271
+ const diagnostics = filterUnknownPlanningVisualsTypeDiagnostics(
272
+ validatePlanningVisualsFences(body),
273
+ );
274
+ if (diagnostics.length) {
275
+ const msg = diagnostics
276
+ .slice(0, 12)
277
+ .map((d) => ` - ${filePath}:${d.line} ${d.message}`)
278
+ .join("\n");
279
+ throw new Error(`planning-visuals fence contract violations:\n${msg}`);
280
+ }
281
+ const warnings = collectPlanningVisualsContainmentWarnings(body);
282
+ for (const warning of warnings.slice(0, 12)) {
283
+ console.warn(` ⚠ ${filePath}:${warning.line} ${warning.message}`);
284
+ }
285
+ }
245
286
  htmlContent = marked.parse(body) as string;
246
287
  }
247
288
 
@@ -394,26 +435,18 @@ async function renderSlidesIndex(
394
435
  config: SiteConfig,
395
436
  theme: Theme,
396
437
  plugins: PluginInjections,
438
+ fenceValidation: FenceValidationFlags,
397
439
  ): Promise<string> {
398
440
  const uiVersion = provenance.version ? `v${provenance.version}` : "unversioned";
399
441
  const pathPrefix = "./";
400
442
  const slides = await collectSlides(files, {
401
443
  markdownTransform: (raw, file) => applyDataBindingsForSlides(raw, file.path, config),
402
444
  });
403
- let validateFences = false;
404
- try {
405
- const raw = await readFile(join(ROOT, "kitfly.plugins.yaml"), "utf-8");
406
- const parsed = parseYaml(raw) as unknown as Record<string, unknown>;
407
- const enabled = Array.isArray(parsed?.plugins) ? (parsed.plugins as unknown[]) : [];
408
- validateFences = enabled.some((p) => typeof p === "string" && p.startsWith("slides-visuals@"));
409
- } catch {
410
- // no config, skip
411
- }
412
445
  const renderedSlides = await Promise.all(
413
446
  slides.map(async (slide, i) => {
414
447
  let inner = "";
415
448
  if (slide.kind === "markdown") {
416
- if (validateFences) {
449
+ if (fenceValidation.slidesVisuals) {
417
450
  const diagnostics = filterUnknownSlidesVisualsTypeDiagnostics(
418
451
  validateSlidesVisualsFences(slide.body),
419
452
  );
@@ -425,6 +458,22 @@ async function renderSlidesIndex(
425
458
  throw new Error(`slides-visuals fence contract violations:\n${msg}`);
426
459
  }
427
460
  }
461
+ if (fenceValidation.planningVisuals) {
462
+ const diagnostics = filterUnknownPlanningVisualsTypeDiagnostics(
463
+ validatePlanningVisualsFences(slide.body),
464
+ );
465
+ if (diagnostics.length) {
466
+ const msg = diagnostics
467
+ .slice(0, 12)
468
+ .map((d) => ` - ${slide.sourcePath}:${d.line} ${d.message}`)
469
+ .join("\n");
470
+ throw new Error(`planning-visuals fence contract violations:\n${msg}`);
471
+ }
472
+ const warnings = collectPlanningVisualsContainmentWarnings(slide.body);
473
+ for (const warning of warnings.slice(0, 12)) {
474
+ console.warn(` ⚠ ${slide.sourcePath}:${warning.line} ${warning.message}`);
475
+ }
476
+ }
428
477
  inner = marked.parse(slide.body) as string;
429
478
  } else if (slide.kind === "yaml") {
430
479
  inner = `<pre><code class="language-yaml">${escapeHtml(slide.body)}</code></pre>`;
@@ -581,6 +630,7 @@ async function buildSite() {
581
630
  root: ROOT,
582
631
  mode: config.mode === "slides" ? "slides" : "docs",
583
632
  });
633
+ const fenceValidation = await getFenceValidationFlags(ROOT);
584
634
 
585
635
  // Copy CSS
586
636
  const css = await readFile(await resolveStylesPath(ROOT), "utf-8");
@@ -631,7 +681,15 @@ async function buildSite() {
631
681
  }
632
682
 
633
683
  if (config.mode === "slides") {
634
- const html = await renderSlidesIndex(template, files, provenance, config, theme, plugins);
684
+ const html = await renderSlidesIndex(
685
+ template,
686
+ files,
687
+ provenance,
688
+ config,
689
+ theme,
690
+ plugins,
691
+ fenceValidation,
692
+ );
635
693
  await writeFile(join(DIST, "index.html"), html);
636
694
  console.log(` ✓ index.html (slides mode, ${files.length} source files)`);
637
695
  await generateAIAccessibility(DIST, files, config, provenance);
@@ -650,6 +708,7 @@ async function buildSite() {
650
708
  config,
651
709
  theme,
652
710
  plugins,
711
+ fenceValidation,
653
712
  );
654
713
 
655
714
  // Create output path
@@ -676,6 +735,7 @@ async function buildSite() {
676
735
  config,
677
736
  theme,
678
737
  plugins,
738
+ fenceValidation,
679
739
  );
680
740
  await writeFile(join(DIST, "index.html"), homeHtml);
681
741
  console.log(` ✓ index.html (from ${config.home})`);
@@ -691,6 +751,7 @@ async function buildSite() {
691
751
  config,
692
752
  theme,
693
753
  plugins,
754
+ fenceValidation,
694
755
  );
695
756
  await writeFile(join(DIST, "index.html"), indexHtml);
696
757
  console.log(" ✓ index.html");
@@ -708,6 +769,7 @@ async function buildSite() {
708
769
  config,
709
770
  theme,
710
771
  plugins,
772
+ fenceValidation,
711
773
  );
712
774
  await writeFile(join(DIST, "index.html"), indexHtml);
713
775
  console.log(" ✓ index.html");
package/scripts/bundle.ts CHANGED
@@ -26,9 +26,10 @@ import {
26
26
  buildSectionNav,
27
27
  // Navigation/template building
28
28
  buildSlideNavHierarchical,
29
- // Types
30
29
  type ContentFile,
31
30
  collectFiles,
31
+ // Types
32
+ collectPlanningVisualsContainmentWarnings,
32
33
  collectSlides,
33
34
  envBool,
34
35
  // Config helpers
@@ -36,6 +37,7 @@ import {
36
37
  // Formatting
37
38
  escapeHtml,
38
39
  filterByProfile,
40
+ filterUnknownPlanningVisualsTypeDiagnostics,
39
41
  filterUnknownSlidesVisualsTypeDiagnostics,
40
42
  // YAML/Config parsing
41
43
  loadDataBindings,
@@ -52,6 +54,7 @@ import {
52
54
  type SiteConfig,
53
55
  slugify,
54
56
  validatePath,
57
+ validatePlanningVisualsFences,
55
58
  validateSlidesVisualsFences,
56
59
  } from "../src/shared.ts";
57
60
  import { generateThemeCSS, getPrismUrls, loadTheme } from "../src/theme.ts";
@@ -354,24 +357,35 @@ function buildBundleNav(files: ContentFile[], config: SiteConfig): string {
354
357
  return html;
355
358
  }
356
359
 
357
- async function buildSlidesBundleContent(files: ContentFile[], config: SiteConfig): Promise<string> {
358
- const slides = await collectSlides(files, {
359
- markdownTransform: (raw, file) => applyDataBindingsForSlides(raw, file.path, config),
360
- });
361
- let validateFences = false;
360
+ async function getFenceValidationFlags(root: string): Promise<{
361
+ slidesVisuals: boolean;
362
+ planningVisuals: boolean;
363
+ }> {
362
364
  try {
363
- const raw = await readFile(join(ROOT, "kitfly.plugins.yaml"), "utf-8");
365
+ const raw = await readFile(join(root, "kitfly.plugins.yaml"), "utf-8");
364
366
  const parsed = parseYaml(raw) as unknown as Record<string, unknown>;
365
367
  const enabled = Array.isArray(parsed?.plugins) ? (parsed.plugins as unknown[]) : [];
366
- validateFences = enabled.some((p) => typeof p === "string" && p.startsWith("slides-visuals@"));
368
+ return {
369
+ slidesVisuals: enabled.some((p) => typeof p === "string" && p.startsWith("slides-visuals@")),
370
+ planningVisuals: enabled.some(
371
+ (p) => typeof p === "string" && p.startsWith("planning-visuals@"),
372
+ ),
373
+ };
367
374
  } catch {
368
- // no config, skip
375
+ return { slidesVisuals: false, planningVisuals: false };
369
376
  }
377
+ }
378
+
379
+ async function buildSlidesBundleContent(files: ContentFile[], config: SiteConfig): Promise<string> {
380
+ const slides = await collectSlides(files, {
381
+ markdownTransform: (raw, file) => applyDataBindingsForSlides(raw, file.path, config),
382
+ });
383
+ const fenceValidation = await getFenceValidationFlags(ROOT);
370
384
  const renderedSlides = await Promise.all(
371
385
  slides.map(async (slide, i) => {
372
386
  let inner = "";
373
387
  if (slide.kind === "markdown") {
374
- if (validateFences) {
388
+ if (fenceValidation.slidesVisuals) {
375
389
  const diagnostics = filterUnknownSlidesVisualsTypeDiagnostics(
376
390
  validateSlidesVisualsFences(slide.body),
377
391
  );
@@ -383,6 +397,22 @@ async function buildSlidesBundleContent(files: ContentFile[], config: SiteConfig
383
397
  throw new Error(`slides-visuals fence contract violations:\n${msg}`);
384
398
  }
385
399
  }
400
+ if (fenceValidation.planningVisuals) {
401
+ const diagnostics = filterUnknownPlanningVisualsTypeDiagnostics(
402
+ validatePlanningVisualsFences(slide.body),
403
+ );
404
+ if (diagnostics.length) {
405
+ const msg = diagnostics
406
+ .slice(0, 12)
407
+ .map((d) => ` - ${slide.sourcePath}:${d.line} ${d.message}`)
408
+ .join("\n");
409
+ throw new Error(`planning-visuals fence contract violations:\n${msg}`);
410
+ }
411
+ const warnings = collectPlanningVisualsContainmentWarnings(slide.body);
412
+ for (const warning of warnings.slice(0, 12)) {
413
+ console.warn(` ⚠ ${slide.sourcePath}:${warning.line} ${warning.message}`);
414
+ }
415
+ }
386
416
  inner = marked.parse(slide.body) as string;
387
417
  } else if (slide.kind === "yaml") {
388
418
  inner = `<pre><code class="language-yaml">${escapeHtml(slide.body)}</code></pre>`;
@@ -602,6 +632,7 @@ async function bundle() {
602
632
  navHtml = buildSlideNavHierarchical(slides, config, "slide-1");
603
633
  contentHtml = await buildSlidesBundleContent(files, config);
604
634
  } else {
635
+ const fenceValidation = await getFenceValidationFlags(ROOT);
605
636
  // Build navigation and content sections
606
637
  const sections: Map<string, { id: string; title: string; html: string }[]> = new Map();
607
638
 
@@ -617,6 +648,22 @@ async function bundle() {
617
648
  homePath,
618
649
  config,
619
650
  );
651
+ if (fenceValidation.planningVisuals) {
652
+ const diagnostics = filterUnknownPlanningVisualsTypeDiagnostics(
653
+ validatePlanningVisualsFences(body),
654
+ );
655
+ if (diagnostics.length) {
656
+ const msg = diagnostics
657
+ .slice(0, 12)
658
+ .map((d) => ` - ${homePath}:${d.line} ${d.message}`)
659
+ .join("\n");
660
+ throw new Error(`planning-visuals fence contract violations:\n${msg}`);
661
+ }
662
+ const warnings = collectPlanningVisualsContainmentWarnings(body);
663
+ for (const warning of warnings.slice(0, 12)) {
664
+ console.warn(` ⚠ ${homePath}:${warning.line} ${warning.message}`);
665
+ }
666
+ }
620
667
  const title = (frontmatter.title as string) || "Home";
621
668
  let htmlContent = marked.parse(body) as string;
622
669
  htmlContent = await inlineLocalImages(htmlContent, config);
@@ -654,6 +701,22 @@ async function bundle() {
654
701
  if (frontmatter.description) {
655
702
  description = frontmatter.description as string;
656
703
  }
704
+ if (fenceValidation.planningVisuals) {
705
+ const diagnostics = filterUnknownPlanningVisualsTypeDiagnostics(
706
+ validatePlanningVisualsFences(body),
707
+ );
708
+ if (diagnostics.length) {
709
+ const msg = diagnostics
710
+ .slice(0, 12)
711
+ .map((d) => ` - ${file.path}:${d.line} ${d.message}`)
712
+ .join("\n");
713
+ throw new Error(`planning-visuals fence contract violations:\n${msg}`);
714
+ }
715
+ const warnings = collectPlanningVisualsContainmentWarnings(body);
716
+ for (const warning of warnings.slice(0, 12)) {
717
+ console.warn(` ⚠ ${file.path}:${warning.line} ${warning.message}`);
718
+ }
719
+ }
657
720
  htmlContent = marked.parse(body) as string;
658
721
 
659
722
  // Collect raw markdown for AI accessibility
package/scripts/dev.ts CHANGED
@@ -40,6 +40,7 @@ import {
40
40
  checkPortOrExit,
41
41
  // Navigation/template building
42
42
  collectFiles,
43
+ collectPlanningVisualsContainmentWarnings,
43
44
  collectSlides,
44
45
  envBool,
45
46
  envInt,
@@ -48,6 +49,7 @@ import {
48
49
  // Formatting
49
50
  escapeHtml,
50
51
  filterByProfile,
52
+ filterUnknownPlanningVisualsTypeDiagnostics,
51
53
  filterUnknownSlidesVisualsTypeDiagnostics,
52
54
  // Provenance
53
55
  generateProvenance,
@@ -68,6 +70,7 @@ import {
68
70
  type SiteConfig,
69
71
  slugify,
70
72
  validatePath,
73
+ validatePlanningVisualsFences,
71
74
  validateSlidesVisualsFences,
72
75
  } from "../src/shared.ts";
73
76
  import { generateThemeCSS, getPrismUrls, loadTheme, type Theme } from "../src/theme.ts";
@@ -302,15 +305,23 @@ const clients: Set<ReadableStreamDefaultController> = new Set();
302
305
 
303
306
  let pluginCache: { key: string; head: string; bodyEnd: string } | null = null;
304
307
 
305
- async function isSlidesVisualsEnabled(): Promise<boolean> {
308
+ async function getFenceValidationFlags(): Promise<{
309
+ slidesVisuals: boolean;
310
+ planningVisuals: boolean;
311
+ }> {
306
312
  const configPath = join(ROOT, "kitfly.plugins.yaml");
307
313
  try {
308
314
  const raw = await readFile(configPath, "utf-8");
309
315
  const parsed = parseYaml(raw) as unknown as Record<string, unknown>;
310
316
  const plugins = Array.isArray(parsed?.plugins) ? (parsed.plugins as unknown[]) : [];
311
- return plugins.some((p) => typeof p === "string" && p.startsWith("slides-visuals@"));
317
+ return {
318
+ slidesVisuals: plugins.some((p) => typeof p === "string" && p.startsWith("slides-visuals@")),
319
+ planningVisuals: plugins.some(
320
+ (p) => typeof p === "string" && p.startsWith("planning-visuals@"),
321
+ ),
322
+ };
312
323
  } catch {
313
- return false;
324
+ return { slidesVisuals: false, planningVisuals: false };
314
325
  }
315
326
  }
316
327
 
@@ -400,6 +411,23 @@ async function renderPage(
400
411
  title = frontmatter.title as string;
401
412
  }
402
413
  pageMeta = buildPageMeta(frontmatter);
414
+ const fenceValidation = await getFenceValidationFlags();
415
+ if (fenceValidation.planningVisuals) {
416
+ const diagnostics = filterUnknownPlanningVisualsTypeDiagnostics(
417
+ validatePlanningVisualsFences(body),
418
+ );
419
+ if (diagnostics.length) {
420
+ const msg = diagnostics
421
+ .slice(0, 12)
422
+ .map((d) => ` - ${filePath}:${d.line} ${d.message}`)
423
+ .join("\n");
424
+ throw new Error(`planning-visuals fence contract violations:\n${msg}`);
425
+ }
426
+ const warnings = collectPlanningVisualsContainmentWarnings(body);
427
+ for (const warning of warnings.slice(0, 12)) {
428
+ logWarn(`${filePath}:${warning.line} ${warning.message}`);
429
+ }
430
+ }
403
431
  htmlContent = marked.parse(body) as string;
404
432
  }
405
433
 
@@ -487,13 +515,13 @@ async function renderSlidesPage(
487
515
  return renderGettingStarted(provenance, config, theme);
488
516
  }
489
517
  const pathPrefix = "/";
490
- const validateFences = await isSlidesVisualsEnabled();
518
+ const fenceValidation = await getFenceValidationFlags();
491
519
 
492
520
  const sections = await Promise.all(
493
521
  slides.map(async (slide, i) => {
494
522
  let inner = "";
495
523
  if (slide.kind === "markdown") {
496
- if (validateFences) {
524
+ if (fenceValidation.slidesVisuals) {
497
525
  const diagnostics = filterUnknownSlidesVisualsTypeDiagnostics(
498
526
  validateSlidesVisualsFences(slide.body),
499
527
  );
@@ -505,6 +533,22 @@ async function renderSlidesPage(
505
533
  throw new Error(`slides-visuals fence contract violations:\n${msg}`);
506
534
  }
507
535
  }
536
+ if (fenceValidation.planningVisuals) {
537
+ const diagnostics = filterUnknownPlanningVisualsTypeDiagnostics(
538
+ validatePlanningVisualsFences(slide.body),
539
+ );
540
+ if (diagnostics.length) {
541
+ const msg = diagnostics
542
+ .slice(0, 12)
543
+ .map((d) => ` - ${slide.sourcePath}:${d.line} ${d.message}`)
544
+ .join("\n");
545
+ throw new Error(`planning-visuals fence contract violations:\n${msg}`);
546
+ }
547
+ const warnings = collectPlanningVisualsContainmentWarnings(slide.body);
548
+ for (const warning of warnings.slice(0, 12)) {
549
+ logWarn(`${slide.sourcePath}:${warning.line} ${warning.message}`);
550
+ }
551
+ }
508
552
  inner = marked.parse(slide.body) as string;
509
553
  } else if (slide.kind === "yaml") {
510
554
  inner = `<pre><code class="language-yaml">${escapeHtml(slide.body)}</code></pre>`;
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Build-time codegen: reads docs/embed-manifest.yaml, resolves globs,
5
+ * and generates src/generated/embedded-docs.ts with all matched content.
6
+ *
7
+ * Usage:
8
+ * bun scripts/embed-docs.ts
9
+ */
10
+
11
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
12
+ import { basename, dirname } from "node:path";
13
+ import { Glob } from "bun";
14
+ import { parseFrontmatter, parseYaml } from "../src/shared.ts";
15
+
16
+ const MANIFEST_PATH = "docs/embed-manifest.yaml";
17
+ const OUTPUT_PATH = "src/generated/embedded-docs.ts";
18
+
19
+ function toSlug(filePath: string): string {
20
+ // Normalize path separators first (Windows compat: Bun.Glob returns backslashes)
21
+ const normalized = filePath.replace(/\\/g, "/");
22
+ // Strip first segment (docs/ or content/) and .md extension
23
+ const withoutPrefix = normalized.replace(/^[^/]+\//, "");
24
+ const withoutExt = withoutPrefix.replace(/\.md$/, "");
25
+ const base = basename(withoutExt).toLowerCase();
26
+ if (base === "readme" || base === "index") {
27
+ return dirname(withoutPrefix).replace(/\\/g, "/");
28
+ }
29
+ return withoutExt;
30
+ }
31
+
32
+ function extractTitle(body: string): string {
33
+ for (const line of body.split("\n")) {
34
+ const m = line.match(/^#\s+(.+)/);
35
+ if (m) return m[1].trim();
36
+ }
37
+ return "(untitled)";
38
+ }
39
+
40
+ async function resolveGlobs(includes: string[], excludes: string[]): Promise<string[]> {
41
+ const matched = new Set<string>();
42
+
43
+ for (const pattern of includes) {
44
+ const glob = new Glob(pattern);
45
+ for await (const path of glob.scan({ cwd: ".", dot: false })) {
46
+ matched.add(path);
47
+ }
48
+ }
49
+
50
+ for (const pattern of excludes) {
51
+ const glob = new Glob(pattern);
52
+ for await (const path of glob.scan({ cwd: ".", dot: false })) {
53
+ matched.delete(path);
54
+ }
55
+ }
56
+
57
+ return [...matched].sort();
58
+ }
59
+
60
+ function escapeForTemplate(s: string): string {
61
+ return s.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
62
+ }
63
+
64
+ async function main(): Promise<void> {
65
+ const raw = readFileSync(MANIFEST_PATH, "utf-8");
66
+ const manifest = parseYaml(raw);
67
+ const includes = manifest.include as string[];
68
+ const excludes = (manifest.exclude as string[]) ?? [];
69
+
70
+ if (!includes?.length) {
71
+ console.error(`Error: no include patterns in ${MANIFEST_PATH}`);
72
+ process.exit(1);
73
+ }
74
+
75
+ const files = await resolveGlobs(includes, excludes);
76
+
77
+ if (files.length === 0) {
78
+ console.error("Error: no files matched the manifest patterns");
79
+ process.exit(1);
80
+ }
81
+
82
+ const entries: Array<[string, string, string]> = [];
83
+
84
+ for (const file of files) {
85
+ const content = readFileSync(file, "utf-8");
86
+ const { body } = parseFrontmatter(content);
87
+ const trimmed = body.trim();
88
+ const slug = toSlug(file);
89
+ const title = extractTitle(trimmed);
90
+ entries.push([slug, title, trimmed]);
91
+ }
92
+
93
+ // Sort by slug for deterministic output
94
+ entries.sort((a, b) => a[0].localeCompare(b[0]));
95
+
96
+ const lines = [
97
+ "// AUTO-GENERATED by scripts/embed-docs.ts — do not edit",
98
+ "export const EMBEDDED_DOCS: ReadonlyArray<readonly [string, string, string]> = [",
99
+ ];
100
+
101
+ for (const [slug, title, content] of entries) {
102
+ lines.push(
103
+ `\t[${JSON.stringify(slug)}, ${JSON.stringify(title)}, \`${escapeForTemplate(content)}\`],`,
104
+ );
105
+ }
106
+
107
+ lines.push("];");
108
+ lines.push("");
109
+
110
+ mkdirSync(dirname(OUTPUT_PATH), { recursive: true });
111
+ writeFileSync(OUTPUT_PATH, lines.join("\n"));
112
+
113
+ console.log(`Embedded ${entries.length} docs into ${OUTPUT_PATH}`);
114
+ for (const [slug, title] of entries) {
115
+ console.log(` ${slug} — ${title}`);
116
+ }
117
+ }
118
+
119
+ main();