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.
- package/.claude-plugin/marketplace.json +18 -0
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +7 -1
- package/README.md +16 -12
- package/agents/ace-code-discovery-analyst.md +245 -245
- package/agents/ace-code-integration-analyst.md +248 -248
- package/agents/ace-code-reviewer.md +375 -375
- package/agents/ace-product-owner.md +365 -361
- package/agents/ace-project-researcher.md +606 -606
- package/agents/ace-technical-application-architect.md +315 -315
- package/bin/install.js +587 -173
- package/hooks/ace-check-update.js +15 -14
- package/hooks/ace-statusline.js +30 -12
- package/hooks/hooks.json +14 -0
- package/package.json +3 -2
- package/shared/lib/ace-core.js +53 -0
- package/shared/lib/ace-core.test.js +308 -308
- package/shared/lib/ace-story.test.js +250 -250
- package/skills/execute-story/SKILL.md +116 -110
- package/skills/execute-story/script.js +13 -27
- package/skills/execute-story/script.test.js +261 -261
- package/skills/execute-story/story-template.xml +451 -451
- package/skills/execute-story/workflow.xml +3 -1
- package/skills/help/SKILL.md +71 -69
- package/skills/help/script.js +32 -35
- package/skills/help/script.test.js +183 -183
- package/skills/help/workflow.xml +14 -3
- package/skills/init-coding-standards/SKILL.md +91 -72
- package/skills/init-coding-standards/coding-standards-template.xml +531 -531
- package/skills/init-coding-standards/script.js +50 -59
- package/skills/init-coding-standards/script.test.js +70 -70
- package/skills/init-coding-standards/workflow.xml +1 -1
- package/skills/map-cross-cutting/SKILL.md +126 -89
- package/skills/map-cross-cutting/workflow.xml +1 -1
- package/skills/map-guide/SKILL.md +126 -89
- package/skills/map-guide/workflow.xml +1 -1
- package/skills/map-pattern/SKILL.md +125 -89
- package/skills/map-pattern/workflow.xml +1 -1
- package/skills/map-story/SKILL.md +180 -127
- package/skills/map-story/templates/tech-debt-index.xml +125 -125
- package/skills/map-story/workflow.xml +2 -2
- package/skills/map-subsystem/SKILL.md +155 -111
- package/skills/map-subsystem/script.js +51 -60
- package/skills/map-subsystem/script.test.js +68 -68
- package/skills/map-subsystem/templates/subsystem-architecture.xml +343 -343
- package/skills/map-subsystem/templates/subsystem-structure.xml +234 -234
- package/skills/map-subsystem/workflow.xml +1173 -1173
- package/skills/map-sys-doc/SKILL.md +125 -90
- package/skills/map-sys-doc/workflow.xml +1 -1
- package/skills/map-system/SKILL.md +103 -85
- package/skills/map-system/script.js +75 -84
- package/skills/map-system/script.test.js +73 -73
- package/skills/map-system/templates/system-structure.xml +177 -177
- package/skills/map-system/templates/testing-framework.xml +283 -283
- package/skills/map-system/workflow.xml +667 -667
- package/skills/map-walkthrough/SKILL.md +140 -92
- package/skills/map-walkthrough/workflow.xml +457 -457
- package/skills/plan-backlog/SKILL.md +93 -75
- package/skills/plan-backlog/script.js +121 -136
- package/skills/plan-backlog/script.test.js +83 -83
- package/skills/plan-backlog/workflow.xml +1348 -1348
- package/skills/plan-feature/SKILL.md +99 -76
- package/skills/plan-feature/feature-template.xml +361 -361
- package/skills/plan-feature/script.js +131 -148
- package/skills/plan-feature/script.test.js +80 -80
- package/skills/plan-feature/workflow.xml +1 -1
- package/skills/plan-product-vision/SKILL.md +91 -75
- package/skills/plan-product-vision/product-vision-template.xml +227 -227
- package/skills/plan-product-vision/script.js +51 -60
- package/skills/plan-product-vision/script.test.js +69 -69
- package/skills/plan-product-vision/workflow.xml +337 -337
- package/skills/plan-story/SKILL.md +125 -102
- package/skills/plan-story/script.js +18 -49
- package/skills/plan-story/story-template.xml +8 -1
- package/skills/plan-story/workflow.xml +17 -1
- package/skills/research-external-solution/SKILL.md +120 -107
- package/skills/research-external-solution/external-solution-template.xml +832 -832
- package/skills/research-external-solution/script.js +229 -238
- package/skills/research-external-solution/script.test.js +134 -134
- package/skills/research-external-solution/workflow.xml +657 -657
- package/skills/research-integration-solution/SKILL.md +121 -98
- package/skills/research-integration-solution/integration-solution-template.xml +1015 -1015
- package/skills/research-integration-solution/script.js +223 -231
- package/skills/research-integration-solution/script.test.js +134 -134
- package/skills/research-integration-solution/workflow.xml +711 -711
- package/skills/research-story-wiki/SKILL.md +101 -92
- package/skills/research-story-wiki/script.js +223 -231
- package/skills/research-story-wiki/script.test.js +138 -138
- package/skills/research-story-wiki/story-wiki-template.xml +194 -194
- package/skills/research-story-wiki/workflow.xml +473 -473
- package/skills/research-technical-solution/SKILL.md +131 -103
- package/skills/research-technical-solution/script.js +223 -231
- package/skills/research-technical-solution/script.test.js +134 -134
- package/skills/research-technical-solution/technical-solution-template.xml +1025 -1025
- package/skills/research-technical-solution/workflow.xml +761 -761
- package/skills/review-story/SKILL.md +99 -100
- package/skills/review-story/script.js +8 -16
- package/skills/review-story/script.test.js +169 -169
- package/skills/review-story/story-template.xml +451 -451
- package/skills/review-story/workflow.xml +1 -1
- package/skills/update/SKILL.md +65 -53
- 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
|
+
});
|