@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,120 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { join, dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { SpecParser } from '../speckit/parsers/spec-parser.js';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+ const fixturesDir = join(__dirname, 'fixtures/speckit-official/auth-feature');
10
+
11
+ describe('SpecParser - Official Spec-Kit Format', () => {
12
+ let parser: SpecParser;
13
+
14
+ beforeEach(() => {
15
+ parser = new SpecParser();
16
+ });
17
+
18
+ it('should parse spec.md metadata', async () => {
19
+ const content = await readFile(join(fixturesDir, 'spec.md'), 'utf-8');
20
+ const parsed = parser.parseSpec(content);
21
+
22
+ expect(parsed.metadata.feature).toBe('User Authentication System');
23
+ expect(parsed.metadata.branch).toBe('feature/001-auth-system');
24
+ expect(parsed.metadata.date).toBe('2025-01-15');
25
+ expect(parsed.metadata.status).toBe('Draft');
26
+ });
27
+
28
+ it('should parse user scenarios with priorities', async () => {
29
+ const content = await readFile(join(fixturesDir, 'spec.md'), 'utf-8');
30
+ const parsed = parser.parseSpec(content);
31
+
32
+ expect(parsed.userScenarios).toHaveLength(4);
33
+
34
+ const registration = parsed.userScenarios[0];
35
+ expect(registration.priority).toBe('P1');
36
+ expect(registration.title).toBe('User Registration');
37
+ expect(registration.asA).toContain('new user');
38
+ expect(registration.iWantTo).toContain('create an account');
39
+ expect(registration.soThat).toContain('access the application securely');
40
+ expect(registration.acceptanceScenarios.length).toBeGreaterThan(0);
41
+ expect(registration.edgeCases.length).toBeGreaterThan(0);
42
+ });
43
+
44
+ it('should parse functional requirements', async () => {
45
+ const content = await readFile(join(fixturesDir, 'spec.md'), 'utf-8');
46
+ const parsed = parser.parseSpec(content);
47
+
48
+ expect(parsed.requirements.functional.length).toBeGreaterThan(0);
49
+
50
+ const fr001 = parsed.requirements.functional.find((r) => r.code === 'FR-001');
51
+ expect(fr001).toBeDefined();
52
+ expect(fr001?.description).toContain('RFC 5322');
53
+ expect(fr001?.needsClarification).toBe(false);
54
+ });
55
+
56
+ it('should identify requirements needing clarification', async () => {
57
+ const content = await readFile(join(fixturesDir, 'spec.md'), 'utf-8');
58
+ const parsed = parser.parseSpec(content);
59
+
60
+ const clarificationReqs = parsed.requirements.functional.filter((r) => r.needsClarification);
61
+ expect(clarificationReqs.length).toBeGreaterThan(0);
62
+
63
+ const fr008 = parsed.requirements.functional.find((r) => r.code === 'FR-008');
64
+ expect(fr008?.needsClarification).toBe(true);
65
+ expect(fr008?.clarificationQuestion).toContain('Token expiration time');
66
+ });
67
+
68
+ it('should parse key entities', async () => {
69
+ const content = await readFile(join(fixturesDir, 'spec.md'), 'utf-8');
70
+ const parsed = parser.parseSpec(content);
71
+
72
+ expect(parsed.requirements.entities.length).toBeGreaterThan(0);
73
+
74
+ const userEntity = parsed.requirements.entities.find((e) => e.name === 'User');
75
+ expect(userEntity).toBeDefined();
76
+ expect(userEntity?.represents).toContain('person with access');
77
+ expect(userEntity?.keyAttributes).toContain('email');
78
+ expect(userEntity?.relationships.length).toBeGreaterThan(0);
79
+ });
80
+
81
+ it('should parse success criteria', async () => {
82
+ const content = await readFile(join(fixturesDir, 'spec.md'), 'utf-8');
83
+ const parsed = parser.parseSpec(content);
84
+
85
+ expect(parsed.successCriteria.quantitative.length).toBeGreaterThan(0);
86
+ expect(parsed.successCriteria.qualitative.length).toBeGreaterThan(0);
87
+ expect(parsed.successCriteria.security).toBeDefined();
88
+ expect(parsed.successCriteria.security!.length).toBeGreaterThan(0);
89
+ });
90
+
91
+ it('should extract all clarification markers', async () => {
92
+ const content = await readFile(join(fixturesDir, 'spec.md'), 'utf-8');
93
+ const parsed = parser.parseSpec(content);
94
+
95
+ expect(parsed.clarifications.length).toBeGreaterThan(0);
96
+ expect(parsed.clarifications.some((c) => c.includes('remember me'))).toBe(true);
97
+ expect(parsed.clarifications.some((c) => c.includes('token expiration'))).toBe(true);
98
+ });
99
+
100
+ it('should handle edge cases in user scenarios', async () => {
101
+ const content = await readFile(join(fixturesDir, 'spec.md'), 'utf-8');
102
+ const parsed = parser.parseSpec(content);
103
+
104
+ const login = parsed.userScenarios.find((s) => s.title === 'User Login');
105
+ expect(login).toBeDefined();
106
+ expect(login?.edgeCases.length).toBeGreaterThan(0);
107
+ expect(login?.edgeCases.some((e) => e.includes('Invalid credentials'))).toBe(true);
108
+ });
109
+
110
+ it('should parse P2 and P3 scenarios', async () => {
111
+ const content = await readFile(join(fixturesDir, 'spec.md'), 'utf-8');
112
+ const parsed = parser.parseSpec(content);
113
+
114
+ const p2Scenarios = parsed.userScenarios.filter((s) => s.priority === 'P2');
115
+ const p3Scenarios = parsed.userScenarios.filter((s) => s.priority === 'P3');
116
+
117
+ expect(p2Scenarios.length).toBeGreaterThan(0);
118
+ expect(p3Scenarios.length).toBeGreaterThan(0);
119
+ });
120
+ });
@@ -0,0 +1,17 @@
1
+ # Authentication Feature
2
+
3
+ **Scope:** AUTH **Owner:** @alice **Priority:** high
4
+
5
+ ## Tasks
6
+
7
+ ### AUTH-001: Implement login endpoint
8
+
9
+ **Intent:** Create POST /auth/login endpoint with JWT response **Expected
10
+ Outcome:** Returns JWT token on success, 401 on failure **Validation:**
11
+ `pnpm test -- --grep "login"` **Confidence:** high **Tags:** security, api
12
+ **Files:** src/auth/login.ts, src/auth/jwt.ts
13
+
14
+ ### AUTH-002: Add password reset
15
+
16
+ **Intent:** Implement password reset flow with email verification
17
+ **Confidence:** medium **Dependencies:** AUTH-001
@@ -0,0 +1,393 @@
1
+ /**
2
+ * APSMarkdownAdapter Tests
3
+ * Tests for format detection and parsing of APS markdown documents
4
+ */
5
+
6
+ import { readFileSync } from 'node:fs';
7
+ import { join, dirname } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { describe, it, expect } from 'vitest';
10
+ import { APSMarkdownAdapter } from '../adapter.js';
11
+
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+
14
+ describe('APSMarkdownAdapter', () => {
15
+ const adapter = new APSMarkdownAdapter();
16
+
17
+ describe('metadata', () => {
18
+ it('has correct metadata', () => {
19
+ expect(adapter.metadata.name).toBe('aps-markdown');
20
+ expect(adapter.metadata.extensions).toContain('.aps.md');
21
+ expect(adapter.metadata.formats).toContain('aps');
22
+ });
23
+ });
24
+
25
+ describe('detect', () => {
26
+ it('detects .aps.md content with Tasks section', () => {
27
+ const content = `# Feature Plan
28
+
29
+ **Scope:** AUTH **Owner:** @alice
30
+
31
+ ## Tasks
32
+
33
+ ### AUTH-001: Implement login
34
+
35
+ **Intent:** Create login endpoint
36
+ `;
37
+ const result = adapter.detect(content);
38
+ expect(result.detected).toBe(true);
39
+ expect(result.confidence).toBeGreaterThanOrEqual(80);
40
+ });
41
+
42
+ it('detects index file with Modules section', () => {
43
+ const content = `# Project Plan
44
+
45
+ ## Modules
46
+
47
+ ### auth
48
+
49
+ - **Path:** [./modules/auth.aps.md](./modules/auth.aps.md)
50
+ - **Scope:** AUTH
51
+ `;
52
+ const result = adapter.detect(content);
53
+ expect(result.detected).toBe(true);
54
+ // modules(20) + aps-link(25) + scope(10) = 55
55
+ expect(result.confidence).toBeGreaterThanOrEqual(55);
56
+ });
57
+
58
+ it('does not detect regular markdown', () => {
59
+ const content = `# README
60
+
61
+ This is a regular readme file.
62
+
63
+ ## Installation
64
+
65
+ Run npm install.
66
+ `;
67
+ const result = adapter.detect(content);
68
+ expect(result.detected).toBe(false);
69
+ });
70
+
71
+ it('does not detect SpecKit format', () => {
72
+ const content = `# Feature: User Login
73
+
74
+ ## User Story
75
+ As a user I want to login
76
+
77
+ ## Acceptance Criteria
78
+ - Given valid credentials
79
+ - When I submit login form
80
+ - Then I am authenticated
81
+ `;
82
+ const result = adapter.detect(content);
83
+ expect(result.detected).toBe(false);
84
+ });
85
+ });
86
+
87
+ describe('canImport / canExport', () => {
88
+ it('should support importing aps format', () => {
89
+ expect(adapter.canImport('aps')).toBe(true);
90
+ expect(adapter.canImport('aps-markdown')).toBe(true);
91
+ expect(adapter.canImport('.aps.md')).toBe(true);
92
+ });
93
+
94
+ it('should support exporting to aps format', () => {
95
+ expect(adapter.canExport('aps')).toBe(true);
96
+ expect(adapter.canExport('.aps.md')).toBe(true);
97
+ });
98
+
99
+ it('should not support unknown formats', () => {
100
+ expect(adapter.canImport('speckit')).toBe(false);
101
+ expect(adapter.canImport('bmad')).toBe(false);
102
+ });
103
+ });
104
+
105
+ describe('confidence scoring', () => {
106
+ it('has high confidence (90+) for leaf spec with SCOPE-NNN task and Intent', () => {
107
+ const content = `# Authentication Module
108
+
109
+ **Scope:** AUTH **Owner:** @alice **Priority:** high
110
+
111
+ > Handles user authentication and session management.
112
+
113
+ ## Tasks
114
+
115
+ ### AUTH-001: Implement login endpoint
116
+
117
+ **Intent:** Create POST /auth/login endpoint with JWT response
118
+ **Confidence:** high
119
+ **Expected Outcome:** Returns JWT token on success, 401 on failure
120
+ **Tags:** security, api
121
+
122
+ ### AUTH-002: Add password reset
123
+
124
+ **Intent:** Implement password reset flow with email verification
125
+ **Confidence:** medium
126
+ **Dependencies:** AUTH-001
127
+ `;
128
+ const result = adapter.detect(content);
129
+ expect(result.detected).toBe(true);
130
+ expect(result.confidence).toBeGreaterThanOrEqual(90);
131
+ });
132
+
133
+ it('has medium-high confidence (70+) for index with Modules and .aps.md paths', () => {
134
+ const content = `# Project Plan
135
+
136
+ > A multi-module project plan.
137
+
138
+ ## Modules
139
+
140
+ ### auth
141
+
142
+ - **Path:** [./modules/auth.aps.md](./modules/auth.aps.md)
143
+ - **Scope:** AUTH
144
+ - **Owner:** @alice
145
+ - **Priority:** high
146
+
147
+ ### payments
148
+
149
+ - **Path:** [./modules/payments.aps.md](./modules/payments.aps.md)
150
+ - **Scope:** PAY
151
+ - **Owner:** @bob
152
+ - **Priority:** medium
153
+ - **Dependencies:** auth
154
+ `;
155
+ const result = adapter.detect(content);
156
+ expect(result.detected).toBe(true);
157
+ // modules(20) + aps-links(25+5) + scope(10) + owner(5) + priority(5) = 70
158
+ expect(result.confidence).toBeGreaterThanOrEqual(70);
159
+ });
160
+
161
+ it('has low confidence for markdown without APS markers', () => {
162
+ const content = `# Some Document
163
+
164
+ This has a header but no APS-specific content.
165
+
166
+ ## Section One
167
+
168
+ Just regular text here.
169
+ `;
170
+ const result = adapter.detect(content);
171
+ expect(result.confidence).toBeLessThan(50);
172
+ expect(result.detected).toBe(false);
173
+ });
174
+
175
+ it('has partial confidence for partial APS markers', () => {
176
+ const content = `# Feature Plan
177
+
178
+ **Scope:** TEST
179
+
180
+ ## Tasks
181
+
182
+ Some tasks without proper formatting.
183
+ `;
184
+ const result = adapter.detect(content);
185
+ // Has scope(10) and Tasks section(15) but no SCOPE-NNN pattern = 25
186
+ expect(result.confidence).toBeGreaterThanOrEqual(25);
187
+ expect(result.confidence).toBeLessThan(80);
188
+ });
189
+ });
190
+
191
+ describe('detects ID and Packages fields', () => {
192
+ it('detects **ID:** field as APS indicator', () => {
193
+ const content = `# Feature Plan
194
+
195
+ **ID:** AUTH
196
+
197
+ ## Tasks
198
+
199
+ ### AUTH-001: Implement login
200
+
201
+ **Intent:** Create login endpoint
202
+ `;
203
+ const result = adapter.detect(content);
204
+ expect(result.detected).toBe(true);
205
+ expect(result.reason).toContain('scope-field');
206
+ });
207
+
208
+ it('detects **Packages:** field as APS indicator', () => {
209
+ const content = `# Feature Plan
210
+
211
+ **Scope:** AUTH **Packages:** @app/core, @app/utils
212
+
213
+ ## Tasks
214
+
215
+ ### AUTH-001: Implement login
216
+
217
+ **Intent:** Create login endpoint
218
+ `;
219
+ const result = adapter.detect(content);
220
+ expect(result.detected).toBe(true);
221
+ expect(result.reason).toContain('packages-field');
222
+ });
223
+ });
224
+
225
+ describe('detection reasons', () => {
226
+ it('provides reason for leaf spec detection', () => {
227
+ const content = `# Feature
228
+
229
+ **Scope:** AUTH
230
+
231
+ ## Tasks
232
+
233
+ ### AUTH-001: Task one
234
+
235
+ **Intent:** Do something
236
+ `;
237
+ const result = adapter.detect(content);
238
+ expect(result.reason).toBeDefined();
239
+ expect(result.reason).toContain('tasks-section');
240
+ });
241
+
242
+ it('provides reason for index detection', () => {
243
+ const content = `# Project
244
+
245
+ ## Modules
246
+
247
+ ### auth
248
+
249
+ - **Path:** [./auth.aps.md](./auth.aps.md)
250
+ `;
251
+ const result = adapter.detect(content);
252
+ expect(result.reason).toBeDefined();
253
+ expect(result.reason).toContain('modules-section');
254
+ });
255
+ });
256
+
257
+ describe('parse', () => {
258
+ it('parses a leaf spec to APSPlan', async () => {
259
+ const content = readFileSync(join(__dirname, '__fixtures__/simple-leaf.aps.md'), 'utf-8');
260
+
261
+ const result = await adapter.parse(content, {
262
+ repositoryPath: '/test/repo',
263
+ author: 'test-user',
264
+ });
265
+
266
+ expect(result.success).toBe(true);
267
+ expect(result.data).toBeDefined();
268
+ expect(result.data!.intent).toContain('Authentication Feature');
269
+ expect(result.data!.proposed_changes).toHaveLength(2);
270
+ });
271
+
272
+ it('maps task fields to change metadata', async () => {
273
+ const content = readFileSync(join(__dirname, '__fixtures__/simple-leaf.aps.md'), 'utf-8');
274
+
275
+ const result = await adapter.parse(content);
276
+
277
+ expect(result.success).toBe(true);
278
+ const change = result.data!.proposed_changes[0];
279
+ expect(change.description).toContain('AUTH-001');
280
+ expect(change.metadata?.taskId).toBe('AUTH-001');
281
+ expect(change.metadata?.confidence).toBe('high');
282
+ });
283
+
284
+ it('preserves task dependencies', async () => {
285
+ const content = readFileSync(join(__dirname, '__fixtures__/simple-leaf.aps.md'), 'utf-8');
286
+
287
+ const result = await adapter.parse(content);
288
+
289
+ expect(result.success).toBe(true);
290
+ const change2 = result.data!.proposed_changes[1];
291
+ expect(change2.metadata?.dependencies).toEqual(['AUTH-001']);
292
+ });
293
+
294
+ it('infers file_create type from create/add intent', async () => {
295
+ const content = `# Test Feature
296
+
297
+ **Scope:** TEST
298
+
299
+ ## Tasks
300
+
301
+ ### TEST-001: Create new file
302
+
303
+ **Intent:** Create a new configuration file
304
+ **Confidence:** high
305
+ `;
306
+
307
+ const result = await adapter.parse(content);
308
+
309
+ expect(result.success).toBe(true);
310
+ expect(result.data!.proposed_changes[0].type).toBe('file_create');
311
+ });
312
+
313
+ it('infers file_update type from update/modify intent', async () => {
314
+ const content = `# Test Feature
315
+
316
+ **Scope:** TEST
317
+
318
+ ## Tasks
319
+
320
+ ### TEST-001: Update existing code
321
+
322
+ **Intent:** Update the existing handler logic
323
+ **Confidence:** high
324
+ `;
325
+
326
+ const result = await adapter.parse(content);
327
+
328
+ expect(result.success).toBe(true);
329
+ expect(result.data!.proposed_changes[0].type).toBe('file_update');
330
+ });
331
+
332
+ it('infers file_delete type from delete/remove intent', async () => {
333
+ const content = `# Test Feature
334
+
335
+ **Scope:** TEST
336
+
337
+ ## Tasks
338
+
339
+ ### TEST-001: Remove deprecated file
340
+
341
+ **Intent:** Delete the legacy module
342
+ **Confidence:** high
343
+ `;
344
+
345
+ const result = await adapter.parse(content);
346
+
347
+ expect(result.success).toBe(true);
348
+ expect(result.data!.proposed_changes[0].type).toBe('file_delete');
349
+ });
350
+
351
+ it('generates valid plan ID and hash', async () => {
352
+ const content = readFileSync(join(__dirname, '__fixtures__/simple-leaf.aps.md'), 'utf-8');
353
+
354
+ const result = await adapter.parse(content);
355
+
356
+ expect(result.success).toBe(true);
357
+ expect(result.data!.id).toMatch(/^aps-[a-f0-9]{8,16}$/);
358
+ expect(result.data!.hash).toMatch(/^[a-f0-9]{64}$/);
359
+ });
360
+
361
+ it('uses provided plan ID from context', async () => {
362
+ const content = readFileSync(join(__dirname, '__fixtures__/simple-leaf.aps.md'), 'utf-8');
363
+
364
+ const result = await adapter.parse(content, {
365
+ planId: 'aps-12345678',
366
+ });
367
+
368
+ expect(result.success).toBe(true);
369
+ expect(result.data!.id).toBe('aps-12345678');
370
+ });
371
+
372
+ it('returns error for invalid content', async () => {
373
+ const content = 'Invalid content without H1 title';
374
+
375
+ const result = await adapter.parse(content);
376
+
377
+ expect(result.success).toBe(false);
378
+ expect(result.errors).toBeDefined();
379
+ expect(result.errors!.length).toBeGreaterThan(0);
380
+ expect(result.errors![0].code).toBe('PARSE_ERROR');
381
+ });
382
+
383
+ it('uses first file from files array as change path', async () => {
384
+ const content = readFileSync(join(__dirname, '__fixtures__/simple-leaf.aps.md'), 'utf-8');
385
+
386
+ const result = await adapter.parse(content);
387
+
388
+ expect(result.success).toBe(true);
389
+ // AUTH-001 has Files: src/auth/login.ts, src/auth/jwt.ts
390
+ expect(result.data!.proposed_changes[0].path).toBe('src/auth/login.ts');
391
+ });
392
+ });
393
+ });