@eddacraft/anvil-adapters 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 (183) hide show
  1. package/AGENTS.md +180 -0
  2. package/BMAD_ADAPTER_SPEC.md +489 -0
  3. package/LICENSE +14 -0
  4. package/README.md +500 -0
  5. package/dist/aps-markdown/adapter.d.ts +102 -0
  6. package/dist/aps-markdown/adapter.d.ts.map +1 -0
  7. package/dist/aps-markdown/adapter.js +351 -0
  8. package/dist/aps-markdown/index.d.ts +8 -0
  9. package/dist/aps-markdown/index.d.ts.map +1 -0
  10. package/dist/aps-markdown/index.js +7 -0
  11. package/dist/base/file-discovery.d.ts +63 -0
  12. package/dist/base/file-discovery.d.ts.map +1 -0
  13. package/dist/base/file-discovery.js +246 -0
  14. package/dist/base/index.d.ts +10 -0
  15. package/dist/base/index.d.ts.map +1 -0
  16. package/dist/base/index.js +9 -0
  17. package/dist/base/registry.d.ts +155 -0
  18. package/dist/base/registry.d.ts.map +1 -0
  19. package/dist/base/registry.js +227 -0
  20. package/dist/base/testing.d.ts +102 -0
  21. package/dist/base/testing.d.ts.map +1 -0
  22. package/dist/base/testing.js +221 -0
  23. package/dist/base/types.d.ts +255 -0
  24. package/dist/base/types.d.ts.map +1 -0
  25. package/dist/base/types.js +78 -0
  26. package/dist/base/utils.d.ts +127 -0
  27. package/dist/base/utils.d.ts.map +1 -0
  28. package/dist/base/utils.js +254 -0
  29. package/dist/bmad/format-adapter.d.ts +76 -0
  30. package/dist/bmad/format-adapter.d.ts.map +1 -0
  31. package/dist/bmad/format-adapter.js +186 -0
  32. package/dist/bmad/index.d.ts +12 -0
  33. package/dist/bmad/index.d.ts.map +1 -0
  34. package/dist/bmad/index.js +10 -0
  35. package/dist/bmad/parser.d.ts +12 -0
  36. package/dist/bmad/parser.d.ts.map +1 -0
  37. package/dist/bmad/parser.js +181 -0
  38. package/dist/bmad/serializer.d.ts +16 -0
  39. package/dist/bmad/serializer.d.ts.map +1 -0
  40. package/dist/bmad/serializer.js +170 -0
  41. package/dist/bmad/types.d.ts +127 -0
  42. package/dist/bmad/types.d.ts.map +1 -0
  43. package/dist/bmad/types.js +47 -0
  44. package/dist/bmad/utils.d.ts +120 -0
  45. package/dist/bmad/utils.d.ts.map +1 -0
  46. package/dist/bmad/utils.js +480 -0
  47. package/dist/common/index.d.ts +3 -0
  48. package/dist/common/index.d.ts.map +1 -0
  49. package/dist/common/index.js +2 -0
  50. package/dist/common/registry.d.ts +18 -0
  51. package/dist/common/registry.d.ts.map +1 -0
  52. package/dist/common/registry.js +58 -0
  53. package/dist/common/types.d.ts +68 -0
  54. package/dist/common/types.d.ts.map +1 -0
  55. package/dist/common/types.js +12 -0
  56. package/dist/generic/format-adapter.d.ts +64 -0
  57. package/dist/generic/format-adapter.d.ts.map +1 -0
  58. package/dist/generic/format-adapter.js +159 -0
  59. package/dist/generic/index.d.ts +10 -0
  60. package/dist/generic/index.d.ts.map +1 -0
  61. package/dist/generic/index.js +9 -0
  62. package/dist/generic/parser.d.ts +11 -0
  63. package/dist/generic/parser.d.ts.map +1 -0
  64. package/dist/generic/parser.js +106 -0
  65. package/dist/generic/serializer.d.ts +11 -0
  66. package/dist/generic/serializer.d.ts.map +1 -0
  67. package/dist/generic/serializer.js +118 -0
  68. package/dist/generic/types.d.ts +52 -0
  69. package/dist/generic/types.d.ts.map +1 -0
  70. package/dist/generic/types.js +6 -0
  71. package/dist/generic/utils.d.ts +51 -0
  72. package/dist/generic/utils.d.ts.map +1 -0
  73. package/dist/generic/utils.js +232 -0
  74. package/dist/index.d.ts +15 -0
  75. package/dist/index.d.ts.map +1 -0
  76. package/dist/index.js +31 -0
  77. package/dist/speckit/export.d.ts +22 -0
  78. package/dist/speckit/export.d.ts.map +1 -0
  79. package/dist/speckit/export.js +384 -0
  80. package/dist/speckit/format-adapter.d.ts +104 -0
  81. package/dist/speckit/format-adapter.d.ts.map +1 -0
  82. package/dist/speckit/format-adapter.js +488 -0
  83. package/dist/speckit/import-v2.d.ts +33 -0
  84. package/dist/speckit/import-v2.d.ts.map +1 -0
  85. package/dist/speckit/import-v2.js +361 -0
  86. package/dist/speckit/import.d.ts +16 -0
  87. package/dist/speckit/import.d.ts.map +1 -0
  88. package/dist/speckit/import.js +247 -0
  89. package/dist/speckit/index.d.ts +5 -0
  90. package/dist/speckit/index.d.ts.map +1 -0
  91. package/dist/speckit/index.js +4 -0
  92. package/dist/speckit/parser.d.ts +28 -0
  93. package/dist/speckit/parser.d.ts.map +1 -0
  94. package/dist/speckit/parser.js +283 -0
  95. package/dist/speckit/parsers/plan-parser.d.ts +71 -0
  96. package/dist/speckit/parsers/plan-parser.d.ts.map +1 -0
  97. package/dist/speckit/parsers/plan-parser.js +216 -0
  98. package/dist/speckit/parsers/spec-parser.d.ts +67 -0
  99. package/dist/speckit/parsers/spec-parser.d.ts.map +1 -0
  100. package/dist/speckit/parsers/spec-parser.js +255 -0
  101. package/dist/speckit/parsers/tasks-parser.d.ts +57 -0
  102. package/dist/speckit/parsers/tasks-parser.d.ts.map +1 -0
  103. package/dist/speckit/parsers/tasks-parser.js +157 -0
  104. package/package.json +23 -0
  105. package/project.json +29 -0
  106. package/src/__tests__/adapter-edge-cases.test.ts +937 -0
  107. package/src/__tests__/bmad-format-adapter.test.ts +1470 -0
  108. package/src/__tests__/fixtures/aps/expected-output.json +83 -0
  109. package/src/__tests__/fixtures/bmad/invalid-malformed-yaml.md +16 -0
  110. package/src/__tests__/fixtures/bmad/invalid-no-requirements.md +23 -0
  111. package/src/__tests__/fixtures/bmad/invalid-only-yaml.md +16 -0
  112. package/src/__tests__/fixtures/bmad/invalid-too-short.md +3 -0
  113. package/src/__tests__/fixtures/bmad/invalid-wrong-format.md +40 -0
  114. package/src/__tests__/fixtures/bmad/valid-agent.md +27 -0
  115. package/src/__tests__/fixtures/bmad/valid-architecture.md +116 -0
  116. package/src/__tests__/fixtures/bmad/valid-complex-prd.md +161 -0
  117. package/src/__tests__/fixtures/bmad/valid-epic.md +73 -0
  118. package/src/__tests__/fixtures/bmad/valid-minimal-prd.md +19 -0
  119. package/src/__tests__/fixtures/bmad/valid-prd.md +107 -0
  120. package/src/__tests__/fixtures/bmad/valid-story.md +107 -0
  121. package/src/__tests__/fixtures/bmad/valid-task.md +79 -0
  122. package/src/__tests__/fixtures/bmad/valid-v6-prd.md +35 -0
  123. package/src/__tests__/fixtures/generic/plan-detailed.md +39 -0
  124. package/src/__tests__/fixtures/generic/prd-simple.md +27 -0
  125. package/src/__tests__/fixtures/generic/rfc-example.md +26 -0
  126. package/src/__tests__/fixtures/generic/todo-list.md +23 -0
  127. package/src/__tests__/fixtures/speckit/sample-plan.md +63 -0
  128. package/src/__tests__/fixtures/speckit/sample-spec-namespaced.md +50 -0
  129. package/src/__tests__/fixtures/speckit/sample-spec.md +105 -0
  130. package/src/__tests__/fixtures/speckit/sample-tasks.md +87 -0
  131. package/src/__tests__/fixtures/speckit-official/auth-feature/plan.md +272 -0
  132. package/src/__tests__/fixtures/speckit-official/auth-feature/spec.md +149 -0
  133. package/src/__tests__/fixtures/speckit-official/auth-feature/tasks.md +169 -0
  134. package/src/__tests__/generic-format-adapter.test.ts +398 -0
  135. package/src/__tests__/speckit-export.test.ts +233 -0
  136. package/src/__tests__/speckit-format-adapter.test.ts +832 -0
  137. package/src/__tests__/speckit-import-v2.test.ts +253 -0
  138. package/src/__tests__/speckit-import.test.ts +209 -0
  139. package/src/__tests__/speckit-parser.test.ts +219 -0
  140. package/src/__tests__/speckit-spec-parser.test.ts +120 -0
  141. package/src/aps-markdown/__tests__/__fixtures__/simple-leaf.aps.md +17 -0
  142. package/src/aps-markdown/__tests__/adapter.test.ts +393 -0
  143. package/src/aps-markdown/adapter.ts +455 -0
  144. package/src/aps-markdown/index.ts +8 -0
  145. package/src/base/__tests__/registry.test.ts +515 -0
  146. package/src/base/file-discovery.ts +305 -0
  147. package/src/base/index.ts +10 -0
  148. package/src/base/registry.ts +263 -0
  149. package/src/base/testing.ts +334 -0
  150. package/src/base/types.ts +342 -0
  151. package/src/base/utils.ts +306 -0
  152. package/src/bmad/format-adapter.ts +227 -0
  153. package/src/bmad/index.ts +21 -0
  154. package/src/bmad/parser.ts +224 -0
  155. package/src/bmad/serializer.ts +206 -0
  156. package/src/bmad/types.ts +135 -0
  157. package/src/bmad/utils.ts +575 -0
  158. package/src/common/index.ts +2 -0
  159. package/src/common/registry.ts +72 -0
  160. package/src/common/types.ts +84 -0
  161. package/src/generic/__tests__/serializer.test.ts +167 -0
  162. package/src/generic/format-adapter.ts +200 -0
  163. package/src/generic/index.ts +11 -0
  164. package/src/generic/parser.ts +129 -0
  165. package/src/generic/serializer.ts +134 -0
  166. package/src/generic/types.ts +53 -0
  167. package/src/generic/utils.ts +270 -0
  168. package/src/index.ts +48 -0
  169. package/src/speckit/export.ts +489 -0
  170. package/src/speckit/format-adapter.ts +595 -0
  171. package/src/speckit/import-v2.ts +445 -0
  172. package/src/speckit/import.ts +305 -0
  173. package/src/speckit/index.ts +4 -0
  174. package/src/speckit/parser.ts +351 -0
  175. package/src/speckit/parsers/plan-parser.ts +342 -0
  176. package/src/speckit/parsers/spec-parser.ts +379 -0
  177. package/src/speckit/parsers/tasks-parser.ts +246 -0
  178. package/tsconfig.json +26 -0
  179. package/tsconfig.lib.json +21 -0
  180. package/tsconfig.lib.tsbuildinfo +1 -0
  181. package/tsconfig.spec.json +9 -0
  182. package/tsconfig.tsbuildinfo +1 -0
  183. package/vitest.config.ts +14 -0
@@ -0,0 +1,595 @@
1
+ /**
2
+ * SpecKit Format Adapter
3
+ *
4
+ * FormatAdapter implementation for GitHub spec-kit format.
5
+ * Handles simple SpecKit specification documents with Intent, Overview, Goals,
6
+ * Requirements, and Changes sections.
7
+ *
8
+ * Uses SpecKitParser to parse markdown with ## sections and convert to/from APS format.
9
+ */
10
+
11
+ import {
12
+ generateHash,
13
+ type APSPlan,
14
+ type ValidationResult,
15
+ type Change,
16
+ createPlan,
17
+ validateRelativePath,
18
+ } from '@eddacraft/anvil-core';
19
+ import {
20
+ BaseFormatAdapter,
21
+ type AdapterMetadata,
22
+ type DetectionResult,
23
+ type ParseResult,
24
+ type SerializeResult,
25
+ type ParseContext,
26
+ type AdapterOptions,
27
+ type PathDetectionHint,
28
+ } from '../base/types.js';
29
+ import { createDetection, generateDeterministicPlanId } from '../base/utils.js';
30
+ import { SpecKitParser } from './parser.js';
31
+
32
+ /**
33
+ * Detection indicators for SpecKit format
34
+ */
35
+ interface SpecKitIndicators {
36
+ hasSpecificationHeader: boolean;
37
+ hasIntentSection: boolean;
38
+ hasOverviewSection: boolean;
39
+ hasGoalsSection: boolean;
40
+ hasRequirementsSection: boolean;
41
+ hasChangesSection: boolean;
42
+ hasFilesToCreateSection: boolean;
43
+ hasFilesToUpdateSection: boolean;
44
+ hasCodeBlocks: boolean;
45
+ sectionCount: number;
46
+ /** Agent-first: content references speckit.* namespace commands */
47
+ hasSpeckitNamespace: boolean;
48
+ /** Agent-first: sibling AGENTS.md file detected */
49
+ hasAgentsMdSibling: boolean;
50
+ }
51
+
52
+ /**
53
+ * SpecKit FormatAdapter implementation
54
+ *
55
+ * Converts between SpecKit format documents and APS plans.
56
+ */
57
+ export class SpecKitFormatAdapter extends BaseFormatAdapter {
58
+ readonly metadata: AdapterMetadata = {
59
+ name: 'speckit',
60
+ version: '2.0.0',
61
+ displayName: 'GitHub SpecKit',
62
+ description: 'GitHub spec-kit format adapter (spec.md, plan.md, tasks.md)',
63
+ formats: ['speckit', 'spec-kit', 'spec.md', 'plan.md', 'tasks.md'],
64
+ extensions: ['.md'],
65
+ };
66
+
67
+ private parser: SpecKitParser;
68
+
69
+ constructor(options?: AdapterOptions) {
70
+ super(options);
71
+ this.parser = new SpecKitParser();
72
+ }
73
+
74
+ /**
75
+ * Detect if content is SpecKit format
76
+ *
77
+ * Uses confidence scoring based on multiple indicators:
78
+ * - Specification header (20 points)
79
+ * - Intent section (15 points)
80
+ * - Overview section (10 points)
81
+ * - Goals section (10 points)
82
+ * - Requirements section (10 points)
83
+ * - Changes section (20 points)
84
+ * - Files to Create/Update sections (10 points)
85
+ * - Code blocks (5 points)
86
+ *
87
+ * @param content - Document content to analyze
88
+ * @returns Detection result with confidence score
89
+ */
90
+ detect(content: string): DetectionResult {
91
+ const indicators = this.analyzeContent(content);
92
+ const confidence = this.calculateConfidence(indicators);
93
+ const reason = this.buildDetectionReason(indicators);
94
+
95
+ // Detection threshold: 50% confidence
96
+ // Lower threshold than BMAD to accommodate minimal SpecKit documents
97
+ return createDetection(confidence >= 50, confidence, reason);
98
+ }
99
+
100
+ /**
101
+ * Detect with file path hints for improved accuracy
102
+ *
103
+ * Uses sibling file information (e.g., AGENTS.md) and content
104
+ * namespace patterns (e.g., `speckit.*`) to boost detection.
105
+ *
106
+ * @param content - Document content to analyze
107
+ * @param hint - Path and directory information
108
+ * @returns Detection result with confidence score
109
+ */
110
+ detectWithPath(content: string, hint: PathDetectionHint): DetectionResult {
111
+ const indicators = this.analyzeContent(content, hint);
112
+ const confidence = this.calculateConfidence(indicators);
113
+ const reason = this.buildDetectionReason(indicators);
114
+
115
+ return createDetection(confidence >= 50, confidence, reason);
116
+ }
117
+
118
+ /**
119
+ * Parse SpecKit content to APS plan
120
+ *
121
+ * @param content - SpecKit markdown content
122
+ * @param context - Parse context for provenance
123
+ * @param options - Adapter options
124
+ * @returns Parse result with APS plan
125
+ */
126
+ async parse(
127
+ content: string,
128
+ context?: ParseContext,
129
+ _options?: AdapterOptions
130
+ ): Promise<ParseResult> {
131
+ try {
132
+ // Parse SpecKit markdown using SpecKitParser
133
+ const parsed = this.parser.parseSpecMarkdown(content);
134
+
135
+ // Build intent from parsed content
136
+ const intent = parsed.intent || 'Implement Feature';
137
+
138
+ // Build proposed changes from parsed changes
139
+ const changes: Change[] = [];
140
+ if (parsed.changes && parsed.changes.length > 0) {
141
+ for (const change of parsed.changes) {
142
+ const changeType = this.inferChangeType(change.type);
143
+ const rawPath = change.path || this.inferPathFromDescription(change.description);
144
+ let safePath: string;
145
+ try {
146
+ safePath = validateRelativePath(rawPath);
147
+ } catch {
148
+ continue;
149
+ }
150
+ changes.push({
151
+ type: changeType,
152
+ path: safePath,
153
+ description: change.description,
154
+ content: change.content,
155
+ });
156
+ }
157
+ }
158
+
159
+ // Build provenance
160
+ const provenance = {
161
+ timestamp: context?.timestamp || new Date().toISOString(),
162
+ source: 'cli' as const,
163
+ version: this.metadata.version,
164
+ author: context?.author,
165
+ repository: context?.repositoryPath,
166
+ branch: context?.branch,
167
+ commit: context?.commit,
168
+ };
169
+
170
+ const planId = context?.planId ?? generateDeterministicPlanId(content);
171
+
172
+ // Create APS plan
173
+ const plan: APSPlan = {
174
+ ...createPlan({
175
+ id: planId,
176
+ intent,
177
+ provenance,
178
+ changes,
179
+ }),
180
+ schema_version: '0.1.0' as const,
181
+ hash: '0'.repeat(64), // Temporary, will be replaced
182
+ metadata: {
183
+ source_format: 'speckit',
184
+ overview: parsed.overview,
185
+ goals: parsed.goals,
186
+ requirements: parsed.requirements,
187
+ ...parsed.metadata,
188
+ },
189
+ };
190
+
191
+ // Generate hash for the plan
192
+ const planWithHash = {
193
+ ...plan,
194
+ hash: generateHash(plan),
195
+ };
196
+
197
+ return this.createParseSuccess(planWithHash);
198
+ } catch (error) {
199
+ return this.createParseError([
200
+ {
201
+ code: 'PARSE_ERROR',
202
+ message: error instanceof Error ? error.message : 'Failed to parse SpecKit content',
203
+ details: error,
204
+ },
205
+ ]);
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Infer APS change type from SpecKit change type
211
+ */
212
+ private inferChangeType(type: string): Change['type'] {
213
+ const typeLower = type.toLowerCase();
214
+ if (typeLower.includes('create')) return 'file_create';
215
+ if (typeLower.includes('update') || typeLower.includes('modify')) return 'file_update';
216
+ if (typeLower.includes('delete') || typeLower.includes('remove')) return 'file_delete';
217
+ return 'file_update'; // Default to update
218
+ }
219
+
220
+ /**
221
+ * Infer file path from change description
222
+ */
223
+ private inferPathFromDescription(description: string): string {
224
+ // Try to extract path from common patterns like "at path/to/file" or "`path/to/file`"
225
+ const pathMatch =
226
+ description.match(/at\s+`([^`]+)`/) ||
227
+ description.match(/at\s+(\S+\.\w+)/) ||
228
+ description.match(/`([^`]+\.\w+)`/);
229
+
230
+ if (pathMatch) {
231
+ return pathMatch[1];
232
+ }
233
+
234
+ // Fallback: generate a generic path
235
+ return 'src/generated-file.ts';
236
+ }
237
+
238
+ /**
239
+ * Serialize APS plan to SpecKit format
240
+ *
241
+ * @param plan - APS plan to serialize
242
+ * @param options - Adapter options
243
+ * @returns Serialize result with SpecKit markdown
244
+ */
245
+ async serialize(plan: APSPlan, _options?: AdapterOptions): Promise<SerializeResult> {
246
+ try {
247
+ const sections: string[] = [];
248
+
249
+ // Header
250
+ sections.push('# Specification');
251
+ sections.push('');
252
+
253
+ // Intent section
254
+ sections.push('## Intent');
255
+ sections.push('');
256
+ sections.push(plan.intent);
257
+ sections.push('');
258
+
259
+ // Overview section (if available in metadata)
260
+ const overview = plan.metadata?.['overview'];
261
+ if (overview) {
262
+ sections.push('## Overview');
263
+ sections.push('');
264
+ sections.push(overview as string);
265
+ sections.push('');
266
+ }
267
+
268
+ // Goals section (if available in metadata)
269
+ const goals = plan.metadata?.['goals'];
270
+ if (goals && Array.isArray(goals)) {
271
+ sections.push('## Goals');
272
+ sections.push('');
273
+ for (const goal of goals as string[]) {
274
+ sections.push(`- ${goal}`);
275
+ }
276
+ sections.push('');
277
+ }
278
+
279
+ // Requirements section (if available in metadata)
280
+ const requirements = plan.metadata?.['requirements'];
281
+ if (requirements && Array.isArray(requirements)) {
282
+ sections.push('## Requirements');
283
+ sections.push('');
284
+ for (const req of requirements as string[]) {
285
+ sections.push(`- ${req}`);
286
+ }
287
+ sections.push('');
288
+ }
289
+
290
+ // Changes section
291
+ if (plan.proposed_changes.length > 0) {
292
+ sections.push('## Changes');
293
+ sections.push('');
294
+
295
+ // Group changes by type
296
+ const fileCreates = plan.proposed_changes.filter((c) => c.type === 'file_create');
297
+ const fileUpdates = plan.proposed_changes.filter((c) => c.type === 'file_update');
298
+ const fileDeletes = plan.proposed_changes.filter((c) => c.type === 'file_delete');
299
+
300
+ // Files to Create
301
+ if (fileCreates.length > 0) {
302
+ sections.push('### Files to Create');
303
+ sections.push('');
304
+ for (const change of fileCreates) {
305
+ sections.push(`#### Create ${change.path}`);
306
+ sections.push('');
307
+ sections.push(change.description || 'No description provided');
308
+ sections.push('');
309
+ if (change.content) {
310
+ sections.push('```typescript');
311
+ sections.push(change.content);
312
+ sections.push('```');
313
+ sections.push('');
314
+ }
315
+ }
316
+ }
317
+
318
+ // Files to Update
319
+ if (fileUpdates.length > 0) {
320
+ sections.push('### Files to Update');
321
+ sections.push('');
322
+ for (const change of fileUpdates) {
323
+ sections.push(`#### Update ${change.path}`);
324
+ sections.push('');
325
+ sections.push(change.description || 'No description provided');
326
+ sections.push('');
327
+ if (change.content) {
328
+ sections.push('```typescript');
329
+ sections.push(change.content);
330
+ sections.push('```');
331
+ sections.push('');
332
+ }
333
+ }
334
+ }
335
+
336
+ // Files to Delete
337
+ if (fileDeletes.length > 0) {
338
+ sections.push('### Files to Delete');
339
+ sections.push('');
340
+ for (const change of fileDeletes) {
341
+ sections.push(`#### Delete ${change.path}`);
342
+ sections.push('');
343
+ sections.push(change.description || 'No description provided');
344
+ sections.push('');
345
+ }
346
+ }
347
+ }
348
+
349
+ // Metadata section (if additional metadata exists)
350
+ const metadataKeys = Object.keys(plan.metadata || {}).filter(
351
+ (k) => !['source_format', 'overview', 'goals', 'requirements'].includes(k)
352
+ );
353
+ if (metadataKeys.length > 0) {
354
+ sections.push('## Metadata');
355
+ sections.push('');
356
+ sections.push('```json');
357
+ const filteredMetadata: Record<string, unknown> = {};
358
+ for (const key of metadataKeys) {
359
+ filteredMetadata[key] = plan.metadata?.[key];
360
+ }
361
+ sections.push(JSON.stringify(filteredMetadata, null, 2));
362
+ sections.push('```');
363
+ sections.push('');
364
+ }
365
+
366
+ const content = sections.join('\n');
367
+ return this.createSerializeSuccess(content);
368
+ } catch (error) {
369
+ return this.createSerializeError([
370
+ {
371
+ code: 'SERIALIZE_ERROR',
372
+ message: error instanceof Error ? error.message : 'Failed to serialize to SpecKit format',
373
+ details: error,
374
+ },
375
+ ]);
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Validate SpecKit content
381
+ *
382
+ * Checks for required SpecKit elements without full conversion.
383
+ *
384
+ * @param content - SpecKit content to validate
385
+ * @param options - Validation options
386
+ * @returns Validation result
387
+ */
388
+ async validate(content: string, _options?: AdapterOptions): Promise<ValidationResult> {
389
+ const issues: Array<{
390
+ path: string;
391
+ message: string;
392
+ code: string;
393
+ severity: 'error' | 'warning';
394
+ }> = [];
395
+
396
+ // Check for minimum content length
397
+ if (content.trim().length < 100) {
398
+ issues.push({
399
+ code: 'CONTENT_TOO_SHORT',
400
+ path: 'content',
401
+ message: 'Content is too short to be a valid SpecKit document',
402
+ severity: 'error',
403
+ });
404
+ }
405
+
406
+ // Analyze content for SpecKit indicators
407
+ const indicators = this.analyzeContent(content);
408
+ const confidence = this.calculateConfidence(indicators);
409
+
410
+ // Low confidence suggests invalid SpecKit format
411
+ if (confidence < 50) {
412
+ issues.push({
413
+ code: 'LOW_CONFIDENCE',
414
+ path: 'content',
415
+ message: `Content does not appear to be a valid SpecKit document (confidence: ${confidence}%)`,
416
+ severity: 'error',
417
+ });
418
+ }
419
+
420
+ // Check for required sections
421
+ if (!indicators.hasSpecificationHeader && !indicators.hasChangesSection) {
422
+ issues.push({
423
+ code: 'MISSING_REQUIRED_SECTIONS',
424
+ path: 'content',
425
+ message: 'Missing required sections (Specification header or Changes section)',
426
+ severity: 'error',
427
+ });
428
+ }
429
+
430
+ // Warn if missing recommended sections
431
+ if (!indicators.hasIntentSection) {
432
+ issues.push({
433
+ code: 'MISSING_INTENT',
434
+ path: 'content',
435
+ message: 'Missing recommended Intent section',
436
+ severity: 'warning',
437
+ });
438
+ }
439
+
440
+ return {
441
+ valid: issues.filter((i) => i.severity === 'error').length === 0,
442
+ issues: issues.length > 0 ? issues : undefined,
443
+ summary:
444
+ issues.length === 0
445
+ ? 'SpecKit document is valid'
446
+ : `Found ${issues.length} validation issue${issues.length > 1 ? 's' : ''}`,
447
+ };
448
+ }
449
+
450
+ /**
451
+ * Analyze content for SpecKit indicators
452
+ */
453
+ private analyzeContent(content: string, hint?: PathDetectionHint): SpecKitIndicators {
454
+ const lowerContent = content.toLowerCase();
455
+
456
+ // Detect speckit.* namespace commands (e.g., /speckit.clarify, /speckit.analyze)
457
+ const hasSpeckitNamespace = /\b\/?speckit\.\w+\b/i.test(content);
458
+
459
+ // Check for AGENTS.md sibling
460
+ const hasAgentsMdSibling =
461
+ hint?.siblingFiles?.some((f) => f.toLowerCase() === 'agents.md') ?? false;
462
+
463
+ return {
464
+ hasSpecificationHeader: /^#\s+(specification|spec)\s*$/im.test(content),
465
+ hasIntentSection: /^##\s+intent\s*$/im.test(content),
466
+ hasOverviewSection: /^##\s+overview\s*$/im.test(content),
467
+ hasGoalsSection: /^##\s+goals?\s*$/im.test(content),
468
+ hasRequirementsSection: /^##\s+requirements?\s*$/im.test(content),
469
+ hasChangesSection: /^##\s+changes?\s*$/im.test(content),
470
+ hasFilesToCreateSection:
471
+ lowerContent.includes('files to create') || lowerContent.includes('create file'),
472
+ hasFilesToUpdateSection:
473
+ lowerContent.includes('files to update') || lowerContent.includes('update file'),
474
+ hasCodeBlocks: /```[\s\S]*?```/.test(content),
475
+ sectionCount: (content.match(/^##\s+/gim) || []).length,
476
+ hasSpeckitNamespace,
477
+ hasAgentsMdSibling,
478
+ };
479
+ }
480
+
481
+ /**
482
+ * Calculate confidence score
483
+ */
484
+ private calculateConfidence(indicators: SpecKitIndicators): number {
485
+ let score = 0;
486
+
487
+ // Specification header (20 points)
488
+ if (indicators.hasSpecificationHeader) {
489
+ score += 20;
490
+ }
491
+
492
+ // Intent section (15 points)
493
+ if (indicators.hasIntentSection) {
494
+ score += 15;
495
+ }
496
+
497
+ // Overview section (10 points)
498
+ if (indicators.hasOverviewSection) {
499
+ score += 10;
500
+ }
501
+
502
+ // Goals section (10 points)
503
+ if (indicators.hasGoalsSection) {
504
+ score += 10;
505
+ }
506
+
507
+ // Requirements section (10 points)
508
+ if (indicators.hasRequirementsSection) {
509
+ score += 10;
510
+ }
511
+
512
+ // Changes section (20 points)
513
+ if (indicators.hasChangesSection) {
514
+ score += 20;
515
+ }
516
+
517
+ // Files to Create/Update sections (10 points)
518
+ if (indicators.hasFilesToCreateSection || indicators.hasFilesToUpdateSection) {
519
+ score += 10;
520
+ }
521
+
522
+ // Code blocks (5 points)
523
+ if (indicators.hasCodeBlocks) {
524
+ score += 5;
525
+ }
526
+
527
+ // speckit.* namespace commands (10 points)
528
+ if (indicators.hasSpeckitNamespace) {
529
+ score += 10;
530
+ }
531
+
532
+ // AGENTS.md sibling (15 points)
533
+ if (indicators.hasAgentsMdSibling) {
534
+ score += 15;
535
+ }
536
+
537
+ // Bonus: If has both Specification header AND Intent section, ensure at least 50% confidence
538
+ // This accommodates minimal but valid SpecKit documents
539
+ if (indicators.hasSpecificationHeader && indicators.hasIntentSection && score < 50) {
540
+ score = 50;
541
+ }
542
+
543
+ return Math.min(100, score);
544
+ }
545
+
546
+ /**
547
+ * Build detection reason message
548
+ */
549
+ private buildDetectionReason(indicators: SpecKitIndicators): string {
550
+ const reasons: string[] = [];
551
+
552
+ if (indicators.hasSpecificationHeader) {
553
+ reasons.push('specification-header');
554
+ }
555
+ if (indicators.hasIntentSection) {
556
+ reasons.push('intent-section');
557
+ }
558
+ if (indicators.hasGoalsSection) {
559
+ reasons.push('goals-section');
560
+ }
561
+ if (indicators.hasRequirementsSection) {
562
+ reasons.push('requirements-section');
563
+ }
564
+ if (indicators.hasChangesSection) {
565
+ reasons.push('changes-section');
566
+ }
567
+ if (indicators.hasFilesToCreateSection || indicators.hasFilesToUpdateSection) {
568
+ reasons.push('file-changes');
569
+ }
570
+ if (indicators.hasCodeBlocks) {
571
+ reasons.push('code-blocks');
572
+ }
573
+ if (indicators.hasSpeckitNamespace) {
574
+ reasons.push('speckit-namespace');
575
+ }
576
+ if (indicators.hasAgentsMdSibling) {
577
+ reasons.push('agents-md');
578
+ }
579
+ if (indicators.sectionCount >= 3) {
580
+ reasons.push(`${indicators.sectionCount} sections`);
581
+ }
582
+
583
+ return reasons.length > 0 ? reasons.join(', ') : 'no strong indicators';
584
+ }
585
+ }
586
+
587
+ /**
588
+ * Create a new SpecKit format adapter instance
589
+ *
590
+ * @param options - Adapter options
591
+ * @returns SpecKit adapter instance
592
+ */
593
+ export function createSpecKitAdapter(options?: AdapterOptions): SpecKitFormatAdapter {
594
+ return new SpecKitFormatAdapter(options);
595
+ }