agileflow 3.4.0 → 3.4.2
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/CHANGELOG.md +10 -0
- package/README.md +4 -4
- package/package.json +1 -1
- package/scripts/agileflow-welcome.js +79 -0
- package/scripts/claude-tmux.sh +12 -36
- package/scripts/lib/ac-test-matcher.js +452 -0
- package/scripts/lib/audit-registry.js +94 -2
- package/scripts/lib/configure-features.js +35 -0
- package/scripts/lib/model-profiles.js +25 -5
- package/scripts/lib/quality-gates.js +163 -0
- package/scripts/lib/signal-detectors.js +43 -0
- package/scripts/lib/status-writer.js +255 -0
- package/scripts/lib/story-claiming.js +128 -45
- package/scripts/lib/task-sync.js +32 -38
- package/scripts/lib/tmux-audit-monitor.js +611 -0
- package/scripts/lib/tmux-group-colors.js +2 -2
- package/scripts/lib/tool-registry.yaml +241 -0
- package/scripts/lib/tool-shed.js +441 -0
- package/scripts/native-team-observer.js +219 -0
- package/scripts/obtain-context.js +14 -0
- package/scripts/ralph-loop.js +30 -5
- package/scripts/smart-detect.js +21 -0
- package/scripts/spawn-audit-sessions.js +373 -45
- package/scripts/team-manager.js +19 -0
- package/src/core/agents/a11y-analyzer-aria.md +155 -0
- package/src/core/agents/a11y-analyzer-forms.md +162 -0
- package/src/core/agents/a11y-analyzer-keyboard.md +175 -0
- package/src/core/agents/a11y-analyzer-semantic.md +153 -0
- package/src/core/agents/a11y-analyzer-visual.md +158 -0
- package/src/core/agents/a11y-consensus.md +248 -0
- package/src/core/agents/ads-consensus.md +74 -0
- package/src/core/agents/ads-generate.md +145 -0
- package/src/core/agents/ads-performance-tracker.md +197 -0
- package/src/core/agents/api-quality-analyzer-conventions.md +148 -0
- package/src/core/agents/api-quality-analyzer-docs.md +176 -0
- package/src/core/agents/api-quality-analyzer-errors.md +183 -0
- package/src/core/agents/api-quality-analyzer-pagination.md +171 -0
- package/src/core/agents/api-quality-analyzer-versioning.md +143 -0
- package/src/core/agents/api-quality-consensus.md +214 -0
- package/src/core/agents/arch-analyzer-circular.md +148 -0
- package/src/core/agents/arch-analyzer-complexity.md +171 -0
- package/src/core/agents/arch-analyzer-coupling.md +146 -0
- package/src/core/agents/arch-analyzer-layering.md +151 -0
- package/src/core/agents/arch-analyzer-patterns.md +162 -0
- package/src/core/agents/arch-consensus.md +227 -0
- package/src/core/commands/adr.md +1 -0
- package/src/core/commands/ads/audit.md +67 -5
- package/src/core/commands/ads/generate.md +238 -0
- package/src/core/commands/ads/health.md +327 -0
- package/src/core/commands/ads/test-plan.md +317 -0
- package/src/core/commands/ads/track.md +288 -0
- package/src/core/commands/ads.md +28 -16
- package/src/core/commands/assign.md +1 -0
- package/src/core/commands/audit.md +43 -6
- package/src/core/commands/babysit.md +90 -6
- package/src/core/commands/baseline.md +1 -0
- package/src/core/commands/blockers.md +1 -0
- package/src/core/commands/board.md +1 -0
- package/src/core/commands/changelog.md +1 -0
- package/src/core/commands/choose.md +1 -0
- package/src/core/commands/ci.md +1 -0
- package/src/core/commands/code/accessibility.md +347 -0
- package/src/core/commands/code/api.md +297 -0
- package/src/core/commands/code/architecture.md +297 -0
- package/src/core/commands/code/completeness.md +43 -6
- package/src/core/commands/code/legal.md +43 -6
- package/src/core/commands/code/logic.md +43 -6
- package/src/core/commands/code/performance.md +43 -6
- package/src/core/commands/code/security.md +43 -6
- package/src/core/commands/code/test.md +43 -6
- package/src/core/commands/configure.md +1 -0
- package/src/core/commands/council.md +1 -0
- package/src/core/commands/deploy.md +1 -0
- package/src/core/commands/diagnose.md +1 -0
- package/src/core/commands/docs.md +1 -0
- package/src/core/commands/epic/edit.md +213 -0
- package/src/core/commands/epic.md +1 -0
- package/src/core/commands/export.md +238 -0
- package/src/core/commands/help.md +16 -1
- package/src/core/commands/ideate/discover.md +7 -3
- package/src/core/commands/ideate/features.md +65 -4
- package/src/core/commands/ideate/new.md +158 -124
- package/src/core/commands/impact.md +1 -0
- package/src/core/commands/learn/explain.md +118 -0
- package/src/core/commands/learn/glossary.md +135 -0
- package/src/core/commands/learn/patterns.md +138 -0
- package/src/core/commands/learn/tour.md +126 -0
- package/src/core/commands/migrate/codemods.md +151 -0
- package/src/core/commands/migrate/plan.md +131 -0
- package/src/core/commands/migrate/scan.md +114 -0
- package/src/core/commands/migrate/validate.md +119 -0
- package/src/core/commands/multi-expert.md +1 -0
- package/src/core/commands/pr.md +1 -0
- package/src/core/commands/review.md +1 -0
- package/src/core/commands/seo/audit.md +61 -6
- package/src/core/commands/sprint.md +1 -0
- package/src/core/commands/status/undo.md +191 -0
- package/src/core/commands/status.md +1 -0
- package/src/core/commands/story/edit.md +204 -0
- package/src/core/commands/story/view.md +29 -7
- package/src/core/commands/story-validate.md +1 -0
- package/src/core/commands/story.md +1 -0
- package/src/core/commands/tdd.md +1 -0
- package/src/core/commands/team/start.md +10 -6
- package/src/core/commands/tests.md +1 -0
- package/src/core/commands/verify.md +27 -1
- package/src/core/commands/workflow.md +2 -0
- package/src/core/teams/backend.json +41 -0
- package/src/core/teams/frontend.json +41 -0
- package/src/core/teams/qa.json +41 -0
- package/src/core/teams/solo.json +35 -0
- package/src/core/templates/agileflow-metadata.json +5 -0
- package/tools/cli/commands/setup.js +85 -3
- package/tools/cli/commands/update.js +42 -0
- package/tools/cli/installers/ide/claude-code.js +68 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.4.2] - 2026-03-07
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- DEPTH=ultradeep|extreme for SEO and Ads audit commands
|
|
14
|
+
|
|
15
|
+
## [3.4.1] - 2026-03-06
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- DEPTH=extreme audit mode, CI feedback loops, and DX quick wins
|
|
19
|
+
|
|
10
20
|
## [3.4.0] - 2026-02-28
|
|
11
21
|
|
|
12
22
|
### Added
|
package/README.md
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/agileflow)
|
|
6
|
-
[](https://docs.agileflow.projectquestorg.com/docs/commands)
|
|
7
|
+
[](https://docs.agileflow.projectquestorg.com/docs/agents)
|
|
8
8
|
[](https://docs.agileflow.projectquestorg.com/docs/features/skills)
|
|
9
9
|
|
|
10
10
|
**AI-driven agile development for Claude Code, Cursor, Windsurf, OpenAI Codex, and more.** Combining Scrum, Kanban, ADRs, and docs-as-code principles into one framework-agnostic system.
|
|
@@ -54,8 +54,8 @@ Traditional project management tools create friction between planning and execut
|
|
|
54
54
|
|
|
55
55
|
| Component | Count | Description |
|
|
56
56
|
|-----------|-------|-------------|
|
|
57
|
-
| [Commands](https://docs.agileflow.projectquestorg.com/docs/commands) |
|
|
58
|
-
| [Agents/Experts](https://docs.agileflow.projectquestorg.com/docs/agents) |
|
|
57
|
+
| [Commands](https://docs.agileflow.projectquestorg.com/docs/commands) | 143 | Slash commands for agile workflows |
|
|
58
|
+
| [Agents/Experts](https://docs.agileflow.projectquestorg.com/docs/agents) | 131 | Specialized agents with self-improving knowledge bases |
|
|
59
59
|
| [Skills](https://docs.agileflow.projectquestorg.com/docs/features/skills) | Dynamic | Browse and install from skills.sh marketplace via `/agileflow:skill:recommend` |
|
|
60
60
|
|
|
61
61
|
---
|
package/package.json
CHANGED
|
@@ -1320,6 +1320,49 @@ function getExpertiseCountFast(rootDir) {
|
|
|
1320
1320
|
return result;
|
|
1321
1321
|
}
|
|
1322
1322
|
|
|
1323
|
+
/**
|
|
1324
|
+
* Check if installed commands are in sync with source.
|
|
1325
|
+
* Counts .md files in three directories:
|
|
1326
|
+
* - .agileflow/commands/ (core install)
|
|
1327
|
+
* - .claude/commands/agileflow/ excluding agents/ (IDE install)
|
|
1328
|
+
* - packages/cli/src/core/commands/ (dogfooding source, if exists)
|
|
1329
|
+
* Returns { sourceCount, coreCount, ideCount, stale, staleCoreInstall } or null on error.
|
|
1330
|
+
*/
|
|
1331
|
+
function checkCommandSync(rootDir) {
|
|
1332
|
+
try {
|
|
1333
|
+
const countMdFiles = dir => {
|
|
1334
|
+
if (!fs.existsSync(dir)) return 0;
|
|
1335
|
+
return fs.readdirSync(dir, { recursive: true }).filter(f => String(f).endsWith('.md')).length;
|
|
1336
|
+
};
|
|
1337
|
+
|
|
1338
|
+
const agileflowDir = getAgileflowDir(rootDir);
|
|
1339
|
+
const coreCount = countMdFiles(path.join(agileflowDir, 'commands'));
|
|
1340
|
+
|
|
1341
|
+
// IDE install: .claude/commands/agileflow/ excluding agents/ subdir
|
|
1342
|
+
const ideDir = path.join(rootDir, '.claude', 'commands', 'agileflow');
|
|
1343
|
+
let ideCount = 0;
|
|
1344
|
+
if (fs.existsSync(ideDir)) {
|
|
1345
|
+
ideCount = fs.readdirSync(ideDir, { recursive: true }).filter(f => {
|
|
1346
|
+
const s = String(f);
|
|
1347
|
+
return s.endsWith('.md') && !s.startsWith('agents/') && !s.startsWith('agents\\');
|
|
1348
|
+
}).length;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// Dogfooding source (only exists in the AgileFlow repo itself)
|
|
1352
|
+
const sourceDir = path.join(rootDir, 'packages', 'cli', 'src', 'core', 'commands');
|
|
1353
|
+
const sourceCount = countMdFiles(sourceDir);
|
|
1354
|
+
|
|
1355
|
+
// Determine staleness
|
|
1356
|
+
const referenceCount = sourceCount > 0 ? sourceCount : coreCount;
|
|
1357
|
+
const stale = referenceCount > 0 && ideCount < referenceCount;
|
|
1358
|
+
const staleCoreInstall = sourceCount > 0 && coreCount < sourceCount;
|
|
1359
|
+
|
|
1360
|
+
return { sourceCount, coreCount, ideCount, stale, staleCoreInstall };
|
|
1361
|
+
} catch (e) {
|
|
1362
|
+
return null;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1323
1366
|
// Full validation function (kept for /agileflow:validate-expertise command)
|
|
1324
1367
|
function validateExpertise(rootDir) {
|
|
1325
1368
|
const result = { total: 0, passed: 0, warnings: 0, failed: 0, issues: [] };
|
|
@@ -1894,6 +1937,9 @@ function main() {
|
|
|
1894
1937
|
}
|
|
1895
1938
|
_mark('expertise');
|
|
1896
1939
|
|
|
1940
|
+
const commandSync = checkCommandSync(rootDir);
|
|
1941
|
+
_mark('cmdSync');
|
|
1942
|
+
|
|
1897
1943
|
const damageControl = checkDamageControl(rootDir, cache);
|
|
1898
1944
|
_mark('damageCtl');
|
|
1899
1945
|
|
|
@@ -2057,6 +2103,21 @@ function main() {
|
|
|
2057
2103
|
);
|
|
2058
2104
|
}
|
|
2059
2105
|
|
|
2106
|
+
// Show command sync staleness notification
|
|
2107
|
+
if (commandSync && commandSync.stale) {
|
|
2108
|
+
const ref = commandSync.sourceCount > 0 ? commandSync.sourceCount : commandSync.coreCount;
|
|
2109
|
+
console.log('');
|
|
2110
|
+
console.log(
|
|
2111
|
+
`${c.amber}⚠️ ${ref - commandSync.ideCount} command(s) out of sync${c.reset} ${c.dim}(IDE has ${commandSync.ideCount}/${ref})${c.reset}`
|
|
2112
|
+
);
|
|
2113
|
+
if (commandSync.staleCoreInstall) {
|
|
2114
|
+
console.log(
|
|
2115
|
+
` ${c.dim}Core install also stale: ${commandSync.coreCount}/${commandSync.sourceCount}${c.reset}`
|
|
2116
|
+
);
|
|
2117
|
+
}
|
|
2118
|
+
console.log(` ${c.slate}Auto-syncing in background...${c.reset}`);
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2060
2121
|
// Show tmux installation notice if tmux auto-spawn is enabled but tmux not installed
|
|
2061
2122
|
if (tmuxAutoSpawnEnabled && !tmuxCheck.available) {
|
|
2062
2123
|
console.log('');
|
|
@@ -2141,6 +2202,24 @@ function main() {
|
|
|
2141
2202
|
|
|
2142
2203
|
_mark('deferred');
|
|
2143
2204
|
|
|
2205
|
+
// Spawn background command sync if stale
|
|
2206
|
+
if (commandSync && commandSync.stale) {
|
|
2207
|
+
try {
|
|
2208
|
+
const isDogfooding = commandSync.sourceCount > 0;
|
|
2209
|
+
if (isDogfooding) {
|
|
2210
|
+
const cliPath = path.join(rootDir, 'packages', 'cli', 'tools', 'cli', 'agileflow-cli.js');
|
|
2211
|
+
if (fs.existsSync(cliPath)) {
|
|
2212
|
+
spawnBackground('node', [cliPath, 'update', '--force', '-d', rootDir], { cwd: rootDir });
|
|
2213
|
+
}
|
|
2214
|
+
} else {
|
|
2215
|
+
spawnBackground('npx', ['agileflow', 'update', '--force'], { cwd: rootDir });
|
|
2216
|
+
}
|
|
2217
|
+
} catch (e) {
|
|
2218
|
+
// Command sync spawn failed, non-critical
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
_mark('cmdSyncSpawn');
|
|
2222
|
+
|
|
2144
2223
|
// Record hook metrics
|
|
2145
2224
|
if (timer && hookMetrics) {
|
|
2146
2225
|
hookMetrics.recordHookMetrics(timer, 'success', null, { rootDir });
|
package/scripts/claude-tmux.sh
CHANGED
|
@@ -41,6 +41,7 @@ SHOW_HELP=false
|
|
|
41
41
|
ATTACH_ONLY=false
|
|
42
42
|
FORCE_NEW=false
|
|
43
43
|
REFRESH_CONFIG=false
|
|
44
|
+
CONFIGURE_SESSION=""
|
|
44
45
|
USE_RESUME=false
|
|
45
46
|
RESUME_SESSION_ID=""
|
|
46
47
|
|
|
@@ -74,6 +75,10 @@ for arg in "$@"; do
|
|
|
74
75
|
REFRESH_CONFIG=true
|
|
75
76
|
shift
|
|
76
77
|
;;
|
|
78
|
+
--configure-session=*)
|
|
79
|
+
CONFIGURE_SESSION="${arg#*=}"
|
|
80
|
+
shift
|
|
81
|
+
;;
|
|
77
82
|
--help|-h)
|
|
78
83
|
SHOW_HELP=true
|
|
79
84
|
shift
|
|
@@ -128,7 +133,6 @@ FREEZE RECOVERY:
|
|
|
128
133
|
Alt+R Respawn pane (fresh shell)
|
|
129
134
|
|
|
130
135
|
OTHER:
|
|
131
|
-
Alt+b Scroll mode (browse history)
|
|
132
136
|
Alt+h Show keybind help panel
|
|
133
137
|
EOF
|
|
134
138
|
exit 0
|
|
@@ -347,40 +351,6 @@ configure_tmux_session() {
|
|
|
347
351
|
# Alt+z to zoom/unzoom pane (fullscreen toggle)
|
|
348
352
|
tmux bind-key -n M-z resize-pane -Z
|
|
349
353
|
|
|
350
|
-
# Alt+b to enter copy mode (for scrolling / browsing history)
|
|
351
|
-
# NOTE: Do NOT use Alt+[ here — \e[ is the CSI prefix for arrow keys and
|
|
352
|
-
# function keys. Binding M-[ in the root table causes accidental copy-mode
|
|
353
|
-
# entry whenever an escape sequence is split by network latency, making the
|
|
354
|
-
# terminal appear to "lose focus" until Escape is pressed.
|
|
355
|
-
tmux bind-key -n M-b copy-mode
|
|
356
|
-
|
|
357
|
-
# Disable ALL command-prompt bindings in copy mode — they open a text input
|
|
358
|
-
# in the status bar which intercepts keystrokes and confuses the workflow.
|
|
359
|
-
# Navigation (arrows, scroll, mouse, PgUp/PgDn) and exit (q/Escape) still work.
|
|
360
|
-
#
|
|
361
|
-
# Emacs copy-mode: goto-line, search, jump-to-char, repeat-count
|
|
362
|
-
tmux unbind-key -T copy-mode g
|
|
363
|
-
tmux unbind-key -T copy-mode C-r
|
|
364
|
-
tmux unbind-key -T copy-mode C-s
|
|
365
|
-
tmux unbind-key -T copy-mode f
|
|
366
|
-
tmux unbind-key -T copy-mode F
|
|
367
|
-
tmux unbind-key -T copy-mode t
|
|
368
|
-
tmux unbind-key -T copy-mode T
|
|
369
|
-
for i in 1 2 3 4 5 6 7 8 9; do
|
|
370
|
-
tmux unbind-key -T copy-mode "M-$i"
|
|
371
|
-
done
|
|
372
|
-
# Vi copy-mode: goto-line, search, jump-to-char, repeat-count
|
|
373
|
-
tmux unbind-key -T copy-mode-vi :
|
|
374
|
-
tmux unbind-key -T copy-mode-vi /
|
|
375
|
-
tmux unbind-key -T copy-mode-vi ?
|
|
376
|
-
tmux unbind-key -T copy-mode-vi f
|
|
377
|
-
tmux unbind-key -T copy-mode-vi F
|
|
378
|
-
tmux unbind-key -T copy-mode-vi t
|
|
379
|
-
tmux unbind-key -T copy-mode-vi T
|
|
380
|
-
for i in 1 2 3 4 5 6 7 8 9; do
|
|
381
|
-
tmux unbind-key -T copy-mode-vi "$i"
|
|
382
|
-
done
|
|
383
|
-
|
|
384
354
|
# ─── Session Creation Keybindings ──────────────────────────────────────────
|
|
385
355
|
# Alt+s to create a new Claude window (starts fresh, future re-runs in same pane resume)
|
|
386
356
|
# Window gets sequential name (claude-2, claude-3, ...) so windows are distinguishable
|
|
@@ -438,13 +408,19 @@ configure_tmux_session() {
|
|
|
438
408
|
printf ' Alt+R Respawn pane (fresh)\\n';\
|
|
439
409
|
printf '\\n';\
|
|
440
410
|
printf ' \\033[1;38;5;208mOTHER\\033[0m\\n';\
|
|
441
|
-
printf ' Alt+b Scroll mode\\n';\
|
|
442
411
|
printf ' Alt+k Unfreeze (Ctrl+C x2)\\n';\
|
|
443
412
|
printf ' Alt+h This help\\n';\
|
|
444
413
|
printf '\\n';\
|
|
445
414
|
read -n 1 -s -r -p ' Press any key to close'"
|
|
446
415
|
}
|
|
447
416
|
|
|
417
|
+
# Handle --configure-session flag — apply theme to a specific session and exit
|
|
418
|
+
# Used by spawn-audit-sessions.js to give audit sessions the same status bar
|
|
419
|
+
if [ -n "$CONFIGURE_SESSION" ]; then
|
|
420
|
+
configure_tmux_session "$CONFIGURE_SESSION"
|
|
421
|
+
exit 0
|
|
422
|
+
fi
|
|
423
|
+
|
|
448
424
|
# Handle --refresh flag — re-apply config to all existing claude-* sessions
|
|
449
425
|
if [ "$REFRESH_CONFIG" = true ]; then
|
|
450
426
|
REFRESHED=0
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ac-test-matcher.js - Automated Acceptance Criteria to Test Mapping
|
|
3
|
+
*
|
|
4
|
+
* Reads story AC from status.json, scans test files for keyword overlap,
|
|
5
|
+
* and returns matched/unmatched AC with confidence levels.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const { matchACToTests } = require('./lib/ac-test-matcher');
|
|
9
|
+
*
|
|
10
|
+
* const result = matchACToTests('US-0042', projectRoot);
|
|
11
|
+
* // result = {
|
|
12
|
+
* // storyId: 'US-0042',
|
|
13
|
+
* // total: 5,
|
|
14
|
+
* // matched: [{ index: 0, ac: '...', confidence: 'high', testFiles: [...] }],
|
|
15
|
+
* // unmatched: [{ index: 2, ac: '...' }],
|
|
16
|
+
* // coverage: 0.6
|
|
17
|
+
* // }
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
|
|
25
|
+
const { getProjectRoot, getStatusPath } = require('../../lib/paths');
|
|
26
|
+
const { safeReadJSON } = require('../../lib/errors');
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Keyword Extraction
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Extract meaningful keywords from an AC string.
|
|
34
|
+
* Filters out stop words and short tokens.
|
|
35
|
+
*/
|
|
36
|
+
const STOP_WORDS = new Set([
|
|
37
|
+
'the',
|
|
38
|
+
'a',
|
|
39
|
+
'an',
|
|
40
|
+
'is',
|
|
41
|
+
'are',
|
|
42
|
+
'was',
|
|
43
|
+
'were',
|
|
44
|
+
'be',
|
|
45
|
+
'been',
|
|
46
|
+
'being',
|
|
47
|
+
'have',
|
|
48
|
+
'has',
|
|
49
|
+
'had',
|
|
50
|
+
'do',
|
|
51
|
+
'does',
|
|
52
|
+
'did',
|
|
53
|
+
'will',
|
|
54
|
+
'would',
|
|
55
|
+
'could',
|
|
56
|
+
'should',
|
|
57
|
+
'may',
|
|
58
|
+
'might',
|
|
59
|
+
'must',
|
|
60
|
+
'shall',
|
|
61
|
+
'can',
|
|
62
|
+
'need',
|
|
63
|
+
'dare',
|
|
64
|
+
'to',
|
|
65
|
+
'of',
|
|
66
|
+
'in',
|
|
67
|
+
'for',
|
|
68
|
+
'on',
|
|
69
|
+
'with',
|
|
70
|
+
'at',
|
|
71
|
+
'by',
|
|
72
|
+
'from',
|
|
73
|
+
'as',
|
|
74
|
+
'into',
|
|
75
|
+
'through',
|
|
76
|
+
'during',
|
|
77
|
+
'before',
|
|
78
|
+
'after',
|
|
79
|
+
'above',
|
|
80
|
+
'below',
|
|
81
|
+
'and',
|
|
82
|
+
'but',
|
|
83
|
+
'or',
|
|
84
|
+
'nor',
|
|
85
|
+
'not',
|
|
86
|
+
'so',
|
|
87
|
+
'yet',
|
|
88
|
+
'both',
|
|
89
|
+
'either',
|
|
90
|
+
'neither',
|
|
91
|
+
'each',
|
|
92
|
+
'every',
|
|
93
|
+
'all',
|
|
94
|
+
'any',
|
|
95
|
+
'few',
|
|
96
|
+
'more',
|
|
97
|
+
'most',
|
|
98
|
+
'other',
|
|
99
|
+
'some',
|
|
100
|
+
'such',
|
|
101
|
+
'no',
|
|
102
|
+
'only',
|
|
103
|
+
'own',
|
|
104
|
+
'same',
|
|
105
|
+
'than',
|
|
106
|
+
'too',
|
|
107
|
+
'very',
|
|
108
|
+
'just',
|
|
109
|
+
'that',
|
|
110
|
+
'this',
|
|
111
|
+
'these',
|
|
112
|
+
'those',
|
|
113
|
+
'it',
|
|
114
|
+
'its',
|
|
115
|
+
'when',
|
|
116
|
+
'where',
|
|
117
|
+
'how',
|
|
118
|
+
'what',
|
|
119
|
+
'which',
|
|
120
|
+
'who',
|
|
121
|
+
'whom',
|
|
122
|
+
'then',
|
|
123
|
+
'there',
|
|
124
|
+
'here',
|
|
125
|
+
'if',
|
|
126
|
+
'else',
|
|
127
|
+
'while',
|
|
128
|
+
'about',
|
|
129
|
+
'up',
|
|
130
|
+
'out',
|
|
131
|
+
'also',
|
|
132
|
+
'given',
|
|
133
|
+
'user',
|
|
134
|
+
'system',
|
|
135
|
+
'able',
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
function extractKeywords(text) {
|
|
139
|
+
if (!text || typeof text !== 'string') return [];
|
|
140
|
+
// Split on non-alphanumeric (keep hyphenated words)
|
|
141
|
+
const tokens = text
|
|
142
|
+
.toLowerCase()
|
|
143
|
+
.replace(/[^a-z0-9-_]/g, ' ')
|
|
144
|
+
.split(/\s+/)
|
|
145
|
+
.filter(t => t.length >= 3 && !STOP_WORDS.has(t));
|
|
146
|
+
|
|
147
|
+
return [...new Set(tokens)];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ============================================================================
|
|
151
|
+
// Test File Discovery
|
|
152
|
+
// ============================================================================
|
|
153
|
+
|
|
154
|
+
/** Common test file patterns */
|
|
155
|
+
const TEST_PATTERNS = [
|
|
156
|
+
/\.test\.[jt]sx?$/,
|
|
157
|
+
/\.spec\.[jt]sx?$/,
|
|
158
|
+
/_test\.[jt]sx?$/,
|
|
159
|
+
/_test\.go$/,
|
|
160
|
+
/test_.*\.py$/,
|
|
161
|
+
/.*_test\.py$/,
|
|
162
|
+
/\.test\.ts$/,
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Recursively find test files under a directory.
|
|
167
|
+
* Skips node_modules, dist, .git, coverage, etc.
|
|
168
|
+
*/
|
|
169
|
+
function findTestFiles(dir, maxDepth = 5) {
|
|
170
|
+
const results = [];
|
|
171
|
+
const skipDirs = new Set([
|
|
172
|
+
'node_modules',
|
|
173
|
+
'.git',
|
|
174
|
+
'dist',
|
|
175
|
+
'build',
|
|
176
|
+
'coverage',
|
|
177
|
+
'.next',
|
|
178
|
+
'.nuxt',
|
|
179
|
+
'.agileflow',
|
|
180
|
+
'.claude',
|
|
181
|
+
'vendor',
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
function walk(currentDir, depth) {
|
|
185
|
+
if (depth > maxDepth) return;
|
|
186
|
+
let entries;
|
|
187
|
+
try {
|
|
188
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
189
|
+
} catch {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
for (const entry of entries) {
|
|
193
|
+
if (entry.isDirectory()) {
|
|
194
|
+
if (!skipDirs.has(entry.name)) {
|
|
195
|
+
walk(path.join(currentDir, entry.name), depth + 1);
|
|
196
|
+
}
|
|
197
|
+
} else if (entry.isFile()) {
|
|
198
|
+
if (TEST_PATTERNS.some(pat => pat.test(entry.name))) {
|
|
199
|
+
results.push(path.join(currentDir, entry.name));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
walk(dir, 0);
|
|
206
|
+
return results;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ============================================================================
|
|
210
|
+
// Test Content Scanning
|
|
211
|
+
// ============================================================================
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Read a test file and extract its content for keyword matching.
|
|
215
|
+
* Returns lowercased content (describe/it blocks, comments, etc.)
|
|
216
|
+
*/
|
|
217
|
+
function readTestContent(filePath) {
|
|
218
|
+
try {
|
|
219
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
220
|
+
return content.toLowerCase();
|
|
221
|
+
} catch {
|
|
222
|
+
return '';
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ============================================================================
|
|
227
|
+
// AC-to-Test Matching
|
|
228
|
+
// ============================================================================
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Calculate keyword overlap between AC keywords and test file content.
|
|
232
|
+
* Returns a confidence score.
|
|
233
|
+
*/
|
|
234
|
+
function calculateConfidence(acKeywords, testContent) {
|
|
235
|
+
if (acKeywords.length === 0) return 0;
|
|
236
|
+
let matchCount = 0;
|
|
237
|
+
for (const kw of acKeywords) {
|
|
238
|
+
if (testContent.includes(kw)) {
|
|
239
|
+
matchCount++;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const ratio = matchCount / acKeywords.length;
|
|
243
|
+
return ratio;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Determine confidence level from ratio.
|
|
248
|
+
*/
|
|
249
|
+
function confidenceLevel(ratio) {
|
|
250
|
+
if (ratio >= 0.6) return 'high';
|
|
251
|
+
if (ratio >= 0.3) return 'medium';
|
|
252
|
+
if (ratio > 0) return 'low';
|
|
253
|
+
return 'none';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Match acceptance criteria to test files.
|
|
258
|
+
*
|
|
259
|
+
* @param {string} storyId - Story ID (e.g., 'US-0042')
|
|
260
|
+
* @param {string} [rootDir] - Project root (auto-detected if omitted)
|
|
261
|
+
* @returns {{ storyId, total, matched, unmatched, coverage, error? }}
|
|
262
|
+
*/
|
|
263
|
+
function matchACToTests(storyId, rootDir) {
|
|
264
|
+
rootDir = rootDir || getProjectRoot();
|
|
265
|
+
|
|
266
|
+
if (!rootDir) {
|
|
267
|
+
return {
|
|
268
|
+
storyId,
|
|
269
|
+
total: 0,
|
|
270
|
+
matched: [],
|
|
271
|
+
unmatched: [],
|
|
272
|
+
coverage: 0,
|
|
273
|
+
error: 'Project root not found',
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Load story from status.json
|
|
278
|
+
const statusPath = getStatusPath(rootDir);
|
|
279
|
+
const result = safeReadJSON(statusPath, { defaultValue: { stories: {} } });
|
|
280
|
+
const status = result.ok ? result.data : null;
|
|
281
|
+
if (!status || !status.stories) {
|
|
282
|
+
return {
|
|
283
|
+
storyId,
|
|
284
|
+
total: 0,
|
|
285
|
+
matched: [],
|
|
286
|
+
unmatched: [],
|
|
287
|
+
coverage: 0,
|
|
288
|
+
error: 'status.json not found or invalid',
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const story = status.stories[storyId];
|
|
293
|
+
if (!story) {
|
|
294
|
+
return {
|
|
295
|
+
storyId,
|
|
296
|
+
total: 0,
|
|
297
|
+
matched: [],
|
|
298
|
+
unmatched: [],
|
|
299
|
+
coverage: 0,
|
|
300
|
+
error: `Story ${storyId} not found`,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const acList = story.acceptance_criteria || story.ac || [];
|
|
305
|
+
if (!Array.isArray(acList) || acList.length === 0) {
|
|
306
|
+
return {
|
|
307
|
+
storyId,
|
|
308
|
+
total: 0,
|
|
309
|
+
matched: [],
|
|
310
|
+
unmatched: [],
|
|
311
|
+
coverage: 0,
|
|
312
|
+
error: 'No acceptance criteria defined',
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Find test files
|
|
317
|
+
const testFiles = findTestFiles(rootDir);
|
|
318
|
+
if (testFiles.length === 0) {
|
|
319
|
+
return {
|
|
320
|
+
storyId,
|
|
321
|
+
total: acList.length,
|
|
322
|
+
matched: [],
|
|
323
|
+
unmatched: acList.map((ac, i) => ({
|
|
324
|
+
index: i,
|
|
325
|
+
ac: typeof ac === 'string' ? ac : ac.text || String(ac),
|
|
326
|
+
})),
|
|
327
|
+
coverage: 0,
|
|
328
|
+
error: 'No test files found',
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Read all test content (cached in memory for this run)
|
|
333
|
+
const testContentMap = {};
|
|
334
|
+
for (const tf of testFiles) {
|
|
335
|
+
testContentMap[tf] = readTestContent(tf);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Match each AC against test files
|
|
339
|
+
const matched = [];
|
|
340
|
+
const unmatched = [];
|
|
341
|
+
|
|
342
|
+
for (let i = 0; i < acList.length; i++) {
|
|
343
|
+
const acText = typeof acList[i] === 'string' ? acList[i] : acList[i].text || String(acList[i]);
|
|
344
|
+
const keywords = extractKeywords(acText);
|
|
345
|
+
|
|
346
|
+
if (keywords.length === 0) {
|
|
347
|
+
unmatched.push({ index: i, ac: acText });
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Find best matching test files
|
|
352
|
+
const fileMatches = [];
|
|
353
|
+
for (const [filePath, content] of Object.entries(testContentMap)) {
|
|
354
|
+
const ratio = calculateConfidence(keywords, content);
|
|
355
|
+
if (ratio > 0) {
|
|
356
|
+
fileMatches.push({
|
|
357
|
+
file: path.relative(rootDir, filePath),
|
|
358
|
+
ratio,
|
|
359
|
+
confidence: confidenceLevel(ratio),
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Sort by match ratio descending
|
|
365
|
+
fileMatches.sort((a, b) => b.ratio - a.ratio);
|
|
366
|
+
|
|
367
|
+
// Take top 3 matches
|
|
368
|
+
const topMatches = fileMatches.slice(0, 3);
|
|
369
|
+
const bestConfidence = topMatches.length > 0 ? topMatches[0].confidence : 'none';
|
|
370
|
+
|
|
371
|
+
if (bestConfidence === 'high' || bestConfidence === 'medium') {
|
|
372
|
+
matched.push({
|
|
373
|
+
index: i,
|
|
374
|
+
ac: acText,
|
|
375
|
+
confidence: bestConfidence,
|
|
376
|
+
keywords,
|
|
377
|
+
testFiles: topMatches.map(m => ({ file: m.file, confidence: m.confidence })),
|
|
378
|
+
});
|
|
379
|
+
} else {
|
|
380
|
+
unmatched.push({
|
|
381
|
+
index: i,
|
|
382
|
+
ac: acText,
|
|
383
|
+
keywords,
|
|
384
|
+
testFiles:
|
|
385
|
+
topMatches.length > 0
|
|
386
|
+
? topMatches.map(m => ({ file: m.file, confidence: m.confidence }))
|
|
387
|
+
: undefined,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
storyId,
|
|
394
|
+
total: acList.length,
|
|
395
|
+
matched,
|
|
396
|
+
unmatched,
|
|
397
|
+
coverage: acList.length > 0 ? matched.length / acList.length : 0,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Write ac_status to status.json for a story based on match results.
|
|
403
|
+
*
|
|
404
|
+
* @param {string} storyId - Story ID
|
|
405
|
+
* @param {Object} matchResult - Result from matchACToTests
|
|
406
|
+
* @param {Object} [manualOverrides] - Manual AC verification { index: 'verified' }
|
|
407
|
+
* @param {string} [rootDir] - Project root
|
|
408
|
+
*/
|
|
409
|
+
function writeACStatus(storyId, matchResult, manualOverrides = {}, rootDir) {
|
|
410
|
+
rootDir = rootDir || getProjectRoot();
|
|
411
|
+
const statusPath = getStatusPath(rootDir);
|
|
412
|
+
const result = safeReadJSON(statusPath);
|
|
413
|
+
if (!result.ok || !result.data || !result.data.stories || !result.data.stories[storyId]) return;
|
|
414
|
+
const status = result.data;
|
|
415
|
+
|
|
416
|
+
const acStatus = {};
|
|
417
|
+
|
|
418
|
+
// Auto-verified from high-confidence matches
|
|
419
|
+
for (const m of matchResult.matched) {
|
|
420
|
+
if (m.confidence === 'high') {
|
|
421
|
+
acStatus[m.index] = 'auto-verified';
|
|
422
|
+
} else {
|
|
423
|
+
acStatus[m.index] = 'likely-covered';
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Unmatched remain unverified
|
|
428
|
+
for (const u of matchResult.unmatched) {
|
|
429
|
+
acStatus[u.index] = 'unverified';
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Apply manual overrides
|
|
433
|
+
for (const [idx, val] of Object.entries(manualOverrides)) {
|
|
434
|
+
acStatus[idx] = val;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
status.stories[storyId].ac_status = acStatus;
|
|
438
|
+
status.stories[storyId].ac_coverage = matchResult.coverage;
|
|
439
|
+
|
|
440
|
+
const { safeWriteJSON: writeJSON } = require('../../lib/errors');
|
|
441
|
+
writeJSON(statusPath, status);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
module.exports = {
|
|
445
|
+
matchACToTests,
|
|
446
|
+
writeACStatus,
|
|
447
|
+
// Exported for testing
|
|
448
|
+
extractKeywords,
|
|
449
|
+
findTestFiles,
|
|
450
|
+
calculateConfidence,
|
|
451
|
+
confidenceLevel,
|
|
452
|
+
};
|