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,1043 @@
1
+ /**
2
+ * Tests for the Crucible template definition
3
+ *
4
+ * Covers: src/templates/crucible.ts
5
+ * Strategy: import the template directly, verify structure and generated content
6
+ */
7
+
8
+ import { describe, expect, it } from "vitest";
9
+ import { crucible } from "../templates/crucible.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 Crucible",
20
+ brandName: "Acme Corp",
21
+ brandUrl: "https://acme.example.com",
22
+ primaryColor: "#2563eb",
23
+ footerText: "Footer text",
24
+ };
25
+ return {
26
+ name: "test-crucible",
27
+ branding,
28
+ template: crucible,
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 = crucible.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("crucible template definition", () => {
51
+ it("has correct identity metadata", () => {
52
+ expect(crucible.id).toBe("crucible");
53
+ expect(crucible.name).toBe("Crucible");
54
+ expect(crucible.version).toBe(1);
55
+ expect(crucible.extends).toBe("minimal");
56
+ });
57
+
58
+ it("has a non-empty description", () => {
59
+ expect(crucible.description).toBeTruthy();
60
+ expect(crucible.description.length).toBeGreaterThan(10);
61
+ });
62
+
63
+ it("defines exactly six sections", () => {
64
+ expect(crucible.sections).toHaveLength(6);
65
+ });
66
+
67
+ it("defines the expected section names", () => {
68
+ const names = crucible.sections.map((s) => s.name);
69
+ expect(names).toEqual(["Specs", "Schemas", "Config", "Policies", "Guides", "Reference"]);
70
+ });
71
+
72
+ it("defines the expected section paths", () => {
73
+ const paths = crucible.sections.map((s) => s.path);
74
+ expect(paths).toEqual([
75
+ "content/specs",
76
+ "content/schemas",
77
+ "content/config",
78
+ "content/policies",
79
+ "content/guides",
80
+ "content/reference",
81
+ ]);
82
+ });
83
+
84
+ it("every section has a description", () => {
85
+ for (const section of crucible.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(crucible.files.length).toBeGreaterThan(0);
93
+ });
94
+
95
+ it("every file has a non-empty path", () => {
96
+ for (const file of crucible.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 crucible.files) {
104
+ expect(["string", "function"]).toContain(typeof file.content);
105
+ }
106
+ });
107
+
108
+ it("has no duplicate file paths", () => {
109
+ const paths = crucible.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("crucible 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 Crucible"');
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('"Specs"');
146
+ expect(content).toContain('"Schemas"');
147
+ expect(content).toContain('"Config"');
148
+ expect(content).toContain('"Policies"');
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/specs"');
156
+ expect(content).toContain('"content/schemas"');
157
+ expect(content).toContain('"content/config"');
158
+ expect(content).toContain('"content/policies"');
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 SSOT",
177
+ brandName: "Widget Inc",
178
+ brandUrl: "/",
179
+ },
180
+ });
181
+ const content = resolveContent(file, customCtx);
182
+ expect(content).toContain('title: "Widget SSOT"');
183
+ expect(content).toContain('name: "Widget Inc"');
184
+ expect(content).toContain('url: "/"');
185
+ });
186
+ });
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // index.md Generation
190
+ // ---------------------------------------------------------------------------
191
+
192
+ describe("crucible 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 Crucible - Information Architecture SSOT");
209
+ });
210
+
211
+ it("includes site name in the H1 heading", () => {
212
+ const content = resolveContent(file, ctx);
213
+ expect(content).toContain("# Test Crucible");
214
+ });
215
+
216
+ it("contains a standards status table", () => {
217
+ const content = resolveContent(file, ctx);
218
+ expect(content).toContain("## Standards Status");
219
+ expect(content).toContain("| Area | Documents | Status |");
220
+ expect(content).toContain("Specifications");
221
+ expect(content).toContain("Schemas");
222
+ expect(content).toContain("Policies");
223
+ });
224
+
225
+ it("contains quick links to key sections", () => {
226
+ const content = resolveContent(file, ctx);
227
+ expect(content).toContain("## Quick Links");
228
+ expect(content).toContain("[Specifications](/content/specs/overview)");
229
+ expect(content).toContain("[Schema Catalog](/content/schemas/)");
230
+ expect(content).toContain("[Configuration](/content/config/overview)");
231
+ expect(content).toContain("[Getting Started](/content/guides/getting-started)");
232
+ });
233
+
234
+ it("describes the four-zone layout", () => {
235
+ const content = resolveContent(file, ctx);
236
+ expect(content).toContain("## Four-Zone Layout");
237
+ expect(content).toContain("**Content**");
238
+ expect(content).toContain("**Machine**");
239
+ expect(content).toContain("**Internal**");
240
+ expect(content).toContain("**Engine**");
241
+ });
242
+
243
+ it("includes a last-updated date in ISO format", () => {
244
+ const content = resolveContent(file, ctx);
245
+ // The template uses new Date().toISOString().split("T")[0] which is YYYY-MM-DD
246
+ expect(content).toMatch(/\*Last updated: \d{4}-\d{2}-\d{2}\*/);
247
+ });
248
+ });
249
+
250
+ // ---------------------------------------------------------------------------
251
+ // Specs Section Files
252
+ // ---------------------------------------------------------------------------
253
+
254
+ describe("crucible specs section", () => {
255
+ const ctx = makeCtx();
256
+
257
+ describe("overview.md", () => {
258
+ const file = findFile("content/specs/overview.md");
259
+
260
+ it("exists in the file list", () => {
261
+ expect(file).toBeDefined();
262
+ });
263
+
264
+ it("has YAML frontmatter with title and description", () => {
265
+ const content = resolveContent(file, ctx);
266
+ expect(content).toMatch(/^---\n/);
267
+ expect(content).toContain("title: Specifications");
268
+ expect(content).toContain(`description: Standards catalog for Acme Corp`);
269
+ });
270
+
271
+ it("includes a standards catalog table", () => {
272
+ const content = resolveContent(file, ctx);
273
+ expect(content).toContain("## Standards Catalog");
274
+ expect(content).toContain("| Spec | Version | Status | Description |");
275
+ });
276
+
277
+ it("includes status definitions", () => {
278
+ const content = resolveContent(file, ctx);
279
+ expect(content).toContain("## Status Definitions");
280
+ expect(content).toContain("**Draft**");
281
+ expect(content).toContain("**Stable**");
282
+ expect(content).toContain("**Deprecated**");
283
+ });
284
+
285
+ it("references the spec template", () => {
286
+ const content = resolveContent(file, ctx);
287
+ expect(content).toContain("[Spec Template](./spec-template)");
288
+ });
289
+
290
+ it("describes specification structure requirements", () => {
291
+ const content = resolveContent(file, ctx);
292
+ expect(content).toContain("**Purpose**");
293
+ expect(content).toContain("**Scope**");
294
+ expect(content).toContain("**Definitions**");
295
+ expect(content).toContain("**Requirements**");
296
+ expect(content).toContain("RFC 2119");
297
+ });
298
+
299
+ it("links to related pages", () => {
300
+ const content = resolveContent(file, ctx);
301
+ expect(content).toContain("[Change Procedure](/content/policies/change-procedure)");
302
+ expect(content).toContain("[Decisions](/content/reference/decisions/)");
303
+ });
304
+ });
305
+
306
+ describe("spec-template.md", () => {
307
+ const file = findFile("content/specs/spec-template.md");
308
+
309
+ it("exists in the file list", () => {
310
+ expect(file).toBeDefined();
311
+ });
312
+
313
+ it("has YAML frontmatter", () => {
314
+ const content = resolveContent(file, ctx);
315
+ expect(content).toMatch(/^---\n/);
316
+ expect(content).toContain('title: "SPEC-000: Specification Template"');
317
+ });
318
+
319
+ it("includes all required spec sections", () => {
320
+ const content = resolveContent(file, ctx);
321
+ const requiredSections = [
322
+ "## Status",
323
+ "## Purpose",
324
+ "## Scope",
325
+ "## Definitions",
326
+ "## Requirements",
327
+ "## Examples",
328
+ "## Version History",
329
+ ];
330
+ for (const section of requiredSections) {
331
+ expect(content).toContain(section);
332
+ }
333
+ });
334
+
335
+ it("includes RFC 2119 language guidance", () => {
336
+ const content = resolveContent(file, ctx);
337
+ expect(content).toContain("MUST");
338
+ expect(content).toContain("SHOULD");
339
+ expect(content).toContain("MAY");
340
+ expect(content).toContain("RFC 2119");
341
+ });
342
+
343
+ it("includes compliant and non-compliant example sections", () => {
344
+ const content = resolveContent(file, ctx);
345
+ expect(content).toContain("### Compliant");
346
+ expect(content).toContain("### Non-Compliant");
347
+ });
348
+
349
+ it("is a static template (does not use context)", () => {
350
+ // Call with two different contexts and verify the output is identical
351
+ const ctx2 = makeCtx({
352
+ branding: { siteName: "Other", brandName: "Other" },
353
+ });
354
+ const content1 = resolveContent(file, ctx);
355
+ const content2 = resolveContent(file, ctx2);
356
+ expect(content1).toBe(content2);
357
+ });
358
+ });
359
+ });
360
+
361
+ // ---------------------------------------------------------------------------
362
+ // Schemas Section Files
363
+ // ---------------------------------------------------------------------------
364
+
365
+ describe("crucible schemas section", () => {
366
+ const ctx = makeCtx();
367
+
368
+ describe("index.md", () => {
369
+ const file = findFile("content/schemas/index.md");
370
+
371
+ it("exists in the file list", () => {
372
+ expect(file).toBeDefined();
373
+ });
374
+
375
+ it("has frontmatter referencing brand name", () => {
376
+ const content = resolveContent(file, ctx);
377
+ expect(content).toContain("title: Schema Catalog");
378
+ expect(content).toContain(`description: Schema documentation for Acme Corp`);
379
+ });
380
+
381
+ it("includes a schema catalog table", () => {
382
+ const content = resolveContent(file, ctx);
383
+ expect(content).toContain("| Schema | Version | Path | Description |");
384
+ });
385
+
386
+ it("describes schema organization structure", () => {
387
+ const content = resolveContent(file, ctx);
388
+ expect(content).toContain("## Schema Organization");
389
+ expect(content).toContain("schemas/");
390
+ });
391
+
392
+ it("links to versioning policy", () => {
393
+ const content = resolveContent(file, ctx);
394
+ expect(content).toContain("[Versioning Policy](./versioning)");
395
+ });
396
+ });
397
+
398
+ describe("versioning.md", () => {
399
+ const file = findFile("content/schemas/versioning.md");
400
+
401
+ it("exists in the file list", () => {
402
+ expect(file).toBeDefined();
403
+ });
404
+
405
+ it("has YAML frontmatter", () => {
406
+ const content = resolveContent(file, ctx);
407
+ expect(content).toMatch(/^---\n/);
408
+ expect(content).toContain("title: Schema Versioning");
409
+ });
410
+
411
+ it("includes compatibility rules table", () => {
412
+ const content = resolveContent(file, ctx);
413
+ expect(content).toContain("## Compatibility Rules");
414
+ expect(content).toContain("| Change Type | Example | Compatibility |");
415
+ expect(content).toContain("**Breaking**");
416
+ expect(content).toContain("Backward compatible");
417
+ });
418
+
419
+ it("documents migration process", () => {
420
+ const content = resolveContent(file, ctx);
421
+ expect(content).toContain("## Migration");
422
+ });
423
+
424
+ it("is a static template (does not use context)", () => {
425
+ const ctx2 = makeCtx({
426
+ branding: { siteName: "Other", brandName: "Other" },
427
+ });
428
+ expect(resolveContent(file, ctx)).toBe(resolveContent(file, ctx2));
429
+ });
430
+ });
431
+ });
432
+
433
+ // ---------------------------------------------------------------------------
434
+ // Config Section Files
435
+ // ---------------------------------------------------------------------------
436
+
437
+ describe("crucible config section", () => {
438
+ const ctx = makeCtx();
439
+
440
+ describe("overview.md", () => {
441
+ const file = findFile("content/config/overview.md");
442
+
443
+ it("exists in the file list", () => {
444
+ expect(file).toBeDefined();
445
+ });
446
+
447
+ it("references brand name in frontmatter", () => {
448
+ const content = resolveContent(file, ctx);
449
+ expect(content).toContain(`description: Configuration documentation for Acme Corp`);
450
+ });
451
+
452
+ it("lists configuration areas", () => {
453
+ const content = resolveContent(file, ctx);
454
+ expect(content).toContain("## Configuration Areas");
455
+ expect(content).toContain("Agentic Roles");
456
+ expect(content).toContain("Agentic Prompts");
457
+ expect(content).toContain("Taxonomies");
458
+ });
459
+
460
+ it("links to roles and prompts pages", () => {
461
+ const content = resolveContent(file, ctx);
462
+ expect(content).toContain("[Role Definitions](./roles)");
463
+ expect(content).toContain("[Prompt Templates](./prompts)");
464
+ });
465
+ });
466
+
467
+ describe("roles.md", () => {
468
+ const file = findFile("content/config/roles.md");
469
+
470
+ it("exists and references brand name", () => {
471
+ expect(file).toBeDefined();
472
+ const content = resolveContent(file, ctx);
473
+ expect(content).toContain(`description: AI agent role catalog for Acme Corp`);
474
+ });
475
+
476
+ it("lists a role catalog table", () => {
477
+ const content = resolveContent(file, ctx);
478
+ expect(content).toContain("## Role Catalog");
479
+ expect(content).toContain("`devlead`");
480
+ expect(content).toContain("`infoarch`");
481
+ expect(content).toContain("`qa`");
482
+ });
483
+
484
+ it("shows role file YAML example", () => {
485
+ const content = resolveContent(file, ctx);
486
+ expect(content).toContain("```yaml");
487
+ expect(content).toContain("name: devlead");
488
+ expect(content).toContain("category: agentic");
489
+ });
490
+ });
491
+
492
+ describe("prompts.md", () => {
493
+ const file = findFile("content/config/prompts.md");
494
+
495
+ it("exists and references brand name", () => {
496
+ expect(file).toBeDefined();
497
+ const content = resolveContent(file, ctx);
498
+ expect(content).toContain(`description: Reusable AI prompt templates for Acme Corp`);
499
+ });
500
+
501
+ it("shows prompt file YAML example", () => {
502
+ const content = resolveContent(file, ctx);
503
+ expect(content).toContain("```yaml");
504
+ expect(content).toContain("name: write-spec");
505
+ });
506
+ });
507
+ });
508
+
509
+ // ---------------------------------------------------------------------------
510
+ // Policies Section Files
511
+ // ---------------------------------------------------------------------------
512
+
513
+ describe("crucible policies section", () => {
514
+ const ctx = makeCtx();
515
+
516
+ describe("security-model.md", () => {
517
+ const file = findFile("content/policies/security-model.md");
518
+
519
+ it("exists in the file list", () => {
520
+ expect(file).toBeDefined();
521
+ });
522
+
523
+ it("references brand name in frontmatter", () => {
524
+ const content = resolveContent(file, ctx);
525
+ expect(content).toContain(`description: Security baseline for Acme Corp`);
526
+ });
527
+
528
+ it("defines security principles", () => {
529
+ const content = resolveContent(file, ctx);
530
+ expect(content).toContain("## Principles");
531
+ expect(content).toContain("Safe by default");
532
+ expect(content).toContain("Least privilege");
533
+ expect(content).toContain("Defense in depth");
534
+ expect(content).toContain("Audit everything");
535
+ });
536
+
537
+ it("includes access control table", () => {
538
+ const content = resolveContent(file, ctx);
539
+ expect(content).toContain("## Access Control");
540
+ expect(content).toContain("| Resource | Who | Access Level |");
541
+ });
542
+
543
+ it("includes review requirements", () => {
544
+ const content = resolveContent(file, ctx);
545
+ expect(content).toContain("## Review Requirements");
546
+ });
547
+
548
+ it("warns against storing secrets", () => {
549
+ const content = resolveContent(file, ctx);
550
+ expect(content).toContain("Never store secrets in this repository");
551
+ });
552
+ });
553
+
554
+ describe("dependency-policy.md", () => {
555
+ const file = findFile("content/policies/dependency-policy.md");
556
+
557
+ it("exists and is a static template", () => {
558
+ expect(file).toBeDefined();
559
+ const ctx2 = makeCtx({
560
+ branding: { siteName: "Other", brandName: "Other" },
561
+ });
562
+ expect(resolveContent(file, ctx)).toBe(resolveContent(file, ctx2));
563
+ });
564
+
565
+ it("includes dependency categories table", () => {
566
+ const content = resolveContent(file, ctx);
567
+ expect(content).toContain("## Dependency Categories");
568
+ expect(content).toContain("Runtime");
569
+ expect(content).toContain("Dev/Build");
570
+ });
571
+
572
+ it("describes adding and removing dependencies", () => {
573
+ const content = resolveContent(file, ctx);
574
+ expect(content).toContain("## Adding a Dependency");
575
+ expect(content).toContain("## Removing a Dependency");
576
+ });
577
+ });
578
+
579
+ describe("change-procedure.md", () => {
580
+ const file = findFile("content/policies/change-procedure.md");
581
+
582
+ it("exists and references brand name", () => {
583
+ expect(file).toBeDefined();
584
+ const content = resolveContent(file, ctx);
585
+ expect(content).toContain(`description: How changes flow through Acme Corp`);
586
+ });
587
+
588
+ it("defines change types with approval levels", () => {
589
+ const content = resolveContent(file, ctx);
590
+ expect(content).toContain("## Change Types");
591
+ expect(content).toContain("New spec");
592
+ expect(content).toContain("Schema breaking change");
593
+ expect(content).toContain("Policy change");
594
+ });
595
+
596
+ it("documents the four-step process", () => {
597
+ const content = resolveContent(file, ctx);
598
+ expect(content).toContain("### 1. Propose");
599
+ expect(content).toContain("### 2. Review");
600
+ expect(content).toContain("### 3. Accept");
601
+ expect(content).toContain("### 4. Communicate");
602
+ });
603
+ });
604
+ });
605
+
606
+ // ---------------------------------------------------------------------------
607
+ // Guides Section Files
608
+ // ---------------------------------------------------------------------------
609
+
610
+ describe("crucible guides section", () => {
611
+ const ctx = makeCtx();
612
+
613
+ describe("getting-started.md", () => {
614
+ const file = findFile("content/guides/getting-started.md");
615
+
616
+ it("exists and references branding", () => {
617
+ expect(file).toBeDefined();
618
+ const content = resolveContent(file, ctx);
619
+ expect(content).toContain("title: Getting Started");
620
+ expect(content).toContain(`description: How to consume and use Acme Corp`);
621
+ });
622
+
623
+ it("uses site name in body content", () => {
624
+ const content = resolveContent(file, ctx);
625
+ expect(content).toContain("Test Crucible is the single source of truth");
626
+ });
627
+
628
+ it("covers consumers, contributors, and AI agents", () => {
629
+ const content = resolveContent(file, ctx);
630
+ expect(content).toContain("## For Consumers");
631
+ expect(content).toContain("## For Contributors");
632
+ expect(content).toContain("## For AI Agents");
633
+ });
634
+
635
+ it("references schema and config paths", () => {
636
+ const content = resolveContent(file, ctx);
637
+ expect(content).toContain("schemas/");
638
+ expect(content).toContain("config/");
639
+ });
640
+
641
+ it("links to change procedure and contributing guide", () => {
642
+ const content = resolveContent(file, ctx);
643
+ expect(content).toContain("[Change Procedure](/content/policies/change-procedure)");
644
+ expect(content).toContain("[Contributing Guide](./contributing)");
645
+ });
646
+ });
647
+
648
+ describe("contributing.md", () => {
649
+ const file = findFile("content/guides/contributing.md");
650
+
651
+ it("exists and references brand name", () => {
652
+ expect(file).toBeDefined();
653
+ const content = resolveContent(file, ctx);
654
+ expect(content).toContain(`description: How to add specs, schemas, and config to Acme Corp`);
655
+ });
656
+
657
+ it("documents how to add specs, schemas, config, and decisions", () => {
658
+ const content = resolveContent(file, ctx);
659
+ expect(content).toContain("## Adding a Specification");
660
+ expect(content).toContain("## Adding a Schema");
661
+ expect(content).toContain("## Adding Configuration");
662
+ expect(content).toContain("## Adding a Decision Record");
663
+ });
664
+
665
+ it("includes a style guide", () => {
666
+ const content = resolveContent(file, ctx);
667
+ expect(content).toContain("## Style Guide");
668
+ });
669
+ });
670
+ });
671
+
672
+ // ---------------------------------------------------------------------------
673
+ // Reference Section Files
674
+ // ---------------------------------------------------------------------------
675
+
676
+ describe("crucible reference section", () => {
677
+ const ctx = makeCtx();
678
+
679
+ describe("decisions/index.md", () => {
680
+ const file = findFile("content/reference/decisions/index.md");
681
+
682
+ it("exists in the file list", () => {
683
+ expect(file).toBeDefined();
684
+ });
685
+
686
+ it("is a static template", () => {
687
+ const ctx2 = makeCtx({
688
+ branding: { siteName: "Other", brandName: "Other" },
689
+ });
690
+ expect(resolveContent(file, ctx)).toBe(resolveContent(file, ctx2));
691
+ });
692
+
693
+ it("includes a decision log table", () => {
694
+ const content = resolveContent(file, ctx);
695
+ expect(content).toContain("# Decision Log");
696
+ expect(content).toContain("| ID | Decision | Date | Status |");
697
+ });
698
+
699
+ it("links to the ADR template", () => {
700
+ const content = resolveContent(file, ctx);
701
+ expect(content).toContain("[ADR Template](./adr-template)");
702
+ });
703
+ });
704
+
705
+ describe("decisions/adr-template.md", () => {
706
+ const file = findFile("content/reference/decisions/adr-template.md");
707
+
708
+ it("exists in the file list", () => {
709
+ expect(file).toBeDefined();
710
+ });
711
+
712
+ it("has frontmatter with ADR-000 title", () => {
713
+ const content = resolveContent(file, ctx);
714
+ expect(content).toContain('title: "ADR-000: Decision Template"');
715
+ });
716
+
717
+ it("includes all ADR sections", () => {
718
+ const content = resolveContent(file, ctx);
719
+ const requiredSections = [
720
+ "## Status",
721
+ "## Context",
722
+ "## Options Considered",
723
+ "## Decision",
724
+ "## Consequences",
725
+ ];
726
+ for (const section of requiredSections) {
727
+ expect(content).toContain(section);
728
+ }
729
+ });
730
+
731
+ it("includes positive, negative, and risks subsections", () => {
732
+ const content = resolveContent(file, ctx);
733
+ expect(content).toContain("### Positive");
734
+ expect(content).toContain("### Negative");
735
+ expect(content).toContain("### Risks");
736
+ });
737
+ });
738
+
739
+ describe("changelog/index.md", () => {
740
+ const file = findFile("content/reference/changelog/index.md");
741
+
742
+ it("exists and references brand name", () => {
743
+ expect(file).toBeDefined();
744
+ const content = resolveContent(file, ctx);
745
+ expect(content).toContain(`description: Release history for Acme Corp`);
746
+ });
747
+
748
+ it("includes versioning and release process", () => {
749
+ const content = resolveContent(file, ctx);
750
+ expect(content).toContain("## Versioning");
751
+ expect(content).toContain("## Release Process");
752
+ });
753
+ });
754
+
755
+ describe("glossary.md", () => {
756
+ const file = findFile("content/reference/glossary.md");
757
+
758
+ it("exists and references brand name", () => {
759
+ expect(file).toBeDefined();
760
+ const content = resolveContent(file, ctx);
761
+ expect(content).toContain(`description: Terminology and definitions for Acme Corp`);
762
+ });
763
+
764
+ it("defines key ecosystem terms", () => {
765
+ const content = resolveContent(file, ctx);
766
+ expect(content).toContain("Crucible");
767
+ expect(content).toContain("SSOT");
768
+ expect(content).toContain("Spec");
769
+ expect(content).toContain("Schema");
770
+ expect(content).toContain("ADR");
771
+ });
772
+
773
+ it("includes naming conventions table", () => {
774
+ const content = resolveContent(file, ctx);
775
+ expect(content).toContain("## Naming Conventions");
776
+ expect(content).toContain("SPEC-NNN");
777
+ expect(content).toContain("ADR-NNN");
778
+ });
779
+ });
780
+ });
781
+
782
+ // ---------------------------------------------------------------------------
783
+ // Internal Zone
784
+ // ---------------------------------------------------------------------------
785
+
786
+ describe("crucible internal zone", () => {
787
+ const ctx = makeCtx();
788
+
789
+ describe("internal/ops/README.md", () => {
790
+ const file = findFile("internal/ops/README.md");
791
+
792
+ it("exists in the file list", () => {
793
+ expect(file).toBeDefined();
794
+ });
795
+
796
+ it("describes what goes in the internal zone", () => {
797
+ const content = resolveContent(file, ctx);
798
+ expect(content).toContain("## What Goes Here");
799
+ expect(content).toContain("## What Does NOT Go Here");
800
+ });
801
+
802
+ it("describes the four-zone model", () => {
803
+ const content = resolveContent(file, ctx);
804
+ expect(content).toContain("## Four-Zone Model");
805
+ expect(content).toContain("Content");
806
+ expect(content).toContain("Machine");
807
+ expect(content).toContain("**Internal**");
808
+ expect(content).toContain("Engine");
809
+ });
810
+ });
811
+ });
812
+
813
+ // ---------------------------------------------------------------------------
814
+ // CUSTOMIZING.md
815
+ // ---------------------------------------------------------------------------
816
+
817
+ describe("crucible CUSTOMIZING.md", () => {
818
+ const ctx = makeCtx();
819
+ const file = findFile("CUSTOMIZING.md");
820
+
821
+ it("exists in the file list", () => {
822
+ expect(file).toBeDefined();
823
+ });
824
+
825
+ it("has frontmatter with template metadata", () => {
826
+ const content = resolveContent(file, ctx);
827
+ expect(content).toMatch(/^---\n/);
828
+ expect(content).toContain("template: crucible");
829
+ expect(content).toContain("template_version: 1");
830
+ });
831
+
832
+ it("includes the site name in the heading", () => {
833
+ const content = resolveContent(file, ctx);
834
+ expect(content).toContain("# Customizing Test Crucible");
835
+ });
836
+
837
+ it("documents the four-zone layout", () => {
838
+ const content = resolveContent(file, ctx);
839
+ expect(content).toContain("## Four-Zone Layout");
840
+ expect(content).toContain("**Content**");
841
+ expect(content).toContain("**Machine**");
842
+ expect(content).toContain("**Internal**");
843
+ expect(content).toContain("**Engine**");
844
+ });
845
+
846
+ it("uses the project name in the directory tree", () => {
847
+ const content = resolveContent(file, ctx);
848
+ expect(content).toContain("test-crucible/");
849
+ });
850
+
851
+ it("documents site.yaml configuration", () => {
852
+ const content = resolveContent(file, ctx);
853
+ expect(content).toContain("### site.yaml");
854
+ expect(content).toContain('title: "Test Crucible"');
855
+ expect(content).toContain('name: "Acme Corp"');
856
+ });
857
+
858
+ it("documents theme.yaml customization", () => {
859
+ const content = resolveContent(file, ctx);
860
+ expect(content).toContain("### theme.yaml");
861
+ });
862
+
863
+ it("includes the current year in footer example", () => {
864
+ const content = resolveContent(file, ctx);
865
+ expect(content).toContain("2026 Acme Corp");
866
+ });
867
+
868
+ it("covers adding content types", () => {
869
+ const content = resolveContent(file, ctx);
870
+ expect(content).toContain("### New Specification");
871
+ expect(content).toContain("### New Schema");
872
+ expect(content).toContain("### New Config Catalog");
873
+ expect(content).toContain("### New Policy");
874
+ expect(content).toContain("### New Decision Record");
875
+ expect(content).toContain("### New Section");
876
+ });
877
+
878
+ it("covers machine artifacts", () => {
879
+ const content = resolveContent(file, ctx);
880
+ expect(content).toContain("## Machine Artifacts");
881
+ expect(content).toContain("### Adding Schemas");
882
+ expect(content).toContain("### Adding Config");
883
+ });
884
+
885
+ it("includes document conventions for each content type", () => {
886
+ const content = resolveContent(file, ctx);
887
+ expect(content).toContain("### Specifications");
888
+ expect(content).toContain("### Schemas");
889
+ expect(content).toContain("### Config");
890
+ expect(content).toContain("### Policies");
891
+ });
892
+
893
+ it("responds to different context values", () => {
894
+ const customCtx = makeCtx({
895
+ name: "my-project",
896
+ branding: {
897
+ siteName: "My SSOT",
898
+ brandName: "My Brand",
899
+ brandUrl: "/",
900
+ },
901
+ year: 2027,
902
+ });
903
+ const content = resolveContent(file, customCtx);
904
+ expect(content).toContain("# Customizing My SSOT");
905
+ expect(content).toContain("my-project/");
906
+ expect(content).toContain('name: "My Brand"');
907
+ expect(content).toContain("2027 My Brand");
908
+ });
909
+ });
910
+
911
+ // ---------------------------------------------------------------------------
912
+ // File Coverage Completeness
913
+ // ---------------------------------------------------------------------------
914
+
915
+ describe("crucible file coverage", () => {
916
+ const expectedFiles = [
917
+ "site.yaml",
918
+ "index.md",
919
+ "content/specs/overview.md",
920
+ "content/specs/spec-template.md",
921
+ "content/schemas/index.md",
922
+ "content/schemas/versioning.md",
923
+ "content/config/overview.md",
924
+ "content/config/roles.md",
925
+ "content/config/prompts.md",
926
+ "content/policies/security-model.md",
927
+ "content/policies/dependency-policy.md",
928
+ "content/policies/change-procedure.md",
929
+ "content/guides/getting-started.md",
930
+ "content/guides/contributing.md",
931
+ "content/reference/decisions/index.md",
932
+ "content/reference/decisions/adr-template.md",
933
+ "content/reference/changelog/index.md",
934
+ "content/reference/glossary.md",
935
+ "internal/ops/README.md",
936
+ "CUSTOMIZING.md",
937
+ ];
938
+
939
+ it("defines exactly the expected set of files", () => {
940
+ const actualPaths = crucible.files.map((f) => f.path).sort();
941
+ const expected = [...expectedFiles].sort();
942
+ expect(actualPaths).toEqual(expected);
943
+ });
944
+
945
+ it("all content generators produce non-empty strings", () => {
946
+ const ctx = makeCtx();
947
+ for (const file of crucible.files) {
948
+ const content = resolveContent(file, ctx);
949
+ expect(content.length).toBeGreaterThan(0);
950
+ expect(typeof content).toBe("string");
951
+ }
952
+ });
953
+
954
+ it("all markdown files start with YAML frontmatter or a markdown heading", () => {
955
+ const ctx = makeCtx();
956
+ for (const file of crucible.files) {
957
+ if (!file.path.endsWith(".md")) continue;
958
+ const content = resolveContent(file, ctx);
959
+ const startsWithFrontmatter = content.startsWith("---\n");
960
+ const startsWithHeading = content.startsWith("#");
961
+ expect(startsWithFrontmatter || startsWithHeading).toBe(true);
962
+ }
963
+ });
964
+
965
+ it("site.yaml starts with a YAML comment", () => {
966
+ const ctx = makeCtx();
967
+ const file = findFile("site.yaml");
968
+ const content = resolveContent(file, ctx);
969
+ expect(content.startsWith("#")).toBe(true);
970
+ });
971
+ });
972
+
973
+ // ---------------------------------------------------------------------------
974
+ // Branding Substitution Across All Context-Dependent Files
975
+ // ---------------------------------------------------------------------------
976
+
977
+ describe("crucible branding substitution", () => {
978
+ it("context-dependent files use brandName, not a hardcoded value", () => {
979
+ const ctx1 = makeCtx({
980
+ branding: {
981
+ siteName: "Alpha SSOT",
982
+ brandName: "Alpha Corp",
983
+ brandUrl: "/",
984
+ },
985
+ });
986
+ const ctx2 = makeCtx({
987
+ branding: {
988
+ siteName: "Beta SSOT",
989
+ brandName: "Beta Corp",
990
+ brandUrl: "/",
991
+ },
992
+ });
993
+
994
+ // Files that use ctx.branding.brandName in their description
995
+ const brandDependentFiles = [
996
+ "content/specs/overview.md",
997
+ "content/schemas/index.md",
998
+ "content/config/overview.md",
999
+ "content/config/roles.md",
1000
+ "content/config/prompts.md",
1001
+ "content/policies/security-model.md",
1002
+ "content/policies/change-procedure.md",
1003
+ "content/guides/getting-started.md",
1004
+ "content/guides/contributing.md",
1005
+ "content/reference/changelog/index.md",
1006
+ "content/reference/glossary.md",
1007
+ ];
1008
+
1009
+ for (const path of brandDependentFiles) {
1010
+ const file = findFile(path);
1011
+ const content1 = resolveContent(file, ctx1);
1012
+ const content2 = resolveContent(file, ctx2);
1013
+
1014
+ expect(content1).toContain("Alpha Corp");
1015
+ expect(content1).not.toContain("Beta Corp");
1016
+ expect(content2).toContain("Beta Corp");
1017
+ expect(content2).not.toContain("Alpha Corp");
1018
+ }
1019
+ });
1020
+
1021
+ it("context-dependent files use siteName where appropriate", () => {
1022
+ const ctx1 = makeCtx({
1023
+ branding: {
1024
+ siteName: "Gamma Hub",
1025
+ brandName: "Gamma Inc",
1026
+ brandUrl: "/",
1027
+ },
1028
+ });
1029
+
1030
+ // site.yaml and index.md use siteName
1031
+ const siteYaml = resolveContent(findFile("site.yaml"), ctx1);
1032
+ expect(siteYaml).toContain("Gamma Hub");
1033
+
1034
+ const indexMd = resolveContent(findFile("index.md"), ctx1);
1035
+ expect(indexMd).toContain("Gamma Hub");
1036
+
1037
+ const customizing = resolveContent(findFile("CUSTOMIZING.md"), ctx1);
1038
+ expect(customizing).toContain("Gamma Hub");
1039
+
1040
+ const gettingStarted = resolveContent(findFile("content/guides/getting-started.md"), ctx1);
1041
+ expect(gettingStarted).toContain("Gamma Hub");
1042
+ });
1043
+ });