@millstone/synapse-site 0.1.0

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.
@@ -0,0 +1,638 @@
1
+ /**
2
+ * Browser-compatible validation for Decap CMS
3
+ *
4
+ * This module provides client-side validation that mirrors the server-side
5
+ * `synapse validate` command, running entirely in the browser.
6
+ */
7
+
8
+ import Ajv from 'ajv';
9
+ import addFormats from 'ajv-formats';
10
+ import { unified } from 'unified';
11
+ import remarkParse from 'remark-parse';
12
+ import remarkGfm from 'remark-gfm';
13
+ import type { Root, Heading, Content, List, PhrasingContent } from 'mdast';
14
+
15
+ // ============================================================================
16
+ // Types
17
+ // ============================================================================
18
+
19
+ export const DOC_TYPES = [
20
+ 'adr', 'agreement', 'capability', 'meeting', 'policy', 'prd',
21
+ 'process', 'reference', 'runbook', 'scorecard', 'sop', 'sow',
22
+ 'standard', 'system', 'tdd'
23
+ ] as const;
24
+
25
+ export type DocType = typeof DOC_TYPES[number];
26
+
27
+ export function isDocType(value: string): value is DocType {
28
+ return DOC_TYPES.includes(value as DocType);
29
+ }
30
+
31
+ export interface ValidationIssue {
32
+ type: 'error' | 'warning';
33
+ code: string;
34
+ message: string;
35
+ field?: string;
36
+ sectionId?: string;
37
+ line?: number;
38
+ }
39
+
40
+ export interface ValidationResult {
41
+ success: boolean;
42
+ issues: ValidationIssue[];
43
+ }
44
+
45
+ export interface ValidationContext {
46
+ /** All valid wikilink targets (file basenames without .md) */
47
+ vaultIndex: string[];
48
+ /** JSON schemas keyed by document type */
49
+ schemas: Record<string, any>;
50
+ /** Body grammars keyed by document type */
51
+ bodyGrammars: Record<string, any>;
52
+ }
53
+
54
+ export interface SectionNode {
55
+ id: string;
56
+ title: string;
57
+ depth: number;
58
+ line: number;
59
+ column: number;
60
+ content: Content[];
61
+ }
62
+
63
+ // ============================================================================
64
+ // Schema Validation
65
+ // ============================================================================
66
+
67
+ /**
68
+ * Validates frontmatter against a JSON schema
69
+ */
70
+ export function validateSchema(
71
+ frontmatter: Record<string, any>,
72
+ schema: any
73
+ ): ValidationIssue[] {
74
+ const issues: ValidationIssue[] = [];
75
+
76
+ // Clean schema for Ajv compatibility
77
+ const cleanSchema = { ...schema };
78
+ delete cleanSchema.$schema;
79
+ delete cleanSchema.$id;
80
+
81
+ const ajv = new Ajv({
82
+ allErrors: true,
83
+ strict: false,
84
+ validateSchema: false
85
+ });
86
+
87
+ addFormats(ajv);
88
+
89
+ try {
90
+ const validate = ajv.compile(cleanSchema);
91
+ const valid = validate(frontmatter);
92
+
93
+ if (!valid && validate.errors) {
94
+ for (const error of validate.errors) {
95
+ const path = error.instancePath || '/';
96
+ let message = error.message || 'validation error';
97
+
98
+ if (error.keyword === 'required') {
99
+ const missingProp = (error.params as any).missingProperty;
100
+ message = `missing required property '${missingProp}'`;
101
+ } else if (error.keyword === 'type') {
102
+ const expectedType = (error.params as any).type;
103
+ message = `must be ${expectedType}`;
104
+ } else if (error.keyword === 'const') {
105
+ const expectedValue = (error.params as any).allowedValue;
106
+ message = `must be equal to '${expectedValue}'`;
107
+ }
108
+
109
+ issues.push({
110
+ type: 'error',
111
+ code: 'SCHEMA_VALIDATION_ERROR',
112
+ message: `${path} ${message}`,
113
+ field: path.replace(/^\//, '') || undefined
114
+ });
115
+ }
116
+ }
117
+ } catch (error) {
118
+ issues.push({
119
+ type: 'error',
120
+ code: 'SCHEMA_COMPILATION_ERROR',
121
+ message: `Schema error: ${(error as Error).message}`
122
+ });
123
+ }
124
+
125
+ return issues;
126
+ }
127
+
128
+ // ============================================================================
129
+ // Markdown Parsing
130
+ // ============================================================================
131
+
132
+ /**
133
+ * Parses markdown into AST
134
+ */
135
+ function parseMarkdown(body: string): Root {
136
+ const processor = unified().use(remarkParse).use(remarkGfm);
137
+ return processor.parse(body) as Root;
138
+ }
139
+
140
+ /**
141
+ * Extracts heading text from AST node
142
+ */
143
+ function extractHeadingText(heading: Heading): string {
144
+ let text = '';
145
+ for (const child of heading.children) {
146
+ if (child.type === 'text') {
147
+ text += child.value;
148
+ } else if (child.type === 'html') {
149
+ continue;
150
+ } else if ('children' in child) {
151
+ text += extractInlineText(child.children as PhrasingContent[]);
152
+ }
153
+ }
154
+ return text.trim();
155
+ }
156
+
157
+ function extractInlineText(children: PhrasingContent[]): string {
158
+ let text = '';
159
+ for (const child of children) {
160
+ if (child.type === 'text') {
161
+ text += child.value;
162
+ } else if ('children' in child) {
163
+ text += extractInlineText(child.children as PhrasingContent[]);
164
+ }
165
+ }
166
+ return text;
167
+ }
168
+
169
+ /**
170
+ * Normalizes section title to ID
171
+ */
172
+ function normalizeSectionId(title: string): string {
173
+ return title
174
+ .toLowerCase()
175
+ .trim()
176
+ .replace(/[^\w\s-]/g, '')
177
+ .replace(/\s+/g, '-')
178
+ .replace(/-+/g, '-')
179
+ .replace(/^-+|-+$/g, '');
180
+ }
181
+
182
+ /**
183
+ * Finds all H2 sections in the AST
184
+ */
185
+ function findSections(ast: Root): SectionNode[] {
186
+ const sections: SectionNode[] = [];
187
+ let currentSection: SectionNode | null = null;
188
+
189
+ for (const node of ast.children) {
190
+ if (node.type === 'heading' && node.depth === 2) {
191
+ if (currentSection) {
192
+ sections.push(currentSection);
193
+ }
194
+ const title = extractHeadingText(node);
195
+ currentSection = {
196
+ id: normalizeSectionId(title),
197
+ title,
198
+ depth: node.depth,
199
+ line: node.position?.start.line || 0,
200
+ column: node.position?.start.column || 0,
201
+ content: []
202
+ };
203
+ } else if (currentSection) {
204
+ currentSection.content.push(node);
205
+ }
206
+ }
207
+
208
+ if (currentSection) {
209
+ sections.push(currentSection);
210
+ }
211
+
212
+ return sections;
213
+ }
214
+
215
+ // ============================================================================
216
+ // Body Grammar Validation
217
+ // ============================================================================
218
+
219
+ /**
220
+ * Validates body structure against grammar rules
221
+ */
222
+ export function validateBodyGrammar(
223
+ body: string,
224
+ grammar: any,
225
+ docType: DocType
226
+ ): ValidationIssue[] {
227
+ const issues: ValidationIssue[] = [];
228
+
229
+ if (!grammar || !grammar.sections) {
230
+ return issues; // No grammar defined, skip
231
+ }
232
+
233
+ const ast = parseMarkdown(body);
234
+ const sections = findSections(ast);
235
+
236
+ // Check required sections
237
+ for (const rule of grammar.sections) {
238
+ const found = sections.find(s =>
239
+ s.id === rule.id ||
240
+ s.title.toLowerCase() === rule.title.toLowerCase()
241
+ );
242
+
243
+ if (rule.required && !found) {
244
+ issues.push({
245
+ type: 'error',
246
+ code: 'MISSING_REQUIRED_SECTION',
247
+ message: `Missing required section. Add this H2 heading to your document:\n\n## ${rule.title}`,
248
+ sectionId: rule.id
249
+ });
250
+ }
251
+
252
+ // Validate section shape if present
253
+ if (found && rule.shape) {
254
+ const shapeIssues = validateSectionShape(found, rule);
255
+ issues.push(...shapeIssues);
256
+ }
257
+ }
258
+
259
+ // Check section order
260
+ const ruleOrder = new Map(grammar.sections.map((r: any) => [r.id, r.order]));
261
+ const ruleById = new Map(grammar.sections.map((r: any) => [r.id, r]));
262
+ let lastOrder = 0;
263
+ let lastSectionTitle = '';
264
+ for (const section of sections) {
265
+ const order = ruleOrder.get(section.id);
266
+ if (order !== undefined && order < lastOrder) {
267
+ // Find what section should come before this one
268
+ const expectedBefore = grammar.sections
269
+ .filter((r: any) => r.order < order)
270
+ .sort((a: any, b: any) => b.order - a.order)[0];
271
+
272
+ issues.push({
273
+ type: 'error',
274
+ code: 'SECTION_OUT_OF_ORDER',
275
+ message: `Section "## ${section.title}" is out of order. It should appear ${expectedBefore ? `after "## ${expectedBefore.title}"` : 'earlier in the document'}, not after "## ${lastSectionTitle}".`,
276
+ sectionId: section.id,
277
+ line: section.line
278
+ });
279
+ }
280
+ if (order !== undefined) {
281
+ lastOrder = order;
282
+ lastSectionTitle = section.title;
283
+ }
284
+ }
285
+
286
+ return issues;
287
+ }
288
+
289
+ /**
290
+ * Validates section content shape
291
+ */
292
+ function validateSectionShape(section: SectionNode, rule: any): ValidationIssue[] {
293
+ const issues: ValidationIssue[] = [];
294
+ const shape = rule.shape;
295
+
296
+ if (shape.type === 'list') {
297
+ const lists = section.content.filter(n => n.type === 'list') as List[];
298
+ const listTypeExample = shape.ordered
299
+ ? '1. First item\n2. Second item\n3. Third item'
300
+ : '- First item\n- Second item\n- Third item';
301
+
302
+ if (lists.length === 0 && shape.minItems && shape.minItems > 0) {
303
+ issues.push({
304
+ type: 'error',
305
+ code: 'MISSING_LIST',
306
+ message: `Section "## ${rule.title}" requires a ${shape.ordered ? 'numbered' : 'bulleted'} list. Add items like:\n\n${listTypeExample}`,
307
+ sectionId: rule.id
308
+ });
309
+ }
310
+
311
+ for (const list of lists) {
312
+ if (shape.ordered !== undefined && list.ordered !== shape.ordered) {
313
+ const currentType = list.ordered ? 'numbered (1. 2. 3.)' : 'bulleted (-)';
314
+ const expectedType = shape.ordered ? 'numbered (1. 2. 3.)' : 'bulleted (-)';
315
+ issues.push({
316
+ type: 'error',
317
+ code: 'WRONG_LIST_TYPE',
318
+ message: `Section "## ${rule.title}" has a ${currentType} list but requires a ${expectedType} list. Change to:\n\n${listTypeExample}`,
319
+ sectionId: rule.id
320
+ });
321
+ }
322
+ if (shape.minItems && list.children.length < shape.minItems) {
323
+ issues.push({
324
+ type: 'error',
325
+ code: 'INSUFFICIENT_LIST_ITEMS',
326
+ message: `Section "## ${rule.title}" needs at least ${shape.minItems} list items (currently has ${list.children.length}).`,
327
+ sectionId: rule.id
328
+ });
329
+ }
330
+ }
331
+ }
332
+
333
+ if (shape.type === 'table') {
334
+ const tables = section.content.filter(n => n.type === 'table');
335
+ if (tables.length === 0) {
336
+ issues.push({
337
+ type: 'error',
338
+ code: 'MISSING_TABLE',
339
+ message: `Section "## ${rule.title}" requires a table. Add a markdown table like:\n\n| Column 1 | Column 2 |\n|----------|----------|\n| Data | Data |`,
340
+ sectionId: rule.id
341
+ });
342
+ }
343
+ }
344
+
345
+ if (shape.type === 'flow' && shape.allowedNodes) {
346
+ const nodeTypeNames: Record<string, string> = {
347
+ 'paragraph': 'paragraphs',
348
+ 'list': 'lists',
349
+ 'table': 'tables',
350
+ 'code': 'code blocks',
351
+ 'blockquote': 'blockquotes',
352
+ 'heading': 'headings'
353
+ };
354
+ for (const node of section.content) {
355
+ if (!shape.allowedNodes.includes(node.type)) {
356
+ const nodeName = nodeTypeNames[node.type] || node.type;
357
+ const allowedNames = shape.allowedNodes.map((t: string) => nodeTypeNames[t] || t).join(', ');
358
+ issues.push({
359
+ type: 'error',
360
+ code: 'DISALLOWED_NODE_TYPE',
361
+ message: `Section "## ${rule.title}" does not allow ${nodeName}. Only ${allowedNames} are permitted.`,
362
+ sectionId: rule.id,
363
+ line: node.position?.start.line
364
+ });
365
+ }
366
+ }
367
+ }
368
+
369
+ return issues;
370
+ }
371
+
372
+ // ============================================================================
373
+ // Wikilink Validation
374
+ // ============================================================================
375
+
376
+ /**
377
+ * Extracts wikilinks from markdown content
378
+ */
379
+ export function extractWikilinks(content: string): string[] {
380
+ const wikilinks: string[] = [];
381
+ const regex = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g;
382
+ let match;
383
+
384
+ while ((match = regex.exec(content)) !== null) {
385
+ const target = match[1].trim();
386
+ if (target && !wikilinks.includes(target)) {
387
+ wikilinks.push(target);
388
+ }
389
+ }
390
+
391
+ return wikilinks;
392
+ }
393
+
394
+ /**
395
+ * Validates that wikilinks exist in the vault
396
+ */
397
+ export function validateWikilinks(
398
+ body: string,
399
+ vaultIndex: string[]
400
+ ): ValidationIssue[] {
401
+ const issues: ValidationIssue[] = [];
402
+ const wikilinks = extractWikilinks(body);
403
+
404
+ // Normalize vault index for matching
405
+ const normalizedIndex = new Set(
406
+ vaultIndex.map(f => f.toLowerCase().replace(/\s+/g, '-'))
407
+ );
408
+
409
+ for (const link of wikilinks) {
410
+ const normalized = link.toLowerCase().replace(/\s+/g, '-');
411
+
412
+ // Check various matching patterns
413
+ const exists = normalizedIndex.has(normalized) ||
414
+ normalizedIndex.has(normalized.replace(/\.md$/, '')) ||
415
+ [...normalizedIndex].some(f =>
416
+ f.endsWith(`/${normalized}`) ||
417
+ f.endsWith(`-${normalized}`) ||
418
+ f === normalized
419
+ );
420
+
421
+ if (!exists) {
422
+ issues.push({
423
+ type: 'error',
424
+ code: 'BROKEN_LINK',
425
+ message: `Broken link: [[${link}]] - This document doesn't exist. Check the spelling or create the document first.`
426
+ });
427
+ }
428
+ }
429
+
430
+ return issues;
431
+ }
432
+
433
+ // ============================================================================
434
+ // Cross-Link Validation (Type-Specific Rules)
435
+ // ============================================================================
436
+
437
+ /**
438
+ * Validates type-specific cross-link requirements
439
+ */
440
+ export function validateCrossLinks(
441
+ frontmatter: Record<string, any>,
442
+ body: string,
443
+ vaultIndex: string[]
444
+ ): ValidationIssue[] {
445
+ const issues: ValidationIssue[] = [];
446
+ const docType = frontmatter.type as DocType;
447
+
448
+ if (!docType || !isDocType(docType)) {
449
+ return issues;
450
+ }
451
+
452
+ switch (docType) {
453
+ case 'process':
454
+ // Process must reference at least one standard
455
+ if (!frontmatter.related_standards ||
456
+ !Array.isArray(frontmatter.related_standards) ||
457
+ frontmatter.related_standards.length === 0) {
458
+ issues.push({
459
+ type: 'error',
460
+ code: 'MISSING_REQUIRED_LINK',
461
+ message: 'Process documents must reference at least one standard in related_standards',
462
+ field: 'related_standards'
463
+ });
464
+ }
465
+ break;
466
+
467
+ case 'sop':
468
+ // SOP must reference a parent process
469
+ if (!frontmatter.related_process ||
470
+ typeof frontmatter.related_process !== 'string' ||
471
+ frontmatter.related_process.trim() === '') {
472
+ issues.push({
473
+ type: 'error',
474
+ code: 'MISSING_REQUIRED_LINK',
475
+ message: 'SOP must reference a parent process in related_process',
476
+ field: 'related_process'
477
+ });
478
+ }
479
+ break;
480
+
481
+ case 'runbook':
482
+ // Runbook must reference a system in ## Service section
483
+ const serviceMatch = body.match(/##\s+Service\s*\n+([^\n#]+)/);
484
+ const serviceContent = serviceMatch ? serviceMatch[1].trim() : '';
485
+ const serviceLinks = extractWikilinks(serviceContent);
486
+
487
+ if (!serviceContent || serviceLinks.length === 0) {
488
+ issues.push({
489
+ type: 'error',
490
+ code: 'MISSING_REQUIRED_LINK',
491
+ message: 'Runbook must reference a system/service in the ## Service section',
492
+ field: 'service'
493
+ });
494
+ }
495
+ break;
496
+ }
497
+
498
+ return issues;
499
+ }
500
+
501
+ // ============================================================================
502
+ // Naming Validation
503
+ // ============================================================================
504
+
505
+ const TEMPLATE_REGISTRY: Record<DocType, { folder: string }> = {
506
+ 'adr': { folder: '90_Architecture/ADRs' },
507
+ 'agreement': { folder: '120_Legal/agreements' },
508
+ 'capability': { folder: '110_Capabilities' },
509
+ 'meeting': { folder: '60_Meetings' },
510
+ 'policy': { folder: '10_Policies' },
511
+ 'prd': { folder: '100_Products/PRDs' },
512
+ 'process': { folder: '30_Processes' },
513
+ 'reference': { folder: '200_References' },
514
+ 'runbook': { folder: '50_Runbooks' },
515
+ 'scorecard': { folder: '80_Scorecards' },
516
+ 'sop': { folder: '40_SOPs' },
517
+ 'sow': { folder: '120_Legal/SOWs' },
518
+ 'standard': { folder: '20_Standards' },
519
+ 'system': { folder: '70_Systems' },
520
+ 'tdd': { folder: '90_Architecture/TDDs' }
521
+ };
522
+
523
+ /**
524
+ * Validates document is in correct folder
525
+ */
526
+ export function validateNaming(
527
+ filePath: string,
528
+ frontmatter: Record<string, any>
529
+ ): ValidationIssue[] {
530
+ const issues: ValidationIssue[] = [];
531
+ const docType = frontmatter.type as DocType;
532
+
533
+ if (!docType || !isDocType(docType)) {
534
+ return issues;
535
+ }
536
+
537
+ // Check folder placement
538
+ const expectedFolder = TEMPLATE_REGISTRY[docType].folder;
539
+ if (!filePath.includes(expectedFolder)) {
540
+ issues.push({
541
+ type: 'error',
542
+ code: 'WRONG_FOLDER',
543
+ message: `Document of type '${docType}' should be in ${expectedFolder}`,
544
+ field: 'path'
545
+ });
546
+ }
547
+
548
+ // Validate filename for ADRs
549
+ if (docType === 'adr') {
550
+ const filename = filePath.split('/').pop()?.replace('.md', '') || '';
551
+ const adrPattern = /^ADR-\d{4,}-[a-z0-9-]+$/;
552
+ if (!adrPattern.test(filename)) {
553
+ issues.push({
554
+ type: 'error',
555
+ code: 'INVALID_ADR_FILENAME',
556
+ message: 'ADR filename must match pattern ADR-####-slug.md',
557
+ field: 'filename'
558
+ });
559
+ }
560
+ }
561
+
562
+ return issues;
563
+ }
564
+
565
+ // ============================================================================
566
+ // Main Validation Function
567
+ // ============================================================================
568
+
569
+ /**
570
+ * Validates a document for Decap CMS
571
+ *
572
+ * @param frontmatter - The document frontmatter
573
+ * @param body - The document body (markdown)
574
+ * @param filePath - The file path within the vault
575
+ * @param context - Validation context (vault index, schemas, grammars)
576
+ * @returns Validation result with any issues
577
+ */
578
+ export function validateDocument(
579
+ frontmatter: Record<string, any>,
580
+ body: string,
581
+ filePath: string,
582
+ context: ValidationContext
583
+ ): ValidationResult {
584
+ const issues: ValidationIssue[] = [];
585
+
586
+ const docType = frontmatter.type;
587
+
588
+ // Validate type is present
589
+ if (!docType) {
590
+ issues.push({
591
+ type: 'error',
592
+ code: 'MISSING_TYPE',
593
+ message: 'Document type is required',
594
+ field: 'type'
595
+ });
596
+ return { success: false, issues };
597
+ }
598
+
599
+ if (!isDocType(docType)) {
600
+ issues.push({
601
+ type: 'error',
602
+ code: 'INVALID_TYPE',
603
+ message: `Invalid document type: '${docType}'`,
604
+ field: 'type'
605
+ });
606
+ return { success: false, issues };
607
+ }
608
+
609
+ // 1. Schema validation
610
+ const schema = context.schemas[docType];
611
+ if (schema) {
612
+ issues.push(...validateSchema(frontmatter, schema));
613
+ }
614
+
615
+ // 2. Body grammar validation
616
+ const grammar = context.bodyGrammars[docType];
617
+ if (grammar) {
618
+ issues.push(...validateBodyGrammar(body, grammar, docType));
619
+ }
620
+
621
+ // 3. Wikilink validation
622
+ issues.push(...validateWikilinks(body, context.vaultIndex));
623
+
624
+ // 4. Cross-link validation (type-specific rules)
625
+ issues.push(...validateCrossLinks(frontmatter, body, context.vaultIndex));
626
+
627
+ // 5. Naming validation
628
+ issues.push(...validateNaming(filePath, frontmatter));
629
+
630
+ const errors = issues.filter(i => i.type === 'error');
631
+
632
+ return {
633
+ success: errors.length === 0,
634
+ issues
635
+ };
636
+ }
637
+
638
+ export default validateDocument;
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@millstone/synapse-site",
3
+ "version": "0.1.0",
4
+ "description": "Quartz 4 configuration for Synapse documentation site",
5
+ "type": "module",
6
+ "files": [
7
+ "dist/**/*",
8
+ "plugins/**/*",
9
+ "decap/**/*",
10
+ "static/**/*",
11
+ "*.ts",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "setup": "tsx setup-quartz.ts",
16
+ "sync-plugins": "tsx sync-plugins.ts",
17
+ "build:decap-config": "tsx decap/build-config.ts",
18
+ "build:decap": "npm run build:decap-config && tsx decap/build-vault-index.ts && npm run build:validate-bundle",
19
+ "build:validate-bundle": "esbuild decap/validate-browser.ts --bundle --format=esm --outfile=static/edit/validate.bundle.js --platform=browser --target=es2020",
20
+ "build": "npm run sync-plugins && npm run build:decap && cd quartz && npx quartz build && npm run copy:decap",
21
+ "copy:decap": "cp -r static/edit quartz/quartz/static/",
22
+ "serve": "npm run sync-plugins && npm run build:decap && cd quartz && npx quartz build --serve",
23
+ "dev": "npm run sync-plugins && npm run build:decap && npm run copy:decap && cd quartz && npx quartz build --serve --port 8080",
24
+ "update-submodule": "git submodule update --remote --merge",
25
+ "clean": "rm -rf quartz/public quartz/.quartz-cache static/edit/*.json static/edit/schemas static/edit/body-grammars static/edit/validate.bundle.js"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/millstonehq/synapse.git",
33
+ "directory": "packages/site"
34
+ },
35
+ "homepage": "https://github.com/millstonehq/synapse#readme",
36
+ "bugs": {
37
+ "url": "https://github.com/millstonehq/synapse/issues"
38
+ },
39
+ "peerDependencies": {
40
+ "@millstone/synapse-cli": ">=0.1.0"
41
+ },
42
+ "keywords": [
43
+ "synapse",
44
+ "quartz",
45
+ "documentation",
46
+ "static-site"
47
+ ],
48
+ "devDependencies": {
49
+ "@types/node": "^20.0.0",
50
+ "chalk": "^5.3.0",
51
+ "esbuild": "^0.20.0",
52
+ "tsx": "^4.0.0",
53
+ "typescript": "^5.3.0"
54
+ },
55
+ "dependencies": {
56
+ "ajv": "^8.12.0",
57
+ "ajv-formats": "^2.1.1",
58
+ "fast-glob": "^3.3.2",
59
+ "js-yaml": "^4.1.1",
60
+ "remark-gfm": "^4.0.0",
61
+ "remark-parse": "^11.0.0",
62
+ "unified": "^11.0.0"
63
+ },
64
+ "engines": {
65
+ "node": ">=20.0.0"
66
+ }
67
+ }