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.
- package/CHANGELOG.md +23 -0
- package/README.md +13 -11
- package/VERSION +1 -1
- package/dist/_raw/content/reference/gantt-widget.md +468 -0
- package/dist/_raw/content/reference/plugins.md +157 -2
- package/dist/content/deployment/preflight.html +5 -6
- package/dist/content/deployment/recipes/aws-s3.html +5 -6
- package/dist/content/deployment/recipes/cloudflare-pages.html +5 -6
- package/dist/content/deployment/recipes/cloudflare-r2.html +5 -6
- package/dist/content/deployment/recipes/fly-io.html +5 -6
- package/dist/content/deployment/recipes/github-pages.html +5 -6
- package/dist/content/deployment/recipes/netlify.html +5 -6
- package/dist/content/deployment/recipes/vercel.html +5 -6
- package/dist/content/deployment/secrets-and-env-vars.html +5 -6
- package/dist/content/deployment.html +5 -6
- package/dist/content/guide/approaches.html +5 -6
- package/dist/content/guide/branding.html +5 -6
- package/dist/content/guide/data-driven-content.html +5 -6
- package/dist/content/guide/features.html +5 -6
- package/dist/content/guide/getting-started.html +5 -6
- package/dist/content/guide/kitfly-overview.html +5 -6
- package/dist/content/reference/configuration.html +5 -6
- package/dist/content/reference/design-catalog.html +5 -6
- package/dist/content/reference/environment-variables.html +5 -6
- package/dist/content/reference/gantt-widget.html +899 -0
- package/dist/content/reference/glossary.html +5 -6
- package/dist/content/reference/key-concepts.html +5 -6
- package/dist/content/reference/plugins.html +245 -9
- package/dist/content/reference/slides-authoring-guidelines.html +5 -6
- package/dist/content/reference/structure.html +5 -6
- package/dist/content/reference.html +5 -6
- package/dist/content/templates/crucible.html +5 -6
- package/dist/content/templates/handbook.html +5 -6
- package/dist/content/templates/minimal.html +5 -6
- package/dist/content/templates/overview.html +5 -6
- package/dist/content/templates/pipeline.html +5 -6
- package/dist/content/templates/productbook.html +5 -6
- package/dist/content/templates/runbook.html +5 -6
- package/dist/content/templates/servicebook.html +5 -6
- package/dist/content-index.json +10 -2
- package/dist/docs/decisions/ADR-0001-minimalist-site-code.html +5 -6
- package/dist/docs/decisions/ADR-0002-ai-accessibility.html +5 -6
- package/dist/docs/decisions/ADR-0003-single-file-bundle.html +5 -6
- package/dist/docs/decisions/ADR-0004-bun-runtime.html +5 -6
- package/dist/docs/decisions/ADR-0005-plugin-contract-and-distribution.html +5 -6
- package/dist/docs/decisions/ADR-0006-data-driven-content.html +5 -6
- package/dist/docs/decisions/DDR-0001-viewport-locked-layout.html +5 -6
- package/dist/docs/decisions/DDR-0002-theme-system.html +5 -6
- package/dist/docs/decisions/DDR-0003-bounded-logo-slot.html +5 -6
- package/dist/docs/decisions/DDR-0004-slides-rendering-model.html +5 -6
- package/dist/docs/decisions/DDR-0005-deterministic-layout-boundary.html +5 -6
- package/dist/docs/userguide/cli/build.html +5 -6
- package/dist/docs/userguide/cli/bundle.html +5 -6
- package/dist/docs/userguide/cli/dev.html +5 -6
- package/dist/docs/userguide/cli/init.html +5 -6
- package/dist/docs/userguide/cli/servers.html +5 -6
- package/dist/docs/userguide/cli/stop.html +5 -6
- package/dist/docs/userguide/cli/update.html +5 -6
- package/dist/docs/userguide/cli/version.html +5 -6
- package/dist/docs/userguide/cli.html +5 -6
- package/dist/docs/userguide/sharing.html +5 -6
- package/dist/index.html +5 -6
- package/dist/llms.txt +3 -3
- package/dist/provenance.json +4 -5
- package/dist/reports/license-inventory.csv +199 -0
- package/dist/schemas/plugin-registry.schema.html +5 -6
- package/dist/schemas/plugin-schemas-notes.html +5 -6
- package/dist/schemas/plugin.schema.html +5 -6
- package/dist/schemas/plugins.schema.html +5 -6
- package/dist/schemas/v0/common.schema.html +5 -6
- package/dist/schemas/v0/plugin-registry.schema.html +5 -6
- package/dist/schemas/v0/plugin.schema.html +5 -6
- package/dist/schemas/v0/plugins.schema.html +5 -6
- package/dist/schemas/v0/site.schema.html +5 -6
- package/dist/schemas/v0/theme.schema.html +5 -6
- package/dist/schemas.html +5 -6
- package/package.json +1 -1
- package/plugins-dist/planning-visuals.css +261 -0
- package/plugins-dist/planning-visuals.js +669 -0
- package/registry/plugins.yaml +15 -1
- package/scripts/build-all.ts +5 -0
- package/scripts/build.ts +73 -11
- package/scripts/bundle.ts +73 -10
- package/scripts/dev.ts +49 -5
- package/scripts/embed-docs.ts +119 -0
- package/src/__tests__/build.test.ts +124 -0
- package/src/__tests__/bundle.test.ts +61 -0
- package/src/__tests__/docs.test.ts +117 -0
- package/src/__tests__/fixtures/fences/planning-visuals/invalid/bad-month-format.md +10 -0
- package/src/__tests__/fixtures/fences/planning-visuals/invalid/marker-format-mismatch.md +13 -0
- package/src/__tests__/fixtures/fences/planning-visuals/invalid/milestone-format-mismatch.md +13 -0
- package/src/__tests__/fixtures/fences/planning-visuals/invalid/missing-tracks.md +5 -0
- package/src/__tests__/fixtures/fences/planning-visuals/invalid/track-reversed.md +10 -0
- package/src/__tests__/fixtures/fences/planning-visuals/valid/markers-basic.md +15 -0
- package/src/__tests__/fixtures/fences/planning-visuals/valid/markers-no-milestones.md +13 -0
- package/src/__tests__/fixtures/fences/planning-visuals/valid/month-basic.md +16 -0
- package/src/__tests__/fixtures/fences/planning-visuals/valid/no-milestones.md +10 -0
- package/src/__tests__/fixtures/fences/planning-visuals/valid/week-basic.md +20 -0
- package/src/__tests__/planning-visuals-fence-contract.test.ts +28 -0
- package/src/__tests__/planning-visuals-runtime-regressions.bun.test.ts +68 -0
- package/src/__tests__/planning-visuals-runtime.bun.test.ts +192 -0
- package/src/__tests__/shared.test.ts +121 -0
- package/src/cli.ts +113 -18
- package/src/commands/docs.ts +71 -0
- package/src/generated/embedded-docs.ts +2384 -0
- package/src/server-registry.ts +50 -10
- 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,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,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,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
|
+
});
|