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