agile-context-engineering 0.5.0 → 0.5.1

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 (102) hide show
  1. package/.claude-plugin/marketplace.json +18 -0
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +7 -1
  4. package/README.md +16 -12
  5. package/agents/ace-code-discovery-analyst.md +245 -245
  6. package/agents/ace-code-integration-analyst.md +248 -248
  7. package/agents/ace-code-reviewer.md +375 -375
  8. package/agents/ace-product-owner.md +365 -361
  9. package/agents/ace-project-researcher.md +606 -606
  10. package/agents/ace-technical-application-architect.md +315 -315
  11. package/bin/install.js +587 -173
  12. package/hooks/ace-check-update.js +15 -14
  13. package/hooks/ace-statusline.js +30 -12
  14. package/hooks/hooks.json +14 -0
  15. package/package.json +3 -2
  16. package/shared/lib/ace-core.js +53 -0
  17. package/shared/lib/ace-core.test.js +308 -308
  18. package/shared/lib/ace-story.test.js +250 -250
  19. package/skills/execute-story/SKILL.md +116 -110
  20. package/skills/execute-story/script.js +13 -27
  21. package/skills/execute-story/script.test.js +261 -261
  22. package/skills/execute-story/story-template.xml +451 -451
  23. package/skills/execute-story/workflow.xml +3 -1
  24. package/skills/help/SKILL.md +71 -69
  25. package/skills/help/script.js +32 -35
  26. package/skills/help/script.test.js +183 -183
  27. package/skills/help/workflow.xml +14 -3
  28. package/skills/init-coding-standards/SKILL.md +91 -72
  29. package/skills/init-coding-standards/coding-standards-template.xml +531 -531
  30. package/skills/init-coding-standards/script.js +50 -59
  31. package/skills/init-coding-standards/script.test.js +70 -70
  32. package/skills/init-coding-standards/workflow.xml +1 -1
  33. package/skills/map-cross-cutting/SKILL.md +126 -89
  34. package/skills/map-cross-cutting/workflow.xml +1 -1
  35. package/skills/map-guide/SKILL.md +126 -89
  36. package/skills/map-guide/workflow.xml +1 -1
  37. package/skills/map-pattern/SKILL.md +125 -89
  38. package/skills/map-pattern/workflow.xml +1 -1
  39. package/skills/map-story/SKILL.md +180 -127
  40. package/skills/map-story/templates/tech-debt-index.xml +125 -125
  41. package/skills/map-story/workflow.xml +2 -2
  42. package/skills/map-subsystem/SKILL.md +155 -111
  43. package/skills/map-subsystem/script.js +51 -60
  44. package/skills/map-subsystem/script.test.js +68 -68
  45. package/skills/map-subsystem/templates/subsystem-architecture.xml +343 -343
  46. package/skills/map-subsystem/templates/subsystem-structure.xml +234 -234
  47. package/skills/map-subsystem/workflow.xml +1173 -1173
  48. package/skills/map-sys-doc/SKILL.md +125 -90
  49. package/skills/map-sys-doc/workflow.xml +1 -1
  50. package/skills/map-system/SKILL.md +103 -85
  51. package/skills/map-system/script.js +75 -84
  52. package/skills/map-system/script.test.js +73 -73
  53. package/skills/map-system/templates/system-structure.xml +177 -177
  54. package/skills/map-system/templates/testing-framework.xml +283 -283
  55. package/skills/map-system/workflow.xml +667 -667
  56. package/skills/map-walkthrough/SKILL.md +140 -92
  57. package/skills/map-walkthrough/workflow.xml +457 -457
  58. package/skills/plan-backlog/SKILL.md +93 -75
  59. package/skills/plan-backlog/script.js +121 -136
  60. package/skills/plan-backlog/script.test.js +83 -83
  61. package/skills/plan-backlog/workflow.xml +1348 -1348
  62. package/skills/plan-feature/SKILL.md +99 -76
  63. package/skills/plan-feature/feature-template.xml +361 -361
  64. package/skills/plan-feature/script.js +131 -148
  65. package/skills/plan-feature/script.test.js +80 -80
  66. package/skills/plan-feature/workflow.xml +1 -1
  67. package/skills/plan-product-vision/SKILL.md +91 -75
  68. package/skills/plan-product-vision/product-vision-template.xml +227 -227
  69. package/skills/plan-product-vision/script.js +51 -60
  70. package/skills/plan-product-vision/script.test.js +69 -69
  71. package/skills/plan-product-vision/workflow.xml +337 -337
  72. package/skills/plan-story/SKILL.md +125 -102
  73. package/skills/plan-story/script.js +18 -49
  74. package/skills/plan-story/story-template.xml +8 -1
  75. package/skills/plan-story/workflow.xml +17 -1
  76. package/skills/research-external-solution/SKILL.md +120 -107
  77. package/skills/research-external-solution/external-solution-template.xml +832 -832
  78. package/skills/research-external-solution/script.js +229 -238
  79. package/skills/research-external-solution/script.test.js +134 -134
  80. package/skills/research-external-solution/workflow.xml +657 -657
  81. package/skills/research-integration-solution/SKILL.md +121 -98
  82. package/skills/research-integration-solution/integration-solution-template.xml +1015 -1015
  83. package/skills/research-integration-solution/script.js +223 -231
  84. package/skills/research-integration-solution/script.test.js +134 -134
  85. package/skills/research-integration-solution/workflow.xml +711 -711
  86. package/skills/research-story-wiki/SKILL.md +101 -92
  87. package/skills/research-story-wiki/script.js +223 -231
  88. package/skills/research-story-wiki/script.test.js +138 -138
  89. package/skills/research-story-wiki/story-wiki-template.xml +194 -194
  90. package/skills/research-story-wiki/workflow.xml +473 -473
  91. package/skills/research-technical-solution/SKILL.md +131 -103
  92. package/skills/research-technical-solution/script.js +223 -231
  93. package/skills/research-technical-solution/script.test.js +134 -134
  94. package/skills/research-technical-solution/technical-solution-template.xml +1025 -1025
  95. package/skills/research-technical-solution/workflow.xml +761 -761
  96. package/skills/review-story/SKILL.md +99 -100
  97. package/skills/review-story/script.js +8 -16
  98. package/skills/review-story/script.test.js +169 -169
  99. package/skills/review-story/story-template.xml +451 -451
  100. package/skills/review-story/workflow.xml +1 -1
  101. package/skills/update/SKILL.md +65 -53
  102. package/skills/update/workflow.xml +21 -5
@@ -1,250 +1,250 @@
1
- const { describe, it } = require('node:test');
2
- const assert = require('node:assert');
3
-
4
- const {
5
- classifyStoryParam, extractMarkdownSection, extractStoryMetadata,
6
- extractIssueNumber, extractStoryRequirements, extractWikiReferences,
7
- computeStoryPaths,
8
- } = require('./ace-story');
9
-
10
- const SAMPLE_STORY = `# S3: Display OAuth Provider Buttons
11
-
12
- **Feature**: F3 OAuth2 Login Flow | **Epic**: #45 User Authentication
13
- **Status**: Refined | **Size**: 3 | **Sprint**: Sprint 2 | **Link**: [#95](https://github.com/owner/repo/issues/95)
14
-
15
- ## User Story
16
-
17
- > As a returning customer,
18
- > I want to click a Google or GitHub login button,
19
- > so that I can authenticate without remembering a site-specific password.
20
-
21
- ## Description
22
-
23
- This story adds OAuth provider buttons to the login page. It builds on the
24
- auth service foundation (S1) and enables the token exchange flow (S4).
25
-
26
- ## Acceptance Criteria
27
-
28
- ### Scenario: Successful Google login
29
-
30
- **Given** the user is on the login page and has a valid Google account
31
- **When** they click the "Sign in with Google" button and complete Google's OAuth flow
32
- **Then** they are redirected to the dashboard and see their Google profile name
33
-
34
- ### Scenario: Provider unavailable
35
-
36
- **Given** the user is on the login page and the Google OAuth service is unreachable
37
- **When** they click the "Sign in with Google" button
38
- **Then** they see an error message "Login service temporarily unavailable. Please try again."
39
-
40
- ### Scenario: GitHub login button displayed
41
-
42
- **Given** the user navigates to the login page
43
- **When** the page loads
44
- **Then** they see a "Sign in with GitHub" button alongside the Google button
45
-
46
- ## Out of Scope
47
-
48
- - Token refresh logic (handled by S4)
49
- - Account linking (future feature)
50
-
51
- ## Definition of Done
52
-
53
- - [ ] All acceptance criteria scenarios pass
54
- - [ ] Code reviewed and approved
55
-
56
- ## Relevant Wiki
57
-
58
- ### System-Wide
59
-
60
- - \`.docs/wiki/system-wide/system-structure.md\` — Mandatory system-wide context
61
- - \`.docs/wiki/system-wide/coding-standards.md\` — Mandatory system-wide context
62
-
63
- ### Systems
64
- - \`.docs/wiki/subsystems/auth/systems/oauth-provider.md\` — Implements the provider abstraction
65
-
66
- ### Patterns
67
- - \`.docs/wiki/subsystems/auth/patterns/strategy-pattern.md\` — Each OAuth provider is a strategy
68
- `;
69
-
70
- // ─── classifyStoryParam ──────────────────────────────────────────────────────
71
-
72
- describe('classifyStoryParam', () => {
73
- it('classifies file path', () => {
74
- const result = classifyStoryParam('.ace/artifacts/product/e1/f1/s1/s1.md');
75
- assert.strictEqual(result.type, 'file');
76
- assert.ok(result.filePath.includes('s1.md'));
77
- });
78
-
79
- it('classifies GitHub URL', () => {
80
- const result = classifyStoryParam('https://github.com/owner/repo/issues/123');
81
- assert.strictEqual(result.type, 'github-url');
82
- assert.strictEqual(result.repo, 'owner/repo');
83
- assert.strictEqual(result.issueNumber, 123);
84
- });
85
-
86
- it('classifies issue number', () => {
87
- const result = classifyStoryParam('42');
88
- assert.strictEqual(result.type, 'issue-number');
89
- assert.strictEqual(result.issueNumber, 42);
90
- });
91
-
92
- it('returns null type for empty input', () => {
93
- assert.strictEqual(classifyStoryParam(null).type, null);
94
- assert.strictEqual(classifyStoryParam('').type, null);
95
- assert.strictEqual(classifyStoryParam(undefined).type, null);
96
- });
97
-
98
- it('returns invalid for unrecognized GitHub URL', () => {
99
- const result = classifyStoryParam('https://github.com/owner/repo/pulls/5');
100
- assert.strictEqual(result.type, 'invalid');
101
- });
102
- });
103
-
104
- // ─── extractMarkdownSection ──────────────────────────────────────────────────
105
-
106
- describe('extractMarkdownSection', () => {
107
- it('extracts section content', () => {
108
- const result = extractMarkdownSection(SAMPLE_STORY, 'Description', 2);
109
- assert.ok(result.includes('OAuth provider buttons'));
110
- });
111
-
112
- it('returns null for non-existent section', () => {
113
- assert.strictEqual(extractMarkdownSection(SAMPLE_STORY, 'Nonexistent', 2), null);
114
- });
115
-
116
- it('stops at next heading of same level', () => {
117
- const result = extractMarkdownSection(SAMPLE_STORY, 'Out of Scope', 2);
118
- assert.ok(result.includes('Token refresh'));
119
- assert.ok(!result.includes('Definition of Done'));
120
- });
121
- });
122
-
123
- // ─── extractStoryMetadata ────────────────────────────────────────────────────
124
-
125
- describe('extractStoryMetadata', () => {
126
- it('extracts full metadata from sample story', () => {
127
- const meta = extractStoryMetadata(SAMPLE_STORY);
128
- assert.strictEqual(meta.id, 'S3');
129
- assert.strictEqual(meta.title, 'Display OAuth Provider Buttons');
130
- assert.strictEqual(meta.status, 'Refined');
131
- assert.strictEqual(meta.size, '3');
132
- assert.strictEqual(meta.sprint, 'Sprint 2');
133
- assert.strictEqual(meta.feature.id, 'F3');
134
- assert.strictEqual(meta.feature.title, 'OAuth2 Login Flow');
135
- assert.strictEqual(meta.epic.id, '#45');
136
- assert.strictEqual(meta.epic.title, 'User Authentication');
137
- });
138
-
139
- it('returns nulls for empty content', () => {
140
- const meta = extractStoryMetadata(null);
141
- assert.strictEqual(meta.id, null);
142
- assert.strictEqual(meta.title, null);
143
- assert.strictEqual(meta.feature.id, null);
144
- });
145
-
146
- it('extracts link field', () => {
147
- const meta = extractStoryMetadata(SAMPLE_STORY);
148
- assert.ok(meta.link.includes('#95'));
149
- });
150
- });
151
-
152
- // ─── extractIssueNumber ──────────────────────────────────────────────────────
153
-
154
- describe('extractIssueNumber', () => {
155
- it('extracts from markdown link format', () => {
156
- assert.strictEqual(extractIssueNumber('[#187](https://github.com/owner/repo/issues/187)'), 187);
157
- });
158
-
159
- it('extracts from hash format', () => {
160
- assert.strictEqual(extractIssueNumber('#95'), 95);
161
- });
162
-
163
- it('returns null for null input', () => {
164
- assert.strictEqual(extractIssueNumber(null), null);
165
- });
166
-
167
- it('returns null for no match', () => {
168
- assert.strictEqual(extractIssueNumber('no number here'), null);
169
- });
170
- });
171
-
172
- // ─── extractStoryRequirements ────────────────────────────────────────────────
173
-
174
- describe('extractStoryRequirements', () => {
175
- it('extracts user story, description, and AC count', () => {
176
- const req = extractStoryRequirements(SAMPLE_STORY);
177
- assert.ok(req.user_story.includes('returning customer'));
178
- assert.ok(req.description.includes('OAuth provider buttons'));
179
- assert.strictEqual(req.acceptance_criteria_count, 3);
180
- });
181
-
182
- it('strips blockquote prefix from user story', () => {
183
- const req = extractStoryRequirements(SAMPLE_STORY);
184
- assert.ok(!req.user_story.startsWith('>'));
185
- });
186
-
187
- it('returns zeros/nulls for empty content', () => {
188
- const req = extractStoryRequirements(null);
189
- assert.strictEqual(req.user_story, null);
190
- assert.strictEqual(req.description, null);
191
- assert.strictEqual(req.acceptance_criteria_count, 0);
192
- });
193
- });
194
-
195
- // ─── extractWikiReferences ───────────────────────────────────────────────────
196
-
197
- describe('extractWikiReferences', () => {
198
- it('extracts system-wide references', () => {
199
- const refs = extractWikiReferences(SAMPLE_STORY);
200
- assert.strictEqual(refs.system_wide.length, 2);
201
- assert.ok(refs.system_wide.includes('.docs/wiki/system-wide/system-structure.md'));
202
- });
203
-
204
- it('extracts subsystem docs with categories', () => {
205
- const refs = extractWikiReferences(SAMPLE_STORY);
206
- assert.strictEqual(refs.subsystem_docs.length, 2);
207
-
208
- const oauthDoc = refs.subsystem_docs.find(d => d.path.includes('oauth-provider'));
209
- assert.ok(oauthDoc);
210
- assert.strictEqual(oauthDoc.category, 'systems');
211
-
212
- const strategyDoc = refs.subsystem_docs.find(d => d.path.includes('strategy-pattern'));
213
- assert.ok(strategyDoc);
214
- assert.strictEqual(strategyDoc.category, 'patterns');
215
- });
216
-
217
- it('computes total count', () => {
218
- const refs = extractWikiReferences(SAMPLE_STORY);
219
- assert.strictEqual(refs.total_count, 4);
220
- });
221
-
222
- it('returns empty for content without wiki section', () => {
223
- const refs = extractWikiReferences('# No wiki here');
224
- assert.strictEqual(refs.total_count, 0);
225
- assert.deepStrictEqual(refs.system_wide, []);
226
- });
227
- });
228
-
229
- // ─── computeStoryPaths ───────────────────────────────────────────────────────
230
-
231
- describe('computeStoryPaths', () => {
232
- it('generates correct slugs and paths', () => {
233
- const paths = computeStoryPaths('E1', 'Platform', 'F3', 'OAuth Login', 'S1', 'Add Button');
234
- assert.strictEqual(paths.epic_slug, 'e1-platform');
235
- assert.strictEqual(paths.feature_slug, 'f3-oauth-login');
236
- assert.strictEqual(paths.story_slug, 's1-add-button');
237
- assert.strictEqual(paths.story_dir, '.ace/artifacts/product/e1-platform/f3-oauth-login/s1-add-button');
238
- assert.strictEqual(paths.story_file, '.ace/artifacts/product/e1-platform/f3-oauth-login/s1-add-button/s1-add-button.md');
239
- assert.ok(paths.external_analysis_file.endsWith('external-analysis.md'));
240
- assert.ok(paths.integration_analysis_file.endsWith('integration-analysis.md'));
241
- assert.ok(paths.feature_file.endsWith('f3-oauth-login.md'));
242
- });
243
-
244
- it('handles missing titles with fallback slugs', () => {
245
- const paths = computeStoryPaths('', '', '', '', '', '');
246
- assert.strictEqual(paths.epic_slug, 'unknown-epic');
247
- assert.strictEqual(paths.feature_slug, 'unknown-feature');
248
- assert.strictEqual(paths.story_slug, 'unknown-story');
249
- });
250
- });
1
+ const { describe, it } = require('node:test');
2
+ const assert = require('node:assert');
3
+
4
+ const {
5
+ classifyStoryParam, extractMarkdownSection, extractStoryMetadata,
6
+ extractIssueNumber, extractStoryRequirements, extractWikiReferences,
7
+ computeStoryPaths,
8
+ } = require('./ace-story');
9
+
10
+ const SAMPLE_STORY = `# S3: Display OAuth Provider Buttons
11
+
12
+ **Feature**: F3 OAuth2 Login Flow | **Epic**: #45 User Authentication
13
+ **Status**: Refined | **Size**: 3 | **Sprint**: Sprint 2 | **Link**: [#95](https://github.com/owner/repo/issues/95)
14
+
15
+ ## User Story
16
+
17
+ > As a returning customer,
18
+ > I want to click a Google or GitHub login button,
19
+ > so that I can authenticate without remembering a site-specific password.
20
+
21
+ ## Description
22
+
23
+ This story adds OAuth provider buttons to the login page. It builds on the
24
+ auth service foundation (S1) and enables the token exchange flow (S4).
25
+
26
+ ## Acceptance Criteria
27
+
28
+ ### Scenario: Successful Google login
29
+
30
+ **Given** the user is on the login page and has a valid Google account
31
+ **When** they click the "Sign in with Google" button and complete Google's OAuth flow
32
+ **Then** they are redirected to the dashboard and see their Google profile name
33
+
34
+ ### Scenario: Provider unavailable
35
+
36
+ **Given** the user is on the login page and the Google OAuth service is unreachable
37
+ **When** they click the "Sign in with Google" button
38
+ **Then** they see an error message "Login service temporarily unavailable. Please try again."
39
+
40
+ ### Scenario: GitHub login button displayed
41
+
42
+ **Given** the user navigates to the login page
43
+ **When** the page loads
44
+ **Then** they see a "Sign in with GitHub" button alongside the Google button
45
+
46
+ ## Out of Scope
47
+
48
+ - Token refresh logic (handled by S4)
49
+ - Account linking (future feature)
50
+
51
+ ## Definition of Done
52
+
53
+ - [ ] All acceptance criteria scenarios pass
54
+ - [ ] Code reviewed and approved
55
+
56
+ ## Relevant Wiki
57
+
58
+ ### System-Wide
59
+
60
+ - \`.docs/wiki/system-wide/system-structure.md\` — Mandatory system-wide context
61
+ - \`.docs/wiki/system-wide/coding-standards.md\` — Mandatory system-wide context
62
+
63
+ ### Systems
64
+ - \`.docs/wiki/subsystems/auth/systems/oauth-provider.md\` — Implements the provider abstraction
65
+
66
+ ### Patterns
67
+ - \`.docs/wiki/subsystems/auth/patterns/strategy-pattern.md\` — Each OAuth provider is a strategy
68
+ `;
69
+
70
+ // ─── classifyStoryParam ──────────────────────────────────────────────────────
71
+
72
+ describe('classifyStoryParam', () => {
73
+ it('classifies file path', () => {
74
+ const result = classifyStoryParam('.ace/artifacts/product/e1/f1/s1/s1.md');
75
+ assert.strictEqual(result.type, 'file');
76
+ assert.ok(result.filePath.includes('s1.md'));
77
+ });
78
+
79
+ it('classifies GitHub URL', () => {
80
+ const result = classifyStoryParam('https://github.com/owner/repo/issues/123');
81
+ assert.strictEqual(result.type, 'github-url');
82
+ assert.strictEqual(result.repo, 'owner/repo');
83
+ assert.strictEqual(result.issueNumber, 123);
84
+ });
85
+
86
+ it('classifies issue number', () => {
87
+ const result = classifyStoryParam('42');
88
+ assert.strictEqual(result.type, 'issue-number');
89
+ assert.strictEqual(result.issueNumber, 42);
90
+ });
91
+
92
+ it('returns null type for empty input', () => {
93
+ assert.strictEqual(classifyStoryParam(null).type, null);
94
+ assert.strictEqual(classifyStoryParam('').type, null);
95
+ assert.strictEqual(classifyStoryParam(undefined).type, null);
96
+ });
97
+
98
+ it('returns invalid for unrecognized GitHub URL', () => {
99
+ const result = classifyStoryParam('https://github.com/owner/repo/pulls/5');
100
+ assert.strictEqual(result.type, 'invalid');
101
+ });
102
+ });
103
+
104
+ // ─── extractMarkdownSection ──────────────────────────────────────────────────
105
+
106
+ describe('extractMarkdownSection', () => {
107
+ it('extracts section content', () => {
108
+ const result = extractMarkdownSection(SAMPLE_STORY, 'Description', 2);
109
+ assert.ok(result.includes('OAuth provider buttons'));
110
+ });
111
+
112
+ it('returns null for non-existent section', () => {
113
+ assert.strictEqual(extractMarkdownSection(SAMPLE_STORY, 'Nonexistent', 2), null);
114
+ });
115
+
116
+ it('stops at next heading of same level', () => {
117
+ const result = extractMarkdownSection(SAMPLE_STORY, 'Out of Scope', 2);
118
+ assert.ok(result.includes('Token refresh'));
119
+ assert.ok(!result.includes('Definition of Done'));
120
+ });
121
+ });
122
+
123
+ // ─── extractStoryMetadata ────────────────────────────────────────────────────
124
+
125
+ describe('extractStoryMetadata', () => {
126
+ it('extracts full metadata from sample story', () => {
127
+ const meta = extractStoryMetadata(SAMPLE_STORY);
128
+ assert.strictEqual(meta.id, 'S3');
129
+ assert.strictEqual(meta.title, 'Display OAuth Provider Buttons');
130
+ assert.strictEqual(meta.status, 'Refined');
131
+ assert.strictEqual(meta.size, '3');
132
+ assert.strictEqual(meta.sprint, 'Sprint 2');
133
+ assert.strictEqual(meta.feature.id, 'F3');
134
+ assert.strictEqual(meta.feature.title, 'OAuth2 Login Flow');
135
+ assert.strictEqual(meta.epic.id, '#45');
136
+ assert.strictEqual(meta.epic.title, 'User Authentication');
137
+ });
138
+
139
+ it('returns nulls for empty content', () => {
140
+ const meta = extractStoryMetadata(null);
141
+ assert.strictEqual(meta.id, null);
142
+ assert.strictEqual(meta.title, null);
143
+ assert.strictEqual(meta.feature.id, null);
144
+ });
145
+
146
+ it('extracts link field', () => {
147
+ const meta = extractStoryMetadata(SAMPLE_STORY);
148
+ assert.ok(meta.link.includes('#95'));
149
+ });
150
+ });
151
+
152
+ // ─── extractIssueNumber ──────────────────────────────────────────────────────
153
+
154
+ describe('extractIssueNumber', () => {
155
+ it('extracts from markdown link format', () => {
156
+ assert.strictEqual(extractIssueNumber('[#187](https://github.com/owner/repo/issues/187)'), 187);
157
+ });
158
+
159
+ it('extracts from hash format', () => {
160
+ assert.strictEqual(extractIssueNumber('#95'), 95);
161
+ });
162
+
163
+ it('returns null for null input', () => {
164
+ assert.strictEqual(extractIssueNumber(null), null);
165
+ });
166
+
167
+ it('returns null for no match', () => {
168
+ assert.strictEqual(extractIssueNumber('no number here'), null);
169
+ });
170
+ });
171
+
172
+ // ─── extractStoryRequirements ────────────────────────────────────────────────
173
+
174
+ describe('extractStoryRequirements', () => {
175
+ it('extracts user story, description, and AC count', () => {
176
+ const req = extractStoryRequirements(SAMPLE_STORY);
177
+ assert.ok(req.user_story.includes('returning customer'));
178
+ assert.ok(req.description.includes('OAuth provider buttons'));
179
+ assert.strictEqual(req.acceptance_criteria_count, 3);
180
+ });
181
+
182
+ it('strips blockquote prefix from user story', () => {
183
+ const req = extractStoryRequirements(SAMPLE_STORY);
184
+ assert.ok(!req.user_story.startsWith('>'));
185
+ });
186
+
187
+ it('returns zeros/nulls for empty content', () => {
188
+ const req = extractStoryRequirements(null);
189
+ assert.strictEqual(req.user_story, null);
190
+ assert.strictEqual(req.description, null);
191
+ assert.strictEqual(req.acceptance_criteria_count, 0);
192
+ });
193
+ });
194
+
195
+ // ─── extractWikiReferences ───────────────────────────────────────────────────
196
+
197
+ describe('extractWikiReferences', () => {
198
+ it('extracts system-wide references', () => {
199
+ const refs = extractWikiReferences(SAMPLE_STORY);
200
+ assert.strictEqual(refs.system_wide.length, 2);
201
+ assert.ok(refs.system_wide.includes('.docs/wiki/system-wide/system-structure.md'));
202
+ });
203
+
204
+ it('extracts subsystem docs with categories', () => {
205
+ const refs = extractWikiReferences(SAMPLE_STORY);
206
+ assert.strictEqual(refs.subsystem_docs.length, 2);
207
+
208
+ const oauthDoc = refs.subsystem_docs.find(d => d.path.includes('oauth-provider'));
209
+ assert.ok(oauthDoc);
210
+ assert.strictEqual(oauthDoc.category, 'systems');
211
+
212
+ const strategyDoc = refs.subsystem_docs.find(d => d.path.includes('strategy-pattern'));
213
+ assert.ok(strategyDoc);
214
+ assert.strictEqual(strategyDoc.category, 'patterns');
215
+ });
216
+
217
+ it('computes total count', () => {
218
+ const refs = extractWikiReferences(SAMPLE_STORY);
219
+ assert.strictEqual(refs.total_count, 4);
220
+ });
221
+
222
+ it('returns empty for content without wiki section', () => {
223
+ const refs = extractWikiReferences('# No wiki here');
224
+ assert.strictEqual(refs.total_count, 0);
225
+ assert.deepStrictEqual(refs.system_wide, []);
226
+ });
227
+ });
228
+
229
+ // ─── computeStoryPaths ───────────────────────────────────────────────────────
230
+
231
+ describe('computeStoryPaths', () => {
232
+ it('generates correct slugs and paths', () => {
233
+ const paths = computeStoryPaths('E1', 'Platform', 'F3', 'OAuth Login', 'S1', 'Add Button');
234
+ assert.strictEqual(paths.epic_slug, 'e1-platform');
235
+ assert.strictEqual(paths.feature_slug, 'f3-oauth-login');
236
+ assert.strictEqual(paths.story_slug, 's1-add-button');
237
+ assert.strictEqual(paths.story_dir, '.ace/artifacts/product/e1-platform/f3-oauth-login/s1-add-button');
238
+ assert.strictEqual(paths.story_file, '.ace/artifacts/product/e1-platform/f3-oauth-login/s1-add-button/s1-add-button.md');
239
+ assert.ok(paths.external_analysis_file.endsWith('external-analysis.md'));
240
+ assert.ok(paths.integration_analysis_file.endsWith('integration-analysis.md'));
241
+ assert.ok(paths.feature_file.endsWith('f3-oauth-login.md'));
242
+ });
243
+
244
+ it('handles missing titles with fallback slugs', () => {
245
+ const paths = computeStoryPaths('', '', '', '', '', '');
246
+ assert.strictEqual(paths.epic_slug, 'unknown-epic');
247
+ assert.strictEqual(paths.feature_slug, 'unknown-feature');
248
+ assert.strictEqual(paths.story_slug, 'unknown-story');
249
+ });
250
+ });