@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.
- package/AGENTS.md +180 -0
- package/BMAD_ADAPTER_SPEC.md +489 -0
- package/LICENSE +14 -0
- package/README.md +500 -0
- package/dist/aps-markdown/adapter.d.ts +102 -0
- package/dist/aps-markdown/adapter.d.ts.map +1 -0
- package/dist/aps-markdown/adapter.js +351 -0
- package/dist/aps-markdown/index.d.ts +8 -0
- package/dist/aps-markdown/index.d.ts.map +1 -0
- package/dist/aps-markdown/index.js +7 -0
- package/dist/base/file-discovery.d.ts +63 -0
- package/dist/base/file-discovery.d.ts.map +1 -0
- package/dist/base/file-discovery.js +246 -0
- package/dist/base/index.d.ts +10 -0
- package/dist/base/index.d.ts.map +1 -0
- package/dist/base/index.js +9 -0
- package/dist/base/registry.d.ts +155 -0
- package/dist/base/registry.d.ts.map +1 -0
- package/dist/base/registry.js +227 -0
- package/dist/base/testing.d.ts +102 -0
- package/dist/base/testing.d.ts.map +1 -0
- package/dist/base/testing.js +221 -0
- package/dist/base/types.d.ts +255 -0
- package/dist/base/types.d.ts.map +1 -0
- package/dist/base/types.js +78 -0
- package/dist/base/utils.d.ts +127 -0
- package/dist/base/utils.d.ts.map +1 -0
- package/dist/base/utils.js +254 -0
- package/dist/bmad/format-adapter.d.ts +76 -0
- package/dist/bmad/format-adapter.d.ts.map +1 -0
- package/dist/bmad/format-adapter.js +186 -0
- package/dist/bmad/index.d.ts +12 -0
- package/dist/bmad/index.d.ts.map +1 -0
- package/dist/bmad/index.js +10 -0
- package/dist/bmad/parser.d.ts +12 -0
- package/dist/bmad/parser.d.ts.map +1 -0
- package/dist/bmad/parser.js +181 -0
- package/dist/bmad/serializer.d.ts +16 -0
- package/dist/bmad/serializer.d.ts.map +1 -0
- package/dist/bmad/serializer.js +170 -0
- package/dist/bmad/types.d.ts +127 -0
- package/dist/bmad/types.d.ts.map +1 -0
- package/dist/bmad/types.js +47 -0
- package/dist/bmad/utils.d.ts +120 -0
- package/dist/bmad/utils.d.ts.map +1 -0
- package/dist/bmad/utils.js +480 -0
- package/dist/common/index.d.ts +3 -0
- package/dist/common/index.d.ts.map +1 -0
- package/dist/common/index.js +2 -0
- package/dist/common/registry.d.ts +18 -0
- package/dist/common/registry.d.ts.map +1 -0
- package/dist/common/registry.js +58 -0
- package/dist/common/types.d.ts +68 -0
- package/dist/common/types.d.ts.map +1 -0
- package/dist/common/types.js +12 -0
- package/dist/generic/format-adapter.d.ts +64 -0
- package/dist/generic/format-adapter.d.ts.map +1 -0
- package/dist/generic/format-adapter.js +159 -0
- package/dist/generic/index.d.ts +10 -0
- package/dist/generic/index.d.ts.map +1 -0
- package/dist/generic/index.js +9 -0
- package/dist/generic/parser.d.ts +11 -0
- package/dist/generic/parser.d.ts.map +1 -0
- package/dist/generic/parser.js +106 -0
- package/dist/generic/serializer.d.ts +11 -0
- package/dist/generic/serializer.d.ts.map +1 -0
- package/dist/generic/serializer.js +118 -0
- package/dist/generic/types.d.ts +52 -0
- package/dist/generic/types.d.ts.map +1 -0
- package/dist/generic/types.js +6 -0
- package/dist/generic/utils.d.ts +51 -0
- package/dist/generic/utils.d.ts.map +1 -0
- package/dist/generic/utils.js +232 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/speckit/export.d.ts +22 -0
- package/dist/speckit/export.d.ts.map +1 -0
- package/dist/speckit/export.js +384 -0
- package/dist/speckit/format-adapter.d.ts +104 -0
- package/dist/speckit/format-adapter.d.ts.map +1 -0
- package/dist/speckit/format-adapter.js +488 -0
- package/dist/speckit/import-v2.d.ts +33 -0
- package/dist/speckit/import-v2.d.ts.map +1 -0
- package/dist/speckit/import-v2.js +361 -0
- package/dist/speckit/import.d.ts +16 -0
- package/dist/speckit/import.d.ts.map +1 -0
- package/dist/speckit/import.js +247 -0
- package/dist/speckit/index.d.ts +5 -0
- package/dist/speckit/index.d.ts.map +1 -0
- package/dist/speckit/index.js +4 -0
- package/dist/speckit/parser.d.ts +28 -0
- package/dist/speckit/parser.d.ts.map +1 -0
- package/dist/speckit/parser.js +283 -0
- package/dist/speckit/parsers/plan-parser.d.ts +71 -0
- package/dist/speckit/parsers/plan-parser.d.ts.map +1 -0
- package/dist/speckit/parsers/plan-parser.js +216 -0
- package/dist/speckit/parsers/spec-parser.d.ts +67 -0
- package/dist/speckit/parsers/spec-parser.d.ts.map +1 -0
- package/dist/speckit/parsers/spec-parser.js +255 -0
- package/dist/speckit/parsers/tasks-parser.d.ts +57 -0
- package/dist/speckit/parsers/tasks-parser.d.ts.map +1 -0
- package/dist/speckit/parsers/tasks-parser.js +157 -0
- package/package.json +23 -0
- package/project.json +29 -0
- package/src/__tests__/adapter-edge-cases.test.ts +937 -0
- package/src/__tests__/bmad-format-adapter.test.ts +1470 -0
- package/src/__tests__/fixtures/aps/expected-output.json +83 -0
- package/src/__tests__/fixtures/bmad/invalid-malformed-yaml.md +16 -0
- package/src/__tests__/fixtures/bmad/invalid-no-requirements.md +23 -0
- package/src/__tests__/fixtures/bmad/invalid-only-yaml.md +16 -0
- package/src/__tests__/fixtures/bmad/invalid-too-short.md +3 -0
- package/src/__tests__/fixtures/bmad/invalid-wrong-format.md +40 -0
- package/src/__tests__/fixtures/bmad/valid-agent.md +27 -0
- package/src/__tests__/fixtures/bmad/valid-architecture.md +116 -0
- package/src/__tests__/fixtures/bmad/valid-complex-prd.md +161 -0
- package/src/__tests__/fixtures/bmad/valid-epic.md +73 -0
- package/src/__tests__/fixtures/bmad/valid-minimal-prd.md +19 -0
- package/src/__tests__/fixtures/bmad/valid-prd.md +107 -0
- package/src/__tests__/fixtures/bmad/valid-story.md +107 -0
- package/src/__tests__/fixtures/bmad/valid-task.md +79 -0
- package/src/__tests__/fixtures/bmad/valid-v6-prd.md +35 -0
- package/src/__tests__/fixtures/generic/plan-detailed.md +39 -0
- package/src/__tests__/fixtures/generic/prd-simple.md +27 -0
- package/src/__tests__/fixtures/generic/rfc-example.md +26 -0
- package/src/__tests__/fixtures/generic/todo-list.md +23 -0
- package/src/__tests__/fixtures/speckit/sample-plan.md +63 -0
- package/src/__tests__/fixtures/speckit/sample-spec-namespaced.md +50 -0
- package/src/__tests__/fixtures/speckit/sample-spec.md +105 -0
- package/src/__tests__/fixtures/speckit/sample-tasks.md +87 -0
- package/src/__tests__/fixtures/speckit-official/auth-feature/plan.md +272 -0
- package/src/__tests__/fixtures/speckit-official/auth-feature/spec.md +149 -0
- package/src/__tests__/fixtures/speckit-official/auth-feature/tasks.md +169 -0
- package/src/__tests__/generic-format-adapter.test.ts +398 -0
- package/src/__tests__/speckit-export.test.ts +233 -0
- package/src/__tests__/speckit-format-adapter.test.ts +832 -0
- package/src/__tests__/speckit-import-v2.test.ts +253 -0
- package/src/__tests__/speckit-import.test.ts +209 -0
- package/src/__tests__/speckit-parser.test.ts +219 -0
- package/src/__tests__/speckit-spec-parser.test.ts +120 -0
- package/src/aps-markdown/__tests__/__fixtures__/simple-leaf.aps.md +17 -0
- package/src/aps-markdown/__tests__/adapter.test.ts +393 -0
- package/src/aps-markdown/adapter.ts +455 -0
- package/src/aps-markdown/index.ts +8 -0
- package/src/base/__tests__/registry.test.ts +515 -0
- package/src/base/file-discovery.ts +305 -0
- package/src/base/index.ts +10 -0
- package/src/base/registry.ts +263 -0
- package/src/base/testing.ts +334 -0
- package/src/base/types.ts +342 -0
- package/src/base/utils.ts +306 -0
- package/src/bmad/format-adapter.ts +227 -0
- package/src/bmad/index.ts +21 -0
- package/src/bmad/parser.ts +224 -0
- package/src/bmad/serializer.ts +206 -0
- package/src/bmad/types.ts +135 -0
- package/src/bmad/utils.ts +575 -0
- package/src/common/index.ts +2 -0
- package/src/common/registry.ts +72 -0
- package/src/common/types.ts +84 -0
- package/src/generic/__tests__/serializer.test.ts +167 -0
- package/src/generic/format-adapter.ts +200 -0
- package/src/generic/index.ts +11 -0
- package/src/generic/parser.ts +129 -0
- package/src/generic/serializer.ts +134 -0
- package/src/generic/types.ts +53 -0
- package/src/generic/utils.ts +270 -0
- package/src/index.ts +48 -0
- package/src/speckit/export.ts +489 -0
- package/src/speckit/format-adapter.ts +595 -0
- package/src/speckit/import-v2.ts +445 -0
- package/src/speckit/import.ts +305 -0
- package/src/speckit/index.ts +4 -0
- package/src/speckit/parser.ts +351 -0
- package/src/speckit/parsers/plan-parser.ts +342 -0
- package/src/speckit/parsers/spec-parser.ts +379 -0
- package/src/speckit/parsers/tasks-parser.ts +246 -0
- package/tsconfig.json +26 -0
- package/tsconfig.lib.json +21 -0
- package/tsconfig.lib.tsbuildinfo +1 -0
- package/tsconfig.spec.json +9 -0
- package/tsconfig.tsbuildinfo +1 -0
- 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
|
+
}
|