@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,575 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BMAD Adapter Utilities
|
|
3
|
+
*
|
|
4
|
+
* Helper functions for BMAD format parsing and serialization.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
BMADFrontMatter,
|
|
9
|
+
BMADRequirement,
|
|
10
|
+
BMADUserStory,
|
|
11
|
+
BMADChangeLogEntry,
|
|
12
|
+
RequirementType,
|
|
13
|
+
BMADDocumentType,
|
|
14
|
+
DetectionIndicators,
|
|
15
|
+
BMAD_FOLDERS,
|
|
16
|
+
} from './types.js';
|
|
17
|
+
import type { PathDetectionHint } from '../base/types.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Analyze a file path for BMAD folder structure indicators
|
|
21
|
+
*
|
|
22
|
+
* Detects both v6 (`_bmad`, `_config`) and legacy (`.bmad`, `_cfg`) paths.
|
|
23
|
+
*
|
|
24
|
+
* @param hint - Path detection hint with file path and directory info
|
|
25
|
+
* @returns Object indicating which BMAD path patterns were found
|
|
26
|
+
*/
|
|
27
|
+
export function analyzePath(hint: PathDetectionHint): {
|
|
28
|
+
isBmadFolder: boolean;
|
|
29
|
+
isConfigFolder: boolean;
|
|
30
|
+
} {
|
|
31
|
+
const { filePath, parentDirs } = hint;
|
|
32
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
33
|
+
const allDirs = parentDirs ?? [];
|
|
34
|
+
|
|
35
|
+
const bmadDirs: readonly string[] = [BMAD_FOLDERS.PROJECT, BMAD_FOLDERS.PROJECT_LEGACY];
|
|
36
|
+
const configDirs: readonly string[] = [BMAD_FOLDERS.CONFIG, BMAD_FOLDERS.CONFIG_LEGACY];
|
|
37
|
+
|
|
38
|
+
const isBmadFolder =
|
|
39
|
+
bmadDirs.some((d) => {
|
|
40
|
+
const pattern = new RegExp(`(?:^|/)${escapeRegExp(d)}(?:/|$)`);
|
|
41
|
+
return pattern.test(normalizedPath);
|
|
42
|
+
}) || allDirs.some((d) => bmadDirs.includes(d));
|
|
43
|
+
|
|
44
|
+
const isConfigFolder =
|
|
45
|
+
configDirs.some((d) => {
|
|
46
|
+
const pattern = new RegExp(`(?:^|/)${escapeRegExp(d)}(?:/|$)`);
|
|
47
|
+
return pattern.test(normalizedPath);
|
|
48
|
+
}) || allDirs.some((d) => configDirs.includes(d));
|
|
49
|
+
|
|
50
|
+
return { isBmadFolder, isConfigFolder };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Expand BMAD template variables in content
|
|
55
|
+
*
|
|
56
|
+
* Supports both legacy underscore syntax `{project_root}` and
|
|
57
|
+
* v6 hyphenated syntax `{project-root}`.
|
|
58
|
+
*
|
|
59
|
+
* @param content - Content with variable placeholders
|
|
60
|
+
* @param variables - Variable values to substitute
|
|
61
|
+
* @returns Content with variables expanded
|
|
62
|
+
*/
|
|
63
|
+
export function expandVariables(content: string, variables: Record<string, string>): string {
|
|
64
|
+
let result = content;
|
|
65
|
+
|
|
66
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
67
|
+
// Support both underscore and hyphenated forms
|
|
68
|
+
const underscoreKey = key.replace(/-/g, '_');
|
|
69
|
+
const hyphenKey = key.replace(/_/g, '-');
|
|
70
|
+
|
|
71
|
+
result = result
|
|
72
|
+
.replace(new RegExp(`\\{${escapeRegExp(underscoreKey)}\\}`, 'g'), () => value)
|
|
73
|
+
.replace(new RegExp(`\\{${escapeRegExp(hyphenKey)}\\}`, 'g'), () => value);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Check if content contains hyphenated variable syntax (v6)
|
|
81
|
+
*
|
|
82
|
+
* @param content - Content to check
|
|
83
|
+
* @returns True if hyphenated variables are found
|
|
84
|
+
*/
|
|
85
|
+
export function hasHyphenatedVariables(content: string): boolean {
|
|
86
|
+
return /\{[a-z]+-[a-z]+(?:-[a-z]+)*\}/i.test(content);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function escapeRegExp(str: string): string {
|
|
90
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Parse a YAML boolean value
|
|
95
|
+
*
|
|
96
|
+
* Handles YAML 1.1 boolean forms: true/false, yes/no, on/off.
|
|
97
|
+
*
|
|
98
|
+
* @param value - String value from YAML
|
|
99
|
+
* @returns Boolean or undefined if not a boolean value
|
|
100
|
+
*/
|
|
101
|
+
export function parseYamlBoolean(value: string): boolean | undefined {
|
|
102
|
+
const lower = value.toLowerCase().trim();
|
|
103
|
+
if (['true', 'yes', 'on'].includes(lower)) return true;
|
|
104
|
+
if (['false', 'no', 'off'].includes(lower)) return false;
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Extract YAML front-matter from markdown content
|
|
110
|
+
*
|
|
111
|
+
* @param content - Markdown content
|
|
112
|
+
* @returns Parsed front-matter or null
|
|
113
|
+
*/
|
|
114
|
+
export function extractFrontMatter(content: string): BMADFrontMatter | null {
|
|
115
|
+
const frontMatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n/;
|
|
116
|
+
const match = content.match(frontMatterRegex);
|
|
117
|
+
|
|
118
|
+
if (!match) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const yamlContent = match[1];
|
|
123
|
+
const frontMatter: BMADFrontMatter = {};
|
|
124
|
+
|
|
125
|
+
// Simple YAML parser (handles basic key: value pairs)
|
|
126
|
+
const lines = yamlContent.split('\n');
|
|
127
|
+
|
|
128
|
+
for (const line of lines) {
|
|
129
|
+
const trimmed = line.trim();
|
|
130
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
131
|
+
|
|
132
|
+
// Handle key: value
|
|
133
|
+
const keyValueMatch = trimmed.match(/^(\w+):\s*(.*)$/);
|
|
134
|
+
if (keyValueMatch) {
|
|
135
|
+
const [, key, value] = keyValueMatch;
|
|
136
|
+
|
|
137
|
+
// Remove quotes
|
|
138
|
+
let cleanValue = value.replace(/^["']|["']$/g, '').trim();
|
|
139
|
+
// Handle template variables
|
|
140
|
+
cleanValue = cleanValue.replace(/\{\{.*?\}\}/g, '');
|
|
141
|
+
|
|
142
|
+
if (key === 'variables') {
|
|
143
|
+
frontMatter[key] = {};
|
|
144
|
+
} else if (key === 'hasSidecar') {
|
|
145
|
+
// Parse boolean value
|
|
146
|
+
const boolVal = parseYamlBoolean(cleanValue);
|
|
147
|
+
if (boolVal !== undefined) {
|
|
148
|
+
frontMatter.hasSidecar = boolVal;
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
frontMatter[key as keyof BMADFrontMatter] = cleanValue as never;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return frontMatter;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Extract requirements from content
|
|
161
|
+
*
|
|
162
|
+
* @param content - Document content
|
|
163
|
+
* @returns Array of requirements
|
|
164
|
+
*/
|
|
165
|
+
export function extractRequirements(content: string): BMADRequirement[] {
|
|
166
|
+
const requirements: BMADRequirement[] = [];
|
|
167
|
+
const lines = content.split('\n');
|
|
168
|
+
|
|
169
|
+
for (let i = 0; i < lines.length; i++) {
|
|
170
|
+
const line = lines[i];
|
|
171
|
+
const trimmed = line.trim();
|
|
172
|
+
|
|
173
|
+
// Match FR-01, NFR-01, US-01 patterns (optionally prefixed by list markers)
|
|
174
|
+
const reqMatch = trimmed.match(/^(?:[-*+]\s+)?(FR|NFR|US)-(\d{2}):\s*(.+)$/);
|
|
175
|
+
if (reqMatch) {
|
|
176
|
+
const [, typeStr, numStr, description] = reqMatch;
|
|
177
|
+
requirements.push({
|
|
178
|
+
type: typeStr as RequirementType,
|
|
179
|
+
id: `${typeStr}-${numStr}`,
|
|
180
|
+
number: parseInt(numStr, 10),
|
|
181
|
+
description,
|
|
182
|
+
line: i + 1,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return requirements;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Extract user stories from content
|
|
192
|
+
*
|
|
193
|
+
* @param content - Document content
|
|
194
|
+
* @returns Array of user stories
|
|
195
|
+
*/
|
|
196
|
+
export function extractUserStories(content: string): BMADUserStory[] {
|
|
197
|
+
const stories: BMADUserStory[] = [];
|
|
198
|
+
const lines = content.split('\n');
|
|
199
|
+
|
|
200
|
+
for (let i = 0; i < lines.length; i++) {
|
|
201
|
+
const line = lines[i];
|
|
202
|
+
|
|
203
|
+
// Match US-01: Title format
|
|
204
|
+
const storyMatch = line.trim().match(/^(?:[-*+]\s+)?(US-\d{2}):\s*(.+)$/);
|
|
205
|
+
if (!storyMatch) continue;
|
|
206
|
+
|
|
207
|
+
const [, id, title] = storyMatch;
|
|
208
|
+
const story: BMADUserStory = {
|
|
209
|
+
id,
|
|
210
|
+
title,
|
|
211
|
+
line: i + 1,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// Look for "As a... I want... so that..." pattern in following lines
|
|
215
|
+
let j = i + 1;
|
|
216
|
+
while (j < lines.length && j < i + 10) {
|
|
217
|
+
const storyLine = lines[j].trim();
|
|
218
|
+
|
|
219
|
+
const asMatch = storyLine.match(/^As an?\s+(.+?),?\s*$/i);
|
|
220
|
+
if (asMatch) {
|
|
221
|
+
story.userType = asMatch[1];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const wantMatch = storyLine.match(/^I want\s+(.+?),?\s*$/i);
|
|
225
|
+
if (wantMatch) {
|
|
226
|
+
story.action = wantMatch[1];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const soThatMatch = storyLine.match(/^so that\s+(.+?)\.?\s*$/i);
|
|
230
|
+
if (soThatMatch) {
|
|
231
|
+
story.benefit = soThatMatch[1];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Look for acceptance criteria
|
|
235
|
+
if (storyLine.match(/^acceptance criteria:?$/i)) {
|
|
236
|
+
const criteria: string[] = [];
|
|
237
|
+
let k = j + 1;
|
|
238
|
+
while (k < lines.length && k < j + 20) {
|
|
239
|
+
const criteriaLine = lines[k].trim();
|
|
240
|
+
const criteriaMatch = criteriaLine.match(/^\d+\.\s+(.+)$/);
|
|
241
|
+
if (criteriaMatch) {
|
|
242
|
+
criteria.push(criteriaMatch[1]);
|
|
243
|
+
} else if (criteriaLine && !criteriaLine.match(/^\d+\./)) {
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
k++;
|
|
247
|
+
}
|
|
248
|
+
if (criteria.length > 0) {
|
|
249
|
+
story.acceptanceCriteria = criteria;
|
|
250
|
+
}
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
j++;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
stories.push(story);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return stories;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Extract change log entries from content
|
|
265
|
+
*
|
|
266
|
+
* @param content - Document content
|
|
267
|
+
* @returns Array of change log entries
|
|
268
|
+
*/
|
|
269
|
+
export function extractChangeLog(content: string): BMADChangeLogEntry[] {
|
|
270
|
+
const entries: BMADChangeLogEntry[] = [];
|
|
271
|
+
const lines = content.split('\n');
|
|
272
|
+
|
|
273
|
+
let inTable = false;
|
|
274
|
+
for (const line of lines) {
|
|
275
|
+
// Detect table header
|
|
276
|
+
if (line.match(/\|\s*Date\s*\|\s*Version\s*\|\s*Description\s*\|\s*Author\s*\|/i)) {
|
|
277
|
+
inTable = true;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Skip separator row
|
|
282
|
+
if (inTable && line.match(/\|[\s:-]+\|/)) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Parse table rows
|
|
287
|
+
if (inTable) {
|
|
288
|
+
const rowMatch = line.match(/\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|/);
|
|
289
|
+
if (rowMatch) {
|
|
290
|
+
const [, date, version, description, author] = rowMatch;
|
|
291
|
+
entries.push({
|
|
292
|
+
date: date.trim(),
|
|
293
|
+
version: version.trim(),
|
|
294
|
+
description: description.trim(),
|
|
295
|
+
author: author.trim(),
|
|
296
|
+
});
|
|
297
|
+
} else if (line.trim() && !line.startsWith('|')) {
|
|
298
|
+
// End of table
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return entries;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Identify document type from content
|
|
309
|
+
*
|
|
310
|
+
* @param content - Document content
|
|
311
|
+
* @param frontMatter - Parsed front-matter
|
|
312
|
+
* @returns Document type
|
|
313
|
+
*/
|
|
314
|
+
export function identifyDocumentType(
|
|
315
|
+
content: string,
|
|
316
|
+
frontMatter?: BMADFrontMatter | null
|
|
317
|
+
): BMADDocumentType {
|
|
318
|
+
// Check front-matter
|
|
319
|
+
if (frontMatter?.name) {
|
|
320
|
+
if (/product requirements/i.test(frontMatter.name)) {
|
|
321
|
+
return BMADDocumentType.PRD;
|
|
322
|
+
}
|
|
323
|
+
if (/architecture/i.test(frontMatter.name)) {
|
|
324
|
+
return BMADDocumentType.ARCHITECTURE;
|
|
325
|
+
}
|
|
326
|
+
if (/\bagent\b/i.test(frontMatter.name)) {
|
|
327
|
+
return BMADDocumentType.AGENT;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// v6: hasSidecar field is a strong agent indicator
|
|
332
|
+
if (frontMatter?.hasSidecar !== undefined) {
|
|
333
|
+
return BMADDocumentType.AGENT;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Check content
|
|
337
|
+
if (/(Product Requirements Document|PRD)/i.test(content)) {
|
|
338
|
+
return BMADDocumentType.PRD;
|
|
339
|
+
}
|
|
340
|
+
if (/Architecture Document/i.test(content)) {
|
|
341
|
+
return BMADDocumentType.ARCHITECTURE;
|
|
342
|
+
}
|
|
343
|
+
if (/Epic:/i.test(content) || /Epic Goal/i.test(content)) {
|
|
344
|
+
return BMADDocumentType.EPIC;
|
|
345
|
+
}
|
|
346
|
+
if (/As an?\s+[^\n]{1,200},\s*\nI want\s+[^\n]{1,200},\s*\nso that/i.test(content)) {
|
|
347
|
+
return BMADDocumentType.STORY;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return BMADDocumentType.UNKNOWN;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Analyze content for detection indicators
|
|
355
|
+
*
|
|
356
|
+
* @param content - Document content
|
|
357
|
+
* @param hint - Optional path detection hint
|
|
358
|
+
* @returns Detection indicators
|
|
359
|
+
*/
|
|
360
|
+
export function analyzeContent(content: string, hint?: PathDetectionHint): DetectionIndicators {
|
|
361
|
+
const frontMatter = extractFrontMatter(content);
|
|
362
|
+
const requirements = extractRequirements(content);
|
|
363
|
+
|
|
364
|
+
const pathAnalysis = hint ? analyzePath(hint) : { isBmadFolder: false, isConfigFolder: false };
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
hasYamlFrontMatter: frontMatter !== null && Object.keys(frontMatter).length > 0,
|
|
368
|
+
hasFunctionalRequirements: requirements.some((r) => r.type === RequirementType.FUNCTIONAL),
|
|
369
|
+
hasNonFunctionalRequirements: requirements.some(
|
|
370
|
+
(r) => r.type === RequirementType.NON_FUNCTIONAL
|
|
371
|
+
),
|
|
372
|
+
hasUserStories: requirements.some((r) => r.type === RequirementType.USER_STORY),
|
|
373
|
+
hasUserStoryFormat: /As an?\s+[^\n]{1,200}[,\s]+I want\s+[^\n]{1,200}[,\s]+so that/i.test(
|
|
374
|
+
content
|
|
375
|
+
),
|
|
376
|
+
hasChangeLogTable: /\|\s*Date\s*\|\s*Version\s*\|\s*Description\s*\|\s*Author\s*\|/i.test(
|
|
377
|
+
content
|
|
378
|
+
),
|
|
379
|
+
hasDocumentTitle: /(Product Requirements|Architecture) Document/i.test(content),
|
|
380
|
+
requirementCount: requirements.length,
|
|
381
|
+
hasBmadFolderPath: pathAnalysis.isBmadFolder,
|
|
382
|
+
hasBmadConfigPath: pathAnalysis.isConfigFolder,
|
|
383
|
+
hasHasSidecar: frontMatter?.hasSidecar !== undefined,
|
|
384
|
+
hasHyphenatedVariables: hasHyphenatedVariables(content),
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Calculate detection confidence score
|
|
390
|
+
*
|
|
391
|
+
* @param indicators - Detection indicators
|
|
392
|
+
* @returns Confidence score (0-100)
|
|
393
|
+
*/
|
|
394
|
+
export function calculateConfidenceScore(indicators: DetectionIndicators): number {
|
|
395
|
+
let score = 0;
|
|
396
|
+
|
|
397
|
+
// YAML front-matter (30 points)
|
|
398
|
+
if (indicators.hasYamlFrontMatter) {
|
|
399
|
+
score += 30;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Requirement identifiers (25 points)
|
|
403
|
+
if (
|
|
404
|
+
indicators.hasFunctionalRequirements ||
|
|
405
|
+
indicators.hasNonFunctionalRequirements ||
|
|
406
|
+
indicators.hasUserStories
|
|
407
|
+
) {
|
|
408
|
+
score += 25;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// User story format (20 points)
|
|
412
|
+
if (indicators.hasUserStoryFormat) {
|
|
413
|
+
score += 20;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Change log table (15 points)
|
|
417
|
+
if (indicators.hasChangeLogTable) {
|
|
418
|
+
score += 15;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Document title (10 points)
|
|
422
|
+
if (indicators.hasDocumentTitle) {
|
|
423
|
+
score += 10;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// v6: BMAD folder path (20 points bonus)
|
|
427
|
+
if (indicators.hasBmadFolderPath) {
|
|
428
|
+
score += 20;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// v6: Config folder path (5 points bonus)
|
|
432
|
+
if (indicators.hasBmadConfigPath) {
|
|
433
|
+
score += 5;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// v6: hasSidecar field (15 points bonus)
|
|
437
|
+
if (indicators.hasHasSidecar) {
|
|
438
|
+
score += 15;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// v6: Hyphenated variables like {{var-name}} (10 points bonus)
|
|
442
|
+
if (indicators.hasHyphenatedVariables) {
|
|
443
|
+
score += 10;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return Math.min(100, score);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Build detection reason message
|
|
451
|
+
*
|
|
452
|
+
* @param indicators - Detection indicators
|
|
453
|
+
* @returns Reason message
|
|
454
|
+
*/
|
|
455
|
+
export function buildDetectionReason(indicators: DetectionIndicators): string {
|
|
456
|
+
const reasons: string[] = [];
|
|
457
|
+
|
|
458
|
+
if (indicators.hasYamlFrontMatter) {
|
|
459
|
+
reasons.push('yaml-frontmatter');
|
|
460
|
+
}
|
|
461
|
+
if (
|
|
462
|
+
indicators.hasFunctionalRequirements ||
|
|
463
|
+
indicators.hasNonFunctionalRequirements ||
|
|
464
|
+
indicators.hasUserStories
|
|
465
|
+
) {
|
|
466
|
+
reasons.push(`${indicators.requirementCount} requirements`);
|
|
467
|
+
}
|
|
468
|
+
if (indicators.hasUserStoryFormat) {
|
|
469
|
+
reasons.push('user-story-format');
|
|
470
|
+
}
|
|
471
|
+
if (indicators.hasChangeLogTable) {
|
|
472
|
+
reasons.push('change-log-table');
|
|
473
|
+
}
|
|
474
|
+
if (indicators.hasDocumentTitle) {
|
|
475
|
+
reasons.push('document-title');
|
|
476
|
+
}
|
|
477
|
+
if (indicators.hasBmadFolderPath) {
|
|
478
|
+
reasons.push('bmad-folder');
|
|
479
|
+
}
|
|
480
|
+
if (indicators.hasBmadConfigPath) {
|
|
481
|
+
reasons.push('bmad-config');
|
|
482
|
+
}
|
|
483
|
+
if (indicators.hasHasSidecar) {
|
|
484
|
+
reasons.push('has-sidecar');
|
|
485
|
+
}
|
|
486
|
+
if (indicators.hasHyphenatedVariables) {
|
|
487
|
+
reasons.push('hyphenated-variables');
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return reasons.length > 0 ? reasons.join(', ') : 'no strong indicators';
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Extract document title from content
|
|
495
|
+
*
|
|
496
|
+
* @param content - Document content
|
|
497
|
+
* @returns Title or null
|
|
498
|
+
*/
|
|
499
|
+
export function extractTitle(content: string): string | null {
|
|
500
|
+
const lines = content.split('\n');
|
|
501
|
+
|
|
502
|
+
for (const line of lines) {
|
|
503
|
+
const trimmed = line.trim();
|
|
504
|
+
// Match h1 header
|
|
505
|
+
const h1Match = trimmed.match(/^#\s+(.+)$/);
|
|
506
|
+
if (h1Match) {
|
|
507
|
+
return h1Match[1];
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Extract intent/summary from document
|
|
516
|
+
*
|
|
517
|
+
* @param content - Document content
|
|
518
|
+
* @param docType - Document type
|
|
519
|
+
* @returns Intent description
|
|
520
|
+
*/
|
|
521
|
+
export function extractIntent(content: string, docType: BMADDocumentType): string {
|
|
522
|
+
const lines = content.split('\n');
|
|
523
|
+
let inTargetSection = false;
|
|
524
|
+
const intentLines: string[] = [];
|
|
525
|
+
|
|
526
|
+
// Section headers to look for based on document type
|
|
527
|
+
const targetSections: Record<string, string[]> = {
|
|
528
|
+
[BMADDocumentType.PRD]: ['Executive Summary', 'Product Vision', 'Overview'],
|
|
529
|
+
[BMADDocumentType.ARCHITECTURE]: ['Technical Summary', 'Overview'],
|
|
530
|
+
[BMADDocumentType.EPIC]: ['Epic Goal', 'Goal', 'Overview'],
|
|
531
|
+
[BMADDocumentType.STORY]: ['Description', 'Story'],
|
|
532
|
+
[BMADDocumentType.AGENT]: ['Purpose', 'Role', 'Overview'],
|
|
533
|
+
[BMADDocumentType.UNKNOWN]: ['Overview', 'Summary'],
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
const sections = targetSections[docType] ?? targetSections[BMADDocumentType.UNKNOWN];
|
|
537
|
+
|
|
538
|
+
for (let i = 0; i < lines.length; i++) {
|
|
539
|
+
const line = lines[i];
|
|
540
|
+
const trimmed = line.trim();
|
|
541
|
+
|
|
542
|
+
// Check if this is a target section header
|
|
543
|
+
for (const section of sections) {
|
|
544
|
+
if (trimmed.match(new RegExp(`^#{1,3}\\s+${section}\\s*$`, 'i'))) {
|
|
545
|
+
inTargetSection = true;
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// If in target section, collect lines until next header
|
|
551
|
+
if (inTargetSection) {
|
|
552
|
+
if (trimmed.match(/^#{1,3}\s+/)) {
|
|
553
|
+
// Hit another header, stop
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
if (trimmed && !trimmed.startsWith('#')) {
|
|
557
|
+
intentLines.push(trimmed);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Return first paragraph or first 2 sentences
|
|
563
|
+
const intent = intentLines.join(' ').trim();
|
|
564
|
+
if (intent) {
|
|
565
|
+
const sentences = intent.match(/[^.!?]+[.!?]+/g);
|
|
566
|
+
if (sentences && sentences.length > 0) {
|
|
567
|
+
return sentences.slice(0, 2).join(' ').trim();
|
|
568
|
+
}
|
|
569
|
+
return intent.substring(0, 200).trim() + (intent.length > 200 ? '...' : '');
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Fallback: use title or first non-empty line
|
|
573
|
+
const title = extractTitle(content);
|
|
574
|
+
return title || 'BMAD document';
|
|
575
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { SpecToolAdapter, ExternalSpec } from './types.js';
|
|
2
|
+
|
|
3
|
+
export class AdapterRegistry {
|
|
4
|
+
private static instance: AdapterRegistry;
|
|
5
|
+
private adapters: Map<string, SpecToolAdapter> = new Map();
|
|
6
|
+
|
|
7
|
+
private constructor() {}
|
|
8
|
+
|
|
9
|
+
static getInstance(): AdapterRegistry {
|
|
10
|
+
if (!AdapterRegistry.instance) {
|
|
11
|
+
AdapterRegistry.instance = new AdapterRegistry();
|
|
12
|
+
}
|
|
13
|
+
return AdapterRegistry.instance;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
register(adapter: SpecToolAdapter): void {
|
|
17
|
+
if (this.adapters.has(adapter.name)) {
|
|
18
|
+
throw new Error(`Adapter '${adapter.name}' is already registered`);
|
|
19
|
+
}
|
|
20
|
+
this.adapters.set(adapter.name, adapter);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
unregister(name: string): void {
|
|
24
|
+
this.adapters.delete(name);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
getAdapter(name: string): SpecToolAdapter | undefined {
|
|
28
|
+
return this.adapters.get(name);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getAdapterForFormat(format: string): SpecToolAdapter | undefined {
|
|
32
|
+
for (const adapter of this.adapters.values()) {
|
|
33
|
+
if (adapter.canImport(format)) {
|
|
34
|
+
return adapter;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
listAdapters(): ReadonlyArray<SpecToolAdapter> {
|
|
41
|
+
return Array.from(this.adapters.values());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
listAdapterNames(): ReadonlyArray<string> {
|
|
45
|
+
return Array.from(this.adapters.keys());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
listSupportedFormats(): ReadonlyArray<string> {
|
|
49
|
+
const formats = new Set<string>();
|
|
50
|
+
for (const adapter of this.adapters.values()) {
|
|
51
|
+
for (const format of adapter.supportedFormats) {
|
|
52
|
+
formats.add(format);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return Array.from(formats);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
clear(): void {
|
|
59
|
+
this.adapters.clear();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
detectFormat(spec: ExternalSpec): string | undefined {
|
|
63
|
+
for (const adapter of this.adapters.values()) {
|
|
64
|
+
if (adapter.canImport(spec.format)) {
|
|
65
|
+
return adapter.name;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const registry = AdapterRegistry.getInstance();
|