all-hands-cli 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.
Files changed (305) hide show
  1. package/.allhands/README.md +75 -0
  2. package/.allhands/agents/compounder.yaml +15 -0
  3. package/.allhands/agents/coordinator.yaml +17 -0
  4. package/.allhands/agents/documentor.yaml +15 -0
  5. package/.allhands/agents/e2e-test-planner.yaml +17 -0
  6. package/.allhands/agents/emergent.yaml +22 -0
  7. package/.allhands/agents/executor.yaml +14 -0
  8. package/.allhands/agents/ideation.yaml +11 -0
  9. package/.allhands/agents/initiative-steering.yaml +19 -0
  10. package/.allhands/agents/judge.yaml +13 -0
  11. package/.allhands/agents/planner.yaml +19 -0
  12. package/.allhands/agents/pr-reviewer.yaml +15 -0
  13. package/.allhands/docs.json +5 -0
  14. package/.allhands/docs.local.json +26 -0
  15. package/.allhands/flows/COMPOUNDING.md +203 -0
  16. package/.allhands/flows/COORDINATION.md +89 -0
  17. package/.allhands/flows/CORE.md +87 -0
  18. package/.allhands/flows/DOCUMENTATION.md +218 -0
  19. package/.allhands/flows/E2E_TEST_PLAN_BUILDING.md +140 -0
  20. package/.allhands/flows/EMERGENT_PLANNING.md +57 -0
  21. package/.allhands/flows/IDEATION_SCOPING.md +154 -0
  22. package/.allhands/flows/INITIATIVE_STEERING.md +110 -0
  23. package/.allhands/flows/JUDGE_REVIEWING.md +79 -0
  24. package/.allhands/flows/PROMPT_TASK_EXECUTION.md +68 -0
  25. package/.allhands/flows/PR_REVIEWING.md +43 -0
  26. package/.allhands/flows/SPEC_PLANNING.md +216 -0
  27. package/.allhands/flows/harness/WRITING_HARNESS_FLOWS.md +27 -0
  28. package/.allhands/flows/harness/WRITING_HARNESS_KNOWLEDGE.md +27 -0
  29. package/.allhands/flows/harness/WRITING_HARNESS_ORCHESTRATION.md +27 -0
  30. package/.allhands/flows/harness/WRITING_HARNESS_SKILLS.md +27 -0
  31. package/.allhands/flows/harness/WRITING_HARNESS_TOOLS.md +27 -0
  32. package/.allhands/flows/harness/WRITING_HARNESS_VALIDATION_TOOLING.md +27 -0
  33. package/.allhands/flows/shared/CODEBASE_UNDERSTANDING.md +72 -0
  34. package/.allhands/flows/shared/CREATE_HARNESS_SPEC.md +48 -0
  35. package/.allhands/flows/shared/CREATE_SPEC.md +41 -0
  36. package/.allhands/flows/shared/CREATE_VALIDATION_TOOLING_SPEC.md +70 -0
  37. package/.allhands/flows/shared/DOCUMENTATION_DISCOVERY.md +123 -0
  38. package/.allhands/flows/shared/DOCUMENTATION_WRITER.md +101 -0
  39. package/.allhands/flows/shared/EMERGENT_REFINEMENT_ANALYSIS.md +76 -0
  40. package/.allhands/flows/shared/EXTERNAL_TECH_GUIDANCE.md +97 -0
  41. package/.allhands/flows/shared/IDEATION_CODEBASE_GROUNDING.md +49 -0
  42. package/.allhands/flows/shared/PLAN_DEEPENING.md +152 -0
  43. package/.allhands/flows/shared/PROMPT_TASKS_CURATION.md +113 -0
  44. package/.allhands/flows/shared/PROMPT_VALIDATION_REVIEW.MD +99 -0
  45. package/.allhands/flows/shared/QUICK_PREMORTEM.md +70 -0
  46. package/.allhands/flows/shared/RESEARCH_GUIDANCE.md +38 -0
  47. package/.allhands/flows/shared/REVIEW_OPTIONS_BREAKDOWN.md +68 -0
  48. package/.allhands/flows/shared/SKILL_EXTRACTION.md +84 -0
  49. package/.allhands/flows/shared/SPEC_FLOW_ANALYSIS.md +119 -0
  50. package/.allhands/flows/shared/TDD_WORKFLOW.md +109 -0
  51. package/.allhands/flows/shared/UTILIZE_VALIDATION_TOOLING.md +84 -0
  52. package/.allhands/flows/shared/WRITING_HARNESS_FLOWS.md +11 -0
  53. package/.allhands/flows/shared/WRITING_HARNESS_MCP_TOOLS.md +84 -0
  54. package/.allhands/flows/shared/jury/ARCHITECTURE_REVIEW.md +91 -0
  55. package/.allhands/flows/shared/jury/BEST_PRACTICES_REVIEW.md +80 -0
  56. package/.allhands/flows/shared/jury/CLAIM_VERIFICATION_REVIEW.md +101 -0
  57. package/.allhands/flows/shared/jury/EXPECTATIONS_FIT_REVIEW.md +78 -0
  58. package/.allhands/flows/shared/jury/MAINTAINABILITY_REVIEW.md +110 -0
  59. package/.allhands/flows/shared/jury/PROMPTS_EXPECTATIONS_FIT.md +74 -0
  60. package/.allhands/flows/shared/jury/PROMPTS_FLOW_ANALYSIS.md +92 -0
  61. package/.allhands/flows/shared/jury/PROMPTS_YAGNI.md +78 -0
  62. package/.allhands/flows/shared/jury/PROMPT_PREMORTEM.md +125 -0
  63. package/.allhands/flows/shared/jury/SECURITY_REVIEW.md +86 -0
  64. package/.allhands/flows/shared/jury/YAGNI_REVIEW.md +82 -0
  65. package/.allhands/flows/wip/DEBUG_INVESTIGATION.md +162 -0
  66. package/.allhands/flows/wip/MEMORY_RECALL.md +62 -0
  67. package/.allhands/harness/ah +131 -0
  68. package/.allhands/harness/package-lock.json +5292 -0
  69. package/.allhands/harness/package.json +52 -0
  70. package/.allhands/harness/src/__tests__/e2e/commands.test.ts +307 -0
  71. package/.allhands/harness/src/__tests__/e2e/event-loop.test.ts +539 -0
  72. package/.allhands/harness/src/__tests__/e2e/hooks.test.ts +427 -0
  73. package/.allhands/harness/src/__tests__/e2e/new-initiative-routing.test.ts +137 -0
  74. package/.allhands/harness/src/__tests__/e2e/run-e2e.ts +109 -0
  75. package/.allhands/harness/src/__tests__/e2e/specs-type.test.ts +210 -0
  76. package/.allhands/harness/src/__tests__/e2e/validation-hooks.test.ts +669 -0
  77. package/.allhands/harness/src/__tests__/e2e/validation-path-consistency.test.ts +354 -0
  78. package/.allhands/harness/src/__tests__/e2e/validation.test.ts +528 -0
  79. package/.allhands/harness/src/__tests__/harness/assertions.ts +318 -0
  80. package/.allhands/harness/src/__tests__/harness/cli-runner.ts +359 -0
  81. package/.allhands/harness/src/__tests__/harness/fixture.ts +384 -0
  82. package/.allhands/harness/src/__tests__/harness/hook-runner.ts +411 -0
  83. package/.allhands/harness/src/__tests__/harness/index.ts +122 -0
  84. package/.allhands/harness/src/cli.ts +36 -0
  85. package/.allhands/harness/src/commands/complexity.ts +177 -0
  86. package/.allhands/harness/src/commands/context7.ts +202 -0
  87. package/.allhands/harness/src/commands/docs.ts +557 -0
  88. package/.allhands/harness/src/commands/hooks.ts +24 -0
  89. package/.allhands/harness/src/commands/index.ts +51 -0
  90. package/.allhands/harness/src/commands/knowledge.ts +382 -0
  91. package/.allhands/harness/src/commands/memories.ts +302 -0
  92. package/.allhands/harness/src/commands/notify.ts +61 -0
  93. package/.allhands/harness/src/commands/oracle.ts +158 -0
  94. package/.allhands/harness/src/commands/perplexity.ts +220 -0
  95. package/.allhands/harness/src/commands/planning.ts +245 -0
  96. package/.allhands/harness/src/commands/schema.ts +73 -0
  97. package/.allhands/harness/src/commands/skills.ts +128 -0
  98. package/.allhands/harness/src/commands/solutions.ts +353 -0
  99. package/.allhands/harness/src/commands/spawn.ts +158 -0
  100. package/.allhands/harness/src/commands/specs.ts +532 -0
  101. package/.allhands/harness/src/commands/tavily.ts +226 -0
  102. package/.allhands/harness/src/commands/tools.ts +579 -0
  103. package/.allhands/harness/src/commands/trace.ts +327 -0
  104. package/.allhands/harness/src/commands/tui.ts +960 -0
  105. package/.allhands/harness/src/commands/validate.ts +143 -0
  106. package/.allhands/harness/src/commands/validation-tools.ts +108 -0
  107. package/.allhands/harness/src/hooks/context.ts +1442 -0
  108. package/.allhands/harness/src/hooks/enforcement.ts +170 -0
  109. package/.allhands/harness/src/hooks/index.ts +54 -0
  110. package/.allhands/harness/src/hooks/lifecycle.ts +229 -0
  111. package/.allhands/harness/src/hooks/notification.ts +104 -0
  112. package/.allhands/harness/src/hooks/observability.ts +551 -0
  113. package/.allhands/harness/src/hooks/session.ts +88 -0
  114. package/.allhands/harness/src/hooks/shared.ts +815 -0
  115. package/.allhands/harness/src/hooks/transcript-parser.ts +208 -0
  116. package/.allhands/harness/src/hooks/validation.ts +617 -0
  117. package/.allhands/harness/src/lib/__tests__/ctags.test.ts +244 -0
  118. package/.allhands/harness/src/lib/__tests__/docs-validation.test.ts +344 -0
  119. package/.allhands/harness/src/lib/__tests__/mcp-runtime.test.ts +190 -0
  120. package/.allhands/harness/src/lib/__tests__/schema.test.ts +861 -0
  121. package/.allhands/harness/src/lib/base-command.ts +198 -0
  122. package/.allhands/harness/src/lib/cli-daemon.ts +343 -0
  123. package/.allhands/harness/src/lib/compaction.ts +313 -0
  124. package/.allhands/harness/src/lib/ctags.ts +497 -0
  125. package/.allhands/harness/src/lib/docs-validation.ts +907 -0
  126. package/.allhands/harness/src/lib/event-loop.ts +662 -0
  127. package/.allhands/harness/src/lib/flows.ts +155 -0
  128. package/.allhands/harness/src/lib/git.ts +276 -0
  129. package/.allhands/harness/src/lib/knowledge-worker.ts +72 -0
  130. package/.allhands/harness/src/lib/knowledge.ts +810 -0
  131. package/.allhands/harness/src/lib/llm.ts +255 -0
  132. package/.allhands/harness/src/lib/mcp-client.ts +432 -0
  133. package/.allhands/harness/src/lib/mcp-daemon.ts +486 -0
  134. package/.allhands/harness/src/lib/mcp-runtime.ts +418 -0
  135. package/.allhands/harness/src/lib/notification.ts +115 -0
  136. package/.allhands/harness/src/lib/opencode/index.ts +70 -0
  137. package/.allhands/harness/src/lib/opencode/profiles.ts +300 -0
  138. package/.allhands/harness/src/lib/opencode/prompts/codesearch.md +98 -0
  139. package/.allhands/harness/src/lib/opencode/prompts/knowledge-aggregator.md +67 -0
  140. package/.allhands/harness/src/lib/opencode/runner.ts +281 -0
  141. package/.allhands/harness/src/lib/oracle.ts +926 -0
  142. package/.allhands/harness/src/lib/planning-utils.ts +150 -0
  143. package/.allhands/harness/src/lib/planning.ts +605 -0
  144. package/.allhands/harness/src/lib/pr-review.ts +225 -0
  145. package/.allhands/harness/src/lib/prompts.ts +522 -0
  146. package/.allhands/harness/src/lib/schema.ts +418 -0
  147. package/.allhands/harness/src/lib/schemas/agent-profile.ts +141 -0
  148. package/.allhands/harness/src/lib/schemas/template-vars.ts +138 -0
  149. package/.allhands/harness/src/lib/session.ts +164 -0
  150. package/.allhands/harness/src/lib/specs.ts +348 -0
  151. package/.allhands/harness/src/lib/tldr.ts +829 -0
  152. package/.allhands/harness/src/lib/tmux.ts +1051 -0
  153. package/.allhands/harness/src/lib/trace-store.ts +714 -0
  154. package/.allhands/harness/src/mcp/__tests__/index.test.ts +46 -0
  155. package/.allhands/harness/src/mcp/_template.ts +47 -0
  156. package/.allhands/harness/src/mcp/filesystem.ts +33 -0
  157. package/.allhands/harness/src/mcp/index.ts +69 -0
  158. package/.allhands/harness/src/mcp/playwright.ts +34 -0
  159. package/.allhands/harness/src/mcp/xcodebuild.ts +29 -0
  160. package/.allhands/harness/src/schemas/docs.schema.json +44 -0
  161. package/.allhands/harness/src/schemas/settings.schema.json +214 -0
  162. package/.allhands/harness/src/tui/actions.ts +227 -0
  163. package/.allhands/harness/src/tui/file-viewer-modal.ts +270 -0
  164. package/.allhands/harness/src/tui/index.ts +1574 -0
  165. package/.allhands/harness/src/tui/modal.ts +232 -0
  166. package/.allhands/harness/src/tui/prompts-pane.ts +186 -0
  167. package/.allhands/harness/src/tui/status-pane.ts +434 -0
  168. package/.allhands/harness/tsconfig.json +22 -0
  169. package/.allhands/harness/vitest.config.ts +13 -0
  170. package/.allhands/pillars.md +33 -0
  171. package/.allhands/principles.md +88 -0
  172. package/.allhands/schemas/alignment.yaml +51 -0
  173. package/.allhands/schemas/documentation.yaml +10 -0
  174. package/.allhands/schemas/prompt.yaml +92 -0
  175. package/.allhands/schemas/skill.yaml +34 -0
  176. package/.allhands/schemas/solution.yaml +131 -0
  177. package/.allhands/schemas/spec.yaml +67 -0
  178. package/.allhands/schemas/validation-suite.yaml +49 -0
  179. package/.allhands/schemas/workflow.yaml +51 -0
  180. package/.allhands/settings.json +57 -0
  181. package/.allhands/skills/claude-code-patterns/SKILL.md +60 -0
  182. package/.allhands/skills/claude-code-patterns/docs/context-hygiene.md +19 -0
  183. package/.allhands/skills/harness-maintenance/SKILL.md +449 -0
  184. package/.allhands/skills/harness-maintenance/references/core-architecture.md +187 -0
  185. package/.allhands/skills/harness-maintenance/references/harness-skills.md +87 -0
  186. package/.allhands/skills/harness-maintenance/references/knowledge-compounding.md +78 -0
  187. package/.allhands/skills/harness-maintenance/references/tools-commands-mcp-hooks.md +115 -0
  188. package/.allhands/skills/harness-maintenance/references/validation-tooling.md +77 -0
  189. package/.allhands/skills/harness-maintenance/references/writing-flows.md +84 -0
  190. package/.allhands/validation/browser-automation.md +109 -0
  191. package/.allhands/validation/xcode-automation.md +195 -0
  192. package/.allhands/workflows/documentation.md +86 -0
  193. package/.allhands/workflows/investigation.md +81 -0
  194. package/.allhands/workflows/milestone.md +91 -0
  195. package/.allhands/workflows/optimization.md +85 -0
  196. package/.allhands/workflows/refactor.md +99 -0
  197. package/.allhands/workflows/triage.md +81 -0
  198. package/.claude/README.md +1 -0
  199. package/.claude/agents/explorer.md +10 -0
  200. package/.claude/agents/researcher.md +11 -0
  201. package/.claude/agents/task-runner.md +8 -0
  202. package/.claude/settings.json +231 -0
  203. package/.env.ai.example +7 -0
  204. package/.github/workflows/npm-publish.yml +69 -0
  205. package/.internal.json +45 -0
  206. package/.tldr/config.json +11 -0
  207. package/.tldrignore +90 -0
  208. package/CLAUDE.md +6 -0
  209. package/README.md +98 -0
  210. package/bin/sync-cli.js +7552 -0
  211. package/concerns.md +7 -0
  212. package/docs/README.md +41 -0
  213. package/docs/agents/README.md +24 -0
  214. package/docs/agents/agent-configuration-system.md +86 -0
  215. package/docs/agents/execution-agents.md +50 -0
  216. package/docs/agents/knowledge-agents.md +61 -0
  217. package/docs/agents/orchestration-agent.md +57 -0
  218. package/docs/agents/planning-agents.md +84 -0
  219. package/docs/agents/quality-review-agents.md +67 -0
  220. package/docs/agents/workflow-agent-orchestration.md +69 -0
  221. package/docs/flows/README.md +44 -0
  222. package/docs/flows/compounding.md +126 -0
  223. package/docs/flows/coordination.md +72 -0
  224. package/docs/flows/core-harness-integration.md +63 -0
  225. package/docs/flows/documentation-orchestration.md +98 -0
  226. package/docs/flows/e2e-test-plan-building.md +83 -0
  227. package/docs/flows/emergent-refinement.md +104 -0
  228. package/docs/flows/flow-authoring-and-mcp-tools.md +89 -0
  229. package/docs/flows/judge-reviewing.md +112 -0
  230. package/docs/flows/plan-deepening-and-research.md +107 -0
  231. package/docs/flows/plan-review-jury.md +114 -0
  232. package/docs/flows/pr-reviewing.md +54 -0
  233. package/docs/flows/prompt-task-execution.md +119 -0
  234. package/docs/flows/spec-planning.md +162 -0
  235. package/docs/flows/type-specific-scoping-flows.md +49 -0
  236. package/docs/flows/validation-and-skills-integration.md +145 -0
  237. package/docs/flows/wip/wip-flows.md +102 -0
  238. package/docs/harness/README.md +23 -0
  239. package/docs/harness/agent-profiles.md +84 -0
  240. package/docs/harness/cli/README.md +24 -0
  241. package/docs/harness/cli/cli-entry-and-command-discovery.md +91 -0
  242. package/docs/harness/cli/docs-command.md +87 -0
  243. package/docs/harness/cli/knowledge-command.md +91 -0
  244. package/docs/harness/cli/minor-cli-commands.md +65 -0
  245. package/docs/harness/cli/oracle-command.md +113 -0
  246. package/docs/harness/cli/planning-command.md +95 -0
  247. package/docs/harness/cli/schema-and-validation-commands.md +154 -0
  248. package/docs/harness/cli/search-commands.md +97 -0
  249. package/docs/harness/cli/spawn-command.md +136 -0
  250. package/docs/harness/cli/specs-command.md +102 -0
  251. package/docs/harness/cli/tools-command.md +122 -0
  252. package/docs/harness/cli/trace-command.md +122 -0
  253. package/docs/harness/cli-daemon.md +92 -0
  254. package/docs/harness/event-loop.md +184 -0
  255. package/docs/harness/hooks/README.md +15 -0
  256. package/docs/harness/hooks/context-hooks.md +96 -0
  257. package/docs/harness/hooks/lifecycle-and-observability-hooks.md +135 -0
  258. package/docs/harness/hooks/validation-hooks.md +97 -0
  259. package/docs/harness/test-harness.md +149 -0
  260. package/docs/harness/tui.md +176 -0
  261. package/docs/memories.md +20 -0
  262. package/docs/solutions/agentic-issues/premature-agent-deletion-tui-action-dependency-20260130.md +49 -0
  263. package/docs/solutions/agentic-issues/ref-anchor-scope-mismatch-skill-references-20260131.md +55 -0
  264. package/docs/solutions/agentic-issues/tautological-tests-routing-20260131.md +52 -0
  265. package/docs/solutions/integration_issue/blocktool-output-format-mismatch-hook-runner-20260130.md +52 -0
  266. package/docs/solutions/integration_issue/dual-validation-path-divergence-schema-20260130.md +66 -0
  267. package/docs/solutions/security-issues/unsanitized-domain-path-join-20260131.md +52 -0
  268. package/docs/solutions/test-failures/event-loop-mock-ordering-checkAgentWindows-20260130.md +63 -0
  269. package/docs/sync-cli/README.md +19 -0
  270. package/docs/sync-cli/cli-entrypoint-and-commands.md +39 -0
  271. package/docs/sync-cli/commands/README.md +11 -0
  272. package/docs/sync-cli/commands/pull-manifest-command.md +36 -0
  273. package/docs/sync-cli/commands/push-command.md +84 -0
  274. package/docs/sync-cli/commands/sync-command.md +71 -0
  275. package/docs/sync-cli/systems/README.md +14 -0
  276. package/docs/sync-cli/systems/git-and-github-integration.md +49 -0
  277. package/docs/sync-cli/systems/interactive-ui.md +43 -0
  278. package/docs/sync-cli/systems/manifest-and-distribution.md +51 -0
  279. package/docs/sync-cli/systems/path-resolution.md +42 -0
  280. package/package.json +46 -0
  281. package/scripts/install-shim.sh +40 -0
  282. package/scripts/pre-pack.sh +25 -0
  283. package/specs/harness-maintenance-skill.spec.md +138 -0
  284. package/specs/roadmap/git-spec-lifecycle-management.spec.md +113 -0
  285. package/specs/sync-init-flag.spec.md +117 -0
  286. package/specs/unified-workflow-orchestration.spec.md +250 -0
  287. package/specs/validation-tooling-practice.spec.md +98 -0
  288. package/specs/workflow-domain-configuration.spec.md +265 -0
  289. package/src/commands/pull-manifest.ts +31 -0
  290. package/src/commands/push.ts +344 -0
  291. package/src/commands/sync.ts +289 -0
  292. package/src/lib/constants.ts +10 -0
  293. package/src/lib/dotfiles.ts +36 -0
  294. package/src/lib/fs-utils.ts +18 -0
  295. package/src/lib/gh.ts +40 -0
  296. package/src/lib/git.ts +63 -0
  297. package/src/lib/gitignore.ts +167 -0
  298. package/src/lib/manifest.ts +121 -0
  299. package/src/lib/marker-sync.ts +39 -0
  300. package/src/lib/paths.ts +38 -0
  301. package/src/lib/target-lines.ts +66 -0
  302. package/src/lib/ui.ts +78 -0
  303. package/src/sync-cli.ts +120 -0
  304. package/target-lines.json +23 -0
  305. package/tsconfig.json +20 -0
@@ -0,0 +1,861 @@
1
+ /**
2
+ * Unit Tests - Schema Validation Library
3
+ *
4
+ * Tests the core schema validation functions including:
5
+ * - validateField() for all type branches (string, integer, boolean, date, enum, array, object)
6
+ * - Array item-type validation (added in validation-tooling-practice Prompt 04)
7
+ * - Schema loading and listing
8
+ * - Frontmatter extraction and validation
9
+ * - Schema type detection
10
+ * - Default application and error formatting
11
+ */
12
+
13
+ import { describe, it, expect } from 'vitest';
14
+ import {
15
+ loadSchema,
16
+ listSchemas,
17
+ extractFrontmatter,
18
+ validateFrontmatter,
19
+ validateFile,
20
+ applyDefaults,
21
+ formatErrors,
22
+ detectSchemaType,
23
+ inferSchemaType,
24
+ type SchemaField,
25
+ type Schema,
26
+ type ValidationResult,
27
+ } from '../schema.js';
28
+
29
+ // ─────────────────────────────────────────────────────────────────────────────
30
+ // Helpers
31
+ // ─────────────────────────────────────────────────────────────────────────────
32
+
33
+ /**
34
+ * Build a minimal schema with a single field for isolated validateField testing.
35
+ * validateFrontmatter delegates to validateField per-field, so we test through
36
+ * the public API by constructing single-field schemas.
37
+ */
38
+ function schemaWith(fieldName: string, field: SchemaField): Schema {
39
+ return { frontmatter: { [fieldName]: field } };
40
+ }
41
+
42
+ function validate(fieldName: string, field: SchemaField, value: unknown): ValidationResult {
43
+ const schema = schemaWith(fieldName, field);
44
+ const frontmatter: Record<string, unknown> = {};
45
+ if (value !== undefined) {
46
+ frontmatter[fieldName] = value;
47
+ }
48
+ return validateFrontmatter(frontmatter, schema);
49
+ }
50
+
51
+ // ─────────────────────────────────────────────────────────────────────────────
52
+ // validateField — Type Branches
53
+ // ─────────────────────────────────────────────────────────────────────────────
54
+
55
+ describe('validateField via validateFrontmatter', () => {
56
+ // --- Required / Optional ---
57
+
58
+ describe('required and optional fields', () => {
59
+ it('returns error when required field is missing', () => {
60
+ const result = validate('name', { type: 'string', required: true }, undefined);
61
+ expect(result.valid).toBe(false);
62
+ expect(result.errors).toHaveLength(1);
63
+ expect(result.errors[0].field).toBe('name');
64
+ });
65
+
66
+ it('returns valid when optional field is missing', () => {
67
+ const result = validate('name', { type: 'string', required: false }, undefined);
68
+ expect(result.valid).toBe(true);
69
+ expect(result.errors).toHaveLength(0);
70
+ });
71
+
72
+ it('returns valid when optional field with no required flag is missing', () => {
73
+ const result = validate('name', { type: 'string' }, undefined);
74
+ expect(result.valid).toBe(true);
75
+ });
76
+ });
77
+
78
+ // --- String ---
79
+
80
+ describe('string type', () => {
81
+ it('accepts a valid string', () => {
82
+ const result = validate('title', { type: 'string', required: true }, 'hello');
83
+ expect(result.valid).toBe(true);
84
+ });
85
+
86
+ it('accepts an empty string', () => {
87
+ const result = validate('title', { type: 'string', required: true }, '');
88
+ expect(result.valid).toBe(true);
89
+ });
90
+
91
+ it('rejects a number', () => {
92
+ const result = validate('title', { type: 'string', required: true }, 42);
93
+ expect(result.valid).toBe(false);
94
+ expect(result.errors[0].field).toBe('title');
95
+ });
96
+
97
+ it('rejects a boolean', () => {
98
+ const result = validate('title', { type: 'string', required: true }, true);
99
+ expect(result.valid).toBe(false);
100
+ });
101
+ });
102
+
103
+ // --- Integer ---
104
+
105
+ describe('integer type', () => {
106
+ it('accepts a valid integer', () => {
107
+ const result = validate('count', { type: 'integer', required: true }, 5);
108
+ expect(result.valid).toBe(true);
109
+ });
110
+
111
+ it('accepts zero', () => {
112
+ const result = validate('count', { type: 'integer', required: true }, 0);
113
+ expect(result.valid).toBe(true);
114
+ });
115
+
116
+ it('accepts negative integer', () => {
117
+ const result = validate('count', { type: 'integer', required: true }, -3);
118
+ expect(result.valid).toBe(true);
119
+ });
120
+
121
+ it('rejects a float', () => {
122
+ const result = validate('count', { type: 'integer', required: true }, 3.14);
123
+ expect(result.valid).toBe(false);
124
+ expect(result.errors[0].field).toBe('count');
125
+ });
126
+
127
+ it('rejects a string', () => {
128
+ const result = validate('count', { type: 'integer', required: true }, '5');
129
+ expect(result.valid).toBe(false);
130
+ });
131
+ });
132
+
133
+ // --- Boolean ---
134
+
135
+ describe('boolean type', () => {
136
+ it('accepts true', () => {
137
+ const result = validate('flag', { type: 'boolean', required: true }, true);
138
+ expect(result.valid).toBe(true);
139
+ });
140
+
141
+ it('accepts false', () => {
142
+ const result = validate('flag', { type: 'boolean', required: true }, false);
143
+ expect(result.valid).toBe(true);
144
+ });
145
+
146
+ it('rejects a string', () => {
147
+ const result = validate('flag', { type: 'boolean', required: true }, 'true');
148
+ expect(result.valid).toBe(false);
149
+ });
150
+
151
+ it('rejects a number', () => {
152
+ const result = validate('flag', { type: 'boolean', required: true }, 1);
153
+ expect(result.valid).toBe(false);
154
+ });
155
+ });
156
+
157
+ // --- Date ---
158
+
159
+ describe('date type', () => {
160
+ it('accepts a valid ISO 8601 date', () => {
161
+ const result = validate('created', { type: 'date', required: true }, '2025-01-15');
162
+ expect(result.valid).toBe(true);
163
+ });
164
+
165
+ it('accepts a full ISO datetime', () => {
166
+ const result = validate('created', { type: 'date', required: true }, '2025-01-15T10:30:00Z');
167
+ expect(result.valid).toBe(true);
168
+ });
169
+
170
+ it('rejects an invalid date string', () => {
171
+ const result = validate('created', { type: 'date', required: true }, 'not-a-date');
172
+ expect(result.valid).toBe(false);
173
+ expect(result.errors[0].field).toBe('created');
174
+ });
175
+
176
+ it('rejects a number', () => {
177
+ const result = validate('created', { type: 'date', required: true }, 1705334400000);
178
+ expect(result.valid).toBe(false);
179
+ });
180
+ });
181
+
182
+ // --- Enum ---
183
+
184
+ describe('enum type', () => {
185
+ const enumField: SchemaField = {
186
+ type: 'enum',
187
+ required: true,
188
+ values: ['pending', 'in_progress', 'done'],
189
+ };
190
+
191
+ it('accepts a valid enum value', () => {
192
+ const result = validate('status', enumField, 'pending');
193
+ expect(result.valid).toBe(true);
194
+ });
195
+
196
+ it('accepts another valid enum value', () => {
197
+ const result = validate('status', enumField, 'done');
198
+ expect(result.valid).toBe(true);
199
+ });
200
+
201
+ it('rejects an invalid enum value', () => {
202
+ const result = validate('status', enumField, 'invalid_status');
203
+ expect(result.valid).toBe(false);
204
+ expect(result.errors[0].field).toBe('status');
205
+ });
206
+
207
+ it('rejects an empty string not in values', () => {
208
+ const result = validate('status', enumField, '');
209
+ expect(result.valid).toBe(false);
210
+ });
211
+ });
212
+
213
+ // --- Array ---
214
+
215
+ describe('array type', () => {
216
+ it('accepts a valid array', () => {
217
+ const result = validate('tags', { type: 'array', required: true }, ['a', 'b']);
218
+ expect(result.valid).toBe(true);
219
+ });
220
+
221
+ it('accepts an empty array', () => {
222
+ const result = validate('tags', { type: 'array', required: true }, []);
223
+ expect(result.valid).toBe(true);
224
+ });
225
+
226
+ it('rejects a non-array value', () => {
227
+ const result = validate('tags', { type: 'array', required: true }, 'not-array');
228
+ expect(result.valid).toBe(false);
229
+ expect(result.errors[0].field).toBe('tags');
230
+ });
231
+
232
+ it('rejects an object (not an array)', () => {
233
+ const result = validate('tags', { type: 'array', required: true }, { key: 'val' });
234
+ expect(result.valid).toBe(false);
235
+ });
236
+
237
+ // Array item-type validation (Prompt 04 addition)
238
+ describe('item-type validation', () => {
239
+ it('accepts string array when items: string', () => {
240
+ const result = validate('tools', { type: 'array', required: true, items: 'string' }, ['playwright', 'vitest']);
241
+ expect(result.valid).toBe(true);
242
+ });
243
+
244
+ it('rejects non-string items when items: string', () => {
245
+ const result = validate('tools', { type: 'array', required: true, items: 'string' }, [123, 'valid']);
246
+ expect(result.valid).toBe(false);
247
+ expect(result.errors[0].field).toBe('tools');
248
+ expect(result.errors[0].message).toContain('non-string');
249
+ });
250
+
251
+ it('accepts integer array when items: integer', () => {
252
+ const result = validate('deps', { type: 'array', required: true, items: 'integer' }, [1, 2, 3]);
253
+ expect(result.valid).toBe(true);
254
+ });
255
+
256
+ it('rejects float in integer array', () => {
257
+ const result = validate('deps', { type: 'array', required: true, items: 'integer' }, [1, 2.5, 3]);
258
+ expect(result.valid).toBe(false);
259
+ expect(result.errors[0].message).toContain('non-integer');
260
+ });
261
+
262
+ it('rejects string in integer array', () => {
263
+ const result = validate('deps', { type: 'array', required: true, items: 'integer' }, [1, '2', 3]);
264
+ expect(result.valid).toBe(false);
265
+ });
266
+
267
+ it('accepts empty array with items constraint', () => {
268
+ const result = validate('tools', { type: 'array', required: true, items: 'string' }, []);
269
+ expect(result.valid).toBe(true);
270
+ });
271
+
272
+ it('accepts array without items constraint (no type checking)', () => {
273
+ const result = validate('mixed', { type: 'array', required: true }, [1, 'two', true]);
274
+ expect(result.valid).toBe(true);
275
+ });
276
+ });
277
+ });
278
+
279
+ // --- Object ---
280
+
281
+ describe('object type', () => {
282
+ it('accepts a valid object', () => {
283
+ const result = validate('config', { type: 'object', required: true }, { key: 'val' });
284
+ expect(result.valid).toBe(true);
285
+ });
286
+
287
+ it('rejects null', () => {
288
+ // null is present but not a valid object — required field with null value
289
+ // In validateField, null triggers the required check first
290
+ const schema = schemaWith('config', { type: 'object', required: true });
291
+ const r = validateFrontmatter({ config: null }, schema);
292
+ expect(r.valid).toBe(false);
293
+ });
294
+
295
+ it('rejects an array (which is typeof object)', () => {
296
+ const result = validate('config', { type: 'object', required: true }, [1, 2]);
297
+ expect(result.valid).toBe(false);
298
+ });
299
+
300
+ it('rejects a string', () => {
301
+ const result = validate('config', { type: 'object', required: true }, 'not-object');
302
+ expect(result.valid).toBe(false);
303
+ });
304
+
305
+ describe('nested property validation', () => {
306
+ const nestedField: SchemaField = {
307
+ type: 'object',
308
+ required: true,
309
+ properties: {
310
+ name: { type: 'string', required: true },
311
+ count: { type: 'integer', required: false },
312
+ },
313
+ };
314
+
315
+ it('accepts object with valid nested properties', () => {
316
+ const result = validate('config', nestedField, { name: 'test', count: 5 });
317
+ expect(result.valid).toBe(true);
318
+ });
319
+
320
+ it('accepts object with optional nested property missing', () => {
321
+ const result = validate('config', nestedField, { name: 'test' });
322
+ expect(result.valid).toBe(true);
323
+ });
324
+
325
+ it('rejects object with missing required nested property', () => {
326
+ const result = validate('config', nestedField, { count: 5 });
327
+ expect(result.valid).toBe(false);
328
+ expect(result.errors[0].field).toBe('config.name');
329
+ });
330
+
331
+ it('rejects object with wrong-type nested property', () => {
332
+ const result = validate('config', nestedField, { name: 123, count: 5 });
333
+ expect(result.valid).toBe(false);
334
+ expect(result.errors[0].field).toBe('config.name');
335
+ });
336
+ });
337
+ });
338
+ });
339
+
340
+ // ─────────────────────────────────────────────────────────────────────────────
341
+ // Schema Loading & Listing
342
+ // ─────────────────────────────────────────────────────────────────────────────
343
+
344
+ describe('loadSchema', () => {
345
+ it('returns a schema object for known type "prompt"', () => {
346
+ const schema = loadSchema('prompt');
347
+ expect(schema).not.toBeNull();
348
+ expect(schema!.frontmatter).toBeDefined();
349
+ });
350
+
351
+ it('returns a schema object for "validation-suite"', () => {
352
+ const schema = loadSchema('validation-suite');
353
+ expect(schema).not.toBeNull();
354
+ expect(schema!.frontmatter).toBeDefined();
355
+ expect(schema!.frontmatter!['tools']).toBeDefined();
356
+ expect(schema!.frontmatter!['tools'].type).toBe('array');
357
+ expect(schema!.frontmatter!['tools'].items).toBe('string');
358
+ });
359
+
360
+ it('returns a schema object for "workflow"', () => {
361
+ const schema = loadSchema('workflow');
362
+ expect(schema).not.toBeNull();
363
+ expect(schema!.frontmatter).toBeDefined();
364
+ expect(schema!.frontmatter!['name']).toBeDefined();
365
+ expect(schema!.frontmatter!['type'].type).toBe('enum');
366
+ expect(schema!.frontmatter!['planning_depth'].type).toBe('enum');
367
+ expect(schema!.frontmatter!['jury_required'].type).toBe('boolean');
368
+ expect(schema!.frontmatter!['max_tangential_hypotheses'].type).toBe('integer');
369
+ expect(schema!.frontmatter!['required_ideation_questions'].type).toBe('array');
370
+ expect(schema!.frontmatter!['required_ideation_questions'].items).toBe('string');
371
+ });
372
+
373
+ it('returns null for unknown schema type', () => {
374
+ const schema = loadSchema('nonexistent-schema-type');
375
+ expect(schema).toBeNull();
376
+ });
377
+
378
+ it('caches schema on subsequent calls', () => {
379
+ const first = loadSchema('prompt');
380
+ const second = loadSchema('prompt');
381
+ expect(first).toBe(second); // same reference
382
+ });
383
+
384
+ it('returns null consistently for nonexistent type (no stale cache)', () => {
385
+ // Verify that looking up a nonexistent type multiple times
386
+ // always returns null and doesn't corrupt the cache
387
+ const first = loadSchema('totally-fake-schema');
388
+ const second = loadSchema('totally-fake-schema');
389
+ expect(first).toBeNull();
390
+ expect(second).toBeNull();
391
+ });
392
+
393
+ it('does not cross-contaminate cache between different schema types', () => {
394
+ const prompt = loadSchema('prompt');
395
+ const suite = loadSchema('validation-suite');
396
+ expect(prompt).not.toBeNull();
397
+ expect(suite).not.toBeNull();
398
+ // They should be different objects (different schema definitions)
399
+ expect(prompt).not.toBe(suite);
400
+ });
401
+
402
+ it('cached schema retains full structure on repeated access', () => {
403
+ // Load once to populate cache, then verify structure is intact on cache hit
404
+ loadSchema('prompt'); // warm cache
405
+ const cached = loadSchema('prompt');
406
+ expect(cached).not.toBeNull();
407
+ expect(cached!.frontmatter).toBeDefined();
408
+ expect(cached!.frontmatter!['number']).toBeDefined();
409
+ expect(cached!.frontmatter!['number'].type).toBe('integer');
410
+ });
411
+ });
412
+
413
+ describe('listSchemas', () => {
414
+ it('returns an array of schema type strings', () => {
415
+ const schemas = listSchemas();
416
+ expect(Array.isArray(schemas)).toBe(true);
417
+ expect(schemas.length).toBeGreaterThan(0);
418
+ });
419
+
420
+ it('includes known schema types', () => {
421
+ const schemas = listSchemas();
422
+ expect(schemas).toContain('prompt');
423
+ expect(schemas).toContain('validation-suite');
424
+ });
425
+ });
426
+
427
+ // ─────────────────────────────────────────────────────────────────────────────
428
+ // Frontmatter Extraction
429
+ // ─────────────────────────────────────────────────────────────────────────────
430
+
431
+ describe('extractFrontmatter', () => {
432
+ it('parses valid YAML frontmatter', () => {
433
+ const content = `---
434
+ title: Hello
435
+ count: 5
436
+ ---
437
+
438
+ Body content here.`;
439
+ const result = extractFrontmatter(content);
440
+ expect(result.frontmatter).not.toBeNull();
441
+ expect(result.frontmatter!['title']).toBe('Hello');
442
+ expect(result.frontmatter!['count']).toBe(5);
443
+ });
444
+
445
+ it('separates body content correctly', () => {
446
+ const content = `---
447
+ key: value
448
+ ---
449
+
450
+ # Body
451
+
452
+ Some text.`;
453
+ const result = extractFrontmatter(content);
454
+ expect(result.body).toContain('# Body');
455
+ expect(result.body).toContain('Some text.');
456
+ });
457
+
458
+ it('returns null frontmatter for content without delimiters', () => {
459
+ const content = '# Just a heading\n\nSome text.';
460
+ const result = extractFrontmatter(content);
461
+ expect(result.frontmatter).toBeNull();
462
+ expect(result.body).toBe(content);
463
+ });
464
+
465
+ it('returns null frontmatter for malformed YAML', () => {
466
+ const content = `---
467
+ : : : invalid yaml [[[
468
+ ---
469
+
470
+ Body.`;
471
+ const result = extractFrontmatter(content);
472
+ // parseYaml may or may not throw depending on how malformed — verify graceful handling
473
+ expect(result.body).toBeDefined();
474
+ });
475
+
476
+ it('handles empty frontmatter', () => {
477
+ const content = `---
478
+ ---
479
+
480
+ Body only.`;
481
+ const result = extractFrontmatter(content);
482
+ // Empty YAML parses to null in some parsers
483
+ expect(result.body).toBeDefined();
484
+ });
485
+ });
486
+
487
+ // ─────────────────────────────────────────────────────────────────────────────
488
+ // extractFrontmatter — Boundary Conditions (Stability)
489
+ // ─────────────────────────────────────────────────────────────────────────────
490
+
491
+ describe('extractFrontmatter boundary conditions', () => {
492
+ it('returns null frontmatter for empty string input', () => {
493
+ const result = extractFrontmatter('');
494
+ expect(result.frontmatter).toBeNull();
495
+ expect(result.body).toBe('');
496
+ });
497
+
498
+ it('returns null frontmatter for only frontmatter delimiters with empty YAML', () => {
499
+ // "---\n---\n" has empty YAML between delimiters
500
+ // parseYaml('') returns null — extractFrontmatter should handle gracefully
501
+ const content = '---\n---\n';
502
+ const result = extractFrontmatter(content);
503
+ // The regex matches but parseYaml on empty string returns null,
504
+ // which is cast to Record<string, unknown> — may be null
505
+ expect(result.body).toBeDefined();
506
+ });
507
+
508
+ it('handles content ending at closing --- with no trailing newline', () => {
509
+ // Regex: /^---\n([\s\S]*?)\n---\n([\s\S]*)$/
510
+ // Content "---\nkey: val\n---" has no trailing \n after closing ---
511
+ // This means the regex will NOT match (requires \n after closing ---)
512
+ // DIVERGENCE: hooks parseFrontmatter regex /^---\n([\s\S]*?)\n---/ DOES match this
513
+ const content = '---\nkey: val\n---';
514
+ const result = extractFrontmatter(content);
515
+ expect(result.frontmatter).toBeNull();
516
+ expect(result.body).toBe(content);
517
+ });
518
+
519
+ it('handles embedded --- in YAML string values', () => {
520
+ // Non-greedy ([\s\S]*?) should stop at first literal \n---\n
521
+ // The quoted "---" inside a value should not confuse the regex
522
+ const content = '---\nseparator: "---"\ntitle: test\n---\n\nBody text.';
523
+ const result = extractFrontmatter(content);
524
+ expect(result.frontmatter).not.toBeNull();
525
+ expect(result.frontmatter!['title']).toBe('test');
526
+ expect(result.body).toContain('Body text.');
527
+ });
528
+
529
+ it('returns null frontmatter for content with only opening ---', () => {
530
+ const content = '---\nkey: value\nmore: stuff';
531
+ const result = extractFrontmatter(content);
532
+ expect(result.frontmatter).toBeNull();
533
+ expect(result.body).toBe(content);
534
+ });
535
+
536
+ it('captures first --- block only, not body content with triple-dash', () => {
537
+ const content = '---\nfoo: bar\n---\n\nBody\n\n---\n\nMore body.';
538
+ const result = extractFrontmatter(content);
539
+ expect(result.frontmatter).not.toBeNull();
540
+ expect(result.frontmatter!['foo']).toBe('bar');
541
+ // Body should contain everything after the first closing ---\n
542
+ expect(result.body).toContain('Body');
543
+ expect(result.body).toContain('More body.');
544
+ });
545
+
546
+ it('returns null frontmatter when content starts with whitespace before ---', () => {
547
+ // Regex anchors with ^--- so leading whitespace prevents match
548
+ const content = ' ---\nkey: val\n---\n\nBody.';
549
+ const result = extractFrontmatter(content);
550
+ expect(result.frontmatter).toBeNull();
551
+ });
552
+ });
553
+
554
+ // ─────────────────────────────────────────────────────────────────────────────
555
+ // validateFrontmatter — Multi-field
556
+ // ─────────────────────────────────────────────────────────────────────────────
557
+
558
+ describe('validateFrontmatter', () => {
559
+ it('returns valid for conforming frontmatter', () => {
560
+ const schema: Schema = {
561
+ frontmatter: {
562
+ name: { type: 'string', required: true },
563
+ count: { type: 'integer', required: false, default: 0 },
564
+ },
565
+ };
566
+ const result = validateFrontmatter({ name: 'test' }, schema);
567
+ expect(result.valid).toBe(true);
568
+ });
569
+
570
+ it('collects multiple errors', () => {
571
+ const schema: Schema = {
572
+ frontmatter: {
573
+ name: { type: 'string', required: true },
574
+ status: { type: 'enum', required: true, values: ['a', 'b'] },
575
+ },
576
+ };
577
+ const result = validateFrontmatter({}, schema);
578
+ expect(result.valid).toBe(false);
579
+ expect(result.errors.length).toBeGreaterThanOrEqual(2);
580
+ });
581
+
582
+ it('uses schema.fields fallback when frontmatter is absent', () => {
583
+ const schema: Schema = {
584
+ fields: {
585
+ name: { type: 'string', required: true },
586
+ },
587
+ };
588
+ const result = validateFrontmatter({}, schema);
589
+ expect(result.valid).toBe(false);
590
+ expect(result.errors[0].field).toBe('name');
591
+ });
592
+
593
+ it('returns valid for empty schema', () => {
594
+ const result = validateFrontmatter({ anything: 'goes' }, {});
595
+ expect(result.valid).toBe(true);
596
+ });
597
+ });
598
+
599
+ // ─────────────────────────────────────────────────────────────────────────────
600
+ // validateFile — Integration
601
+ // ─────────────────────────────────────────────────────────────────────────────
602
+
603
+ describe('validateFile', () => {
604
+ it('returns error for unknown schema type', () => {
605
+ const result = validateFile('---\nfoo: bar\n---\nBody', 'nonexistent');
606
+ expect(result.valid).toBe(false);
607
+ expect(result.errors[0].field).toBe('_schema');
608
+ });
609
+
610
+ it('returns error for missing frontmatter', () => {
611
+ const result = validateFile('Just body content', 'prompt');
612
+ expect(result.valid).toBe(false);
613
+ expect(result.errors[0].field).toBe('_frontmatter');
614
+ });
615
+
616
+ it('does not crash on empty string content with valid schema type', () => {
617
+ const result = validateFile('', 'prompt');
618
+ expect(result.valid).toBe(false);
619
+ // Empty string has no frontmatter, so should get _frontmatter error
620
+ expect(result.errors[0].field).toBe('_frontmatter');
621
+ });
622
+
623
+ it('returns _frontmatter error for content with only body (no delimiters)', () => {
624
+ const result = validateFile('# Just a heading\n\nSome body text.', 'prompt');
625
+ expect(result.valid).toBe(false);
626
+ expect(result.errors[0].field).toBe('_frontmatter');
627
+ });
628
+
629
+ it('validates valid frontmatter against a real schema type', () => {
630
+ // Minimal valid prompt content (all required fields present)
631
+ const content = `---
632
+ number: 1
633
+ title: "Test Task"
634
+ type: planned
635
+ status: pending
636
+ ---
637
+
638
+ ## Tasks
639
+
640
+ - Do something
641
+
642
+ ## Acceptance Criteria
643
+
644
+ - Something works
645
+ `;
646
+ const result = validateFile(content, 'prompt');
647
+ expect(result.valid).toBe(true);
648
+ });
649
+
650
+ it('returns valid when schema has no frontmatter or fields keys', () => {
651
+ // validateFrontmatter iterates schema.frontmatter || schema.fields || {}
652
+ // An empty schema means zero fields to validate, so everything passes
653
+ const schema: Schema = {};
654
+ const result = validateFrontmatter({ anything: 'goes', extra: 42 }, schema);
655
+ expect(result.valid).toBe(true);
656
+ expect(result.errors).toHaveLength(0);
657
+ });
658
+
659
+ it('silently passes extra fields not defined in schema', () => {
660
+ const schema: Schema = {
661
+ frontmatter: {
662
+ name: { type: 'string', required: true },
663
+ },
664
+ };
665
+ // 'unknown_field' is not in schema — should be ignored, not rejected
666
+ const result = validateFrontmatter({ name: 'valid', unknown_field: 'extra' }, schema);
667
+ expect(result.valid).toBe(true);
668
+ expect(result.errors).toHaveLength(0);
669
+ });
670
+ });
671
+
672
+ // ─────────────────────────────────────────────────────────────────────────────
673
+ // Schema Type Detection
674
+ // ─────────────────────────────────────────────────────────────────────────────
675
+
676
+ describe('detectSchemaType', () => {
677
+ it('detects prompt files', () => {
678
+ expect(detectSchemaType('.planning/my-spec/prompts/01.md')).toBe('prompt');
679
+ });
680
+
681
+ it('detects alignment files', () => {
682
+ expect(detectSchemaType('.planning/my-spec/alignment.md')).toBe('alignment');
683
+ });
684
+
685
+ it('detects spec files', () => {
686
+ expect(detectSchemaType('specs/api.spec.md')).toBe('spec');
687
+ });
688
+
689
+ it('detects spec files in roadmap subdirectory', () => {
690
+ expect(detectSchemaType('specs/roadmap/feature.spec.md')).toBe('spec');
691
+ });
692
+
693
+ it('detects documentation files', () => {
694
+ expect(detectSchemaType('docs/guide.md')).toBe('documentation');
695
+ });
696
+
697
+ it('detects validation-suite files', () => {
698
+ expect(detectSchemaType('.allhands/validation/browser-automation.md')).toBe('validation-suite');
699
+ });
700
+
701
+ it('detects skill files', () => {
702
+ expect(detectSchemaType('.allhands/skills/my-skill/SKILL.md')).toBe('skill');
703
+ });
704
+
705
+ it('detects workflow files', () => {
706
+ expect(detectSchemaType('.allhands/workflows/milestone.md')).toBe('workflow');
707
+ });
708
+
709
+ it('returns null for non-schema files', () => {
710
+ expect(detectSchemaType('README.md')).toBeNull();
711
+ expect(detectSchemaType('src/index.ts')).toBeNull();
712
+ });
713
+
714
+ it('strips projectDir prefix before matching', () => {
715
+ expect(detectSchemaType('/home/user/project/.planning/s1/prompts/01.md', '/home/user/project')).toBe('prompt');
716
+ });
717
+
718
+ it('handles path without projectDir prefix gracefully', () => {
719
+ expect(detectSchemaType('.planning/s1/prompts/01.md', '/different/project')).toBe('prompt');
720
+ });
721
+ });
722
+
723
+ describe('inferSchemaType', () => {
724
+ it('infers prompt from path containing /prompts/', () => {
725
+ expect(inferSchemaType('/some/path/prompts/01.md')).toBe('prompt');
726
+ });
727
+
728
+ it('infers prompt from filename matching prompt*.md', () => {
729
+ expect(inferSchemaType('prompt-file.md')).toBe('prompt');
730
+ });
731
+
732
+ it('infers alignment from path containing alignment', () => {
733
+ expect(inferSchemaType('/planning/alignment.md')).toBe('alignment');
734
+ });
735
+
736
+ it('infers spec from path containing /specs/', () => {
737
+ expect(inferSchemaType('/project/specs/api.spec.md')).toBe('spec');
738
+ });
739
+
740
+ it('infers spec from .spec.md extension', () => {
741
+ expect(inferSchemaType('feature.spec.md')).toBe('spec');
742
+ });
743
+
744
+ it('infers documentation from /docs/ path', () => {
745
+ expect(inferSchemaType('/project/docs/guide.md')).toBe('documentation');
746
+ });
747
+
748
+ it('infers validation-suite from /validation/ path', () => {
749
+ expect(inferSchemaType('.allhands/validation/suite.md')).toBe('validation-suite');
750
+ });
751
+
752
+ it('infers skill from /skills/ path with SKILL.md', () => {
753
+ expect(inferSchemaType('.allhands/skills/my-skill/SKILL.md')).toBe('skill');
754
+ });
755
+
756
+ it('infers workflow from /workflows/ path', () => {
757
+ expect(inferSchemaType('.allhands/workflows/milestone.md')).toBe('workflow');
758
+ });
759
+
760
+ it('returns null for unknown paths', () => {
761
+ expect(inferSchemaType('README.md')).toBeNull();
762
+ expect(inferSchemaType('src/lib/utils.ts')).toBeNull();
763
+ });
764
+ });
765
+
766
+ // ─────────────────────────────────────────────────────────────────────────────
767
+ // applyDefaults
768
+ // ─────────────────────────────────────────────────────────────────────────────
769
+
770
+ describe('applyDefaults', () => {
771
+ const schema: Schema = {
772
+ frontmatter: {
773
+ name: { type: 'string', required: true },
774
+ status: { type: 'enum', required: true, values: ['pending', 'done'], default: 'pending' },
775
+ count: { type: 'integer', default: 0 },
776
+ tags: { type: 'array', default: [] },
777
+ },
778
+ };
779
+
780
+ it('fills missing fields with schema defaults', () => {
781
+ const result = applyDefaults({ name: 'test' }, schema);
782
+ expect(result['status']).toBe('pending');
783
+ expect(result['count']).toBe(0);
784
+ expect(result['tags']).toEqual([]);
785
+ });
786
+
787
+ it('does not overwrite existing values', () => {
788
+ const result = applyDefaults({ name: 'test', status: 'done', count: 5 }, schema);
789
+ expect(result['status']).toBe('done');
790
+ expect(result['count']).toBe(5);
791
+ });
792
+
793
+ it('handles empty frontmatter', () => {
794
+ const result = applyDefaults({}, schema);
795
+ expect(result['status']).toBe('pending');
796
+ expect(result['count']).toBe(0);
797
+ expect(result['tags']).toEqual([]);
798
+ expect(result['name']).toBeUndefined(); // no default for name
799
+ });
800
+
801
+ it('uses schema.fields fallback', () => {
802
+ const fieldsSchema: Schema = {
803
+ fields: {
804
+ level: { type: 'integer', default: 1 },
805
+ },
806
+ };
807
+ const result = applyDefaults({}, fieldsSchema);
808
+ expect(result['level']).toBe(1);
809
+ });
810
+ });
811
+
812
+ // ─────────────────────────────────────────────────────────────────────────────
813
+ // formatErrors
814
+ // ─────────────────────────────────────────────────────────────────────────────
815
+
816
+ describe('formatErrors', () => {
817
+ it('returns "Validation passed" for valid result', () => {
818
+ const result: ValidationResult = { valid: true, errors: [] };
819
+ expect(formatErrors(result)).toBe('Validation passed');
820
+ });
821
+
822
+ it('formats error with field and message', () => {
823
+ const result: ValidationResult = {
824
+ valid: false,
825
+ errors: [{ field: 'status', message: 'Required field is missing' }],
826
+ };
827
+ const output = formatErrors(result);
828
+ expect(output).toContain('status');
829
+ expect(output).toContain('Required field is missing');
830
+ });
831
+
832
+ it('includes expected and received when present', () => {
833
+ const result: ValidationResult = {
834
+ valid: false,
835
+ errors: [{
836
+ field: 'count',
837
+ message: 'Expected integer',
838
+ expected: 'integer',
839
+ received: 'string',
840
+ }],
841
+ };
842
+ const output = formatErrors(result);
843
+ expect(output).toContain('expected: integer');
844
+ expect(output).toContain('got: string');
845
+ });
846
+
847
+ it('formats multiple errors with newlines', () => {
848
+ const result: ValidationResult = {
849
+ valid: false,
850
+ errors: [
851
+ { field: 'a', message: 'Error A' },
852
+ { field: 'b', message: 'Error B' },
853
+ ],
854
+ };
855
+ const output = formatErrors(result);
856
+ const lines = output.split('\n');
857
+ expect(lines).toHaveLength(2);
858
+ expect(lines[0]).toContain('a');
859
+ expect(lines[1]).toContain('b');
860
+ });
861
+ });