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
@@ -624,4 +624,128 @@ plugins:
624
624
  expect(html).toContain('data-kitfly-plugin="slides-visuals@0.2.1"');
625
625
  expect(html).toContain("future-thing");
626
626
  });
627
+
628
+ it("enforces planning-visuals fence contract in docs mode", async () => {
629
+ const siteDir = await makeTempDir();
630
+ const outDir = "out";
631
+ await writeSiteYaml(siteDir, { mode: "docs" });
632
+ await writeMd(
633
+ siteDir,
634
+ "docs/plan.md",
635
+ `# Plan
636
+
637
+ :::gantt
638
+ time-unit: week
639
+ time-start: 2026-W10
640
+ time-end: 2026-W12
641
+ :::
642
+ `,
643
+ );
644
+
645
+ const js = "console.log('planning visuals');";
646
+ const css = ".kitfly-planning-gantt{border:1px solid red;}";
647
+ await mkdir(join(siteDir, "plugins-dist"), { recursive: true });
648
+ await writeFile(join(siteDir, "plugins-dist", "planning-visuals.js"), js, "utf-8");
649
+ await writeFile(join(siteDir, "plugins-dist", "planning-visuals.css"), css, "utf-8");
650
+ await mkdir(join(siteDir, "registry"), { recursive: true });
651
+ await writeFile(
652
+ join(siteDir, "registry", "plugins.yaml"),
653
+ `version: 1
654
+ updated: "2026-03-03"
655
+ baseUrl: ""
656
+ plugins:
657
+ planning-visuals:
658
+ name: "Planning Visuals"
659
+ description: "Test planning visuals"
660
+ version: "0.2.4"
661
+ contract: "1"
662
+ kitfly: ">=0.2.4 <1.0.0"
663
+ license: MIT
664
+ verified: true
665
+ assets:
666
+ js: "plugins-dist/planning-visuals.js"
667
+ css: "plugins-dist/planning-visuals.css"
668
+ assetSha256:
669
+ js: "sha256:${sha256Hex(js)}"
670
+ css: "sha256:${sha256Hex(css)}"
671
+ `,
672
+ "utf-8",
673
+ );
674
+ await writeFile(
675
+ join(siteDir, "kitfly.plugins.yaml"),
676
+ "plugins:\n - planning-visuals@0.2.4\n",
677
+ "utf-8",
678
+ );
679
+
680
+ await expect(build({ folder: siteDir, out: outDir })).rejects.toThrow(
681
+ /planning-visuals fence contract violations/i,
682
+ );
683
+ });
684
+
685
+ it("ignores unknown planning-visuals block types while enforcing known contracts", async () => {
686
+ const siteDir = await makeTempDir();
687
+ const outDir = "out";
688
+ await writeSiteYaml(siteDir, { mode: "docs" });
689
+ await writeMd(
690
+ siteDir,
691
+ "docs/plan.md",
692
+ `# Plan
693
+
694
+ :::future-plan
695
+ note: pass-through
696
+ :::
697
+
698
+ :::gantt
699
+ time-unit: month
700
+ time-start: 2026-04
701
+ time-end: 2026-08
702
+ tracks:
703
+ - label: Wave 1
704
+ depth: 1
705
+ start: 2026-04
706
+ end: 2026-06
707
+ :::
708
+ `,
709
+ );
710
+
711
+ const js = "console.log('planning visuals');";
712
+ const css = ".kitfly-planning-gantt{border:1px solid red;}";
713
+ await mkdir(join(siteDir, "plugins-dist"), { recursive: true });
714
+ await writeFile(join(siteDir, "plugins-dist", "planning-visuals.js"), js, "utf-8");
715
+ await writeFile(join(siteDir, "plugins-dist", "planning-visuals.css"), css, "utf-8");
716
+ await mkdir(join(siteDir, "registry"), { recursive: true });
717
+ await writeFile(
718
+ join(siteDir, "registry", "plugins.yaml"),
719
+ `version: 1
720
+ updated: "2026-03-03"
721
+ baseUrl: ""
722
+ plugins:
723
+ planning-visuals:
724
+ name: "Planning Visuals"
725
+ description: "Test planning visuals"
726
+ version: "0.2.4"
727
+ contract: "1"
728
+ kitfly: ">=0.2.4 <1.0.0"
729
+ license: MIT
730
+ verified: true
731
+ assets:
732
+ js: "plugins-dist/planning-visuals.js"
733
+ css: "plugins-dist/planning-visuals.css"
734
+ assetSha256:
735
+ js: "sha256:${sha256Hex(js)}"
736
+ css: "sha256:${sha256Hex(css)}"
737
+ `,
738
+ "utf-8",
739
+ );
740
+ await writeFile(
741
+ join(siteDir, "kitfly.plugins.yaml"),
742
+ "plugins:\n - planning-visuals@0.2.4\n",
743
+ "utf-8",
744
+ );
745
+
746
+ await expect(build({ folder: siteDir, out: outDir })).resolves.toBeUndefined();
747
+ const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
748
+ expect(html).toContain('data-kitfly-plugin="planning-visuals@0.2.4"');
749
+ expect(html).toContain("future-plan");
750
+ });
627
751
  });
@@ -864,6 +864,67 @@ describe("bundleSite plugin integration", () => {
864
864
  expect(html).toContain("kitfly-chart-wrapper");
865
865
  });
866
866
 
867
+ it("enforces planning-visuals fence contract in docs mode", async () => {
868
+ const siteDir = await makeTempDir();
869
+ await mkdir(join(siteDir, "docs"), { recursive: true });
870
+ await mkdir(join(siteDir, "plugins-dist"), { recursive: true });
871
+ await mkdir(join(siteDir, "registry"), { recursive: true });
872
+
873
+ const js = "console.log('planning visuals');";
874
+ const css = ".kitfly-planning-gantt{border:1px solid red;}";
875
+ await writeFile(join(siteDir, "plugins-dist", "planning-visuals.js"), js, "utf-8");
876
+ await writeFile(join(siteDir, "plugins-dist", "planning-visuals.css"), css, "utf-8");
877
+ await writeFile(
878
+ join(siteDir, "registry", "plugins.yaml"),
879
+ `version: 1
880
+ updated: "2026-03-03"
881
+ baseUrl: ""
882
+ plugins:
883
+ planning-visuals:
884
+ name: "Planning Visuals"
885
+ description: "Test planning visuals"
886
+ version: "0.2.4"
887
+ contract: "1"
888
+ kitfly: ">=0.2.4 <1.0.0"
889
+ license: MIT
890
+ verified: true
891
+ assets:
892
+ js: "plugins-dist/planning-visuals.js"
893
+ css: "plugins-dist/planning-visuals.css"
894
+ assetSha256:
895
+ js: "sha256:4932eca4b9f9735541953e9be72a916d7e8a08213a28cc6e0a34899dab98b880"
896
+ css: "sha256:e73d687d58def8a6608ae51d1a1a09af6eaf2ec4c0b5d0849ebf260679e80e21"
897
+ `,
898
+ "utf-8",
899
+ );
900
+ await writeFile(
901
+ join(siteDir, "site.yaml"),
902
+ 'title: "Bundle Plan Test"\nbrand:\n name: "Test"\n url: "/"\nsections:\n - name: Docs\n path: docs\n',
903
+ "utf-8",
904
+ );
905
+ await writeFile(
906
+ join(siteDir, "docs", "index.md"),
907
+ `# Plan
908
+
909
+ :::gantt
910
+ time-unit: week
911
+ time-start: 2026-W10
912
+ time-end: 2026-W12
913
+ :::
914
+ `,
915
+ "utf-8",
916
+ );
917
+ await writeFile(
918
+ join(siteDir, "kitfly.plugins.yaml"),
919
+ "plugins:\n - planning-visuals@0.2.4\n",
920
+ "utf-8",
921
+ );
922
+
923
+ await expect(
924
+ bundleSite({ folder: siteDir, out: "bundles", name: "bundle.html" }),
925
+ ).rejects.toThrow(/planning-visuals fence contract violations/i);
926
+ });
927
+
867
928
  it("inlines footer logo image when configured", async () => {
868
929
  const siteDir = await makeTempDir();
869
930
  await mkdir(join(siteDir, "docs"), { recursive: true });
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Tests for the kitfly docs command and embed-docs codegen.
3
+ */
4
+
5
+ import { execSync } from "node:child_process";
6
+ import { existsSync } from "node:fs";
7
+ import { describe, expect, it } from "vitest";
8
+ import { findSimilar } from "../commands/docs.ts";
9
+ import { EMBEDDED_DOCS } from "../generated/embedded-docs.ts";
10
+
11
+ describe("embed-docs codegen", () => {
12
+ it("generates embedded-docs.ts with entries", () => {
13
+ expect(existsSync("src/generated/embedded-docs.ts")).toBe(true);
14
+ expect(EMBEDDED_DOCS.length).toBeGreaterThan(0);
15
+ });
16
+
17
+ it("each entry has [slug, title, content] tuple", () => {
18
+ for (const entry of EMBEDDED_DOCS) {
19
+ expect(entry).toHaveLength(3);
20
+ const [slug, title, content] = entry;
21
+ expect(typeof slug).toBe("string");
22
+ expect(slug.length).toBeGreaterThan(0);
23
+ expect(typeof title).toBe("string");
24
+ expect(title.length).toBeGreaterThan(0);
25
+ expect(typeof content).toBe("string");
26
+ expect(content.length).toBeGreaterThan(0);
27
+ }
28
+ });
29
+
30
+ it("slugs do not contain .md extension", () => {
31
+ for (const [slug] of EMBEDDED_DOCS) {
32
+ expect(slug).not.toMatch(/\.md$/);
33
+ }
34
+ });
35
+
36
+ it("slugs do not start with docs/ or content/", () => {
37
+ for (const [slug] of EMBEDDED_DOCS) {
38
+ expect(slug).not.toMatch(/^(docs|content)\//);
39
+ }
40
+ });
41
+
42
+ it("README/index files collapse to parent directory slug", () => {
43
+ // The docs/userguide/cli/README.md should become userguide/cli
44
+ const slugs = EMBEDDED_DOCS.map(([slug]) => slug);
45
+ expect(slugs).toContain("userguide/cli");
46
+ expect(slugs).not.toContain("userguide/cli/readme");
47
+ expect(slugs).not.toContain("userguide/cli/README");
48
+ });
49
+
50
+ it("excludes docs/releases/** and docs/decisions/**", () => {
51
+ const slugs = EMBEDDED_DOCS.map(([slug]) => slug);
52
+ for (const slug of slugs) {
53
+ expect(slug).not.toMatch(/^releases\//);
54
+ expect(slug).not.toMatch(/^decisions\//);
55
+ }
56
+ });
57
+
58
+ it("content has frontmatter stripped", () => {
59
+ for (const [, , content] of EMBEDDED_DOCS) {
60
+ // Content should not start with --- (frontmatter delimiter)
61
+ expect(content.trimStart()).not.toMatch(/^---\s*\n/);
62
+ }
63
+ });
64
+
65
+ it("entries are sorted by slug", () => {
66
+ const slugs = EMBEDDED_DOCS.map(([slug]) => slug);
67
+ const sorted = [...slugs].sort();
68
+ expect(slugs).toEqual(sorted);
69
+ });
70
+ });
71
+
72
+ describe("findSimilar", () => {
73
+ const slugs = [
74
+ "userguide/cli/dev",
75
+ "userguide/cli/build",
76
+ "userguide/cli/bundle",
77
+ "userguide/sharing",
78
+ "reference/configuration",
79
+ "reference/plugins",
80
+ "guide/getting-started",
81
+ ];
82
+
83
+ it("finds prefix matches", () => {
84
+ const results = findSimilar("userguide/cli", slugs, 3);
85
+ expect(results.length).toBeGreaterThan(0);
86
+ expect(results.every((r) => r.startsWith("userguide/cli"))).toBe(true);
87
+ });
88
+
89
+ it("finds substring matches", () => {
90
+ const results = findSimilar("config", slugs, 3);
91
+ expect(results).toContain("reference/configuration");
92
+ });
93
+
94
+ it("returns at most max results", () => {
95
+ const results = findSimilar("userguide", slugs, 2);
96
+ expect(results.length).toBeLessThanOrEqual(2);
97
+ });
98
+
99
+ it("returns empty for no match", () => {
100
+ const results = findSimilar("nonexistent-slug-xyz", slugs, 3);
101
+ expect(results).toEqual([]);
102
+ });
103
+
104
+ it("prioritizes prefix over substring", () => {
105
+ const results = findSimilar("guide", slugs, 3);
106
+ // "guide/getting-started" is a prefix match, should come first
107
+ expect(results[0]).toBe("guide/getting-started");
108
+ });
109
+ });
110
+
111
+ describe("embed-docs script", () => {
112
+ it("regenerates from manifest without errors", () => {
113
+ const output = execSync("bun scripts/embed-docs.ts", { encoding: "utf-8" });
114
+ expect(output).toContain("Embedded");
115
+ expect(output).toContain("docs into src/generated/embedded-docs.ts");
116
+ });
117
+ });
@@ -0,0 +1,10 @@
1
+ :::gantt
2
+ time-unit: month
3
+ time-start: "2026-13"
4
+ time-end: "2026-10"
5
+ tracks:
6
+ - label: "Wave 1"
7
+ depth: 1
8
+ start: "2026-04"
9
+ end: "2026-07"
10
+ :::
@@ -0,0 +1,13 @@
1
+ :::gantt
2
+ time-unit: month
3
+ time-start: "2026-04"
4
+ time-end: "2026-10"
5
+ markers:
6
+ - label: "Gate"
7
+ date: "2026-W20"
8
+ tracks:
9
+ - label: "Wave 1"
10
+ depth: 1
11
+ start: "2026-04"
12
+ end: "2026-07"
13
+ :::
@@ -0,0 +1,13 @@
1
+ :::gantt
2
+ time-unit: month
3
+ time-start: "2026-04"
4
+ time-end: "2026-10"
5
+ tracks:
6
+ - label: "Wave 1"
7
+ depth: 1
8
+ start: "2026-04"
9
+ end: "2026-07"
10
+ milestones:
11
+ - label: "Go-live"
12
+ date: "2026-W20"
13
+ :::
@@ -0,0 +1,5 @@
1
+ :::gantt
2
+ time-unit: week
3
+ time-start: "2026-W01"
4
+ time-end: "2026-W06"
5
+ :::
@@ -0,0 +1,10 @@
1
+ :::gantt
2
+ time-unit: week
3
+ time-start: "2026-W14"
4
+ time-end: "2026-W20"
5
+ tracks:
6
+ - label: "Wave 1"
7
+ depth: 1
8
+ start: "2026-W18"
9
+ end: "2026-W15"
10
+ :::
@@ -0,0 +1,15 @@
1
+ :::gantt
2
+ time-unit: week
3
+ time-start: "2026-W14"
4
+ time-end: "2026-W30"
5
+ markers:
6
+ - label: "Go/No-Go"
7
+ date: "2026-W20"
8
+ - label: "Phase Gate"
9
+ date: "2026-W28"
10
+ tracks:
11
+ - label: "Phase 1"
12
+ depth: 1
13
+ start: "2026-W14"
14
+ end: "2026-W24"
15
+ :::
@@ -0,0 +1,13 @@
1
+ :::gantt
2
+ time-unit: month
3
+ time-start: "2026-03"
4
+ time-end: "2026-10"
5
+ markers:
6
+ - label: "Funding Gate"
7
+ date: "2026-06"
8
+ tracks:
9
+ - label: "Build"
10
+ depth: 1
11
+ start: "2026-03"
12
+ end: "2026-10"
13
+ :::
@@ -0,0 +1,16 @@
1
+ :::gantt
2
+ label: "Rollout"
3
+ time-unit: month
4
+ time-start: "2026-04"
5
+ time-end: "2026-10"
6
+ tracks:
7
+ - label: "Wave 1"
8
+ depth: 1
9
+ start: "2026-04"
10
+ end: "2026-07"
11
+ - label: "Wave 2"
12
+ depth: 1
13
+ start: "2026-08"
14
+ end: "2026-10"
15
+ today: "2026-06"
16
+ :::
@@ -0,0 +1,10 @@
1
+ :::gantt
2
+ time-unit: week
3
+ time-start: "2026-W01"
4
+ time-end: "2026-W06"
5
+ tracks:
6
+ - label: "Wave 1"
7
+ depth: 1
8
+ start: "2026-W01"
9
+ end: "2026-W04"
10
+ :::
@@ -0,0 +1,20 @@
1
+ :::gantt
2
+ label: "Rollout"
3
+ time-unit: week
4
+ time-start: "2026-W14"
5
+ time-end: "2026-W20"
6
+ max-depth: 2
7
+ tracks:
8
+ - label: "Wave 1"
9
+ depth: 1
10
+ start: "2026-W14"
11
+ end: "2026-W18"
12
+ status: active
13
+ - label: "Client A"
14
+ depth: 2
15
+ start: "2026-W15"
16
+ end: "2026-W17"
17
+ milestones:
18
+ - label: "Sign-off"
19
+ date: "2026-W19"
20
+ :::
@@ -0,0 +1,28 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+ import { validatePlanningVisualsFences } from "../shared.ts";
5
+
6
+ const FIXTURES = join(__dirname, "fixtures", "fences", "planning-visuals");
7
+
8
+ describe("planning-visuals fence contract", () => {
9
+ it("accepts valid fixtures", async () => {
10
+ const dir = join(FIXTURES, "valid");
11
+ const files = await readdir(dir);
12
+ for (const f of files) {
13
+ const md = await readFile(join(dir, f), "utf-8");
14
+ const diags = validatePlanningVisualsFences(md);
15
+ expect(diags, `${f} should be valid`).toHaveLength(0);
16
+ }
17
+ });
18
+
19
+ it("rejects invalid fixtures", async () => {
20
+ const dir = join(FIXTURES, "invalid");
21
+ const files = await readdir(dir);
22
+ for (const f of files) {
23
+ const md = await readFile(join(dir, f), "utf-8");
24
+ const diags = validatePlanningVisualsFences(md);
25
+ expect(diags.length, `${f} should be invalid`).toBeGreaterThan(0);
26
+ }
27
+ });
28
+ });
@@ -0,0 +1,68 @@
1
+ import { expect, test } from "bun:test";
2
+
3
+ type PlanningVisualsRegressionHooks = {
4
+ parseGanttNodesWithFirstLines: (
5
+ firstLines: string[],
6
+ between: any[],
7
+ endNode: any,
8
+ ) => Record<string, unknown>;
9
+ };
10
+
11
+ class FakeElement {
12
+ tagName: string;
13
+ textContent: string;
14
+ #children: FakeElement[];
15
+
16
+ constructor(tagName: string, textContent = "", children: FakeElement[] = []) {
17
+ this.tagName = tagName;
18
+ this.textContent = textContent;
19
+ this.#children = children;
20
+ }
21
+
22
+ querySelectorAll(selector: string): FakeElement[] {
23
+ if (selector === ":scope > li") return this.#children;
24
+ if (selector === ":scope > p") return [];
25
+ return [];
26
+ }
27
+ }
28
+
29
+ async function loadHooks(): Promise<PlanningVisualsRegressionHooks> {
30
+ // @ts-expect-error JS plugin registers test hooks on globalThis in non-DOM environments.
31
+ await import("../../plugins-dist/planning-visuals.js");
32
+ const hooks = (globalThis as any).__kitflyPlanningVisualsTest as
33
+ | PlanningVisualsRegressionHooks
34
+ | undefined;
35
+ if (!hooks) throw new Error("planning-visuals test hooks not found on globalThis");
36
+ return hooks;
37
+ }
38
+
39
+ test("planning-visuals: fragmented list-key switch preserves markers and milestones", async () => {
40
+ const { parseGanttNodesWithFirstLines } = await loadHooks();
41
+ const data = parseGanttNodesWithFirstLines(
42
+ ['time-unit: "month"', 'time-start: "2026-03"', 'time-end: "2026-09"', "markers:"],
43
+ [],
44
+ new FakeElement("UL", "", [
45
+ new FakeElement("LI", 'label: "P66 Conf (May 26)"\ndate: "2026-05"\ntracks:'),
46
+ new FakeElement(
47
+ "LI",
48
+ 'label: "Wave 1"\ndepth: 1\nstart: "2026-03"\nend: "2026-04"\nmilestones:',
49
+ ),
50
+ new FakeElement("LI", 'label: "Auto Go-Live May 19"\ndate: "2026-05"\ntracks:'),
51
+ new FakeElement("LI", 'label: "Wave 2"\ndepth: 1\nstart: "2026-05"\nend: "2026-06"'),
52
+ ]),
53
+ );
54
+
55
+ const markers = data.markers as Array<Record<string, string>>;
56
+ const tracks = data.tracks as Array<Record<string, string>>;
57
+ const milestones = data.milestones as Array<Record<string, string>>;
58
+
59
+ expect(markers).toHaveLength(1);
60
+ expect(markers[0].label).toBe("P66 Conf (May 26)");
61
+
62
+ expect(milestones).toHaveLength(1);
63
+ expect(milestones[0].label).toBe("Auto Go-Live May 19");
64
+
65
+ expect(tracks).toHaveLength(2);
66
+ expect(tracks[0].label).toBe("Wave 1");
67
+ expect(tracks[1].label).toBe("Wave 2");
68
+ });