@kynetic-ai/spec 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 (278) hide show
  1. package/README.md +263 -0
  2. package/dist/acp/client.d.ts +159 -0
  3. package/dist/acp/client.d.ts.map +1 -0
  4. package/dist/acp/client.js +255 -0
  5. package/dist/acp/client.js.map +1 -0
  6. package/dist/acp/framing.d.ts +119 -0
  7. package/dist/acp/framing.d.ts.map +1 -0
  8. package/dist/acp/framing.js +302 -0
  9. package/dist/acp/framing.js.map +1 -0
  10. package/dist/acp/index.d.ts +14 -0
  11. package/dist/acp/index.d.ts.map +1 -0
  12. package/dist/acp/index.js +13 -0
  13. package/dist/acp/index.js.map +1 -0
  14. package/dist/acp/types.d.ts +89 -0
  15. package/dist/acp/types.d.ts.map +1 -0
  16. package/dist/acp/types.js +99 -0
  17. package/dist/acp/types.js.map +1 -0
  18. package/dist/agents/adapters.d.ts +55 -0
  19. package/dist/agents/adapters.d.ts.map +1 -0
  20. package/dist/agents/adapters.js +84 -0
  21. package/dist/agents/adapters.js.map +1 -0
  22. package/dist/agents/index.d.ts +8 -0
  23. package/dist/agents/index.d.ts.map +1 -0
  24. package/dist/agents/index.js +10 -0
  25. package/dist/agents/index.js.map +1 -0
  26. package/dist/agents/spawner.d.ts +53 -0
  27. package/dist/agents/spawner.d.ts.map +1 -0
  28. package/dist/agents/spawner.js +83 -0
  29. package/dist/agents/spawner.js.map +1 -0
  30. package/dist/cli/batch.d.ts +82 -0
  31. package/dist/cli/batch.d.ts.map +1 -0
  32. package/dist/cli/batch.js +162 -0
  33. package/dist/cli/batch.js.map +1 -0
  34. package/dist/cli/commands/clone-for-testing.d.ts +6 -0
  35. package/dist/cli/commands/clone-for-testing.d.ts.map +1 -0
  36. package/dist/cli/commands/clone-for-testing.js +176 -0
  37. package/dist/cli/commands/clone-for-testing.js.map +1 -0
  38. package/dist/cli/commands/derive.d.ts +6 -0
  39. package/dist/cli/commands/derive.d.ts.map +1 -0
  40. package/dist/cli/commands/derive.js +450 -0
  41. package/dist/cli/commands/derive.js.map +1 -0
  42. package/dist/cli/commands/help.d.ts +6 -0
  43. package/dist/cli/commands/help.d.ts.map +1 -0
  44. package/dist/cli/commands/help.js +196 -0
  45. package/dist/cli/commands/help.js.map +1 -0
  46. package/dist/cli/commands/inbox.d.ts +6 -0
  47. package/dist/cli/commands/inbox.d.ts.map +1 -0
  48. package/dist/cli/commands/inbox.js +235 -0
  49. package/dist/cli/commands/inbox.js.map +1 -0
  50. package/dist/cli/commands/index.d.ts +20 -0
  51. package/dist/cli/commands/index.d.ts.map +1 -0
  52. package/dist/cli/commands/index.js +21 -0
  53. package/dist/cli/commands/index.js.map +1 -0
  54. package/dist/cli/commands/init.d.ts +6 -0
  55. package/dist/cli/commands/init.d.ts.map +1 -0
  56. package/dist/cli/commands/init.js +245 -0
  57. package/dist/cli/commands/init.js.map +1 -0
  58. package/dist/cli/commands/item.d.ts +6 -0
  59. package/dist/cli/commands/item.d.ts.map +1 -0
  60. package/dist/cli/commands/item.js +1311 -0
  61. package/dist/cli/commands/item.js.map +1 -0
  62. package/dist/cli/commands/link.d.ts +6 -0
  63. package/dist/cli/commands/link.d.ts.map +1 -0
  64. package/dist/cli/commands/link.js +288 -0
  65. package/dist/cli/commands/link.js.map +1 -0
  66. package/dist/cli/commands/log.d.ts +16 -0
  67. package/dist/cli/commands/log.d.ts.map +1 -0
  68. package/dist/cli/commands/log.js +291 -0
  69. package/dist/cli/commands/log.js.map +1 -0
  70. package/dist/cli/commands/meta.d.ts +15 -0
  71. package/dist/cli/commands/meta.d.ts.map +1 -0
  72. package/dist/cli/commands/meta.js +1378 -0
  73. package/dist/cli/commands/meta.js.map +1 -0
  74. package/dist/cli/commands/module.d.ts +6 -0
  75. package/dist/cli/commands/module.d.ts.map +1 -0
  76. package/dist/cli/commands/module.js +102 -0
  77. package/dist/cli/commands/module.js.map +1 -0
  78. package/dist/cli/commands/ralph.d.ts +9 -0
  79. package/dist/cli/commands/ralph.d.ts.map +1 -0
  80. package/dist/cli/commands/ralph.js +465 -0
  81. package/dist/cli/commands/ralph.js.map +1 -0
  82. package/dist/cli/commands/search.d.ts +6 -0
  83. package/dist/cli/commands/search.d.ts.map +1 -0
  84. package/dist/cli/commands/search.js +134 -0
  85. package/dist/cli/commands/search.js.map +1 -0
  86. package/dist/cli/commands/session.d.ts +164 -0
  87. package/dist/cli/commands/session.d.ts.map +1 -0
  88. package/dist/cli/commands/session.js +745 -0
  89. package/dist/cli/commands/session.js.map +1 -0
  90. package/dist/cli/commands/setup.d.ts +26 -0
  91. package/dist/cli/commands/setup.d.ts.map +1 -0
  92. package/dist/cli/commands/setup.js +586 -0
  93. package/dist/cli/commands/setup.js.map +1 -0
  94. package/dist/cli/commands/shadow.d.ts +6 -0
  95. package/dist/cli/commands/shadow.d.ts.map +1 -0
  96. package/dist/cli/commands/shadow.js +299 -0
  97. package/dist/cli/commands/shadow.js.map +1 -0
  98. package/dist/cli/commands/task.d.ts +6 -0
  99. package/dist/cli/commands/task.d.ts.map +1 -0
  100. package/dist/cli/commands/task.js +1514 -0
  101. package/dist/cli/commands/task.js.map +1 -0
  102. package/dist/cli/commands/tasks.d.ts +6 -0
  103. package/dist/cli/commands/tasks.d.ts.map +1 -0
  104. package/dist/cli/commands/tasks.js +347 -0
  105. package/dist/cli/commands/tasks.js.map +1 -0
  106. package/dist/cli/commands/trait.d.ts +10 -0
  107. package/dist/cli/commands/trait.d.ts.map +1 -0
  108. package/dist/cli/commands/trait.js +295 -0
  109. package/dist/cli/commands/trait.js.map +1 -0
  110. package/dist/cli/commands/validate.d.ts +6 -0
  111. package/dist/cli/commands/validate.d.ts.map +1 -0
  112. package/dist/cli/commands/validate.js +626 -0
  113. package/dist/cli/commands/validate.js.map +1 -0
  114. package/dist/cli/exit-codes.d.ts +62 -0
  115. package/dist/cli/exit-codes.d.ts.map +1 -0
  116. package/dist/cli/exit-codes.js +65 -0
  117. package/dist/cli/exit-codes.js.map +1 -0
  118. package/dist/cli/help/content.d.ts +35 -0
  119. package/dist/cli/help/content.d.ts.map +1 -0
  120. package/dist/cli/help/content.js +312 -0
  121. package/dist/cli/help/content.js.map +1 -0
  122. package/dist/cli/index.d.ts +5 -0
  123. package/dist/cli/index.d.ts.map +1 -0
  124. package/dist/cli/index.js +85 -0
  125. package/dist/cli/index.js.map +1 -0
  126. package/dist/cli/introspection.d.ts +87 -0
  127. package/dist/cli/introspection.d.ts.map +1 -0
  128. package/dist/cli/introspection.js +127 -0
  129. package/dist/cli/introspection.js.map +1 -0
  130. package/dist/cli/output.d.ts +56 -0
  131. package/dist/cli/output.d.ts.map +1 -0
  132. package/dist/cli/output.js +467 -0
  133. package/dist/cli/output.js.map +1 -0
  134. package/dist/cli/suggest.d.ts +16 -0
  135. package/dist/cli/suggest.d.ts.map +1 -0
  136. package/dist/cli/suggest.js +72 -0
  137. package/dist/cli/suggest.js.map +1 -0
  138. package/dist/index.d.ts +3 -0
  139. package/dist/index.d.ts.map +1 -0
  140. package/dist/index.js +5 -0
  141. package/dist/index.js.map +1 -0
  142. package/dist/parser/alignment.d.ts +113 -0
  143. package/dist/parser/alignment.d.ts.map +1 -0
  144. package/dist/parser/alignment.js +261 -0
  145. package/dist/parser/alignment.js.map +1 -0
  146. package/dist/parser/assess.d.ts +81 -0
  147. package/dist/parser/assess.d.ts.map +1 -0
  148. package/dist/parser/assess.js +197 -0
  149. package/dist/parser/assess.js.map +1 -0
  150. package/dist/parser/convention-validation.d.ts +48 -0
  151. package/dist/parser/convention-validation.d.ts.map +1 -0
  152. package/dist/parser/convention-validation.js +167 -0
  153. package/dist/parser/convention-validation.js.map +1 -0
  154. package/dist/parser/fix.d.ts +38 -0
  155. package/dist/parser/fix.d.ts.map +1 -0
  156. package/dist/parser/fix.js +185 -0
  157. package/dist/parser/fix.js.map +1 -0
  158. package/dist/parser/index.d.ts +12 -0
  159. package/dist/parser/index.d.ts.map +1 -0
  160. package/dist/parser/index.js +13 -0
  161. package/dist/parser/index.js.map +1 -0
  162. package/dist/parser/items.d.ts +138 -0
  163. package/dist/parser/items.d.ts.map +1 -0
  164. package/dist/parser/items.js +321 -0
  165. package/dist/parser/items.js.map +1 -0
  166. package/dist/parser/meta.d.ts +120 -0
  167. package/dist/parser/meta.d.ts.map +1 -0
  168. package/dist/parser/meta.js +441 -0
  169. package/dist/parser/meta.js.map +1 -0
  170. package/dist/parser/refs.d.ts +185 -0
  171. package/dist/parser/refs.d.ts.map +1 -0
  172. package/dist/parser/refs.js +404 -0
  173. package/dist/parser/refs.js.map +1 -0
  174. package/dist/parser/shadow.d.ts +253 -0
  175. package/dist/parser/shadow.d.ts.map +1 -0
  176. package/dist/parser/shadow.js +1053 -0
  177. package/dist/parser/shadow.js.map +1 -0
  178. package/dist/parser/traits.d.ts +72 -0
  179. package/dist/parser/traits.d.ts.map +1 -0
  180. package/dist/parser/traits.js +120 -0
  181. package/dist/parser/traits.js.map +1 -0
  182. package/dist/parser/validate.d.ts +89 -0
  183. package/dist/parser/validate.d.ts.map +1 -0
  184. package/dist/parser/validate.js +817 -0
  185. package/dist/parser/validate.js.map +1 -0
  186. package/dist/parser/yaml.d.ts +326 -0
  187. package/dist/parser/yaml.d.ts.map +1 -0
  188. package/dist/parser/yaml.js +1383 -0
  189. package/dist/parser/yaml.js.map +1 -0
  190. package/dist/ralph/cli-renderer.d.ts +20 -0
  191. package/dist/ralph/cli-renderer.d.ts.map +1 -0
  192. package/dist/ralph/cli-renderer.js +179 -0
  193. package/dist/ralph/cli-renderer.js.map +1 -0
  194. package/dist/ralph/events.d.ts +65 -0
  195. package/dist/ralph/events.d.ts.map +1 -0
  196. package/dist/ralph/events.js +397 -0
  197. package/dist/ralph/events.js.map +1 -0
  198. package/dist/ralph/index.d.ts +8 -0
  199. package/dist/ralph/index.d.ts.map +1 -0
  200. package/dist/ralph/index.js +10 -0
  201. package/dist/ralph/index.js.map +1 -0
  202. package/dist/schema/common.d.ts +46 -0
  203. package/dist/schema/common.d.ts.map +1 -0
  204. package/dist/schema/common.js +71 -0
  205. package/dist/schema/common.js.map +1 -0
  206. package/dist/schema/inbox.d.ts +90 -0
  207. package/dist/schema/inbox.d.ts.map +1 -0
  208. package/dist/schema/inbox.js +30 -0
  209. package/dist/schema/inbox.js.map +1 -0
  210. package/dist/schema/index.d.ts +6 -0
  211. package/dist/schema/index.d.ts.map +1 -0
  212. package/dist/schema/index.js +7 -0
  213. package/dist/schema/index.js.map +1 -0
  214. package/dist/schema/meta.d.ts +762 -0
  215. package/dist/schema/meta.d.ts.map +1 -0
  216. package/dist/schema/meta.js +144 -0
  217. package/dist/schema/meta.js.map +1 -0
  218. package/dist/schema/spec.d.ts +912 -0
  219. package/dist/schema/spec.d.ts.map +1 -0
  220. package/dist/schema/spec.js +104 -0
  221. package/dist/schema/spec.js.map +1 -0
  222. package/dist/schema/task.d.ts +664 -0
  223. package/dist/schema/task.d.ts.map +1 -0
  224. package/dist/schema/task.js +130 -0
  225. package/dist/schema/task.js.map +1 -0
  226. package/dist/sessions/index.d.ts +11 -0
  227. package/dist/sessions/index.d.ts.map +1 -0
  228. package/dist/sessions/index.js +13 -0
  229. package/dist/sessions/index.js.map +1 -0
  230. package/dist/sessions/store.d.ts +144 -0
  231. package/dist/sessions/store.d.ts.map +1 -0
  232. package/dist/sessions/store.js +325 -0
  233. package/dist/sessions/store.js.map +1 -0
  234. package/dist/sessions/types.d.ts +157 -0
  235. package/dist/sessions/types.d.ts.map +1 -0
  236. package/dist/sessions/types.js +90 -0
  237. package/dist/sessions/types.js.map +1 -0
  238. package/dist/strings/errors.d.ts +420 -0
  239. package/dist/strings/errors.d.ts.map +1 -0
  240. package/dist/strings/errors.js +282 -0
  241. package/dist/strings/errors.js.map +1 -0
  242. package/dist/strings/guidance.d.ts +65 -0
  243. package/dist/strings/guidance.d.ts.map +1 -0
  244. package/dist/strings/guidance.js +66 -0
  245. package/dist/strings/guidance.js.map +1 -0
  246. package/dist/strings/index.d.ts +12 -0
  247. package/dist/strings/index.d.ts.map +1 -0
  248. package/dist/strings/index.js +12 -0
  249. package/dist/strings/index.js.map +1 -0
  250. package/dist/strings/labels.d.ts +74 -0
  251. package/dist/strings/labels.d.ts.map +1 -0
  252. package/dist/strings/labels.js +75 -0
  253. package/dist/strings/labels.js.map +1 -0
  254. package/dist/strings/validation.d.ts +126 -0
  255. package/dist/strings/validation.d.ts.map +1 -0
  256. package/dist/strings/validation.js +135 -0
  257. package/dist/strings/validation.js.map +1 -0
  258. package/dist/utils/commit.d.ts +23 -0
  259. package/dist/utils/commit.d.ts.map +1 -0
  260. package/dist/utils/commit.js +67 -0
  261. package/dist/utils/commit.js.map +1 -0
  262. package/dist/utils/git.d.ts +57 -0
  263. package/dist/utils/git.d.ts.map +1 -0
  264. package/dist/utils/git.js +192 -0
  265. package/dist/utils/git.js.map +1 -0
  266. package/dist/utils/grep.d.ts +28 -0
  267. package/dist/utils/grep.d.ts.map +1 -0
  268. package/dist/utils/grep.js +86 -0
  269. package/dist/utils/grep.js.map +1 -0
  270. package/dist/utils/index.d.ts +8 -0
  271. package/dist/utils/index.d.ts.map +1 -0
  272. package/dist/utils/index.js +6 -0
  273. package/dist/utils/index.js.map +1 -0
  274. package/dist/utils/time.d.ts +18 -0
  275. package/dist/utils/time.d.ts.map +1 -0
  276. package/dist/utils/time.js +61 -0
  277. package/dist/utils/time.js.map +1 -0
  278. package/package.json +62 -0
@@ -0,0 +1,817 @@
1
+ /**
2
+ * Validation module for kspec files.
3
+ *
4
+ * Provides schema validation, reference validation, and orphan detection.
5
+ */
6
+ import * as fs from 'node:fs/promises';
7
+ import * as path from 'node:path';
8
+ import { TaskSchema, TasksFileSchema, ManifestSchema, SpecItemSchema, MetaManifestSchema, AgentSchema, WorkflowSchema, ConventionSchema, ObservationSchema, UlidSchema, } from '../schema/index.js';
9
+ import { readYamlFile, findTaskFiles, loadSpecFile, expandIncludePattern, extractItemsFromRaw, } from './yaml.js';
10
+ import { ReferenceIndex, validateRefs } from './refs.js';
11
+ import { findMetaManifest, loadMetaContext } from './meta.js';
12
+ import { TraitIndex } from './traits.js';
13
+ // ============================================================
14
+ // SCHEMA VALIDATION
15
+ // ============================================================
16
+ /**
17
+ * Validate a manifest file against schema
18
+ */
19
+ async function validateManifestFile(filePath) {
20
+ const errors = [];
21
+ try {
22
+ const raw = await readYamlFile(filePath);
23
+ const result = ManifestSchema.safeParse(raw);
24
+ if (!result.success) {
25
+ for (const issue of result.error.issues) {
26
+ errors.push({
27
+ file: filePath,
28
+ path: issue.path.join('.'),
29
+ message: issue.message,
30
+ details: issue,
31
+ });
32
+ }
33
+ }
34
+ }
35
+ catch (err) {
36
+ errors.push({
37
+ file: filePath,
38
+ message: `Failed to parse YAML: ${err instanceof Error ? err.message : String(err)}`,
39
+ });
40
+ }
41
+ return errors;
42
+ }
43
+ /**
44
+ * Validate a tasks file against schema
45
+ */
46
+ async function validateTasksFile(filePath) {
47
+ const errors = [];
48
+ try {
49
+ const raw = await readYamlFile(filePath);
50
+ // Handle both formats: { tasks: [...] } and plain array
51
+ let taskList;
52
+ if (Array.isArray(raw)) {
53
+ taskList = raw;
54
+ }
55
+ else if (raw && typeof raw === 'object' && 'tasks' in raw) {
56
+ // Try full TasksFile schema first
57
+ const fileResult = TasksFileSchema.safeParse(raw);
58
+ if (!fileResult.success) {
59
+ // If TasksFile fails, just validate individual tasks
60
+ taskList = raw.tasks || [];
61
+ }
62
+ else {
63
+ // File schema passed, validate individual tasks for detailed errors
64
+ taskList = fileResult.data.tasks;
65
+ }
66
+ }
67
+ else {
68
+ errors.push({
69
+ file: filePath,
70
+ message: 'Invalid tasks file format: expected array or { tasks: [...] }',
71
+ });
72
+ return errors;
73
+ }
74
+ // Validate each task
75
+ for (let i = 0; i < taskList.length; i++) {
76
+ const task = taskList[i];
77
+ const result = TaskSchema.safeParse(task);
78
+ if (!result.success) {
79
+ for (const issue of result.error.issues) {
80
+ errors.push({
81
+ file: filePath,
82
+ path: `tasks[${i}].${issue.path.join('.')}`,
83
+ message: issue.message,
84
+ details: issue,
85
+ });
86
+ }
87
+ }
88
+ }
89
+ }
90
+ catch (err) {
91
+ errors.push({
92
+ file: filePath,
93
+ message: `Failed to parse YAML: ${err instanceof Error ? err.message : String(err)}`,
94
+ });
95
+ }
96
+ return errors;
97
+ }
98
+ /**
99
+ * Validate a spec module file against schema
100
+ */
101
+ async function validateSpecFile(filePath) {
102
+ const errors = [];
103
+ try {
104
+ const raw = await readYamlFile(filePath);
105
+ // Recursively validate spec items
106
+ validateSpecItemRecursive(raw, filePath, '', errors);
107
+ }
108
+ catch (err) {
109
+ errors.push({
110
+ file: filePath,
111
+ message: `Failed to parse YAML: ${err instanceof Error ? err.message : String(err)}`,
112
+ });
113
+ }
114
+ return errors;
115
+ }
116
+ /**
117
+ * Validate meta manifest file with strict ULID validation
118
+ * AC-meta-manifest-3: Invalid schema exits with code 1 and shows field path + expected type
119
+ */
120
+ async function validateMetaManifestFile(filePath) {
121
+ const errors = [];
122
+ try {
123
+ const raw = await readYamlFile(filePath);
124
+ // Validate overall manifest structure
125
+ const manifestResult = MetaManifestSchema.safeParse(raw);
126
+ if (!manifestResult.success) {
127
+ for (const issue of manifestResult.error.issues) {
128
+ errors.push({
129
+ file: filePath,
130
+ path: issue.path.join('.'),
131
+ message: issue.message,
132
+ details: issue,
133
+ });
134
+ }
135
+ return errors;
136
+ }
137
+ // Validate each agent with strict ULID validation
138
+ if (raw && typeof raw === 'object' && 'agents' in raw && Array.isArray(raw.agents)) {
139
+ const agents = raw.agents;
140
+ for (let i = 0; i < agents.length; i++) {
141
+ const agent = agents[i];
142
+ const agentResult = AgentSchema.safeParse(agent);
143
+ if (!agentResult.success) {
144
+ for (const issue of agentResult.error.issues) {
145
+ errors.push({
146
+ file: filePath,
147
+ path: `agents[${i}].${issue.path.join('.')}`,
148
+ message: issue.message,
149
+ details: issue,
150
+ });
151
+ }
152
+ }
153
+ // Strict ULID validation
154
+ if (agent && typeof agent === 'object' && '_ulid' in agent) {
155
+ const ulidResult = UlidSchema.safeParse(agent._ulid);
156
+ if (!ulidResult.success) {
157
+ errors.push({
158
+ file: filePath,
159
+ path: `agents[${i}]._ulid`,
160
+ message: 'Invalid ULID format (expected 26 characters)',
161
+ });
162
+ }
163
+ }
164
+ }
165
+ }
166
+ // Validate each workflow with strict ULID validation
167
+ if (raw && typeof raw === 'object' && 'workflows' in raw && Array.isArray(raw.workflows)) {
168
+ const workflows = raw.workflows;
169
+ for (let i = 0; i < workflows.length; i++) {
170
+ const workflow = workflows[i];
171
+ const workflowResult = WorkflowSchema.safeParse(workflow);
172
+ if (!workflowResult.success) {
173
+ for (const issue of workflowResult.error.issues) {
174
+ errors.push({
175
+ file: filePath,
176
+ path: `workflows[${i}].${issue.path.join('.')}`,
177
+ message: issue.message,
178
+ details: issue,
179
+ });
180
+ }
181
+ }
182
+ // Strict ULID validation
183
+ if (workflow && typeof workflow === 'object' && '_ulid' in workflow) {
184
+ const ulidResult = UlidSchema.safeParse(workflow._ulid);
185
+ if (!ulidResult.success) {
186
+ errors.push({
187
+ file: filePath,
188
+ path: `workflows[${i}]._ulid`,
189
+ message: 'Invalid ULID format (expected 26 characters)',
190
+ });
191
+ }
192
+ }
193
+ }
194
+ }
195
+ // Validate each convention with strict ULID validation
196
+ if (raw && typeof raw === 'object' && 'conventions' in raw && Array.isArray(raw.conventions)) {
197
+ const conventions = raw.conventions;
198
+ for (let i = 0; i < conventions.length; i++) {
199
+ const convention = conventions[i];
200
+ const conventionResult = ConventionSchema.safeParse(convention);
201
+ if (!conventionResult.success) {
202
+ for (const issue of conventionResult.error.issues) {
203
+ errors.push({
204
+ file: filePath,
205
+ path: `conventions[${i}].${issue.path.join('.')}`,
206
+ message: issue.message,
207
+ details: issue,
208
+ });
209
+ }
210
+ }
211
+ // Strict ULID validation
212
+ if (convention && typeof convention === 'object' && '_ulid' in convention) {
213
+ const ulidResult = UlidSchema.safeParse(convention._ulid);
214
+ if (!ulidResult.success) {
215
+ errors.push({
216
+ file: filePath,
217
+ path: `conventions[${i}]._ulid`,
218
+ message: 'Invalid ULID format (expected 26 characters)',
219
+ });
220
+ }
221
+ }
222
+ }
223
+ }
224
+ // Validate each observation with strict ULID validation
225
+ if (raw && typeof raw === 'object' && 'observations' in raw && Array.isArray(raw.observations)) {
226
+ const observations = raw.observations;
227
+ for (let i = 0; i < observations.length; i++) {
228
+ const observation = observations[i];
229
+ const observationResult = ObservationSchema.safeParse(observation);
230
+ if (!observationResult.success) {
231
+ for (const issue of observationResult.error.issues) {
232
+ errors.push({
233
+ file: filePath,
234
+ path: `observations[${i}].${issue.path.join('.')}`,
235
+ message: issue.message,
236
+ details: issue,
237
+ });
238
+ }
239
+ }
240
+ // Strict ULID validation
241
+ if (observation && typeof observation === 'object' && '_ulid' in observation) {
242
+ const ulidResult = UlidSchema.safeParse(observation._ulid);
243
+ if (!ulidResult.success) {
244
+ errors.push({
245
+ file: filePath,
246
+ path: `observations[${i}]._ulid`,
247
+ message: 'Invalid ULID format (expected 26 characters)',
248
+ });
249
+ }
250
+ }
251
+ }
252
+ }
253
+ }
254
+ catch (err) {
255
+ errors.push({
256
+ file: filePath,
257
+ message: `Failed to parse YAML: ${err instanceof Error ? err.message : String(err)}`,
258
+ });
259
+ }
260
+ return errors;
261
+ }
262
+ /**
263
+ * Recursively validate spec items in a structure
264
+ */
265
+ function validateSpecItemRecursive(raw, file, pathPrefix, errors) {
266
+ if (!raw || typeof raw !== 'object')
267
+ return;
268
+ // Check if this is a spec item (has _ulid)
269
+ if ('_ulid' in raw) {
270
+ const result = SpecItemSchema.safeParse(raw);
271
+ if (!result.success) {
272
+ for (const issue of result.error.issues) {
273
+ errors.push({
274
+ file,
275
+ path: pathPrefix ? `${pathPrefix}.${issue.path.join('.')}` : issue.path.join('.'),
276
+ message: issue.message,
277
+ details: issue,
278
+ });
279
+ }
280
+ }
281
+ }
282
+ // Recurse into nested structures
283
+ const nestedFields = ['modules', 'features', 'requirements', 'constraints', 'decisions', 'items'];
284
+ const obj = raw;
285
+ for (const field of nestedFields) {
286
+ if (field in obj && Array.isArray(obj[field])) {
287
+ const arr = obj[field];
288
+ for (let i = 0; i < arr.length; i++) {
289
+ const newPath = pathPrefix ? `${pathPrefix}.${field}[${i}]` : `${field}[${i}]`;
290
+ validateSpecItemRecursive(arr[i], file, newPath, errors);
291
+ }
292
+ }
293
+ }
294
+ }
295
+ // ============================================================
296
+ // ORPHAN DETECTION
297
+ // ============================================================
298
+ /**
299
+ * Find items that are not referenced by any other item
300
+ */
301
+ function findOrphans(tasks, items, index) {
302
+ const orphans = [];
303
+ // Build set of all referenced ULIDs
304
+ const referenced = new Set();
305
+ const allItems = [...tasks, ...items];
306
+ // Fields that contain references
307
+ const refFields = [
308
+ 'depends_on',
309
+ 'blocked_by',
310
+ 'implements',
311
+ 'relates_to',
312
+ 'tests',
313
+ 'supersedes',
314
+ 'spec_ref',
315
+ 'context',
316
+ ];
317
+ for (const item of allItems) {
318
+ const obj = item;
319
+ for (const field of refFields) {
320
+ const value = obj[field];
321
+ if (typeof value === 'string' && value.startsWith('@')) {
322
+ const resolved = index.resolve(value);
323
+ if (resolved.ok) {
324
+ referenced.add(resolved.ulid);
325
+ }
326
+ }
327
+ else if (Array.isArray(value)) {
328
+ for (const v of value) {
329
+ if (typeof v === 'string' && v.startsWith('@')) {
330
+ const resolved = index.resolve(v);
331
+ if (resolved.ok) {
332
+ referenced.add(resolved.ulid);
333
+ }
334
+ }
335
+ }
336
+ }
337
+ }
338
+ }
339
+ // Find items not in the referenced set
340
+ // Skip entry point types: modules are spec entry points, tasks are work items
341
+ const entryPointTypes = ['module', 'task', 'epic', 'bug', 'spike', 'infra'];
342
+ for (const item of items) {
343
+ // Only check spec items, not tasks
344
+ if (!referenced.has(item._ulid)) {
345
+ // Skip entry point types
346
+ if (entryPointTypes.includes(item.type || ''))
347
+ continue;
348
+ // Skip nested items - they're implicitly referenced by their parent
349
+ // _path indicates nesting (e.g., "features[0].requirements[2]")
350
+ if (item._path)
351
+ continue;
352
+ orphans.push({
353
+ ulid: item._ulid,
354
+ title: item.title,
355
+ type: item.type || 'unknown',
356
+ file: item._sourceFile,
357
+ });
358
+ }
359
+ }
360
+ return orphans;
361
+ }
362
+ // ============================================================
363
+ // TRAIT CYCLE DETECTION
364
+ // ============================================================
365
+ /**
366
+ * Detect circular trait references
367
+ * AC: @trait-edge-cases ac-2
368
+ */
369
+ function detectTraitCycles(items, index) {
370
+ const errors = [];
371
+ const traits = items.filter(item => item.type === 'trait');
372
+ // Build adjacency list: trait ULID → trait ULIDs it references
373
+ const graph = new Map();
374
+ const traitInfo = new Map();
375
+ for (const trait of traits) {
376
+ const ref = trait.slugs?.[0] ? `@${trait.slugs[0]}` : `@${trait._ulid.slice(0, 8)}`;
377
+ traitInfo.set(trait._ulid, { ref, title: trait.title });
378
+ const dependencies = [];
379
+ if (trait.traits && trait.traits.length > 0) {
380
+ for (const traitRef of trait.traits) {
381
+ const result = index.resolve(traitRef);
382
+ if (result.ok) {
383
+ dependencies.push(result.ulid);
384
+ }
385
+ }
386
+ }
387
+ graph.set(trait._ulid, dependencies);
388
+ }
389
+ // DFS-based cycle detection
390
+ const visiting = new Set();
391
+ const visited = new Set();
392
+ function dfs(ulid, path) {
393
+ if (visiting.has(ulid)) {
394
+ // Found a cycle - return the cycle path
395
+ const cycleStart = path.indexOf(ulid);
396
+ return path.slice(cycleStart);
397
+ }
398
+ if (visited.has(ulid)) {
399
+ return null; // Already checked this path
400
+ }
401
+ visiting.add(ulid);
402
+ path.push(ulid);
403
+ const dependencies = graph.get(ulid) || [];
404
+ for (const depUlid of dependencies) {
405
+ const cycle = dfs(depUlid, path);
406
+ if (cycle) {
407
+ return cycle;
408
+ }
409
+ }
410
+ visiting.delete(ulid);
411
+ visited.add(ulid);
412
+ path.pop();
413
+ return null;
414
+ }
415
+ // Check each trait for cycles
416
+ for (const trait of traits) {
417
+ if (!visited.has(trait._ulid)) {
418
+ const cycle = dfs(trait._ulid, []);
419
+ if (cycle) {
420
+ const info = traitInfo.get(cycle[0]);
421
+ if (info) {
422
+ const cycleRefs = cycle.map(ulid => {
423
+ const cycleInfo = traitInfo.get(ulid);
424
+ return cycleInfo ? cycleInfo.ref : `@${ulid.slice(0, 8)}`;
425
+ });
426
+ errors.push({
427
+ traitRef: info.ref,
428
+ traitTitle: info.title,
429
+ cycle: cycleRefs,
430
+ message: `Circular trait reference: ${cycleRefs.join(' → ')} → ${cycleRefs[0]}`,
431
+ });
432
+ }
433
+ // Mark all traits in cycle as visited to avoid duplicate errors
434
+ for (const ulid of cycle) {
435
+ visited.add(ulid);
436
+ }
437
+ }
438
+ }
439
+ }
440
+ return errors;
441
+ }
442
+ // ============================================================
443
+ // COMPLETENESS VALIDATION
444
+ // ============================================================
445
+ /**
446
+ * Scan test files for AC annotations to build coverage index
447
+ * Returns a Set of covered ACs in format "@spec-ref ac-N"
448
+ */
449
+ async function scanTestCoverage(rootDir) {
450
+ const coveredACs = new Set();
451
+ const testsDir = path.join(rootDir, 'tests');
452
+ try {
453
+ // Check if tests directory exists
454
+ await fs.access(testsDir);
455
+ // Read all test files
456
+ const files = await fs.readdir(testsDir);
457
+ const testFiles = files.filter(f => f.endsWith('.test.ts') || f.endsWith('.test.js'));
458
+ for (const file of testFiles) {
459
+ const filePath = path.join(testsDir, file);
460
+ const content = await fs.readFile(filePath, 'utf-8');
461
+ // Match AC annotations: // AC: @spec-ref ac-N
462
+ // Also handle multiple ACs on one line: // AC: @spec-ref ac-1, ac-2
463
+ const acPattern = /\/\/\s*AC:\s*(@[\w-]+)(?:\s+(ac-\d+(?:\s*,\s*ac-\d+)*))?/g;
464
+ let match;
465
+ while ((match = acPattern.exec(content)) !== null) {
466
+ const specRef = match[1]; // @spec-ref
467
+ const acList = match[2]; // "ac-1, ac-2" or just "ac-1" or undefined
468
+ if (acList) {
469
+ // Split by comma and trim
470
+ const acs = acList.split(',').map(ac => ac.trim());
471
+ for (const ac of acs) {
472
+ coveredACs.add(`${specRef} ${ac}`);
473
+ }
474
+ }
475
+ else {
476
+ // No specific AC mentioned, just the spec ref
477
+ // We'll consider this as generic coverage
478
+ coveredACs.add(specRef);
479
+ }
480
+ }
481
+ }
482
+ }
483
+ catch (err) {
484
+ // Tests directory doesn't exist or can't be read - that's ok
485
+ }
486
+ return coveredACs;
487
+ }
488
+ /**
489
+ * Check spec items for completeness
490
+ * AC: @spec-completeness ac-1, ac-2, ac-3
491
+ * AC: @trait-validation ac-1, ac-2, ac-3
492
+ */
493
+ async function checkCompleteness(items, index, rootDir, traitIndex) {
494
+ const warnings = [];
495
+ // Scan test files for AC coverage
496
+ const coveredACs = await scanTestCoverage(rootDir);
497
+ for (const item of items) {
498
+ const itemRef = item.slugs?.[0] ? `@${item.slugs[0]}` : `@${item._ulid.slice(0, 8)}`;
499
+ const isTrait = item.type === 'trait';
500
+ // AC: @spec-completeness ac-1
501
+ // AC: @trait-type ac-2 - Traits should have acceptance criteria for completeness
502
+ // Check for missing acceptance criteria
503
+ if (!item.acceptance_criteria || item.acceptance_criteria.length === 0) {
504
+ warnings.push({
505
+ type: 'missing_acceptance_criteria',
506
+ itemRef,
507
+ itemTitle: item.title,
508
+ message: `${isTrait ? 'Trait' : 'Item'} ${itemRef} has no acceptance criteria`,
509
+ });
510
+ }
511
+ // AC: @spec-completeness ac-2
512
+ // AC: @trait-type ac-3 - Traits should have description for completeness
513
+ // Check for missing description
514
+ if (!item.description || item.description.trim() === '') {
515
+ warnings.push({
516
+ type: 'missing_description',
517
+ itemRef,
518
+ itemTitle: item.title,
519
+ message: `${isTrait ? 'Trait' : 'Item'} ${itemRef} has no description`,
520
+ });
521
+ }
522
+ // AC: @spec-completeness ac-3
523
+ // Check for status inconsistency between parent and children
524
+ if (item.status?.implementation === 'implemented') {
525
+ // Check if this item has children with not_started status
526
+ const childFields = [
527
+ 'modules',
528
+ 'features',
529
+ 'requirements',
530
+ 'constraints',
531
+ 'epics',
532
+ 'themes',
533
+ 'capabilities',
534
+ ];
535
+ for (const field of childFields) {
536
+ const children = item[field];
537
+ if (Array.isArray(children)) {
538
+ for (const child of children) {
539
+ if (child.status?.implementation === 'not_started') {
540
+ const childRef = child.slugs?.[0]
541
+ ? `@${child.slugs[0]}`
542
+ : `@${child._ulid?.slice(0, 8) || 'unknown'}`;
543
+ warnings.push({
544
+ type: 'status_inconsistency',
545
+ itemRef,
546
+ itemTitle: item.title,
547
+ message: `Parent ${itemRef} is implemented but child ${childRef} is not_started`,
548
+ details: `Child: ${child.title}`,
549
+ });
550
+ }
551
+ }
552
+ }
553
+ }
554
+ }
555
+ // Check for test coverage of acceptance criteria
556
+ if (item.acceptance_criteria && item.acceptance_criteria.length > 0) {
557
+ const uncoveredACs = [];
558
+ for (const ac of item.acceptance_criteria) {
559
+ // Build all possible references for this AC
560
+ const possibleRefs = [];
561
+ // Try with primary slug
562
+ if (item.slugs && item.slugs.length > 0) {
563
+ possibleRefs.push(`@${item.slugs[0]} ${ac.id}`);
564
+ // Also check for just the slug without specific AC
565
+ possibleRefs.push(`@${item.slugs[0]}`);
566
+ }
567
+ // Try with ULID (short form)
568
+ possibleRefs.push(`@${item._ulid.slice(0, 8)} ${ac.id}`);
569
+ possibleRefs.push(`@${item._ulid.slice(0, 8)}`);
570
+ // Check if any of these references are covered
571
+ const isCovered = possibleRefs.some(ref => coveredACs.has(ref));
572
+ if (!isCovered) {
573
+ uncoveredACs.push(ac.id);
574
+ }
575
+ }
576
+ // Only warn if there are uncovered ACs
577
+ if (uncoveredACs.length > 0) {
578
+ warnings.push({
579
+ type: 'missing_test_coverage',
580
+ itemRef,
581
+ itemTitle: item.title,
582
+ message: `Item ${itemRef} has ${uncoveredACs.length} AC(s) without test coverage`,
583
+ details: `Uncovered: ${uncoveredACs.join(', ')}`,
584
+ });
585
+ }
586
+ }
587
+ // AC: @trait-validation ac-1, ac-2
588
+ // Check for test coverage of trait acceptance criteria
589
+ if (traitIndex && item.traits && item.traits.length > 0) {
590
+ const inheritedACs = traitIndex.getInheritedAC(item._ulid);
591
+ const uncoveredTraitACs = [];
592
+ for (const { trait, ac } of inheritedACs) {
593
+ // Build all possible references for this trait AC
594
+ const possibleRefs = [];
595
+ // Try with trait slug
596
+ possibleRefs.push(`@${trait.slug} ${ac.id}`);
597
+ possibleRefs.push(`@${trait.slug}`);
598
+ // Try with trait ULID (short form)
599
+ possibleRefs.push(`@${trait.ulid.slice(0, 8)} ${ac.id}`);
600
+ possibleRefs.push(`@${trait.ulid.slice(0, 8)}`);
601
+ // Check if any of these references are covered
602
+ const isCovered = possibleRefs.some(ref => coveredACs.has(ref));
603
+ if (!isCovered) {
604
+ uncoveredTraitACs.push({ traitSlug: trait.slug, acId: ac.id });
605
+ }
606
+ }
607
+ // Only warn if there are uncovered trait ACs
608
+ if (uncoveredTraitACs.length > 0) {
609
+ const details = uncoveredTraitACs
610
+ .map(({ traitSlug, acId }) => `@${traitSlug} ${acId}`)
611
+ .join(', ');
612
+ warnings.push({
613
+ type: 'missing_test_coverage',
614
+ itemRef,
615
+ itemTitle: item.title,
616
+ message: `Item ${itemRef} has ${uncoveredTraitACs.length} inherited trait AC(s) without test coverage`,
617
+ details: `Uncovered trait ACs: ${details}`,
618
+ });
619
+ }
620
+ }
621
+ }
622
+ return warnings;
623
+ }
624
+ // ============================================================
625
+ // AUTOMATION VALIDATION
626
+ // ============================================================
627
+ /**
628
+ * Check task automation status for warnings
629
+ * AC: @task-automation-eligibility ac-21, ac-23
630
+ */
631
+ function checkAutomationEligibility(tasks, index) {
632
+ const warnings = [];
633
+ for (const task of tasks) {
634
+ const taskRef = task.slugs?.[0] ? `@${task.slugs[0]}` : `@${task._ulid.slice(0, 8)}`;
635
+ // AC: @task-automation-eligibility ac-21
636
+ // Warn if eligible but no spec_ref
637
+ if (task.automation === 'eligible' && !task.spec_ref) {
638
+ warnings.push({
639
+ type: 'automation_eligible_no_spec',
640
+ itemRef: taskRef,
641
+ itemTitle: task.title,
642
+ message: `Task ${taskRef} is automation: eligible but has no spec_ref - eligible tasks should have linked specs`,
643
+ });
644
+ }
645
+ // AC: @task-automation-eligibility ac-23
646
+ // Warn if eligible but spec_ref doesn't resolve
647
+ if (task.automation === 'eligible' && task.spec_ref) {
648
+ const specResult = index.resolve(task.spec_ref);
649
+ if (!specResult.ok) {
650
+ warnings.push({
651
+ type: 'automation_eligible_no_spec',
652
+ itemRef: taskRef,
653
+ itemTitle: task.title,
654
+ message: `Task ${taskRef} is automation: eligible but spec_ref ${task.spec_ref} cannot be resolved`,
655
+ });
656
+ }
657
+ }
658
+ }
659
+ return warnings;
660
+ }
661
+ // ============================================================
662
+ // MAIN VALIDATION
663
+ // ============================================================
664
+ /**
665
+ * Run full validation on a kspec project
666
+ */
667
+ export async function validate(ctx, options = {}) {
668
+ // Default: run all checks
669
+ const runSchema = options.schema !== false;
670
+ const runRefs = options.refs !== false;
671
+ const runOrphans = options.orphans !== false;
672
+ const runCompleteness = options.completeness !== false;
673
+ const result = {
674
+ valid: true,
675
+ schemaErrors: [],
676
+ refErrors: [],
677
+ refWarnings: [],
678
+ orphans: [],
679
+ completenessWarnings: [],
680
+ traitCycleErrors: [],
681
+ stats: {
682
+ filesChecked: 0,
683
+ itemsChecked: 0,
684
+ tasksChecked: 0,
685
+ },
686
+ };
687
+ const allTasks = [];
688
+ const allItems = [];
689
+ // Validate manifest
690
+ if (ctx.manifestPath && runSchema) {
691
+ const manifestErrors = await validateManifestFile(ctx.manifestPath);
692
+ result.schemaErrors.push(...manifestErrors);
693
+ result.stats.filesChecked++;
694
+ }
695
+ // Load items from manifest (traits, inline modules, etc.)
696
+ if (ctx.manifest && ctx.manifestPath) {
697
+ const manifestItems = extractItemsFromRaw(ctx.manifest, ctx.manifestPath);
698
+ allItems.push(...manifestItems);
699
+ result.stats.itemsChecked += manifestItems.length;
700
+ }
701
+ // Find and validate task files
702
+ const taskFiles = await findTaskFiles(ctx.rootDir);
703
+ const specTaskFiles = await findTaskFiles(path.join(ctx.rootDir, 'spec'));
704
+ const allTaskFiles = [...new Set([...taskFiles, ...specTaskFiles])];
705
+ for (const taskFile of allTaskFiles) {
706
+ if (runSchema) {
707
+ const taskErrors = await validateTasksFile(taskFile);
708
+ result.schemaErrors.push(...taskErrors);
709
+ }
710
+ result.stats.filesChecked++;
711
+ // Load tasks for ref validation
712
+ try {
713
+ const raw = await readYamlFile(taskFile);
714
+ let taskList = [];
715
+ if (Array.isArray(raw)) {
716
+ taskList = raw;
717
+ }
718
+ else if (raw && typeof raw === 'object' && 'tasks' in raw) {
719
+ taskList = raw.tasks || [];
720
+ }
721
+ for (const t of taskList) {
722
+ const parsed = TaskSchema.safeParse(t);
723
+ if (parsed.success) {
724
+ allTasks.push({ ...parsed.data, _sourceFile: taskFile });
725
+ result.stats.tasksChecked++;
726
+ }
727
+ }
728
+ }
729
+ catch {
730
+ // Already reported in schema validation
731
+ }
732
+ }
733
+ // Validate spec files (from includes)
734
+ if (ctx.manifest && ctx.manifestPath) {
735
+ const manifestDir = path.dirname(ctx.manifestPath);
736
+ const includes = ctx.manifest.includes || [];
737
+ for (const include of includes) {
738
+ const expandedPaths = await expandIncludePattern(include, manifestDir);
739
+ for (const filePath of expandedPaths) {
740
+ if (runSchema) {
741
+ const specErrors = await validateSpecFile(filePath);
742
+ result.schemaErrors.push(...specErrors);
743
+ }
744
+ result.stats.filesChecked++;
745
+ // Load items for ref validation
746
+ try {
747
+ const items = await loadSpecFile(filePath);
748
+ allItems.push(...items);
749
+ result.stats.itemsChecked += items.length;
750
+ }
751
+ catch {
752
+ // Already reported in schema validation
753
+ }
754
+ }
755
+ }
756
+ }
757
+ // Load meta items for reference validation
758
+ // AC: @agent-definitions ac-agent-3
759
+ const metaCtx = await loadMetaContext(ctx);
760
+ const allMetaItems = [
761
+ ...metaCtx.agents,
762
+ ...metaCtx.workflows,
763
+ ...metaCtx.conventions,
764
+ ...metaCtx.observations,
765
+ ];
766
+ // Reference validation
767
+ if (runRefs && (allTasks.length > 0 || allItems.length > 0 || allMetaItems.length > 0)) {
768
+ const index = new ReferenceIndex(allTasks, allItems, allMetaItems);
769
+ const refResult = validateRefs(index, allTasks, allItems);
770
+ result.refErrors = refResult.errors;
771
+ result.refWarnings = refResult.warnings;
772
+ // AC: @trait-edge-cases ac-2
773
+ // Detect circular trait references
774
+ result.traitCycleErrors = detectTraitCycles(allItems, index);
775
+ // Orphan detection
776
+ if (runOrphans) {
777
+ result.orphans = findOrphans(allTasks, allItems, index);
778
+ }
779
+ // Completeness validation
780
+ // AC: @spec-completeness ac-1, ac-2, ac-3
781
+ // AC: @trait-validation ac-3
782
+ if (runCompleteness) {
783
+ // Build trait index for trait AC coverage validation
784
+ const traitIndex = new TraitIndex(allItems, index);
785
+ result.completenessWarnings = await checkCompleteness(allItems, index, ctx.rootDir, traitIndex);
786
+ // AC: @task-automation-eligibility ac-21, ac-23
787
+ // Check automation eligibility warnings for tasks
788
+ const automationWarnings = checkAutomationEligibility(allTasks, index);
789
+ result.completenessWarnings.push(...automationWarnings);
790
+ }
791
+ }
792
+ // Meta manifest validation (AC-meta-manifest-2, AC-meta-manifest-3)
793
+ const metaManifestPath = await findMetaManifest(ctx.specDir);
794
+ if (metaManifestPath) {
795
+ // Use metaCtx already loaded above
796
+ result.metaStats = {
797
+ agents: metaCtx.agents.length,
798
+ workflows: metaCtx.workflows.length,
799
+ conventions: metaCtx.conventions.length,
800
+ observations: metaCtx.observations.length,
801
+ };
802
+ // Validate meta manifest schema with strict ULID validation
803
+ if (runSchema) {
804
+ const metaErrors = await validateMetaManifestFile(metaManifestPath);
805
+ // Prefix all meta errors with "meta:"
806
+ for (const err of metaErrors) {
807
+ err.path = err.path ? `meta:${err.path}` : 'meta:';
808
+ }
809
+ result.schemaErrors.push(...metaErrors);
810
+ result.stats.filesChecked++;
811
+ }
812
+ }
813
+ // Set valid flag
814
+ result.valid = result.schemaErrors.length === 0 && result.refErrors.length === 0 && result.traitCycleErrors.length === 0;
815
+ return result;
816
+ }
817
+ //# sourceMappingURL=validate.js.map