kitfly 0.1.2

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 (62) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/LICENSE +21 -0
  3. package/README.md +136 -0
  4. package/VERSION +1 -0
  5. package/package.json +63 -0
  6. package/schemas/README.md +32 -0
  7. package/schemas/site.schema.json +5 -0
  8. package/schemas/theme.schema.json +5 -0
  9. package/schemas/v0/site.schema.json +172 -0
  10. package/schemas/v0/theme.schema.json +210 -0
  11. package/scripts/build-all.ts +121 -0
  12. package/scripts/build.ts +601 -0
  13. package/scripts/bundle.ts +781 -0
  14. package/scripts/dev.ts +777 -0
  15. package/scripts/generate-checksums.sh +78 -0
  16. package/scripts/release/export-release-key.sh +28 -0
  17. package/scripts/release/release-guard-tag-version.sh +79 -0
  18. package/scripts/release/sign-release-assets.sh +123 -0
  19. package/scripts/release/upload-release-assets.sh +76 -0
  20. package/scripts/release/upload-release-provenance.sh +52 -0
  21. package/scripts/release/verify-public-key.sh +48 -0
  22. package/scripts/release/verify-signatures.sh +117 -0
  23. package/scripts/version-sync.ts +82 -0
  24. package/src/__tests__/build.test.ts +240 -0
  25. package/src/__tests__/bundle.test.ts +786 -0
  26. package/src/__tests__/cli.test.ts +706 -0
  27. package/src/__tests__/crucible.test.ts +1043 -0
  28. package/src/__tests__/engine.test.ts +157 -0
  29. package/src/__tests__/init.test.ts +450 -0
  30. package/src/__tests__/pipeline.test.ts +1087 -0
  31. package/src/__tests__/productbook.test.ts +1206 -0
  32. package/src/__tests__/runbook.test.ts +974 -0
  33. package/src/__tests__/server-registry.test.ts +1251 -0
  34. package/src/__tests__/servicebook.test.ts +1248 -0
  35. package/src/__tests__/shared.test.ts +2005 -0
  36. package/src/__tests__/styles.test.ts +14 -0
  37. package/src/__tests__/theme-schema.test.ts +47 -0
  38. package/src/__tests__/theme.test.ts +554 -0
  39. package/src/cli.ts +582 -0
  40. package/src/commands/init.ts +92 -0
  41. package/src/commands/update.ts +444 -0
  42. package/src/engine.ts +20 -0
  43. package/src/logger.ts +15 -0
  44. package/src/migrations/0000_schema_versioning.ts +67 -0
  45. package/src/migrations/0001_server_port.ts +52 -0
  46. package/src/migrations/0002_brand_logo.ts +49 -0
  47. package/src/migrations/index.ts +26 -0
  48. package/src/migrations/schema.ts +24 -0
  49. package/src/server-registry.ts +405 -0
  50. package/src/shared.ts +1239 -0
  51. package/src/site/styles.css +931 -0
  52. package/src/site/template.html +193 -0
  53. package/src/templates/crucible.ts +1163 -0
  54. package/src/templates/driver.ts +876 -0
  55. package/src/templates/handbook.ts +339 -0
  56. package/src/templates/minimal.ts +139 -0
  57. package/src/templates/pipeline.ts +966 -0
  58. package/src/templates/productbook.ts +1032 -0
  59. package/src/templates/runbook.ts +829 -0
  60. package/src/templates/schema.ts +119 -0
  61. package/src/templates/servicebook.ts +1242 -0
  62. package/src/theme.ts +245 -0
@@ -0,0 +1,1206 @@
1
+ /**
2
+ * Tests for the Productbook template definition
3
+ *
4
+ * Covers: src/templates/productbook.ts
5
+ * Strategy: import the template directly, verify structure and generated content
6
+ */
7
+
8
+ import { describe, expect, it } from "vitest";
9
+ import { productbook } from "../templates/productbook.ts";
10
+ import type { BrandingConfig, TemplateContext, TemplateFile } from "../templates/schema.ts";
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Helpers
14
+ // ---------------------------------------------------------------------------
15
+
16
+ /** Build a standard TemplateContext for testing */
17
+ function makeCtx(overrides?: Partial<TemplateContext>): TemplateContext {
18
+ const branding: BrandingConfig = {
19
+ siteName: "Test Productbook",
20
+ brandName: "Acme Corp",
21
+ brandUrl: "https://acme.example.com",
22
+ primaryColor: "#2563eb",
23
+ footerText: "Footer text",
24
+ };
25
+ return {
26
+ name: "test-productbook",
27
+ branding,
28
+ template: productbook,
29
+ year: 2026,
30
+ ...overrides,
31
+ };
32
+ }
33
+
34
+ /** Resolve the content of a TemplateFile to a string */
35
+ function resolveContent(file: TemplateFile, ctx: TemplateContext): string {
36
+ return typeof file.content === "function" ? file.content(ctx) : file.content;
37
+ }
38
+
39
+ /** Find a file entry by its path */
40
+ function findFile(path: string): TemplateFile {
41
+ const f = productbook.files.find((entry) => entry.path === path);
42
+ if (!f) throw new Error(`Template file not found: ${path}`);
43
+ return f;
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Template Definition Structure
48
+ // ---------------------------------------------------------------------------
49
+
50
+ describe("productbook template definition", () => {
51
+ it("has correct identity metadata", () => {
52
+ expect(productbook.id).toBe("productbook");
53
+ expect(productbook.name).toBe("Productbook");
54
+ expect(productbook.version).toBe(1);
55
+ expect(productbook.extends).toBe("minimal");
56
+ });
57
+
58
+ it("has a non-empty description", () => {
59
+ expect(productbook.description).toBeTruthy();
60
+ expect(productbook.description.length).toBeGreaterThan(10);
61
+ });
62
+
63
+ it("defines exactly six sections", () => {
64
+ expect(productbook.sections).toHaveLength(6);
65
+ });
66
+
67
+ it("defines the expected section names", () => {
68
+ const names = productbook.sections.map((s) => s.name);
69
+ expect(names).toEqual(["Product", "Domain", "Planning", "Operations", "Guides", "Reference"]);
70
+ });
71
+
72
+ it("defines the expected section paths", () => {
73
+ const paths = productbook.sections.map((s) => s.path);
74
+ expect(paths).toEqual([
75
+ "content/product",
76
+ "content/domain",
77
+ "content/planning",
78
+ "content/operations",
79
+ "content/guides",
80
+ "content/reference",
81
+ ]);
82
+ });
83
+
84
+ it("every section has a description", () => {
85
+ for (const section of productbook.sections) {
86
+ expect(section.description).toBeTruthy();
87
+ expect(typeof section.description).toBe("string");
88
+ }
89
+ });
90
+
91
+ it("defines a non-empty files array", () => {
92
+ expect(productbook.files.length).toBeGreaterThan(0);
93
+ });
94
+
95
+ it("every file has a non-empty path", () => {
96
+ for (const file of productbook.files) {
97
+ expect(file.path).toBeTruthy();
98
+ expect(typeof file.path).toBe("string");
99
+ }
100
+ });
101
+
102
+ it("every file has content (string or function)", () => {
103
+ for (const file of productbook.files) {
104
+ expect(["string", "function"]).toContain(typeof file.content);
105
+ }
106
+ });
107
+
108
+ it("has no duplicate file paths", () => {
109
+ const paths = productbook.files.map((f) => f.path);
110
+ const unique = new Set(paths);
111
+ expect(unique.size).toBe(paths.length);
112
+ });
113
+ });
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // site.yaml Generation
117
+ // ---------------------------------------------------------------------------
118
+
119
+ describe("productbook site.yaml", () => {
120
+ const ctx = makeCtx();
121
+ const file = findFile("site.yaml");
122
+
123
+ it("exists in the file list", () => {
124
+ expect(file).toBeDefined();
125
+ });
126
+
127
+ it("includes the site title from context", () => {
128
+ const content = resolveContent(file, ctx);
129
+ expect(content).toContain('title: "Test Productbook"');
130
+ });
131
+
132
+ it("includes the brand name from context", () => {
133
+ const content = resolveContent(file, ctx);
134
+ expect(content).toContain('name: "Acme Corp"');
135
+ });
136
+
137
+ it("includes the brand url from context", () => {
138
+ const content = resolveContent(file, ctx);
139
+ expect(content).toContain('url: "https://acme.example.com"');
140
+ });
141
+
142
+ it("includes sections block with all six sections", () => {
143
+ const content = resolveContent(file, ctx);
144
+ expect(content).toContain("sections:");
145
+ expect(content).toContain('"Product"');
146
+ expect(content).toContain('"Domain"');
147
+ expect(content).toContain('"Planning"');
148
+ expect(content).toContain('"Operations"');
149
+ expect(content).toContain('"Guides"');
150
+ expect(content).toContain('"Reference"');
151
+ });
152
+
153
+ it("includes section paths", () => {
154
+ const content = resolveContent(file, ctx);
155
+ expect(content).toContain('"content/product"');
156
+ expect(content).toContain('"content/domain"');
157
+ expect(content).toContain('"content/planning"');
158
+ expect(content).toContain('"content/operations"');
159
+ expect(content).toContain('"content/guides"');
160
+ expect(content).toContain('"content/reference"');
161
+ });
162
+
163
+ it("sets home page to index.md", () => {
164
+ const content = resolveContent(file, ctx);
165
+ expect(content).toContain('home: "index.md"');
166
+ });
167
+
168
+ it("includes documentation link", () => {
169
+ const content = resolveContent(file, ctx);
170
+ expect(content).toContain("https://github.com/3leaps/kitfly");
171
+ });
172
+
173
+ it("responds to different branding values", () => {
174
+ const customCtx = makeCtx({
175
+ branding: {
176
+ siteName: "Widget Docs",
177
+ brandName: "Widget Inc",
178
+ brandUrl: "/",
179
+ },
180
+ });
181
+ const content = resolveContent(file, customCtx);
182
+ expect(content).toContain('title: "Widget Docs"');
183
+ expect(content).toContain('name: "Widget Inc"');
184
+ expect(content).toContain('url: "/"');
185
+ });
186
+ });
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // index.md Generation
190
+ // ---------------------------------------------------------------------------
191
+
192
+ describe("productbook index.md", () => {
193
+ const ctx = makeCtx();
194
+ const file = findFile("index.md");
195
+
196
+ it("exists in the file list", () => {
197
+ expect(file).toBeDefined();
198
+ });
199
+
200
+ it("starts with YAML frontmatter", () => {
201
+ const content = resolveContent(file, ctx);
202
+ expect(content).toMatch(/^---\n/);
203
+ expect(content).toContain("title: Home");
204
+ });
205
+
206
+ it("includes site name in frontmatter description", () => {
207
+ const content = resolveContent(file, ctx);
208
+ expect(content).toContain("description: Test Productbook - Product & Domain Documentation");
209
+ });
210
+
211
+ it("includes site name in the H1 heading", () => {
212
+ const content = resolveContent(file, ctx);
213
+ expect(content).toContain("# Test Productbook");
214
+ });
215
+
216
+ it("includes brand name in product description", () => {
217
+ const content = resolveContent(file, ctx);
218
+ expect(content).toContain("Product and domain documentation for Acme Corp.");
219
+ });
220
+
221
+ it("contains a product status table", () => {
222
+ const content = resolveContent(file, ctx);
223
+ expect(content).toContain("## Product");
224
+ expect(content).toContain("| Area | Description | Status |");
225
+ expect(content).toContain("Core Platform");
226
+ expect(content).toContain("Integrations");
227
+ expect(content).toContain("User Experience");
228
+ });
229
+
230
+ it("contains quick links to key sections", () => {
231
+ const content = resolveContent(file, ctx);
232
+ expect(content).toContain("## Quick Links");
233
+ expect(content).toContain("[Product Overview](/content/product/overview)");
234
+ expect(content).toContain("[Domain Model](/content/domain/overview)");
235
+ expect(content).toContain("[Roadmap](/content/planning/roadmap)");
236
+ expect(content).toContain("[Getting Started](/content/guides/getting-started)");
237
+ });
238
+
239
+ it("includes a last-updated date in ISO format", () => {
240
+ const content = resolveContent(file, ctx);
241
+ expect(content).toMatch(/\*Last updated: \d{4}-\d{2}-\d{2}\*/);
242
+ });
243
+ });
244
+
245
+ // ---------------------------------------------------------------------------
246
+ // Product Section Files
247
+ // ---------------------------------------------------------------------------
248
+
249
+ describe("productbook product section", () => {
250
+ const ctx = makeCtx();
251
+
252
+ describe("overview.md", () => {
253
+ const file = findFile("content/product/overview.md");
254
+
255
+ it("exists in the file list", () => {
256
+ expect(file).toBeDefined();
257
+ });
258
+
259
+ it("has YAML frontmatter with title and description", () => {
260
+ const content = resolveContent(file, ctx);
261
+ expect(content).toMatch(/^---\n/);
262
+ expect(content).toContain("title: Product Overview");
263
+ expect(content).toContain("description: Vision, users, and capabilities for Acme Corp");
264
+ });
265
+
266
+ it("includes vision and target users sections", () => {
267
+ const content = resolveContent(file, ctx);
268
+ expect(content).toContain("## Vision");
269
+ expect(content).toContain("## Target Users");
270
+ });
271
+
272
+ it("includes key capabilities section", () => {
273
+ const content = resolveContent(file, ctx);
274
+ expect(content).toContain("## Key Capabilities");
275
+ });
276
+
277
+ it("includes success metrics table", () => {
278
+ const content = resolveContent(file, ctx);
279
+ expect(content).toContain("## Success Metrics");
280
+ expect(content).toContain("| Metric | Target | How Measured |");
281
+ });
282
+
283
+ it("links to related pages", () => {
284
+ const content = resolveContent(file, ctx);
285
+ expect(content).toContain("[Domain Overview](/content/domain/overview)");
286
+ expect(content).toContain("[Roadmap](/content/planning/roadmap)");
287
+ });
288
+ });
289
+
290
+ describe("features/index.md", () => {
291
+ const file = findFile("content/product/features/index.md");
292
+
293
+ it("exists in the file list", () => {
294
+ expect(file).toBeDefined();
295
+ });
296
+
297
+ it("has frontmatter referencing brand name", () => {
298
+ const content = resolveContent(file, ctx);
299
+ expect(content).toContain("title: Features");
300
+ expect(content).toContain("description: Feature catalog for Acme Corp");
301
+ });
302
+
303
+ it("includes a feature catalog table", () => {
304
+ const content = resolveContent(file, ctx);
305
+ expect(content).toContain("| Feature | Status | Description | Spec |");
306
+ });
307
+
308
+ it("documents how to add a feature", () => {
309
+ const content = resolveContent(file, ctx);
310
+ expect(content).toContain("## Adding a Feature");
311
+ expect(content).toContain("content/product/features/");
312
+ });
313
+
314
+ it("links to planning specs", () => {
315
+ const content = resolveContent(file, ctx);
316
+ expect(content).toContain("[Planning](/content/planning/specs/)");
317
+ });
318
+ });
319
+
320
+ describe("releases/index.md", () => {
321
+ const file = findFile("content/product/releases/index.md");
322
+
323
+ it("exists in the file list", () => {
324
+ expect(file).toBeDefined();
325
+ });
326
+
327
+ it("has frontmatter referencing brand name", () => {
328
+ const content = resolveContent(file, ctx);
329
+ expect(content).toContain("title: Releases");
330
+ expect(content).toContain("description: Release history for Acme Corp");
331
+ });
332
+
333
+ it("includes a release table", () => {
334
+ const content = resolveContent(file, ctx);
335
+ expect(content).toContain("| Version | Date | Highlights |");
336
+ });
337
+
338
+ it("documents the release process", () => {
339
+ const content = resolveContent(file, ctx);
340
+ expect(content).toContain("## Release Process");
341
+ expect(content).toContain("[Operations](/content/operations/deployment)");
342
+ });
343
+ });
344
+ });
345
+
346
+ // ---------------------------------------------------------------------------
347
+ // Domain Section Files
348
+ // ---------------------------------------------------------------------------
349
+
350
+ describe("productbook domain section", () => {
351
+ const ctx = makeCtx();
352
+
353
+ describe("overview.md", () => {
354
+ const file = findFile("content/domain/overview.md");
355
+
356
+ it("exists in the file list", () => {
357
+ expect(file).toBeDefined();
358
+ });
359
+
360
+ it("has YAML frontmatter with title and description", () => {
361
+ const content = resolveContent(file, ctx);
362
+ expect(content).toMatch(/^---\n/);
363
+ expect(content).toContain("title: Domain Overview");
364
+ expect(content).toContain("description: Business domain context for Acme Corp");
365
+ });
366
+
367
+ it("includes key concepts table", () => {
368
+ const content = resolveContent(file, ctx);
369
+ expect(content).toContain("## Key Concepts");
370
+ expect(content).toContain("| Concept | Definition | Why It Matters |");
371
+ });
372
+
373
+ it("covers domain boundaries and complexity drivers", () => {
374
+ const content = resolveContent(file, ctx);
375
+ expect(content).toContain("## Domain Boundaries");
376
+ expect(content).toContain("## Complexity Drivers");
377
+ });
378
+
379
+ it("links to related domain pages", () => {
380
+ const content = resolveContent(file, ctx);
381
+ expect(content).toContain("[Business Processes](/content/domain/processes/)");
382
+ expect(content).toContain("[Data Dictionary](/content/domain/data-dictionary)");
383
+ expect(content).toContain("[Industry Notes](/content/domain/industry-notes)");
384
+ });
385
+ });
386
+
387
+ describe("processes/index.md", () => {
388
+ const file = findFile("content/domain/processes/index.md");
389
+
390
+ it("exists in the file list", () => {
391
+ expect(file).toBeDefined();
392
+ });
393
+
394
+ it("has frontmatter referencing brand name", () => {
395
+ const content = resolveContent(file, ctx);
396
+ expect(content).toContain("title: Business Processes");
397
+ expect(content).toContain("description: Business process catalog for Acme Corp");
398
+ });
399
+
400
+ it("includes a process catalog table", () => {
401
+ const content = resolveContent(file, ctx);
402
+ expect(content).toContain("| Process | Trigger | Key Systems | Documentation |");
403
+ });
404
+
405
+ it("documents what each process should capture", () => {
406
+ const content = resolveContent(file, ctx);
407
+ expect(content).toContain("## Documenting a Process");
408
+ expect(content).toContain("**Trigger**");
409
+ expect(content).toContain("**Actors**");
410
+ expect(content).toContain("**Steps**");
411
+ expect(content).toContain("**Variations**");
412
+ expect(content).toContain("**Systems**");
413
+ expect(content).toContain("**Data**");
414
+ expect(content).toContain("**Business Rules**");
415
+ });
416
+
417
+ it("includes a process template example", () => {
418
+ const content = resolveContent(file, ctx);
419
+ expect(content).toContain("## Process Template");
420
+ expect(content).toContain("```markdown");
421
+ });
422
+ });
423
+
424
+ describe("data-dictionary.md", () => {
425
+ const file = findFile("content/domain/data-dictionary.md");
426
+
427
+ it("exists in the file list", () => {
428
+ expect(file).toBeDefined();
429
+ });
430
+
431
+ it("has frontmatter referencing brand name", () => {
432
+ const content = resolveContent(file, ctx);
433
+ expect(content).toContain("title: Data Dictionary");
434
+ expect(content).toContain("description: Canonical term definitions for Acme Corp");
435
+ });
436
+
437
+ it("includes core terms and data entities sections", () => {
438
+ const content = resolveContent(file, ctx);
439
+ expect(content).toContain("## Core Terms");
440
+ expect(content).toContain("| Term | Definition | Also Known As | Used In |");
441
+ expect(content).toContain("## Data Entities");
442
+ });
443
+
444
+ it("includes relationships and naming conventions", () => {
445
+ const content = resolveContent(file, ctx);
446
+ expect(content).toContain("## Relationships");
447
+ expect(content).toContain("## Naming Conventions");
448
+ });
449
+ });
450
+
451
+ describe("industry-notes.md", () => {
452
+ const file = findFile("content/domain/industry-notes.md");
453
+
454
+ it("exists in the file list", () => {
455
+ expect(file).toBeDefined();
456
+ });
457
+
458
+ it("has frontmatter referencing brand name", () => {
459
+ const content = resolveContent(file, ctx);
460
+ expect(content).toContain("title: Industry Notes");
461
+ expect(content).toContain("description: Industry context for Acme Corp");
462
+ });
463
+
464
+ it("covers regulatory environment and industry standards", () => {
465
+ const content = resolveContent(file, ctx);
466
+ expect(content).toContain("## Regulatory Environment");
467
+ expect(content).toContain("| Regulation | Scope | Impact on Product |");
468
+ expect(content).toContain("## Industry Standards");
469
+ });
470
+
471
+ it("covers competitive landscape and market dynamics", () => {
472
+ const content = resolveContent(file, ctx);
473
+ expect(content).toContain("## Competitive Landscape");
474
+ expect(content).toContain("## Market Dynamics");
475
+ });
476
+ });
477
+ });
478
+
479
+ // ---------------------------------------------------------------------------
480
+ // Planning Section Files
481
+ // ---------------------------------------------------------------------------
482
+
483
+ describe("productbook planning section", () => {
484
+ const ctx = makeCtx();
485
+
486
+ describe("roadmap.md", () => {
487
+ const file = findFile("content/planning/roadmap.md");
488
+
489
+ it("exists in the file list", () => {
490
+ expect(file).toBeDefined();
491
+ });
492
+
493
+ it("has frontmatter referencing brand name", () => {
494
+ const content = resolveContent(file, ctx);
495
+ expect(content).toContain("title: Roadmap");
496
+ expect(content).toContain("description: Product roadmap for Acme Corp");
497
+ });
498
+
499
+ it("includes current phase and priorities", () => {
500
+ const content = resolveContent(file, ctx);
501
+ expect(content).toContain("## Current Phase");
502
+ expect(content).toContain("## Priorities");
503
+ expect(content).toContain("| Priority | Initiative | Rationale | Status |");
504
+ });
505
+
506
+ it("includes what we are not doing section", () => {
507
+ const content = resolveContent(file, ctx);
508
+ expect(content).toContain("## What We're NOT Doing (and Why)");
509
+ expect(content).toContain("| Deferred Item | Reason | Revisit When |");
510
+ });
511
+
512
+ it("includes phase history", () => {
513
+ const content = resolveContent(file, ctx);
514
+ expect(content).toContain("## Phase History");
515
+ expect(content).toContain("| Phase | Dates | Outcome |");
516
+ });
517
+
518
+ it("links to related planning pages", () => {
519
+ const content = resolveContent(file, ctx);
520
+ expect(content).toContain("[Specs](/content/planning/specs/)");
521
+ expect(content).toContain("[Decisions](/content/planning/decisions/)");
522
+ expect(content).toContain("[Research](/content/planning/research/)");
523
+ });
524
+ });
525
+
526
+ describe("decisions/index.md", () => {
527
+ const file = findFile("content/planning/decisions/index.md");
528
+
529
+ it("exists in the file list", () => {
530
+ expect(file).toBeDefined();
531
+ });
532
+
533
+ it("has YAML frontmatter", () => {
534
+ const content = resolveContent(file, ctx);
535
+ expect(content).toMatch(/^---\n/);
536
+ expect(content).toContain("title: Decisions");
537
+ });
538
+
539
+ it("includes a decision log table", () => {
540
+ const content = resolveContent(file, ctx);
541
+ expect(content).toContain("# Decision Log");
542
+ expect(content).toContain("| ID | Decision | Date | Status |");
543
+ });
544
+
545
+ it("links to the ADR template", () => {
546
+ const content = resolveContent(file, ctx);
547
+ expect(content).toContain("[ADR Template](./adr-template)");
548
+ });
549
+
550
+ it("is a static template (does not use context)", () => {
551
+ const ctx2 = makeCtx({
552
+ branding: { siteName: "Other", brandName: "Other" },
553
+ });
554
+ expect(resolveContent(file, ctx)).toBe(resolveContent(file, ctx2));
555
+ });
556
+ });
557
+
558
+ describe("decisions/adr-template.md", () => {
559
+ const file = findFile("content/planning/decisions/adr-template.md");
560
+
561
+ it("exists in the file list", () => {
562
+ expect(file).toBeDefined();
563
+ });
564
+
565
+ it("has frontmatter with ADR-000 title", () => {
566
+ const content = resolveContent(file, ctx);
567
+ expect(content).toContain('title: "ADR-000: Decision Template"');
568
+ });
569
+
570
+ it("includes all ADR sections", () => {
571
+ const content = resolveContent(file, ctx);
572
+ const requiredSections = [
573
+ "## Status",
574
+ "## Context",
575
+ "## Options Considered",
576
+ "## Decision",
577
+ "## Consequences",
578
+ ];
579
+ for (const section of requiredSections) {
580
+ expect(content).toContain(section);
581
+ }
582
+ });
583
+
584
+ it("includes positive, negative, and risks subsections", () => {
585
+ const content = resolveContent(file, ctx);
586
+ expect(content).toContain("### Positive");
587
+ expect(content).toContain("### Negative");
588
+ expect(content).toContain("### Risks");
589
+ });
590
+
591
+ it("is a static template (does not use context)", () => {
592
+ const ctx2 = makeCtx({
593
+ branding: { siteName: "Other", brandName: "Other" },
594
+ });
595
+ expect(resolveContent(file, ctx)).toBe(resolveContent(file, ctx2));
596
+ });
597
+ });
598
+
599
+ describe("specs/index.md", () => {
600
+ const file = findFile("content/planning/specs/index.md");
601
+
602
+ it("exists in the file list", () => {
603
+ expect(file).toBeDefined();
604
+ });
605
+
606
+ it("has frontmatter referencing brand name", () => {
607
+ const content = resolveContent(file, ctx);
608
+ expect(content).toContain("title: Specifications");
609
+ expect(content).toContain("description: Product specifications for Acme Corp");
610
+ });
611
+
612
+ it("includes a specifications table", () => {
613
+ const content = resolveContent(file, ctx);
614
+ expect(content).toContain("| Spec | Feature | Status | Owner |");
615
+ });
616
+
617
+ it("documents how to write a spec", () => {
618
+ const content = resolveContent(file, ctx);
619
+ expect(content).toContain("## Writing a Spec");
620
+ expect(content).toContain("**Problem Statement**");
621
+ expect(content).toContain("**Proposed Solution**");
622
+ expect(content).toContain("**Acceptance Criteria**");
623
+ expect(content).toContain("**Out of Scope**");
624
+ expect(content).toContain("**Dependencies**");
625
+ });
626
+ });
627
+
628
+ describe("research/index.md", () => {
629
+ const file = findFile("content/planning/research/index.md");
630
+
631
+ it("exists in the file list", () => {
632
+ expect(file).toBeDefined();
633
+ });
634
+
635
+ it("has frontmatter referencing brand name", () => {
636
+ const content = resolveContent(file, ctx);
637
+ expect(content).toContain("title: Research");
638
+ expect(content).toContain("description: Research and analysis for Acme Corp");
639
+ });
640
+
641
+ it("includes a research table", () => {
642
+ const content = resolveContent(file, ctx);
643
+ expect(content).toContain("| Topic | Type | Date | Key Finding |");
644
+ });
645
+
646
+ it("defines research types", () => {
647
+ const content = resolveContent(file, ctx);
648
+ expect(content).toContain("## Research Types");
649
+ expect(content).toContain("**Market research**");
650
+ expect(content).toContain("**User research**");
651
+ expect(content).toContain("**Technology assessment**");
652
+ expect(content).toContain("**Competitive analysis**");
653
+ });
654
+ });
655
+ });
656
+
657
+ // ---------------------------------------------------------------------------
658
+ // Operations Section Files
659
+ // ---------------------------------------------------------------------------
660
+
661
+ describe("productbook operations section", () => {
662
+ const ctx = makeCtx();
663
+
664
+ describe("environments.md", () => {
665
+ const file = findFile("content/operations/environments.md");
666
+
667
+ it("exists in the file list", () => {
668
+ expect(file).toBeDefined();
669
+ });
670
+
671
+ it("has frontmatter referencing brand name", () => {
672
+ const content = resolveContent(file, ctx);
673
+ expect(content).toContain("title: Environments");
674
+ expect(content).toContain("description: Environment catalog for Acme Corp");
675
+ });
676
+
677
+ it("includes an environments table", () => {
678
+ const content = resolveContent(file, ctx);
679
+ expect(content).toContain("| Environment | URL | Purpose | Access |");
680
+ expect(content).toContain("Development");
681
+ expect(content).toContain("Staging");
682
+ expect(content).toContain("Production");
683
+ });
684
+
685
+ it("covers configuration and access", () => {
686
+ const content = resolveContent(file, ctx);
687
+ expect(content).toContain("## Configuration");
688
+ expect(content).toContain("## Access");
689
+ });
690
+ });
691
+
692
+ describe("deployment.md", () => {
693
+ const file = findFile("content/operations/deployment.md");
694
+
695
+ it("exists in the file list", () => {
696
+ expect(file).toBeDefined();
697
+ });
698
+
699
+ it("has frontmatter referencing brand name", () => {
700
+ const content = resolveContent(file, ctx);
701
+ expect(content).toContain("title: Deployment");
702
+ expect(content).toContain("description: Deployment procedure for Acme Corp");
703
+ });
704
+
705
+ it("includes deployment prerequisites as checkboxes", () => {
706
+ const content = resolveContent(file, ctx);
707
+ expect(content).toContain("## Prerequisites");
708
+ expect(content).toContain("- [ ] Code reviewed and approved");
709
+ expect(content).toContain("- [ ] Tests passing");
710
+ });
711
+
712
+ it("documents the deployment procedure steps", () => {
713
+ const content = resolveContent(file, ctx);
714
+ expect(content).toContain("### 1. Pre-deployment");
715
+ expect(content).toContain("### 2. Deploy");
716
+ expect(content).toContain("### 3. Verify");
717
+ });
718
+
719
+ it("includes rollback section", () => {
720
+ const content = resolveContent(file, ctx);
721
+ expect(content).toContain("## Rollback");
722
+ });
723
+ });
724
+ });
725
+
726
+ // ---------------------------------------------------------------------------
727
+ // Guides Section Files
728
+ // ---------------------------------------------------------------------------
729
+
730
+ describe("productbook guides section", () => {
731
+ const ctx = makeCtx();
732
+
733
+ describe("getting-started.md", () => {
734
+ const file = findFile("content/guides/getting-started.md");
735
+
736
+ it("exists in the file list", () => {
737
+ expect(file).toBeDefined();
738
+ });
739
+
740
+ it("has frontmatter referencing brand name", () => {
741
+ const content = resolveContent(file, ctx);
742
+ expect(content).toContain("title: Getting Started");
743
+ expect(content).toContain("description: Onboarding guide for Acme Corp");
744
+ });
745
+
746
+ it("documents onboarding for new team members", () => {
747
+ const content = resolveContent(file, ctx);
748
+ expect(content).toContain("## For New Team Members");
749
+ expect(content).toContain("### 1. Read the Domain");
750
+ expect(content).toContain("### 2. Understand the Product");
751
+ expect(content).toContain("### 3. Review Current Plan");
752
+ expect(content).toContain("### 4. Set Up");
753
+ });
754
+
755
+ it("links to key documentation sections", () => {
756
+ const content = resolveContent(file, ctx);
757
+ expect(content).toContain("[Domain Overview](/content/domain/overview)");
758
+ expect(content).toContain("[Product Overview](/content/product/overview)");
759
+ expect(content).toContain("[Roadmap](/content/planning/roadmap)");
760
+ expect(content).toContain("[Operations](/content/operations/environments)");
761
+ });
762
+
763
+ it("includes AI agent instructions", () => {
764
+ const content = resolveContent(file, ctx);
765
+ expect(content).toContain("## For AI Agents");
766
+ expect(content).toContain("AGENTS.md");
767
+ expect(content).toContain("CUSTOMIZING.md");
768
+ });
769
+ });
770
+
771
+ describe("user-guide.md", () => {
772
+ const file = findFile("content/guides/user-guide.md");
773
+
774
+ it("exists in the file list", () => {
775
+ expect(file).toBeDefined();
776
+ });
777
+
778
+ it("has frontmatter referencing brand name", () => {
779
+ const content = resolveContent(file, ctx);
780
+ expect(content).toContain("title: User Guide");
781
+ expect(content).toContain("description: End-user documentation for Acme Corp");
782
+ });
783
+
784
+ it("includes overview and getting started sections", () => {
785
+ const content = resolveContent(file, ctx);
786
+ expect(content).toContain("## Overview");
787
+ expect(content).toContain("## Getting Started");
788
+ });
789
+
790
+ it("includes common tasks and FAQ sections", () => {
791
+ const content = resolveContent(file, ctx);
792
+ expect(content).toContain("## Common Tasks");
793
+ expect(content).toContain("## FAQ");
794
+ });
795
+
796
+ it("includes support section", () => {
797
+ const content = resolveContent(file, ctx);
798
+ expect(content).toContain("## Support");
799
+ });
800
+ });
801
+ });
802
+
803
+ // ---------------------------------------------------------------------------
804
+ // Reference Section Files
805
+ // ---------------------------------------------------------------------------
806
+
807
+ describe("productbook reference section", () => {
808
+ const ctx = makeCtx();
809
+
810
+ describe("architecture/overview.md", () => {
811
+ const file = findFile("content/reference/architecture/overview.md");
812
+
813
+ it("exists in the file list", () => {
814
+ expect(file).toBeDefined();
815
+ });
816
+
817
+ it("has frontmatter referencing brand name", () => {
818
+ const content = resolveContent(file, ctx);
819
+ expect(content).toContain("title: Architecture Overview");
820
+ expect(content).toContain("description: System architecture for Acme Corp");
821
+ });
822
+
823
+ it("includes components table", () => {
824
+ const content = resolveContent(file, ctx);
825
+ expect(content).toContain("## Components");
826
+ expect(content).toContain("| Component | Purpose | Technology | Owner |");
827
+ });
828
+
829
+ it("includes data flow and key design decisions sections", () => {
830
+ const content = resolveContent(file, ctx);
831
+ expect(content).toContain("## Data Flow");
832
+ expect(content).toContain("## Key Design Decisions");
833
+ expect(content).toContain("[Decision Log](/content/planning/decisions/)");
834
+ });
835
+ });
836
+
837
+ describe("integrations/index.md", () => {
838
+ const file = findFile("content/reference/integrations/index.md");
839
+
840
+ it("exists in the file list", () => {
841
+ expect(file).toBeDefined();
842
+ });
843
+
844
+ it("has frontmatter referencing brand name", () => {
845
+ const content = resolveContent(file, ctx);
846
+ expect(content).toContain("title: Integrations");
847
+ expect(content).toContain("description: External system integrations for Acme Corp");
848
+ });
849
+
850
+ it("includes an integrations table", () => {
851
+ const content = resolveContent(file, ctx);
852
+ expect(content).toContain("| System | Type | Purpose | Documentation |");
853
+ });
854
+
855
+ it("documents how to add an integration", () => {
856
+ const content = resolveContent(file, ctx);
857
+ expect(content).toContain("## Adding an Integration");
858
+ expect(content).toContain("content/reference/integrations/");
859
+ expect(content).toContain("[Data Dictionary](/content/domain/data-dictionary)");
860
+ });
861
+ });
862
+
863
+ describe("data-models/index.md", () => {
864
+ const file = findFile("content/reference/data-models/index.md");
865
+
866
+ it("exists in the file list", () => {
867
+ expect(file).toBeDefined();
868
+ });
869
+
870
+ it("has frontmatter referencing brand name", () => {
871
+ const content = resolveContent(file, ctx);
872
+ expect(content).toContain("title: Data Models");
873
+ expect(content).toContain("description: Database and API schemas for Acme Corp");
874
+ });
875
+
876
+ it("includes database and API schema sections", () => {
877
+ const content = resolveContent(file, ctx);
878
+ expect(content).toContain("## Database Schema");
879
+ expect(content).toContain("## API Schema");
880
+ });
881
+
882
+ it("links to related pages", () => {
883
+ const content = resolveContent(file, ctx);
884
+ expect(content).toContain("[Data Dictionary](/content/domain/data-dictionary)");
885
+ expect(content).toContain("[Integrations](/content/reference/integrations/)");
886
+ });
887
+ });
888
+
889
+ describe("contacts/directory.md", () => {
890
+ const file = findFile("content/reference/contacts/directory.md");
891
+
892
+ it("exists in the file list", () => {
893
+ expect(file).toBeDefined();
894
+ });
895
+
896
+ it("has frontmatter referencing brand name", () => {
897
+ const content = resolveContent(file, ctx);
898
+ expect(content).toContain("title: Contact Directory");
899
+ expect(content).toContain("description: Team and vendor contacts for Acme Corp");
900
+ });
901
+
902
+ it("includes project team, vendor, and stakeholder tables", () => {
903
+ const content = resolveContent(file, ctx);
904
+ expect(content).toContain("## Project Team");
905
+ expect(content).toContain("| Role | Contact | Responsibility |");
906
+ expect(content).toContain("## Vendor Contacts");
907
+ expect(content).toContain("| Vendor | Type | Contact |");
908
+ expect(content).toContain("## Stakeholders");
909
+ expect(content).toContain("| Stakeholder | Interest | Communication |");
910
+ });
911
+ });
912
+
913
+ describe("metrics/kpis.md", () => {
914
+ const file = findFile("content/reference/metrics/kpis.md");
915
+
916
+ it("exists in the file list", () => {
917
+ expect(file).toBeDefined();
918
+ });
919
+
920
+ it("has frontmatter referencing brand name", () => {
921
+ const content = resolveContent(file, ctx);
922
+ expect(content).toContain("title: KPIs & Metrics");
923
+ expect(content).toContain("description: Key performance indicators for Acme Corp");
924
+ });
925
+
926
+ it("includes product and business metrics tables", () => {
927
+ const content = resolveContent(file, ctx);
928
+ expect(content).toContain("## Product Metrics");
929
+ expect(content).toContain("## Business Metrics");
930
+ });
931
+
932
+ it("includes monitoring section with alerts table", () => {
933
+ const content = resolveContent(file, ctx);
934
+ expect(content).toContain("## Monitoring");
935
+ expect(content).toContain("| Alert | Warning | Critical | Action |");
936
+ });
937
+ });
938
+ });
939
+
940
+ // ---------------------------------------------------------------------------
941
+ // CUSTOMIZING.md
942
+ // ---------------------------------------------------------------------------
943
+
944
+ describe("productbook CUSTOMIZING.md", () => {
945
+ const ctx = makeCtx();
946
+ const file = findFile("CUSTOMIZING.md");
947
+
948
+ it("exists in the file list", () => {
949
+ expect(file).toBeDefined();
950
+ });
951
+
952
+ it("has frontmatter with template metadata", () => {
953
+ const content = resolveContent(file, ctx);
954
+ expect(content).toMatch(/^---\n/);
955
+ expect(content).toContain("template: productbook");
956
+ expect(content).toContain("template_version: 1");
957
+ });
958
+
959
+ it("includes the site name in the heading", () => {
960
+ const content = resolveContent(file, ctx);
961
+ expect(content).toContain("# Customizing Test Productbook");
962
+ });
963
+
964
+ it("uses the project name in the directory tree", () => {
965
+ const content = resolveContent(file, ctx);
966
+ expect(content).toContain("test-productbook/");
967
+ });
968
+
969
+ it("documents site.yaml configuration", () => {
970
+ const content = resolveContent(file, ctx);
971
+ expect(content).toContain("### site.yaml");
972
+ expect(content).toContain('title: "Test Productbook"');
973
+ expect(content).toContain('name: "Acme Corp"');
974
+ });
975
+
976
+ it("documents theme.yaml customization", () => {
977
+ const content = resolveContent(file, ctx);
978
+ expect(content).toContain("### theme.yaml");
979
+ });
980
+
981
+ it("includes the current year in footer example", () => {
982
+ const content = resolveContent(file, ctx);
983
+ expect(content).toContain("2026 Acme Corp");
984
+ });
985
+
986
+ it("documents the productbook site structure", () => {
987
+ const content = resolveContent(file, ctx);
988
+ expect(content).toContain("## Site Structure");
989
+ expect(content).toContain("content/");
990
+ expect(content).toContain("product/");
991
+ expect(content).toContain("domain/");
992
+ expect(content).toContain("planning/");
993
+ expect(content).toContain("operations/");
994
+ expect(content).toContain("guides/");
995
+ expect(content).toContain("reference/");
996
+ });
997
+
998
+ it("covers adding content types", () => {
999
+ const content = resolveContent(file, ctx);
1000
+ expect(content).toContain("### New Feature");
1001
+ expect(content).toContain("### Documenting a Business Process");
1002
+ expect(content).toContain("### Recording a Decision");
1003
+ expect(content).toContain("### Adding an Integration");
1004
+ expect(content).toContain("### New Section");
1005
+ });
1006
+
1007
+ it("covers document conventions for each area", () => {
1008
+ const content = resolveContent(file, ctx);
1009
+ expect(content).toContain("### Product (features, releases)");
1010
+ expect(content).toContain("### Domain (processes, data dictionary)");
1011
+ expect(content).toContain("### Planning (decisions, specs, research)");
1012
+ expect(content).toContain("### Guides (onboarding, user docs)");
1013
+ });
1014
+
1015
+ it("includes linking and references section", () => {
1016
+ const content = resolveContent(file, ctx);
1017
+ expect(content).toContain("## Linking and References");
1018
+ expect(content).toContain("### Internal Links");
1019
+ expect(content).toContain("### External Links");
1020
+ });
1021
+
1022
+ it("includes getting help links", () => {
1023
+ const content = resolveContent(file, ctx);
1024
+ expect(content).toContain("## Getting Help");
1025
+ expect(content).toContain("https://github.com/3leaps/kitfly");
1026
+ });
1027
+
1028
+ it("responds to different context values", () => {
1029
+ const customCtx = makeCtx({
1030
+ name: "my-project",
1031
+ branding: {
1032
+ siteName: "My Docs",
1033
+ brandName: "My Brand",
1034
+ brandUrl: "/",
1035
+ },
1036
+ year: 2027,
1037
+ });
1038
+ const content = resolveContent(file, customCtx);
1039
+ expect(content).toContain("# Customizing My Docs");
1040
+ expect(content).toContain("my-project/");
1041
+ expect(content).toContain('name: "My Brand"');
1042
+ expect(content).toContain("2027 My Brand");
1043
+ });
1044
+ });
1045
+
1046
+ // ---------------------------------------------------------------------------
1047
+ // File Coverage Completeness
1048
+ // ---------------------------------------------------------------------------
1049
+
1050
+ describe("productbook file coverage", () => {
1051
+ const expectedFiles = [
1052
+ "site.yaml",
1053
+ "index.md",
1054
+ "content/product/overview.md",
1055
+ "content/product/features/index.md",
1056
+ "content/product/releases/index.md",
1057
+ "content/domain/overview.md",
1058
+ "content/domain/processes/index.md",
1059
+ "content/domain/data-dictionary.md",
1060
+ "content/domain/industry-notes.md",
1061
+ "content/planning/roadmap.md",
1062
+ "content/planning/decisions/index.md",
1063
+ "content/planning/decisions/adr-template.md",
1064
+ "content/planning/specs/index.md",
1065
+ "content/planning/research/index.md",
1066
+ "content/operations/environments.md",
1067
+ "content/operations/deployment.md",
1068
+ "content/guides/getting-started.md",
1069
+ "content/guides/user-guide.md",
1070
+ "content/reference/architecture/overview.md",
1071
+ "content/reference/integrations/index.md",
1072
+ "content/reference/data-models/index.md",
1073
+ "content/reference/contacts/directory.md",
1074
+ "content/reference/metrics/kpis.md",
1075
+ "CUSTOMIZING.md",
1076
+ ];
1077
+
1078
+ it("defines exactly the expected set of files", () => {
1079
+ const actualPaths = productbook.files.map((f) => f.path).sort();
1080
+ const expected = [...expectedFiles].sort();
1081
+ expect(actualPaths).toEqual(expected);
1082
+ });
1083
+
1084
+ it("all content generators produce non-empty strings", () => {
1085
+ const ctx = makeCtx();
1086
+ for (const file of productbook.files) {
1087
+ const content = resolveContent(file, ctx);
1088
+ expect(content.length).toBeGreaterThan(0);
1089
+ expect(typeof content).toBe("string");
1090
+ }
1091
+ });
1092
+
1093
+ it("all markdown files start with YAML frontmatter or a markdown heading", () => {
1094
+ const ctx = makeCtx();
1095
+ for (const file of productbook.files) {
1096
+ if (!file.path.endsWith(".md")) continue;
1097
+ const content = resolveContent(file, ctx);
1098
+ const startsWithFrontmatter = content.startsWith("---\n");
1099
+ const startsWithHeading = content.startsWith("#");
1100
+ expect(startsWithFrontmatter || startsWithHeading).toBe(true);
1101
+ }
1102
+ });
1103
+
1104
+ it("site.yaml starts with a YAML comment", () => {
1105
+ const ctx = makeCtx();
1106
+ const file = findFile("site.yaml");
1107
+ const content = resolveContent(file, ctx);
1108
+ expect(content.startsWith("#")).toBe(true);
1109
+ });
1110
+ });
1111
+
1112
+ // ---------------------------------------------------------------------------
1113
+ // Branding Substitution Across All Context-Dependent Files
1114
+ // ---------------------------------------------------------------------------
1115
+
1116
+ describe("productbook branding substitution", () => {
1117
+ it("context-dependent files use brandName, not a hardcoded value", () => {
1118
+ const ctx1 = makeCtx({
1119
+ branding: {
1120
+ siteName: "Alpha Docs",
1121
+ brandName: "Alpha Corp",
1122
+ brandUrl: "/",
1123
+ },
1124
+ });
1125
+ const ctx2 = makeCtx({
1126
+ branding: {
1127
+ siteName: "Beta Docs",
1128
+ brandName: "Beta Corp",
1129
+ brandUrl: "/",
1130
+ },
1131
+ });
1132
+
1133
+ // Files that use ctx.branding.brandName in their description
1134
+ const brandDependentFiles = [
1135
+ "content/product/overview.md",
1136
+ "content/product/features/index.md",
1137
+ "content/product/releases/index.md",
1138
+ "content/domain/overview.md",
1139
+ "content/domain/processes/index.md",
1140
+ "content/domain/data-dictionary.md",
1141
+ "content/domain/industry-notes.md",
1142
+ "content/planning/roadmap.md",
1143
+ "content/planning/specs/index.md",
1144
+ "content/planning/research/index.md",
1145
+ "content/operations/environments.md",
1146
+ "content/operations/deployment.md",
1147
+ "content/guides/getting-started.md",
1148
+ "content/guides/user-guide.md",
1149
+ "content/reference/architecture/overview.md",
1150
+ "content/reference/integrations/index.md",
1151
+ "content/reference/data-models/index.md",
1152
+ "content/reference/contacts/directory.md",
1153
+ "content/reference/metrics/kpis.md",
1154
+ ];
1155
+
1156
+ for (const path of brandDependentFiles) {
1157
+ const file = findFile(path);
1158
+ const content1 = resolveContent(file, ctx1);
1159
+ const content2 = resolveContent(file, ctx2);
1160
+
1161
+ expect(content1).toContain("Alpha Corp");
1162
+ expect(content1).not.toContain("Beta Corp");
1163
+ expect(content2).toContain("Beta Corp");
1164
+ expect(content2).not.toContain("Alpha Corp");
1165
+ }
1166
+ });
1167
+
1168
+ it("context-dependent files use siteName where appropriate", () => {
1169
+ const ctx1 = makeCtx({
1170
+ branding: {
1171
+ siteName: "Gamma Hub",
1172
+ brandName: "Gamma Inc",
1173
+ brandUrl: "/",
1174
+ },
1175
+ });
1176
+
1177
+ // site.yaml and index.md use siteName
1178
+ const siteYaml = resolveContent(findFile("site.yaml"), ctx1);
1179
+ expect(siteYaml).toContain("Gamma Hub");
1180
+
1181
+ const indexMd = resolveContent(findFile("index.md"), ctx1);
1182
+ expect(indexMd).toContain("Gamma Hub");
1183
+
1184
+ const customizing = resolveContent(findFile("CUSTOMIZING.md"), ctx1);
1185
+ expect(customizing).toContain("Gamma Hub");
1186
+ });
1187
+
1188
+ it("static templates are unaffected by context changes", () => {
1189
+ const ctx1 = makeCtx({
1190
+ branding: { siteName: "One", brandName: "One" },
1191
+ });
1192
+ const ctx2 = makeCtx({
1193
+ branding: { siteName: "Two", brandName: "Two" },
1194
+ });
1195
+
1196
+ const staticFiles = [
1197
+ "content/planning/decisions/index.md",
1198
+ "content/planning/decisions/adr-template.md",
1199
+ ];
1200
+
1201
+ for (const path of staticFiles) {
1202
+ const file = findFile(path);
1203
+ expect(resolveContent(file, ctx1)).toBe(resolveContent(file, ctx2));
1204
+ }
1205
+ });
1206
+ });