kitfly 0.2.1 → 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 +79 -0
- package/README.md +38 -21
- package/VERSION +1 -1
- package/dist/_raw/content/guide/branding.md +146 -0
- package/dist/_raw/content/guide/data-driven-content.md +204 -0
- package/dist/_raw/content/reference/configuration.md +145 -7
- package/dist/_raw/content/reference/environment-variables.md +26 -1
- package/dist/_raw/content/reference/gantt-widget.md +468 -0
- package/dist/_raw/content/reference/glossary.md +25 -1
- package/dist/_raw/content/reference/key-concepts.md +30 -2
- package/dist/_raw/content/reference/plugins.md +170 -1
- package/dist/_raw/docs/decisions/ADR-0006-data-driven-content.md +350 -0
- package/dist/content/deployment/preflight.html +11 -8
- package/dist/content/deployment/recipes/aws-s3.html +11 -8
- package/dist/content/deployment/recipes/cloudflare-pages.html +11 -8
- package/dist/content/deployment/recipes/cloudflare-r2.html +11 -8
- package/dist/content/deployment/recipes/fly-io.html +11 -8
- package/dist/content/deployment/recipes/github-pages.html +11 -8
- package/dist/content/deployment/recipes/netlify.html +11 -8
- package/dist/content/deployment/recipes/vercel.html +11 -8
- package/dist/content/deployment/secrets-and-env-vars.html +11 -8
- package/dist/content/deployment.html +11 -8
- package/dist/content/guide/approaches.html +11 -8
- package/dist/content/guide/branding.html +509 -0
- package/dist/content/guide/data-driven-content.html +542 -0
- package/dist/content/guide/features.html +11 -8
- package/dist/content/guide/getting-started.html +11 -8
- package/dist/content/guide/kitfly-overview.html +11 -8
- package/dist/content/reference/configuration.html +136 -11
- package/dist/content/reference/design-catalog.html +11 -8
- package/dist/content/reference/environment-variables.html +51 -10
- package/dist/content/reference/gantt-widget.html +899 -0
- package/dist/content/reference/glossary.html +25 -10
- package/dist/content/reference/key-concepts.html +34 -11
- package/dist/content/reference/plugins.html +261 -10
- package/dist/content/reference/slides-authoring-guidelines.html +11 -8
- package/dist/content/reference/structure.html +11 -8
- package/dist/content/reference.html +11 -8
- package/dist/content/templates/crucible.html +11 -8
- package/dist/content/templates/handbook.html +11 -8
- package/dist/content/templates/minimal.html +11 -8
- package/dist/content/templates/overview.html +11 -8
- package/dist/content/templates/pipeline.html +11 -8
- package/dist/content/templates/productbook.html +11 -8
- package/dist/content/templates/runbook.html +11 -8
- package/dist/content/templates/servicebook.html +11 -8
- package/dist/content-index.json +37 -2
- package/dist/docs/decisions/ADR-0001-minimalist-site-code.html +11 -8
- package/dist/docs/decisions/ADR-0002-ai-accessibility.html +11 -8
- package/dist/docs/decisions/ADR-0003-single-file-bundle.html +11 -8
- package/dist/docs/decisions/ADR-0004-bun-runtime.html +11 -8
- package/dist/docs/decisions/ADR-0005-plugin-contract-and-distribution.html +11 -8
- package/dist/docs/decisions/ADR-0006-data-driven-content.html +751 -0
- package/dist/docs/decisions/DDR-0001-viewport-locked-layout.html +11 -8
- package/dist/docs/decisions/DDR-0002-theme-system.html +11 -8
- package/dist/docs/decisions/DDR-0003-bounded-logo-slot.html +11 -8
- package/dist/docs/decisions/DDR-0004-slides-rendering-model.html +11 -8
- package/dist/docs/decisions/DDR-0005-deterministic-layout-boundary.html +11 -8
- package/dist/docs/userguide/cli/build.html +11 -8
- package/dist/docs/userguide/cli/bundle.html +11 -8
- package/dist/docs/userguide/cli/dev.html +11 -8
- package/dist/docs/userguide/cli/init.html +11 -8
- package/dist/docs/userguide/cli/servers.html +11 -8
- package/dist/docs/userguide/cli/stop.html +11 -8
- package/dist/docs/userguide/cli/update.html +11 -8
- package/dist/docs/userguide/cli/version.html +11 -8
- package/dist/docs/userguide/cli.html +11 -8
- package/dist/docs/userguide/sharing.html +11 -8
- package/dist/index.html +11 -8
- 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 +11 -8
- package/dist/schemas/plugin-schemas-notes.html +11 -8
- package/dist/schemas/plugin.schema.html +11 -8
- package/dist/schemas/plugins.schema.html +11 -8
- package/dist/schemas/v0/common.schema.html +15 -12
- package/dist/schemas/v0/plugin-registry.schema.html +14 -11
- package/dist/schemas/v0/plugin.schema.html +14 -11
- package/dist/schemas/v0/plugins.schema.html +14 -11
- package/dist/schemas/v0/site.schema.html +68 -9
- package/dist/schemas/v0/theme.schema.html +22 -19
- package/dist/schemas.html +11 -8
- package/dist/styles.css +39 -4
- package/package.json +1 -1
- package/plugins-dist/latex-runtime.js +140 -0
- package/plugins-dist/latex.js +178 -0
- package/plugins-dist/planning-visuals.css +261 -0
- package/plugins-dist/planning-visuals.js +669 -0
- package/plugins-dist/slides-charts-lite-runtime.js +179 -0
- package/plugins-dist/slides-charts-lite.js +198 -0
- package/registry/plugins.yaml +40 -1
- package/schemas/v0/site.schema.json +56 -0
- package/scripts/build-all.ts +5 -0
- package/scripts/build.ts +264 -80
- package/scripts/bundle.ts +188 -17
- package/scripts/dev.ts +294 -171
- package/scripts/embed-docs.ts +119 -0
- package/src/__tests__/brief.test.ts +151 -0
- package/src/__tests__/build.test.ts +293 -1
- package/src/__tests__/bundle.test.ts +195 -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__/init.test.ts +51 -2
- package/src/__tests__/latex-runtime.bun.test.ts +35 -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 +719 -1
- package/src/__tests__/slides-charts-lite-runtime.bun.test.ts +45 -0
- package/src/cli.ts +124 -22
- package/src/commands/docs.ts +71 -0
- package/src/commands/init.ts +1 -1
- package/src/generated/embedded-docs.ts +2384 -0
- package/src/server-registry.ts +50 -10
- package/src/shared.ts +1174 -43
- package/src/site/styles.css +39 -4
- package/src/site/template.html +5 -2
- package/src/templates/brief.ts +486 -0
- package/src/templates/deck.ts +59 -0
- package/src/templates/driver.ts +46 -13
- package/src/templates/handbook.ts +32 -0
- package/src/templates/runbook.ts +32 -0
|
@@ -5,6 +5,7 @@ import { afterEach, describe, expect, it } from "vitest";
|
|
|
5
5
|
import {
|
|
6
6
|
buildBundleNav,
|
|
7
7
|
buildBundleSidebarHeader,
|
|
8
|
+
bundleSite,
|
|
8
9
|
fileToDataUri,
|
|
9
10
|
imageMime,
|
|
10
11
|
inlineLocalImages,
|
|
@@ -815,3 +816,197 @@ describe("buildBundleSidebarHeader", () => {
|
|
|
815
816
|
expect(html).toContain(`src="${logo}"`);
|
|
816
817
|
});
|
|
817
818
|
});
|
|
819
|
+
|
|
820
|
+
describe("bundleSite plugin integration", () => {
|
|
821
|
+
it("inlines latex plugin script when enabled", async () => {
|
|
822
|
+
const siteDir = await makeTempDir();
|
|
823
|
+
await mkdir(join(siteDir, "docs"), { recursive: true });
|
|
824
|
+
await writeFile(
|
|
825
|
+
join(siteDir, "site.yaml"),
|
|
826
|
+
'title: "Bundle Test"\nbrand:\n name: "Test"\n url: "/"\nsections:\n - name: Docs\n path: docs\n',
|
|
827
|
+
"utf-8",
|
|
828
|
+
);
|
|
829
|
+
await writeFile(join(siteDir, "docs", "index.md"), "# Bundle Math\n\n$E=mc^2$", "utf-8");
|
|
830
|
+
await writeFile(join(siteDir, "kitfly.plugins.yaml"), "plugins:\n - latex@0.2.2\n", "utf-8");
|
|
831
|
+
|
|
832
|
+
await bundleSite({ folder: siteDir, out: "bundles", name: "bundle.html" });
|
|
833
|
+
|
|
834
|
+
const html = await readFile(join(siteDir, "bundles", "bundle.html"), "utf-8");
|
|
835
|
+
expect(html).toContain('data-kitfly-plugin="latex@0.2.2"');
|
|
836
|
+
expect(html).toContain("const KATEX_JS_URL =");
|
|
837
|
+
expect(html).toContain("cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.js");
|
|
838
|
+
expect(html).toContain("cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.css");
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
it("inlines slides-charts-lite plugin in slides mode", async () => {
|
|
842
|
+
const siteDir = await makeTempDir();
|
|
843
|
+
await mkdir(join(siteDir, "slides"), { recursive: true });
|
|
844
|
+
await writeFile(
|
|
845
|
+
join(siteDir, "site.yaml"),
|
|
846
|
+
'title: "Bundle Charts Test"\nmode: "slides"\nbrand:\n name: "Test"\n url: "/"\nsections:\n - name: Slides\n path: slides\n',
|
|
847
|
+
"utf-8",
|
|
848
|
+
);
|
|
849
|
+
await writeFile(
|
|
850
|
+
join(siteDir, "slides", "deck.md"),
|
|
851
|
+
'# Deck\n\n```chart\nkind: line\nlabels: ["W1", "W2"]\ndata: [2, 3]\n```',
|
|
852
|
+
"utf-8",
|
|
853
|
+
);
|
|
854
|
+
await writeFile(
|
|
855
|
+
join(siteDir, "kitfly.plugins.yaml"),
|
|
856
|
+
"plugins:\n - slides-charts-lite@0.2.2\n",
|
|
857
|
+
"utf-8",
|
|
858
|
+
);
|
|
859
|
+
|
|
860
|
+
await bundleSite({ folder: siteDir, out: "bundles", name: "bundle.html" });
|
|
861
|
+
|
|
862
|
+
const html = await readFile(join(siteDir, "bundles", "bundle.html"), "utf-8");
|
|
863
|
+
expect(html).toContain('data-kitfly-plugin="slides-charts-lite@0.2.2"');
|
|
864
|
+
expect(html).toContain("kitfly-chart-wrapper");
|
|
865
|
+
});
|
|
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
|
+
|
|
928
|
+
it("inlines footer logo image when configured", async () => {
|
|
929
|
+
const siteDir = await makeTempDir();
|
|
930
|
+
await mkdir(join(siteDir, "docs"), { recursive: true });
|
|
931
|
+
await mkdir(join(siteDir, "assets", "brand"), { recursive: true });
|
|
932
|
+
await writeFile(
|
|
933
|
+
join(siteDir, "site.yaml"),
|
|
934
|
+
'title: "Bundle Footer Test"\nbrand:\n name: "Test"\n url: "/"\nfooter:\n logo: "assets/brand/footer-logo.png"\n logoAlt: "Footer Brand"\n logoHeight: 24\nsections:\n - name: Docs\n path: docs\n',
|
|
935
|
+
"utf-8",
|
|
936
|
+
);
|
|
937
|
+
await writeFile(join(siteDir, "docs", "index.md"), "# Footer Logo");
|
|
938
|
+
await writeFile(
|
|
939
|
+
join(siteDir, "assets", "brand", "footer-logo.png"),
|
|
940
|
+
Buffer.from(
|
|
941
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wl9x9kAAAAASUVORK5CYII=",
|
|
942
|
+
"base64",
|
|
943
|
+
),
|
|
944
|
+
);
|
|
945
|
+
|
|
946
|
+
await bundleSite({ folder: siteDir, out: "bundles", name: "bundle.html" });
|
|
947
|
+
|
|
948
|
+
const html = await readFile(join(siteDir, "bundles", "bundle.html"), "utf-8");
|
|
949
|
+
expect(html).toContain('class="footer-logo-img"');
|
|
950
|
+
expect(html).toContain('src="data:image/png;base64,');
|
|
951
|
+
expect(html).not.toContain('src="assets/brand/footer-logo.png"');
|
|
952
|
+
expect(html).toContain('alt="Footer Brand"');
|
|
953
|
+
expect(html).toContain("max-height: 24px");
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
it("inlines footer logo from site-root-relative path outside assets", async () => {
|
|
957
|
+
const siteDir = await makeTempDir();
|
|
958
|
+
await mkdir(join(siteDir, "docs"), { recursive: true });
|
|
959
|
+
await mkdir(join(siteDir, "logos"), { recursive: true });
|
|
960
|
+
await writeFile(
|
|
961
|
+
join(siteDir, "site.yaml"),
|
|
962
|
+
'title: "Bundle Footer Root Path Test"\nbrand:\n name: "Test"\n url: "/"\nfooter:\n logo: "logos/footer.png"\n logoAlt: "Footer Root Logo"\nsections:\n - name: Docs\n path: docs\n',
|
|
963
|
+
"utf-8",
|
|
964
|
+
);
|
|
965
|
+
await writeFile(join(siteDir, "docs", "index.md"), "# Footer Root Path");
|
|
966
|
+
await writeFile(
|
|
967
|
+
join(siteDir, "logos", "footer.png"),
|
|
968
|
+
Buffer.from(
|
|
969
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wl9x9kAAAAASUVORK5CYII=",
|
|
970
|
+
"base64",
|
|
971
|
+
),
|
|
972
|
+
);
|
|
973
|
+
|
|
974
|
+
await bundleSite({ folder: siteDir, out: "bundles", name: "bundle.html" });
|
|
975
|
+
|
|
976
|
+
const html = await readFile(join(siteDir, "bundles", "bundle.html"), "utf-8");
|
|
977
|
+
expect(html).toContain('class="footer-logo-img"');
|
|
978
|
+
expect(html).toContain('src="data:image/png;base64,');
|
|
979
|
+
expect(html).not.toContain('src="logos/footer.png"');
|
|
980
|
+
expect(html).toContain('alt="Footer Root Logo"');
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
it("inlines light/dark variants for header and footer logos", async () => {
|
|
984
|
+
const siteDir = await makeTempDir();
|
|
985
|
+
await mkdir(join(siteDir, "docs"), { recursive: true });
|
|
986
|
+
await mkdir(join(siteDir, "assets", "brand"), { recursive: true });
|
|
987
|
+
await writeFile(
|
|
988
|
+
join(siteDir, "site.yaml"),
|
|
989
|
+
'title: "Bundle Dark Logos Test"\nbrand:\n name: "Test"\n url: "/"\n logo: "assets/brand/logo.png"\n logoDark: "assets/brand/logo-dark.png"\nfooter:\n logo: "assets/brand/footer-logo.png"\n logoDark: "assets/brand/footer-logo-dark.png"\nsections:\n - name: Docs\n path: docs\n',
|
|
990
|
+
"utf-8",
|
|
991
|
+
);
|
|
992
|
+
await writeFile(join(siteDir, "docs", "index.md"), "# Dark Logos");
|
|
993
|
+
const pixelPng = Buffer.from(
|
|
994
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wl9x9kAAAAASUVORK5CYII=",
|
|
995
|
+
"base64",
|
|
996
|
+
);
|
|
997
|
+
await writeFile(join(siteDir, "assets", "brand", "logo.png"), pixelPng);
|
|
998
|
+
await writeFile(join(siteDir, "assets", "brand", "logo-dark.png"), pixelPng);
|
|
999
|
+
await writeFile(join(siteDir, "assets", "brand", "footer-logo.png"), pixelPng);
|
|
1000
|
+
await writeFile(join(siteDir, "assets", "brand", "footer-logo-dark.png"), pixelPng);
|
|
1001
|
+
|
|
1002
|
+
await bundleSite({ folder: siteDir, out: "bundles", name: "bundle.html" });
|
|
1003
|
+
|
|
1004
|
+
const html = await readFile(join(siteDir, "bundles", "bundle.html"), "utf-8");
|
|
1005
|
+
expect(html).toContain('class="logo-img logo-light"');
|
|
1006
|
+
expect(html).toContain('class="logo-img logo-dark"');
|
|
1007
|
+
expect(html).toContain('class="footer-logo-img logo-light"');
|
|
1008
|
+
expect(html).toContain('class="footer-logo-img logo-dark"');
|
|
1009
|
+
expect(html).not.toContain('src="assets/brand/logo-dark.png"');
|
|
1010
|
+
expect(html).not.toContain('src="assets/brand/footer-logo-dark.png"');
|
|
1011
|
+
});
|
|
1012
|
+
});
|
|
@@ -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
|
+
:::
|
|
@@ -9,8 +9,8 @@ import { existsSync } from "node:fs";
|
|
|
9
9
|
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
10
10
|
import { tmpdir } from "node:os";
|
|
11
11
|
import { join } from "node:path";
|
|
12
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
13
|
-
import { init } from "../commands/init.ts";
|
|
12
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
13
|
+
import { init, listAvailableTemplates } from "../commands/init.ts";
|
|
14
14
|
import { defaultBranding, getTemplate, listTemplates, runTemplate } from "../templates/driver.ts";
|
|
15
15
|
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
@@ -53,6 +53,7 @@ describe("template registry", () => {
|
|
|
53
53
|
expect(ids).toContain("minimal");
|
|
54
54
|
expect(ids).toContain("deck");
|
|
55
55
|
expect(ids).toContain("handbook");
|
|
56
|
+
expect(ids).toContain("brief");
|
|
56
57
|
expect(templates.length).toBeGreaterThanOrEqual(2);
|
|
57
58
|
});
|
|
58
59
|
|
|
@@ -66,6 +67,17 @@ describe("template registry", () => {
|
|
|
66
67
|
it("returns undefined for unknown template", () => {
|
|
67
68
|
expect(getTemplate("nonexistent")).toBeUndefined();
|
|
68
69
|
});
|
|
70
|
+
|
|
71
|
+
it("listAvailableTemplates output includes brief", () => {
|
|
72
|
+
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
73
|
+
try {
|
|
74
|
+
listAvailableTemplates();
|
|
75
|
+
const output = spy.mock.calls.map((call) => String(call[0] ?? "")).join("\n");
|
|
76
|
+
expect(output).toContain("brief");
|
|
77
|
+
} finally {
|
|
78
|
+
spy.mockRestore();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
69
81
|
});
|
|
70
82
|
|
|
71
83
|
// ---------------------------------------------------------------------------
|
|
@@ -358,6 +370,16 @@ describe("init() entry point", () => {
|
|
|
358
370
|
expect(generatedExists(projectName, "CUSTOMIZING.md")).toBe(true);
|
|
359
371
|
});
|
|
360
372
|
|
|
373
|
+
it("creates a brief site when template flag is set", async () => {
|
|
374
|
+
await init(projectName, { template: "brief", git: false });
|
|
375
|
+
|
|
376
|
+
expect(generatedExists(projectName, "content/product/overview.md")).toBe(true);
|
|
377
|
+
expect(generatedExists(projectName, "content/use-cases/example-use-case.md")).toBe(true);
|
|
378
|
+
expect(generatedExists(projectName, "content/getting-started/requirements.md")).toBe(true);
|
|
379
|
+
expect(generatedExists(projectName, "content/reference/contacts.md")).toBe(true);
|
|
380
|
+
expect(generatedExists(projectName, "CUSTOMIZING.md")).toBe(true);
|
|
381
|
+
});
|
|
382
|
+
|
|
361
383
|
it("passes brand overrides through to template context", async () => {
|
|
362
384
|
await init(projectName, {
|
|
363
385
|
git: false,
|
|
@@ -439,6 +461,33 @@ describe("standalone mode", () => {
|
|
|
439
461
|
});
|
|
440
462
|
});
|
|
441
463
|
|
|
464
|
+
describe("ai-assist mode", () => {
|
|
465
|
+
const projectName = "test-brief-ai";
|
|
466
|
+
|
|
467
|
+
it("brief template writes AGENTS.md and brief-specific role files", async () => {
|
|
468
|
+
await runTemplate({
|
|
469
|
+
name: projectName,
|
|
470
|
+
template: "brief",
|
|
471
|
+
git: false,
|
|
472
|
+
aiAssist: true,
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
expect(generatedExists(projectName, "AGENTS.md")).toBe(true);
|
|
476
|
+
expect(generatedExists(projectName, "config/agentic/roles/devlead.yaml")).toBe(true);
|
|
477
|
+
expect(generatedExists(projectName, "config/agentic/roles/infoarch.yaml")).toBe(true);
|
|
478
|
+
expect(generatedExists(projectName, "config/agentic/roles/qa.yaml")).toBe(true);
|
|
479
|
+
expect(generatedExists(projectName, "config/agentic/roles/prodstrat.yaml")).toBe(true);
|
|
480
|
+
expect(generatedExists(projectName, "config/agentic/roles/advisor.yaml")).toBe(true);
|
|
481
|
+
expect(generatedExists(projectName, "config/agentic/roles/analyst.yaml")).toBe(false);
|
|
482
|
+
expect(generatedExists(projectName, "config/agentic/roles/prodmktg.yaml")).toBe(false);
|
|
483
|
+
|
|
484
|
+
const agents = await readGenerated(projectName, "AGENTS.md");
|
|
485
|
+
expect(agents).toContain("external audience brief");
|
|
486
|
+
expect(agents).toContain("informational and professional");
|
|
487
|
+
expect(agents).toContain("problem -> solution -> outcome");
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
442
491
|
// ---------------------------------------------------------------------------
|
|
443
492
|
// Default Branding Derivation
|
|
444
493
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
type LatexHooks = {
|
|
4
|
+
splitMath: (text: string) => Array<{ text?: string; math?: string; display?: boolean }>;
|
|
5
|
+
isCurrency: (expr: string) => boolean;
|
|
6
|
+
shouldTreatAsLiteralInline: (expr: string, text: string, closingIndex: number) => boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
async function loadHooks(): Promise<LatexHooks> {
|
|
10
|
+
// @ts-expect-error JS plugin helper file
|
|
11
|
+
await import("../../plugins-dist/latex-runtime.js");
|
|
12
|
+
const hooks = (globalThis as any).__kitflyLatexTest as LatexHooks | undefined;
|
|
13
|
+
if (!hooks) throw new Error("latex test hooks not found on globalThis");
|
|
14
|
+
return hooks;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test("latex: parses inline math", async () => {
|
|
18
|
+
const hooks = await loadHooks();
|
|
19
|
+
expect(hooks.splitMath("A $x^2$ value")).toEqual([
|
|
20
|
+
{ text: "A " },
|
|
21
|
+
{ math: "x^2", display: false },
|
|
22
|
+
{ text: " value" },
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("latex: treats $5-$10 as literal currency range", async () => {
|
|
27
|
+
const hooks = await loadHooks();
|
|
28
|
+
expect(hooks.splitMath("$5-$10")).toEqual([{ text: "$5-$10" }]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("latex: currency helper matches dashed ranges", async () => {
|
|
32
|
+
const hooks = await loadHooks();
|
|
33
|
+
expect(hooks.isCurrency("5-10")).toBe(true);
|
|
34
|
+
expect(hooks.isCurrency("5 to 10")).toBe(true);
|
|
35
|
+
});
|
|
@@ -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
|
+
});
|