specweave 0.32.0 → 0.32.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 (191) hide show
  1. package/CLAUDE.md +176 -2
  2. package/README.md +22 -0
  3. package/bin/specweave.js +18 -1
  4. package/dist/src/cli/commands/cache.d.ts +17 -0
  5. package/dist/src/cli/commands/cache.d.ts.map +1 -0
  6. package/dist/src/cli/commands/cache.js +126 -0
  7. package/dist/src/cli/commands/cache.js.map +1 -0
  8. package/dist/src/cli/commands/init.js +1 -1
  9. package/dist/src/cli/commands/init.js.map +1 -1
  10. package/dist/src/cli/commands/plan/increment-detector.js +2 -2
  11. package/dist/src/cli/commands/plan/increment-detector.js.map +1 -1
  12. package/dist/src/cli/commands/sync-spec-commits.js +1 -1
  13. package/dist/src/cli/commands/sync-spec-commits.js.map +1 -1
  14. package/dist/src/cli/commands/sync-specs.js +2 -2
  15. package/dist/src/cli/commands/sync-specs.js.map +1 -1
  16. package/dist/src/cli/helpers/github/increment-profile-selector.js +1 -1
  17. package/dist/src/cli/helpers/github/increment-profile-selector.js.map +1 -1
  18. package/dist/src/cli/workers/living-docs-worker.js +66 -1
  19. package/dist/src/cli/workers/living-docs-worker.js.map +1 -1
  20. package/dist/src/config/types.d.ts +203 -1208
  21. package/dist/src/config/types.d.ts.map +1 -1
  22. package/dist/src/core/discrepancy/increment-generator.d.ts.map +1 -1
  23. package/dist/src/core/discrepancy/increment-generator.js +5 -2
  24. package/dist/src/core/discrepancy/increment-generator.js.map +1 -1
  25. package/dist/src/core/increment/duplicate-detector.js +2 -2
  26. package/dist/src/core/increment/duplicate-detector.js.map +1 -1
  27. package/dist/src/core/increment/increment-archiver.d.ts +24 -0
  28. package/dist/src/core/increment/increment-archiver.d.ts.map +1 -1
  29. package/dist/src/core/increment/increment-archiver.js +59 -2
  30. package/dist/src/core/increment/increment-archiver.js.map +1 -1
  31. package/dist/src/core/increment/increment-status.js +2 -2
  32. package/dist/src/core/increment/increment-status.js.map +1 -1
  33. package/dist/src/core/increment/increment-utils.d.ts +85 -0
  34. package/dist/src/core/increment/increment-utils.d.ts.map +1 -1
  35. package/dist/src/core/increment/increment-utils.js +102 -4
  36. package/dist/src/core/increment/increment-utils.js.map +1 -1
  37. package/dist/src/core/increment/metadata-validator.js +1 -1
  38. package/dist/src/core/increment/metadata-validator.js.map +1 -1
  39. package/dist/src/core/living-docs/feature-id-manager.js +1 -1
  40. package/dist/src/core/living-docs/feature-id-manager.js.map +1 -1
  41. package/dist/src/core/living-docs/hierarchy-mapper.js +3 -3
  42. package/dist/src/core/living-docs/hierarchy-mapper.js.map +1 -1
  43. package/dist/src/core/living-docs/intelligent-analyzer/architecture-generator.d.ts +18 -0
  44. package/dist/src/core/living-docs/intelligent-analyzer/architecture-generator.d.ts.map +1 -0
  45. package/dist/src/core/living-docs/intelligent-analyzer/architecture-generator.js +247 -0
  46. package/dist/src/core/living-docs/intelligent-analyzer/architecture-generator.js.map +1 -0
  47. package/dist/src/core/living-docs/intelligent-analyzer/deep-repo-analyzer.d.ts +15 -0
  48. package/dist/src/core/living-docs/intelligent-analyzer/deep-repo-analyzer.d.ts.map +1 -0
  49. package/dist/src/core/living-docs/intelligent-analyzer/deep-repo-analyzer.js +138 -0
  50. package/dist/src/core/living-docs/intelligent-analyzer/deep-repo-analyzer.js.map +1 -0
  51. package/dist/src/core/living-docs/intelligent-analyzer/file-sampler.d.ts +24 -0
  52. package/dist/src/core/living-docs/intelligent-analyzer/file-sampler.d.ts.map +1 -0
  53. package/dist/src/core/living-docs/intelligent-analyzer/file-sampler.js +198 -0
  54. package/dist/src/core/living-docs/intelligent-analyzer/file-sampler.js.map +1 -0
  55. package/dist/src/core/living-docs/intelligent-analyzer/inconsistency-detector.d.ts +17 -0
  56. package/dist/src/core/living-docs/intelligent-analyzer/inconsistency-detector.d.ts.map +1 -0
  57. package/dist/src/core/living-docs/intelligent-analyzer/inconsistency-detector.js +241 -0
  58. package/dist/src/core/living-docs/intelligent-analyzer/inconsistency-detector.js.map +1 -0
  59. package/dist/src/core/living-docs/intelligent-analyzer/index.d.ts +28 -0
  60. package/dist/src/core/living-docs/intelligent-analyzer/index.d.ts.map +1 -0
  61. package/dist/src/core/living-docs/intelligent-analyzer/index.js +197 -0
  62. package/dist/src/core/living-docs/intelligent-analyzer/index.js.map +1 -0
  63. package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.d.ts +18 -0
  64. package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.d.ts.map +1 -0
  65. package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.js +154 -0
  66. package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.js.map +1 -0
  67. package/dist/src/core/living-docs/intelligent-analyzer/strategy-generator.d.ts +42 -0
  68. package/dist/src/core/living-docs/intelligent-analyzer/strategy-generator.d.ts.map +1 -0
  69. package/dist/src/core/living-docs/intelligent-analyzer/strategy-generator.js +343 -0
  70. package/dist/src/core/living-docs/intelligent-analyzer/strategy-generator.js.map +1 -0
  71. package/dist/src/core/living-docs/intelligent-analyzer/types.d.ts +146 -0
  72. package/dist/src/core/living-docs/intelligent-analyzer/types.d.ts.map +1 -0
  73. package/dist/src/core/living-docs/intelligent-analyzer/types.js +7 -0
  74. package/dist/src/core/living-docs/intelligent-analyzer/types.js.map +1 -0
  75. package/dist/src/core/living-docs/living-docs-sync.d.ts +5 -0
  76. package/dist/src/core/living-docs/living-docs-sync.d.ts.map +1 -1
  77. package/dist/src/core/living-docs/living-docs-sync.js +36 -2
  78. package/dist/src/core/living-docs/living-docs-sync.js.map +1 -1
  79. package/dist/src/core/sync/spec-increment-mapper.js +3 -3
  80. package/dist/src/core/sync/spec-increment-mapper.js.map +1 -1
  81. package/dist/src/importers/item-converter.d.ts +25 -0
  82. package/dist/src/importers/item-converter.d.ts.map +1 -1
  83. package/dist/src/importers/item-converter.js +135 -5
  84. package/dist/src/importers/item-converter.js.map +1 -1
  85. package/dist/src/init/architecture/types.d.ts +33 -140
  86. package/dist/src/init/architecture/types.d.ts.map +1 -1
  87. package/dist/src/init/compliance/types.d.ts +30 -27
  88. package/dist/src/init/compliance/types.d.ts.map +1 -1
  89. package/dist/src/init/repo/types.d.ts +11 -34
  90. package/dist/src/init/repo/types.d.ts.map +1 -1
  91. package/dist/src/init/research/src/config/types.d.ts +15 -82
  92. package/dist/src/init/research/src/config/types.d.ts.map +1 -1
  93. package/dist/src/init/research/types.d.ts +38 -93
  94. package/dist/src/init/research/types.d.ts.map +1 -1
  95. package/dist/src/init/team/types.d.ts +4 -42
  96. package/dist/src/init/team/types.d.ts.map +1 -1
  97. package/dist/src/types/dashboard-cache.d.ts +181 -0
  98. package/dist/src/types/dashboard-cache.d.ts.map +1 -0
  99. package/dist/src/types/dashboard-cache.js +65 -0
  100. package/dist/src/types/dashboard-cache.js.map +1 -0
  101. package/dist/src/utils/docs-validator.d.ts +131 -0
  102. package/dist/src/utils/docs-validator.d.ts.map +1 -0
  103. package/dist/src/utils/docs-validator.js +529 -0
  104. package/dist/src/utils/docs-validator.js.map +1 -0
  105. package/dist/src/utils/feature-id-collision.js +1 -1
  106. package/dist/src/utils/feature-id-collision.js.map +1 -1
  107. package/dist/src/utils/html-to-mdx.d.ts +1 -0
  108. package/dist/src/utils/html-to-mdx.d.ts.map +1 -1
  109. package/dist/src/utils/html-to-mdx.js +43 -5
  110. package/dist/src/utils/html-to-mdx.js.map +1 -1
  111. package/package.json +1 -1
  112. package/plugins/specweave/agents/pm/AGENT.md +10 -7
  113. package/plugins/specweave/commands/specweave-archive-features.md +5 -7
  114. package/plugins/specweave/commands/specweave-archive.md +2 -1
  115. package/plugins/specweave/commands/specweave-do.md +35 -1
  116. package/plugins/specweave/commands/specweave-done.md +96 -0
  117. package/plugins/specweave/commands/specweave-import-external.md +45 -18
  118. package/plugins/specweave/commands/specweave-increment.md +331 -33
  119. package/plugins/specweave/commands/specweave-jobs.md +2 -2
  120. package/plugins/specweave/commands/specweave-progress.md +4 -4
  121. package/plugins/specweave/commands/specweave-restore-feature.md +5 -4
  122. package/plugins/specweave/commands/specweave-sync-docs.md +1 -1
  123. package/plugins/specweave/commands/specweave-sync-specs.md +216 -322
  124. package/plugins/specweave/commands/specweave-validate-features.md +13 -8
  125. package/plugins/specweave/hooks/docs-changed.sh.backup +79 -0
  126. package/plugins/specweave/hooks/hooks.json +33 -4
  127. package/plugins/specweave/hooks/human-input-required.sh.backup +75 -0
  128. package/plugins/specweave/hooks/lib/common-setup.sh +375 -0
  129. package/plugins/specweave/hooks/lib/crash-prevention.sh +336 -0
  130. package/plugins/specweave/hooks/post-first-increment.sh.backup +61 -0
  131. package/plugins/specweave/hooks/post-increment-change.sh.backup +98 -0
  132. package/plugins/specweave/hooks/post-increment-completion.sh.backup +231 -0
  133. package/plugins/specweave/hooks/post-increment-planning.sh.backup +1048 -0
  134. package/plugins/specweave/hooks/post-increment-status-change.sh.backup +147 -0
  135. package/plugins/specweave/hooks/post-spec-update.sh.backup +158 -0
  136. package/plugins/specweave/hooks/post-task-completion.sh +4 -23
  137. package/plugins/specweave/hooks/post-user-story-complete.sh.backup +179 -0
  138. package/plugins/specweave/hooks/pre-command-deduplication.sh +1 -6
  139. package/plugins/specweave/hooks/pre-command-deduplication.sh.backup +83 -0
  140. package/plugins/specweave/hooks/pre-implementation.sh.backup +67 -0
  141. package/plugins/specweave/hooks/pre-task-completion.sh +8 -37
  142. package/plugins/specweave/hooks/pre-task-completion.sh.backup +194 -0
  143. package/plugins/specweave/hooks/pre-tool-use.sh +2 -11
  144. package/plugins/specweave/hooks/pre-tool-use.sh.backup +133 -0
  145. package/plugins/specweave/hooks/universal/dispatcher.mjs +135 -42
  146. package/plugins/specweave/hooks/universal/fail-fast-wrapper.sh +183 -0
  147. package/plugins/specweave/hooks/user-prompt-submit.sh +140 -38
  148. package/plugins/specweave/hooks/user-prompt-submit.sh.backup +386 -0
  149. package/plugins/specweave/hooks/v2/dispatchers/post-tool-use.sh +12 -0
  150. package/plugins/specweave/hooks/v2/dispatchers/session-start.sh +89 -0
  151. package/plugins/specweave/hooks/v2/guards/bash-file-guard.sh +211 -0
  152. package/plugins/specweave/hooks/v2/guards/bash-file-guard.test.sh +163 -0
  153. package/plugins/specweave/hooks/v2/guards/completion-guard.sh +26 -28
  154. package/plugins/specweave/hooks/v2/guards/features-folder-guard.sh +50 -0
  155. package/plugins/specweave/lib/vendor/core/increment/duplicate-detector.js +2 -2
  156. package/plugins/specweave/lib/vendor/core/increment/duplicate-detector.js.map +1 -1
  157. package/plugins/specweave/scripts/README.md +166 -0
  158. package/plugins/specweave/scripts/cleanup-state.sh +142 -0
  159. package/plugins/specweave/scripts/force-kill.sh +142 -0
  160. package/plugins/specweave/scripts/jobs.js +171 -0
  161. package/plugins/specweave/scripts/progress.js +170 -0
  162. package/plugins/specweave/scripts/read-costs.sh +132 -0
  163. package/plugins/specweave/scripts/read-jobs.sh +324 -0
  164. package/plugins/specweave/scripts/read-progress.sh +185 -0
  165. package/plugins/specweave/scripts/read-status.sh +146 -0
  166. package/plugins/specweave/scripts/read-workflow.sh +173 -0
  167. package/plugins/specweave/scripts/rebuild-dashboard-cache.sh +327 -0
  168. package/plugins/specweave/scripts/session-watchdog.sh +192 -0
  169. package/plugins/specweave/scripts/status.js +154 -0
  170. package/plugins/specweave/scripts/update-dashboard-cache.sh +281 -0
  171. package/plugins/specweave/skills/increment-planner/SKILL.md +333 -24
  172. package/plugins/specweave/skills/increment-planner/templates/spec-multi-project.md +17 -9
  173. package/plugins/specweave/skills/increment-planner/templates/spec-single-project.md +6 -2
  174. package/plugins/specweave/skills/instant-status/SKILL.md +70 -0
  175. package/plugins/specweave-ado/hooks/post-living-docs-update.sh.backup +353 -0
  176. package/plugins/specweave-ado/hooks/post-task-completion.sh.backup +172 -0
  177. package/plugins/specweave-ado/lib/enhanced-ado-sync.js +170 -0
  178. package/plugins/specweave-docs/commands/build.md +32 -4
  179. package/plugins/specweave-docs/commands/preview.md +43 -1
  180. package/plugins/specweave-docs/commands/validate.md +250 -0
  181. package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +1262 -0
  182. package/plugins/specweave-github/hooks/post-task-completion.sh.backup +258 -0
  183. package/plugins/specweave-github/lib/enhanced-github-sync.js +220 -0
  184. package/plugins/specweave-jira/hooks/post-task-completion.sh.backup +172 -0
  185. package/plugins/specweave-jira/lib/enhanced-jira-sync.js +134 -0
  186. package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +1254 -0
  187. package/plugins/specweave-release/hooks/post-task-completion.sh.backup +110 -0
  188. package/plugins/specweave/hooks/post-edit-spec.sh +0 -265
  189. package/plugins/specweave/hooks/post-write-spec.sh +0 -267
  190. package/plugins/specweave/hooks/pre-edit-spec.sh +0 -151
  191. package/plugins/specweave/hooks/pre-write-spec.sh +0 -151
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Documentation Pre-Render Validator
3
+ *
4
+ * Validates markdown/MDX files BEFORE Docusaurus server starts.
5
+ * Catches issues proactively so users don't see cryptic webpack errors.
6
+ *
7
+ * Validates:
8
+ * 1. YAML frontmatter syntax
9
+ * 2. MDX/JSX compatibility (unquoted attrs, self-closing tags)
10
+ * 3. Broken internal links
11
+ * 4. Duplicate files/routes
12
+ * 5. Invalid file names
13
+ *
14
+ * @module utils/docs-validator
15
+ */
16
+ import { Logger } from './logger.js';
17
+ /**
18
+ * Issue severity levels
19
+ */
20
+ export type IssueSeverity = 'error' | 'warning' | 'info';
21
+ /**
22
+ * Single validation issue
23
+ */
24
+ export interface ValidationIssue {
25
+ file: string;
26
+ line?: number;
27
+ severity: IssueSeverity;
28
+ type: string;
29
+ message: string;
30
+ autoFixable: boolean;
31
+ fix?: () => void;
32
+ }
33
+ /**
34
+ * Validation result summary
35
+ */
36
+ export interface ValidationResult {
37
+ valid: boolean;
38
+ totalFiles: number;
39
+ errors: number;
40
+ warnings: number;
41
+ infos: number;
42
+ issues: ValidationIssue[];
43
+ fixedCount: number;
44
+ }
45
+ /**
46
+ * Validator options
47
+ */
48
+ export interface DocsValidatorOptions {
49
+ docsPath: string;
50
+ autoFix?: boolean;
51
+ logger?: Logger;
52
+ /** File extensions to validate */
53
+ extensions?: string[];
54
+ /** Paths to ignore (relative to docsPath) */
55
+ ignorePaths?: string[];
56
+ }
57
+ /**
58
+ * Documentation Pre-Render Validator
59
+ *
60
+ * Use before starting Docusaurus to catch issues early.
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * const validator = new DocsValidator({
65
+ * docsPath: '.specweave/docs/internal',
66
+ * autoFix: true
67
+ * });
68
+ * const result = await validator.validate();
69
+ * if (!result.valid) {
70
+ * console.log('Fix issues before starting docs server');
71
+ * }
72
+ * ```
73
+ */
74
+ export declare class DocsValidator {
75
+ private docsPath;
76
+ private autoFix;
77
+ private logger;
78
+ private extensions;
79
+ private ignorePaths;
80
+ private issues;
81
+ private fixedCount;
82
+ constructor(options: DocsValidatorOptions);
83
+ /**
84
+ * Run all validations
85
+ */
86
+ validate(): Promise<ValidationResult>;
87
+ /**
88
+ * Collect all markdown files recursively
89
+ */
90
+ private collectFiles;
91
+ /**
92
+ * Validate a single file
93
+ */
94
+ private validateFile;
95
+ /**
96
+ * Validate YAML frontmatter syntax
97
+ */
98
+ private validateFrontmatter;
99
+ /**
100
+ * Validate MDX/JSX compatibility
101
+ */
102
+ private validateMdxContent;
103
+ /**
104
+ * Check for duplicate routes (same normalized path)
105
+ */
106
+ private checkDuplicateRoutes;
107
+ /**
108
+ * Validate file naming conventions
109
+ */
110
+ private validateFileName;
111
+ /**
112
+ * Validate internal links between documents
113
+ */
114
+ private validateInternalLinks;
115
+ /**
116
+ * Build the validation result
117
+ */
118
+ private buildResult;
119
+ /**
120
+ * Format validation result for display
121
+ */
122
+ static formatResult(result: ValidationResult): string;
123
+ }
124
+ /**
125
+ * Quick validation function for scripts
126
+ */
127
+ export declare function validateDocs(docsPath: string, options?: {
128
+ autoFix?: boolean;
129
+ logger?: Logger;
130
+ }): Promise<ValidationResult>;
131
+ //# sourceMappingURL=docs-validator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"docs-validator.d.ts","sourceRoot":"","sources":["../../../src/utils/docs-validator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,EAAE,MAAM,EAAiB,MAAM,aAAa,CAAC;AAGpD;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAEzD;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,aAAa,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kCAAkC;IAClC,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,6CAA6C;IAC7C,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,OAAO,CAAU;IACzB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,UAAU,CAAW;IAC7B,OAAO,CAAC,WAAW,CAAW;IAC9B,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,UAAU,CAAK;gBAEX,OAAO,EAAE,oBAAoB;IAQzC;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,gBAAgB,CAAC;IAkC3C;;OAEG;IACH,OAAO,CAAC,YAAY;IAuBpB;;OAEG;YACW,YAAY;IAiC1B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAwE3B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IA2D1B;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAuB5B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAqCxB;;OAEG;YACW,qBAAqB;IAmGnC;;OAEG;IACH,OAAO,CAAC,WAAW;IA8BnB;;OAEG;IACH,MAAM,CAAC,YAAY,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM;CA6GtD;AAED;;GAEG;AACH,wBAAsB,YAAY,CAChC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/C,OAAO,CAAC,gBAAgB,CAAC,CAO3B"}
@@ -0,0 +1,529 @@
1
+ /**
2
+ * Documentation Pre-Render Validator
3
+ *
4
+ * Validates markdown/MDX files BEFORE Docusaurus server starts.
5
+ * Catches issues proactively so users don't see cryptic webpack errors.
6
+ *
7
+ * Validates:
8
+ * 1. YAML frontmatter syntax
9
+ * 2. MDX/JSX compatibility (unquoted attrs, self-closing tags)
10
+ * 3. Broken internal links
11
+ * 4. Duplicate files/routes
12
+ * 5. Invalid file names
13
+ *
14
+ * @module utils/docs-validator
15
+ */
16
+ import * as fs from 'fs';
17
+ import * as path from 'path';
18
+ import { consoleLogger } from './logger.js';
19
+ import { sanitizeHtmlForMdx, validateMdxCompatibility } from './html-to-mdx.js';
20
+ /**
21
+ * Documentation Pre-Render Validator
22
+ *
23
+ * Use before starting Docusaurus to catch issues early.
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * const validator = new DocsValidator({
28
+ * docsPath: '.specweave/docs/internal',
29
+ * autoFix: true
30
+ * });
31
+ * const result = await validator.validate();
32
+ * if (!result.valid) {
33
+ * console.log('Fix issues before starting docs server');
34
+ * }
35
+ * ```
36
+ */
37
+ export class DocsValidator {
38
+ constructor(options) {
39
+ this.issues = [];
40
+ this.fixedCount = 0;
41
+ this.docsPath = path.resolve(options.docsPath);
42
+ this.autoFix = options.autoFix ?? false;
43
+ this.logger = options.logger ?? consoleLogger;
44
+ this.extensions = options.extensions ?? ['.md', '.mdx'];
45
+ this.ignorePaths = options.ignorePaths ?? ['node_modules', '.git', '_archive'];
46
+ }
47
+ /**
48
+ * Run all validations
49
+ */
50
+ async validate() {
51
+ this.issues = [];
52
+ this.fixedCount = 0;
53
+ // Check docs path exists
54
+ if (!fs.existsSync(this.docsPath)) {
55
+ this.issues.push({
56
+ file: this.docsPath,
57
+ severity: 'error',
58
+ type: 'missing_docs_path',
59
+ message: `Documentation path does not exist: ${this.docsPath}`,
60
+ autoFixable: false,
61
+ });
62
+ return this.buildResult();
63
+ }
64
+ // Collect all markdown files
65
+ const files = this.collectFiles(this.docsPath);
66
+ this.logger.info(`Validating ${files.length} documentation files...`);
67
+ // Track seen routes for duplicate detection
68
+ const seenRoutes = new Map();
69
+ // Validate each file
70
+ for (const file of files) {
71
+ await this.validateFile(file, seenRoutes);
72
+ }
73
+ // Check for broken internal links
74
+ await this.validateInternalLinks(files);
75
+ return this.buildResult();
76
+ }
77
+ /**
78
+ * Collect all markdown files recursively
79
+ */
80
+ collectFiles(dir) {
81
+ const files = [];
82
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
83
+ for (const entry of entries) {
84
+ const fullPath = path.join(dir, entry.name);
85
+ const relativePath = path.relative(this.docsPath, fullPath);
86
+ // Skip ignored paths
87
+ if (this.ignorePaths.some((p) => relativePath.startsWith(p) || entry.name === p)) {
88
+ continue;
89
+ }
90
+ if (entry.isDirectory()) {
91
+ files.push(...this.collectFiles(fullPath));
92
+ }
93
+ else if (this.extensions.some((ext) => entry.name.endsWith(ext))) {
94
+ files.push(fullPath);
95
+ }
96
+ }
97
+ return files;
98
+ }
99
+ /**
100
+ * Validate a single file
101
+ */
102
+ async validateFile(filePath, seenRoutes) {
103
+ const relativePath = path.relative(this.docsPath, filePath);
104
+ let content;
105
+ try {
106
+ content = fs.readFileSync(filePath, 'utf-8');
107
+ }
108
+ catch (err) {
109
+ this.issues.push({
110
+ file: relativePath,
111
+ severity: 'error',
112
+ type: 'read_error',
113
+ message: `Cannot read file: ${err.message}`,
114
+ autoFixable: false,
115
+ });
116
+ return;
117
+ }
118
+ // 1. Validate YAML frontmatter
119
+ this.validateFrontmatter(relativePath, content, filePath);
120
+ // 2. Validate MDX compatibility
121
+ this.validateMdxContent(relativePath, content, filePath);
122
+ // 3. Check for duplicate routes
123
+ this.checkDuplicateRoutes(relativePath, seenRoutes);
124
+ // 4. Validate file naming
125
+ this.validateFileName(relativePath);
126
+ }
127
+ /**
128
+ * Validate YAML frontmatter syntax
129
+ */
130
+ validateFrontmatter(relativePath, content, filePath) {
131
+ // Check if file has frontmatter
132
+ if (!content.startsWith('---')) {
133
+ // No frontmatter is OK for some files
134
+ return;
135
+ }
136
+ // Find closing ---
137
+ const endMatch = content.indexOf('\n---', 3);
138
+ if (endMatch === -1) {
139
+ this.issues.push({
140
+ file: relativePath,
141
+ severity: 'error',
142
+ type: 'invalid_frontmatter',
143
+ message: 'YAML frontmatter not closed (missing closing ---)',
144
+ autoFixable: false,
145
+ });
146
+ return;
147
+ }
148
+ const frontmatter = content.substring(4, endMatch);
149
+ // Check for common YAML issues
150
+ const lines = frontmatter.split('\n');
151
+ for (let i = 0; i < lines.length; i++) {
152
+ const line = lines[i];
153
+ const lineNum = i + 2; // +2 for opening --- and 0-index
154
+ // Check for unquoted colons in values (common issue)
155
+ // e.g., title: Feature: Something → should be title: "Feature: Something"
156
+ const colonMatch = line.match(/^(\s*)([a-zA-Z_-]+):\s*([^"'\n]*:[^"'\n]*)$/);
157
+ if (colonMatch && !line.includes('#')) {
158
+ const [, indent, key, value] = colonMatch;
159
+ this.issues.push({
160
+ file: relativePath,
161
+ line: lineNum,
162
+ severity: 'error',
163
+ type: 'yaml_unquoted_colon',
164
+ message: `Unquoted colon in YAML value: ${key}: ${value.trim()} (wrap in quotes)`,
165
+ autoFixable: true,
166
+ fix: () => {
167
+ const fixedLine = `${indent}${key}: "${value.trim()}"`;
168
+ const newContent = content.replace(line, fixedLine);
169
+ fs.writeFileSync(filePath, newContent);
170
+ this.fixedCount++;
171
+ },
172
+ });
173
+ }
174
+ // Check for tabs (YAML doesn't allow tabs for indentation)
175
+ if (line.includes('\t')) {
176
+ this.issues.push({
177
+ file: relativePath,
178
+ line: lineNum,
179
+ severity: 'error',
180
+ type: 'yaml_tabs',
181
+ message: 'YAML frontmatter contains tabs (use spaces)',
182
+ autoFixable: true,
183
+ fix: () => {
184
+ const newContent = content.replace(/\t/g, ' ');
185
+ fs.writeFileSync(filePath, newContent);
186
+ this.fixedCount++;
187
+ },
188
+ });
189
+ }
190
+ }
191
+ }
192
+ /**
193
+ * Validate MDX/JSX compatibility
194
+ */
195
+ validateMdxContent(relativePath, content, filePath) {
196
+ const mdxIssues = validateMdxCompatibility(content);
197
+ for (const issue of mdxIssues) {
198
+ this.issues.push({
199
+ file: relativePath,
200
+ severity: 'error',
201
+ type: 'mdx_compatibility',
202
+ message: issue,
203
+ autoFixable: true,
204
+ fix: () => {
205
+ const fixedContent = sanitizeHtmlForMdx(content);
206
+ fs.writeFileSync(filePath, fixedContent);
207
+ this.fixedCount++;
208
+ },
209
+ });
210
+ }
211
+ // Check for JSX-in-markdown issues
212
+ // Curly braces without escape can break MDX
213
+ const curlyBraceMatch = content.match(/(?<!\\)\{[^}]*\}/g);
214
+ if (curlyBraceMatch) {
215
+ for (const match of curlyBraceMatch) {
216
+ // Skip valid code blocks and inline code
217
+ if (content.includes('```') || content.includes('`' + match + '`')) {
218
+ continue;
219
+ }
220
+ // Check if it's a valid JSX expression or needs escaping
221
+ if (!match.match(/^\{[a-zA-Z_$][a-zA-Z0-9_$]*\}$/)) {
222
+ // Not a simple variable reference, might cause issues
223
+ // Only warn, don't error - some valid cases
224
+ }
225
+ }
226
+ }
227
+ // Check for common problematic patterns
228
+ const problematicPatterns = [
229
+ { pattern: /<script/i, message: 'Script tags not allowed in MDX' },
230
+ { pattern: /<style(?!\s)/i, message: 'Style tags may cause issues (use CSS modules)' },
231
+ { pattern: /<!--(?!.*-->)/s, message: 'Unclosed HTML comment' },
232
+ ];
233
+ for (const { pattern, message } of problematicPatterns) {
234
+ if (pattern.test(content)) {
235
+ this.issues.push({
236
+ file: relativePath,
237
+ severity: 'warning',
238
+ type: 'mdx_problematic_content',
239
+ message,
240
+ autoFixable: false,
241
+ });
242
+ }
243
+ }
244
+ }
245
+ /**
246
+ * Check for duplicate routes (same normalized path)
247
+ */
248
+ checkDuplicateRoutes(relativePath, seenRoutes) {
249
+ // Normalize route: remove extension, lowercase, normalize separators
250
+ const route = relativePath
251
+ .replace(/\.(md|mdx)$/, '')
252
+ .toLowerCase()
253
+ .replace(/\\/g, '/');
254
+ if (seenRoutes.has(route)) {
255
+ this.issues.push({
256
+ file: relativePath,
257
+ severity: 'error',
258
+ type: 'duplicate_route',
259
+ message: `Duplicate route: conflicts with ${seenRoutes.get(route)}`,
260
+ autoFixable: false,
261
+ });
262
+ }
263
+ else {
264
+ seenRoutes.set(route, relativePath);
265
+ }
266
+ }
267
+ /**
268
+ * Validate file naming conventions
269
+ */
270
+ validateFileName(relativePath) {
271
+ const fileName = path.basename(relativePath);
272
+ // Check for spaces in filename
273
+ if (fileName.includes(' ')) {
274
+ this.issues.push({
275
+ file: relativePath,
276
+ severity: 'warning',
277
+ type: 'filename_spaces',
278
+ message: 'Filename contains spaces (may cause URL issues)',
279
+ autoFixable: false,
280
+ });
281
+ }
282
+ // Check for special characters
283
+ if (/[<>:"|?*]/.test(fileName)) {
284
+ this.issues.push({
285
+ file: relativePath,
286
+ severity: 'error',
287
+ type: 'filename_invalid_chars',
288
+ message: 'Filename contains invalid characters',
289
+ autoFixable: false,
290
+ });
291
+ }
292
+ // Check for very long filenames
293
+ if (fileName.length > 100) {
294
+ this.issues.push({
295
+ file: relativePath,
296
+ severity: 'warning',
297
+ type: 'filename_too_long',
298
+ message: `Filename too long (${fileName.length} chars, max recommended: 100)`,
299
+ autoFixable: false,
300
+ });
301
+ }
302
+ }
303
+ /**
304
+ * Validate internal links between documents
305
+ */
306
+ async validateInternalLinks(files) {
307
+ const fileSet = new Set(files.map((f) => path.relative(this.docsPath, f).replace(/\\/g, '/')));
308
+ for (const file of files) {
309
+ const relativePath = path.relative(this.docsPath, file).replace(/\\/g, '/');
310
+ let content;
311
+ try {
312
+ content = fs.readFileSync(file, 'utf-8');
313
+ }
314
+ catch {
315
+ continue;
316
+ }
317
+ // Find markdown links: [text](path)
318
+ const linkRegex = /\[([^\]]*)\]\(([^)]+)\)/g;
319
+ let match;
320
+ while ((match = linkRegex.exec(content)) !== null) {
321
+ const [, , linkPath] = match;
322
+ // Skip external links
323
+ if (linkPath.startsWith('http://') || linkPath.startsWith('https://')) {
324
+ continue;
325
+ }
326
+ // Skip anchors
327
+ if (linkPath.startsWith('#')) {
328
+ continue;
329
+ }
330
+ // Skip mailto and tel
331
+ if (linkPath.startsWith('mailto:') || linkPath.startsWith('tel:')) {
332
+ continue;
333
+ }
334
+ // Resolve relative path
335
+ const linkPathClean = linkPath.split('#')[0].split('?')[0];
336
+ if (!linkPathClean)
337
+ continue;
338
+ const fileDir = path.dirname(relativePath);
339
+ let resolvedPath;
340
+ if (linkPathClean.startsWith('/')) {
341
+ resolvedPath = linkPathClean.substring(1);
342
+ }
343
+ else if (linkPathClean.startsWith('./')) {
344
+ resolvedPath = path.join(fileDir, linkPathClean.substring(2));
345
+ }
346
+ else if (linkPathClean.startsWith('../')) {
347
+ resolvedPath = path.join(fileDir, linkPathClean);
348
+ }
349
+ else {
350
+ resolvedPath = path.join(fileDir, linkPathClean);
351
+ }
352
+ // Normalize
353
+ resolvedPath = resolvedPath.replace(/\\/g, '/');
354
+ // Check if link points to existing file
355
+ const possiblePaths = [
356
+ resolvedPath,
357
+ resolvedPath + '.md',
358
+ resolvedPath + '.mdx',
359
+ resolvedPath + '/index.md',
360
+ resolvedPath + '/README.md',
361
+ ];
362
+ const exists = possiblePaths.some((p) => fileSet.has(p) || fileSet.has(p.replace(/\\/g, '/')));
363
+ // Check if it's an external file (outside docs)
364
+ const fullResolvedPath = path.resolve(this.docsPath, resolvedPath);
365
+ const externalExists = fs.existsSync(fullResolvedPath) ||
366
+ fs.existsSync(fullResolvedPath + '.md');
367
+ if (!exists && !externalExists) {
368
+ // Broken links are warnings, not errors - Docusaurus is configured with
369
+ // onBrokenLinks: 'warn' and won't crash, just show console warnings
370
+ // Check if it might be pointing outside docs intentionally
371
+ const isOutsideDocsRef = linkPathClean.includes('CLAUDE.md') ||
372
+ linkPathClean.includes('README.md') ||
373
+ linkPathClean.includes('increments/') ||
374
+ linkPathClean.includes('_archive/') ||
375
+ linkPathClean.includes('_features/') ||
376
+ linkPathClean.includes('plugins/') ||
377
+ linkPathClean.includes('src/');
378
+ this.issues.push({
379
+ file: relativePath,
380
+ severity: 'warning', // Always warning - Docusaurus won't crash on broken links
381
+ type: 'broken_link',
382
+ message: `Broken link: ${linkPath}${isOutsideDocsRef ? ' (external reference)' : ''}`,
383
+ autoFixable: false,
384
+ });
385
+ }
386
+ }
387
+ }
388
+ }
389
+ /**
390
+ * Build the validation result
391
+ */
392
+ buildResult() {
393
+ // Apply auto-fixes if enabled
394
+ if (this.autoFix) {
395
+ for (const issue of this.issues) {
396
+ if (issue.autoFixable && issue.fix) {
397
+ try {
398
+ issue.fix();
399
+ this.logger.info(`✓ Fixed: ${issue.file} - ${issue.message}`);
400
+ }
401
+ catch (err) {
402
+ this.logger.warn(`Failed to fix ${issue.file}: ${err.message}`);
403
+ }
404
+ }
405
+ }
406
+ }
407
+ const errors = this.issues.filter((i) => i.severity === 'error').length;
408
+ const warnings = this.issues.filter((i) => i.severity === 'warning').length;
409
+ const infos = this.issues.filter((i) => i.severity === 'info').length;
410
+ return {
411
+ valid: errors === 0,
412
+ totalFiles: this.issues.length > 0 ? this.issues.length : 0,
413
+ errors,
414
+ warnings,
415
+ infos,
416
+ issues: this.issues,
417
+ fixedCount: this.fixedCount,
418
+ };
419
+ }
420
+ /**
421
+ * Format validation result for display
422
+ */
423
+ static formatResult(result) {
424
+ const lines = [];
425
+ lines.push('');
426
+ lines.push('═══════════════════════════════════════════════════════════════');
427
+ lines.push(' DOCUMENTATION VALIDATION REPORT ');
428
+ lines.push('═══════════════════════════════════════════════════════════════');
429
+ lines.push('');
430
+ if (result.valid) {
431
+ lines.push('✅ Documentation is valid and ready for preview/build');
432
+ }
433
+ else {
434
+ lines.push('❌ Documentation has issues that need to be fixed');
435
+ }
436
+ lines.push('');
437
+ lines.push(` Errors: ${result.errors}`);
438
+ lines.push(` Warnings: ${result.warnings}`);
439
+ lines.push(` Info: ${result.infos}`);
440
+ if (result.fixedCount > 0) {
441
+ lines.push(` Fixed: ${result.fixedCount} (auto-fix applied)`);
442
+ }
443
+ lines.push('');
444
+ // Group issues by file
445
+ const issuesByFile = new Map();
446
+ for (const issue of result.issues) {
447
+ const existing = issuesByFile.get(issue.file) ?? [];
448
+ existing.push(issue);
449
+ issuesByFile.set(issue.file, existing);
450
+ }
451
+ // Show errors first
452
+ const errorFiles = Array.from(issuesByFile.entries())
453
+ .filter(([, issues]) => issues.some((i) => i.severity === 'error'));
454
+ if (errorFiles.length > 0) {
455
+ lines.push('ERRORS (must fix):');
456
+ lines.push('───────────────────────────────────────────────────────────────');
457
+ for (const [file, issues] of errorFiles) {
458
+ const errors = issues.filter((i) => i.severity === 'error');
459
+ lines.push(` 📄 ${file}`);
460
+ for (const issue of errors) {
461
+ const line = issue.line ? `:${issue.line}` : '';
462
+ const fix = issue.autoFixable ? ' [auto-fixable]' : '';
463
+ lines.push(` ❌ ${issue.type}${line}: ${issue.message}${fix}`);
464
+ }
465
+ }
466
+ lines.push('');
467
+ }
468
+ // Show warnings
469
+ const warningFiles = Array.from(issuesByFile.entries())
470
+ .filter(([, issues]) => issues.some((i) => i.severity === 'warning'));
471
+ if (warningFiles.length > 0 && warningFiles.length <= 10) {
472
+ lines.push('WARNINGS (recommended to fix):');
473
+ lines.push('───────────────────────────────────────────────────────────────');
474
+ for (const [file, issues] of warningFiles) {
475
+ const warnings = issues.filter((i) => i.severity === 'warning');
476
+ lines.push(` 📄 ${file}`);
477
+ for (const issue of warnings) {
478
+ lines.push(` ⚠️ ${issue.message}`);
479
+ }
480
+ }
481
+ lines.push('');
482
+ }
483
+ else if (warningFiles.length > 10) {
484
+ lines.push(`WARNINGS: ${result.warnings} (showing summary only)`);
485
+ lines.push('───────────────────────────────────────────────────────────────');
486
+ lines.push(` Run with --verbose to see all warnings`);
487
+ lines.push('');
488
+ }
489
+ // Suggestions
490
+ if (!result.valid) {
491
+ lines.push('QUICK FIXES:');
492
+ lines.push('───────────────────────────────────────────────────────────────');
493
+ lines.push(' 1. Run validation with auto-fix:');
494
+ lines.push(' /specweave-docs:validate --fix');
495
+ lines.push('');
496
+ lines.push(' 2. Or fix manually:');
497
+ const hasYamlIssues = result.issues.some((i) => i.type.startsWith('yaml_'));
498
+ const hasMdxIssues = result.issues.some((i) => i.type.startsWith('mdx_'));
499
+ const hasBrokenLinks = result.issues.some((i) => i.type === 'broken_link');
500
+ if (hasYamlIssues) {
501
+ lines.push(' • YAML: Wrap values with colons in quotes');
502
+ lines.push(' title: "Feature: Auth" (not title: Feature: Auth)');
503
+ }
504
+ if (hasMdxIssues) {
505
+ lines.push(' • MDX: Quote HTML attributes, close self-closing tags');
506
+ lines.push(' target="_blank" (not target=_blank)');
507
+ lines.push(' <br /> (not <br>)');
508
+ }
509
+ if (hasBrokenLinks) {
510
+ lines.push(' • Links: Update or remove broken internal links');
511
+ }
512
+ lines.push('');
513
+ }
514
+ lines.push('═══════════════════════════════════════════════════════════════');
515
+ return lines.join('\n');
516
+ }
517
+ }
518
+ /**
519
+ * Quick validation function for scripts
520
+ */
521
+ export async function validateDocs(docsPath, options) {
522
+ const validator = new DocsValidator({
523
+ docsPath,
524
+ autoFix: options?.autoFix,
525
+ logger: options?.logger,
526
+ });
527
+ return validator.validate();
528
+ }
529
+ //# sourceMappingURL=docs-validator.js.map