tlc-claude-code 2.5.0 → 2.6.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/commands/tlc/autofix.md +34 -1
- package/.claude/commands/tlc/build.md +164 -6
- package/.claude/commands/tlc/ci.md +178 -414
- package/.claude/commands/tlc/coverage.md +34 -0
- package/.claude/commands/tlc/deploy.md +19 -6
- package/.claude/commands/tlc/discuss.md +34 -0
- package/.claude/commands/tlc/docs.md +35 -1
- package/.claude/commands/tlc/e2e.md +300 -0
- package/.claude/commands/tlc/edge-cases.md +35 -1
- package/.claude/commands/tlc/init.md +38 -8
- package/.claude/commands/tlc/new-project.md +46 -4
- package/.claude/commands/tlc/plan.md +33 -0
- package/.claude/commands/tlc/quick.md +33 -0
- package/.claude/commands/tlc/release.md +85 -135
- package/.claude/commands/tlc/restore.md +14 -0
- package/.claude/commands/tlc/review.md +76 -1
- package/.claude/commands/tlc/tlc.md +134 -0
- package/.claude/commands/tlc/verify.md +64 -65
- package/.claude/commands/tlc/watchci.md +10 -0
- package/.claude/hooks/tlc-block-tools.sh +13 -0
- package/.claude/hooks/tlc-session-init.sh +29 -0
- package/CODING-STANDARDS.md +35 -10
- package/package.json +1 -1
- package/server/lib/block-tools-hook.js +23 -0
- package/server/lib/e2e/acceptance-parser.js +132 -0
- package/server/lib/e2e/acceptance-parser.test.js +110 -0
- package/server/lib/e2e/framework-detector.js +47 -0
- package/server/lib/e2e/framework-detector.test.js +94 -0
- package/server/lib/e2e/log-assertions.js +107 -0
- package/server/lib/e2e/log-assertions.test.js +68 -0
- package/server/lib/e2e/test-generator.js +159 -0
- package/server/lib/e2e/test-generator.test.js +121 -0
- package/server/lib/e2e/verify-runner.js +191 -0
- package/server/lib/e2e/verify-runner.test.js +167 -0
- package/server/lib/hooks/block-tools-hook.test.js +54 -0
- package/server/lib/orchestration/cli-dispatch.js +16 -1
- package/server/lib/orchestration/cli-dispatch.test.js +94 -8
- package/server/lib/orchestration/completion-checker.js +101 -0
- package/server/lib/orchestration/completion-checker.test.js +177 -0
- package/server/lib/orchestration/result-verifier.js +143 -0
- package/server/lib/orchestration/result-verifier.test.js +291 -0
- package/server/lib/orchestration/session-dispatcher.js +99 -0
- package/server/lib/orchestration/session-dispatcher.test.js +215 -0
- package/server/lib/orchestration/session-status.js +147 -0
- package/server/lib/orchestration/session-status.test.js +130 -0
- package/server/lib/release/agent-runner-updates.js +24 -0
- package/server/lib/release/agent-runner-updates.test.js +22 -0
- package/server/lib/release/changelog-generator.js +142 -0
- package/server/lib/release/changelog-generator.test.js +113 -0
- package/server/lib/release/ci-watcher.js +83 -0
- package/server/lib/release/ci-watcher.test.js +81 -0
- package/server/lib/release/health-checker.js +111 -0
- package/server/lib/release/health-checker.test.js +121 -0
- package/server/lib/release/release-pipeline.js +187 -0
- package/server/lib/release/release-pipeline.test.js +262 -0
- package/server/lib/release/version-bumper.js +183 -0
- package/server/lib/release/version-bumper.test.js +142 -0
- package/server/lib/routing-preamble.integration.test.js +12 -0
- package/server/lib/routing-preamble.js +13 -2
- package/server/lib/routing-preamble.test.js +49 -0
- package/server/lib/scaffolding/ci-detector.js +139 -0
- package/server/lib/scaffolding/ci-detector.test.js +198 -0
- package/server/lib/scaffolding/ci-scaffolder.js +347 -0
- package/server/lib/scaffolding/ci-scaffolder.test.js +157 -0
- package/server/lib/scaffolding/deploy-detector.js +135 -0
- package/server/lib/scaffolding/deploy-detector.test.js +106 -0
- package/server/lib/scaffolding/health-scaffold.js +374 -0
- package/server/lib/scaffolding/health-scaffold.test.js +99 -0
- package/server/lib/scaffolding/logger-scaffold.js +196 -0
- package/server/lib/scaffolding/logger-scaffold.test.js +146 -0
- package/server/lib/scaffolding/migration-detector.js +78 -0
- package/server/lib/scaffolding/migration-detector.test.js +127 -0
- package/server/lib/scaffolding/snapshot-manager.js +142 -0
- package/server/lib/scaffolding/snapshot-manager.test.js +225 -0
- package/server/lib/task-router-config.js +50 -20
- package/server/lib/task-router-config.test.js +29 -15
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
# /tlc:verify -
|
|
1
|
+
# /tlc:verify - Acceptance Verification
|
|
2
2
|
|
|
3
|
-
Verify the phase
|
|
3
|
+
Verify the phase automatically from its plan acceptance criteria, then leave only the genuinely manual items for human follow-up.
|
|
4
4
|
|
|
5
5
|
## What This Does
|
|
6
6
|
|
|
7
|
-
1.
|
|
8
|
-
2.
|
|
9
|
-
3. Captures
|
|
10
|
-
4. Marks phase
|
|
7
|
+
1. Reads acceptance criteria from the phase plan
|
|
8
|
+
2. Runs `server/lib/e2e/verify-runner.js` against the project automatically
|
|
9
|
+
3. Captures verified, failed, and manual-only criteria
|
|
10
|
+
4. Marks the phase verified when all automatable criteria pass
|
|
11
11
|
|
|
12
12
|
## Usage
|
|
13
13
|
|
|
@@ -19,78 +19,85 @@ If no phase number, auto-detect current phase.
|
|
|
19
19
|
|
|
20
20
|
## Process
|
|
21
21
|
|
|
22
|
-
### Step 1:
|
|
22
|
+
### Step 1: Load the Phase Plan
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
|
-
|
|
25
|
+
cat .planning/phases/{N}-PLAN.md
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
- ❌ Some fail → Report failures, suggest fixing first
|
|
28
|
+
Pass the detected plan path into `server/lib/e2e/verify-runner.js`:
|
|
30
29
|
|
|
31
|
-
|
|
30
|
+
```js
|
|
31
|
+
const { runVerification } = require('./server/lib/e2e/verify-runner.js');
|
|
32
|
+
```
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
- Extract acceptance criteria from each task
|
|
35
|
-
- Build verification checklist
|
|
34
|
+
The runner must be used by default instead of asking the user to validate each criterion manually.
|
|
36
35
|
|
|
37
|
-
### Step
|
|
36
|
+
### Step 2: Run Automated Verification
|
|
38
37
|
|
|
39
|
-
|
|
38
|
+
Call:
|
|
40
39
|
|
|
40
|
+
```js
|
|
41
|
+
await runVerification({
|
|
42
|
+
planPath,
|
|
43
|
+
projectDir,
|
|
44
|
+
exec,
|
|
45
|
+
fs,
|
|
46
|
+
});
|
|
41
47
|
```
|
|
42
|
-
Phase 1: Authentication
|
|
43
48
|
|
|
44
|
-
|
|
49
|
+
Behavior:
|
|
50
|
+
- Parse acceptance criteria from the plan file
|
|
51
|
+
- Detect the available E2E framework
|
|
52
|
+
- Run the matching E2E command automatically
|
|
53
|
+
- Return `{ verified, manual, failed, results }`
|
|
45
54
|
|
|
46
|
-
|
|
55
|
+
### Step 3: Interpret Results
|
|
47
56
|
|
|
48
|
-
|
|
49
|
-
1. Go to /login
|
|
50
|
-
2. Enter valid credentials
|
|
51
|
-
3. Get redirected to dashboard
|
|
57
|
+
Report each criterion with its status:
|
|
52
58
|
|
|
53
|
-
Works as expected? (Y/n/describe issue)
|
|
54
|
-
>
|
|
55
59
|
```
|
|
60
|
+
verified: 5
|
|
61
|
+
manual: 1
|
|
62
|
+
failed: 0
|
|
56
63
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
- User can click the export button → verified
|
|
65
|
+
- GET /health endpoint returns 200 → verified
|
|
66
|
+
- Data is retained for 30 days. → manual
|
|
60
67
|
```
|
|
61
|
-
> n - login works but no error message for wrong password
|
|
62
68
|
|
|
63
|
-
|
|
69
|
+
Only criteria returned as `manual` should be handed back to a human for confirmation.
|
|
64
70
|
|
|
65
|
-
|
|
66
|
-
Location: Login form
|
|
67
|
-
Expected: Show "Invalid credentials" message
|
|
71
|
+
### Step 4: Handle Failures
|
|
68
72
|
|
|
69
|
-
|
|
73
|
+
If any criteria fail:
|
|
74
|
+
```
|
|
75
|
+
Verification failed:
|
|
76
|
+
- User login redirects to dashboard → failed
|
|
77
|
+
logErrors: Timeout while waiting for dashboard
|
|
70
78
|
```
|
|
71
79
|
|
|
80
|
+
Fix the implementation or tests, then rerun `runVerification(...)`.
|
|
81
|
+
|
|
72
82
|
### Step 5: Mark Verified or Loop
|
|
73
83
|
|
|
74
84
|
**All verified:**
|
|
75
85
|
```
|
|
76
86
|
✅ Phase 1 verified
|
|
77
87
|
|
|
78
|
-
All
|
|
88
|
+
All automatable acceptance criteria passed.
|
|
79
89
|
|
|
80
90
|
Creating .planning/phases/1-VERIFIED.md
|
|
81
|
-
|
|
82
|
-
Ready for next phase? (Y/n)
|
|
83
91
|
```
|
|
84
92
|
|
|
85
93
|
**Issues found:**
|
|
86
94
|
```
|
|
87
95
|
Phase 1 verification incomplete
|
|
88
96
|
|
|
89
|
-
|
|
90
|
-
1
|
|
91
|
-
2. Session doesn't persist on refresh
|
|
97
|
+
Automated verification failed for 2 criteria.
|
|
98
|
+
1 manual criterion still requires human confirmation.
|
|
92
99
|
|
|
93
|
-
|
|
100
|
+
Run /tlc:build 1 to implement fixes, then /tlc:verify again.
|
|
94
101
|
```
|
|
95
102
|
|
|
96
103
|
### Step 6: Create Verification Record
|
|
@@ -106,7 +113,7 @@ Verified: {date}
|
|
|
106
113
|
|
|
107
114
|
- [x] User login with email/password
|
|
108
115
|
- [x] Session persistence
|
|
109
|
-
- [
|
|
116
|
+
- [ ] Data retention wording confirmed manually
|
|
110
117
|
|
|
111
118
|
## Issues Found & Fixed
|
|
112
119
|
|
|
@@ -118,42 +125,34 @@ Verified: {date}
|
|
|
118
125
|
{Any additional observations}
|
|
119
126
|
```
|
|
120
127
|
|
|
121
|
-
## Why
|
|
128
|
+
## Why This Order?
|
|
122
129
|
|
|
123
|
-
**
|
|
124
|
-
**
|
|
130
|
+
**Automated verification should handle every criterion it can.**
|
|
131
|
+
**Humans should only validate what cannot be expressed as a reliable automated check.**
|
|
125
132
|
|
|
126
|
-
|
|
133
|
+
Automation catches:
|
|
127
134
|
- Logic errors
|
|
128
135
|
- Regressions
|
|
129
|
-
-
|
|
136
|
+
- Broken user flows
|
|
130
137
|
|
|
131
|
-
|
|
132
|
-
-
|
|
133
|
-
-
|
|
134
|
-
-
|
|
138
|
+
Humans catch:
|
|
139
|
+
- Ambiguous qualitative requirements
|
|
140
|
+
- Visual or product judgment not encoded in tests
|
|
141
|
+
- Criteria intentionally marked as manual
|
|
135
142
|
|
|
136
|
-
Both matter.
|
|
143
|
+
Both still matter, but `/tlc:verify` should not default to a manual checklist when the runner can verify the phase directly.
|
|
137
144
|
|
|
138
145
|
## Example
|
|
139
146
|
|
|
140
147
|
```
|
|
141
148
|
> /tlc:verify 1
|
|
142
149
|
|
|
143
|
-
|
|
150
|
+
Loading phase plan... ✅
|
|
151
|
+
Running verify-runner... ✅
|
|
144
152
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
Works? (Y/n) > y
|
|
149
|
-
|
|
150
|
-
[2/3] Session persists across page refresh
|
|
151
|
-
Works? (Y/n) > y
|
|
152
|
-
|
|
153
|
-
[3/3] Logout clears session
|
|
154
|
-
Works? (Y/n) > y
|
|
153
|
+
verified: 3
|
|
154
|
+
manual: 0
|
|
155
|
+
failed: 0
|
|
155
156
|
|
|
156
157
|
✅ Phase 1 verified!
|
|
157
|
-
|
|
158
|
-
Moving to Phase 2: Dashboard
|
|
159
158
|
```
|
|
@@ -157,3 +157,13 @@ Run #4522 (CI) — in progress...
|
|
|
157
157
|
- After `/tlc:build` when you've pushed your work
|
|
158
158
|
- When CI keeps failing and you want the fix loop automated
|
|
159
159
|
- Pair with `/tlc:e2e-verify` for full verification after CI is green
|
|
160
|
+
|
|
161
|
+
## `--detached`
|
|
162
|
+
|
|
163
|
+
`/tlc:watchci --detached` dispatches CI watching through the local orchestrator instead of keeping the current session attached.
|
|
164
|
+
|
|
165
|
+
- Send a `POST` request to `http://localhost:3100`
|
|
166
|
+
- Ask the orchestrator to run the CI watch workflow for the current project/branch
|
|
167
|
+
- Return the dispatched session details so the user can check status later
|
|
168
|
+
|
|
169
|
+
Detached mode changes how the command is launched, not what it does. The orchestrated worker should still follow the same watch/fix loop defined above.
|
|
@@ -25,11 +25,24 @@ case "$TOOL" in
|
|
|
25
25
|
ExitPlanMode)
|
|
26
26
|
REASON="BLOCKED by TLC. Plans are approved via /tlc:build, not ExitPlanMode."
|
|
27
27
|
;;
|
|
28
|
+
Agent)
|
|
29
|
+
FLAG_FILE=".tlc/.build-routing-active"
|
|
30
|
+
if [[ -f "$FLAG_FILE" ]]; then
|
|
31
|
+
PROVIDER=$(tr -d '[:space:]' < "$FLAG_FILE")
|
|
32
|
+
if [[ -n "$PROVIDER" && "$PROVIDER" != "claude" ]]; then
|
|
33
|
+
REASON="BLOCKED by TLC. Agent is routed to ${PROVIDER} while ${FLAG_FILE} is active."
|
|
34
|
+
fi
|
|
35
|
+
fi
|
|
36
|
+
;;
|
|
28
37
|
*)
|
|
29
38
|
exit 0
|
|
30
39
|
;;
|
|
31
40
|
esac
|
|
32
41
|
|
|
42
|
+
if [[ -z "$REASON" ]]; then
|
|
43
|
+
exit 0
|
|
44
|
+
fi
|
|
45
|
+
|
|
33
46
|
cat <<EOF
|
|
34
47
|
{
|
|
35
48
|
"hookSpecificOutput": {
|
|
@@ -43,6 +43,35 @@ else
|
|
|
43
43
|
done
|
|
44
44
|
fi
|
|
45
45
|
|
|
46
|
+
# --- tlc-core Orchestrator ---
|
|
47
|
+
# Kill switch: export TLC_CORE_AUTOSTART=false to disable auto-start
|
|
48
|
+
TLC_CORE_PORT="${TLC_CORE_PORT:-3100}"
|
|
49
|
+
if [ "${TLC_CORE_AUTOSTART}" = "false" ]; then
|
|
50
|
+
echo "tlc-core orchestrator: auto-start disabled (TLC_CORE_AUTOSTART=false)"
|
|
51
|
+
elif curl -sf --max-time 1 "http://localhost:${TLC_CORE_PORT}/health" > /dev/null 2>&1; then
|
|
52
|
+
SESSIONS=$(curl -sf --max-time 1 "http://localhost:${TLC_CORE_PORT}/sessions" 2>/dev/null | jq "length" 2>/dev/null || echo "0")
|
|
53
|
+
echo "tlc-core orchestrator: healthy (${SESSIONS} sessions)"
|
|
54
|
+
else
|
|
55
|
+
# Try to auto-start if tlc-core CLI is available
|
|
56
|
+
if command -v tlc-core >/dev/null 2>&1; then
|
|
57
|
+
echo "tlc-core orchestrator: not running — starting..."
|
|
58
|
+
tlc-core start > /dev/null 2>&1 &
|
|
59
|
+
# Wait up to 15s for health
|
|
60
|
+
for i in 1 2 3 4 5; do
|
|
61
|
+
sleep 3
|
|
62
|
+
if curl -sf --max-time 1 "http://localhost:${TLC_CORE_PORT}/health" > /dev/null 2>&1; then
|
|
63
|
+
echo "tlc-core orchestrator: started successfully"
|
|
64
|
+
break
|
|
65
|
+
fi
|
|
66
|
+
done
|
|
67
|
+
if ! curl -sf --max-time 1 "http://localhost:${TLC_CORE_PORT}/health" > /dev/null 2>&1; then
|
|
68
|
+
echo "tlc-core orchestrator: failed to start. Run 'tlc-core start' manually."
|
|
69
|
+
fi
|
|
70
|
+
else
|
|
71
|
+
echo "tlc-core orchestrator: not installed. Run: git clone git@github.com:TwentyTwoLabs22/tlc-core.git && cd cli && npm install && npm link"
|
|
72
|
+
fi
|
|
73
|
+
fi
|
|
74
|
+
|
|
46
75
|
# ─── Memory System Init ─────────────────────────────
|
|
47
76
|
mkdir -p "$PROJECT_DIR/.tlc/memory/team/decisions" \
|
|
48
77
|
"$PROJECT_DIR/.tlc/memory/team/gotchas" \
|
package/CODING-STANDARDS.md
CHANGED
|
@@ -425,16 +425,41 @@ export class AppError extends Error {
|
|
|
425
425
|
}
|
|
426
426
|
}
|
|
427
427
|
|
|
428
|
-
export class NotFoundError extends AppError {
|
|
429
|
-
constructor(entity: string, id: string) {
|
|
430
|
-
super(`${entity} not found: ${id}`, 'NOT_FOUND', 404);
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
428
|
+
export class NotFoundError extends AppError {
|
|
429
|
+
constructor(entity: string, id: string) {
|
|
430
|
+
super(`${entity} not found: ${id}`, 'NOT_FOUND', 404);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
## Observability
|
|
436
|
+
|
|
437
|
+
### Structured Logging
|
|
438
|
+
- Use structured JSON logging (`pino` for Node.js, `structlog` for Python, `slog` for Go)
|
|
439
|
+
- Log levels: `ERROR` (needs attention), `WARN` (degraded), `INFO` (business events), `DEBUG` (troubleshooting)
|
|
440
|
+
- Every log must include: `level`, `message`, `context` object, `timestamp`
|
|
441
|
+
- No bare `console.log` for errors; use the project logger
|
|
442
|
+
- No sensitive data in logs (`passwords`, `tokens`, `PII`)
|
|
443
|
+
|
|
444
|
+
### Connection State Logging
|
|
445
|
+
- Every external connection (DB, Redis, WebSocket, Docker, HTTP client, queue) must log: connect, disconnect, reconnect, and failure
|
|
446
|
+
- Connection state changes are `INFO` level, not `DEBUG`
|
|
447
|
+
- Include connection target in log context (`host`, `port`, database name)
|
|
448
|
+
|
|
449
|
+
### Health Endpoints
|
|
450
|
+
- Every server project must expose a health endpoint
|
|
451
|
+
- Docker Compose: `GET /health` -> `{ status, dependencies, uptime }`
|
|
452
|
+
- Kubernetes: `GET /healthz` + `/ready` + `/startup` with appropriate checks
|
|
453
|
+
- Health checks must have timeouts (never hang)
|
|
454
|
+
|
|
455
|
+
### Startup Health
|
|
456
|
+
- Log the state of every dependency on startup
|
|
457
|
+
- `"Connected to Postgres"`, `"Docker socket accessible"`, `"Redis: connection refused"`
|
|
458
|
+
- Never start silently; if a dependency is down, log it at `WARN` level
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
## 13. Deprecated Re-exports
|
|
438
463
|
|
|
439
464
|
When migrating, old file locations should become deprecated re-exports:
|
|
440
465
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const BUILD_ROUTING_FLAG = path.join('.tlc', '.build-routing-active');
|
|
5
|
+
|
|
6
|
+
function shouldBlockAgent(projectRoot = process.cwd()) {
|
|
7
|
+
const flagPath = path.join(projectRoot, BUILD_ROUTING_FLAG);
|
|
8
|
+
if (!fs.existsSync(flagPath)) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const provider = fs.readFileSync(flagPath, 'utf8').trim();
|
|
13
|
+
if (!provider || provider === 'claude') {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return provider;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
BUILD_ROUTING_FLAG,
|
|
22
|
+
shouldBlockAgent,
|
|
23
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse acceptance criteria from plan content into normalized E2E records.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const CHECKBOX_REGEX = /^\s*[-*]\s*\[[ xX]?\]\s+(.+?)\s*$/;
|
|
6
|
+
const SHOULD_REGEX = /^(?:[-*]\s*)?(?:it\s+)?should\s+(.+)$/i;
|
|
7
|
+
const GIVEN_REGEX = /^\s*(?:[-*]\s*)?(?:\*\*)?given(?:\*\*)?\s*:?\s+(.+?)\s*$/i;
|
|
8
|
+
const WHEN_REGEX = /^\s*(?:[-*]\s*)?(?:\*\*)?when(?:\*\*)?\s*:?\s+(.+?)\s*$/i;
|
|
9
|
+
const THEN_REGEX = /^\s*(?:[-*]\s*)?(?:\*\*)?then(?:\*\*)?\s*:?\s+(.+?)\s*$/i;
|
|
10
|
+
|
|
11
|
+
function detectType(text) {
|
|
12
|
+
if (/\b(post|get|api|endpoint)\b/i.test(text)) {
|
|
13
|
+
return 'api';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (/\b(click|page|button)\b/i.test(text)) {
|
|
17
|
+
return 'ui';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return 'manual';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeCriterion(text) {
|
|
24
|
+
return String(text || '')
|
|
25
|
+
.trim()
|
|
26
|
+
.replace(/\s+/g, ' ');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isIgnorableLine(line) {
|
|
30
|
+
const trimmed = line.trim();
|
|
31
|
+
|
|
32
|
+
if (!trimmed) return true;
|
|
33
|
+
if (/^#{1,6}\s/.test(trimmed)) return true;
|
|
34
|
+
if (/^\*\*acceptance criteria:?\*\*$/i.test(trimmed)) return true;
|
|
35
|
+
if (/^acceptance criteria:?$/i.test(trimmed)) return true;
|
|
36
|
+
if (/^-{3,}$/.test(trimmed)) return true;
|
|
37
|
+
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseAcceptanceCriteria({ planContent }) {
|
|
42
|
+
if (!planContent || typeof planContent !== 'string') {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const lines = planContent.replace(/\r\n/g, '\n').split('\n');
|
|
47
|
+
const criteria = [];
|
|
48
|
+
const consumed = new Set();
|
|
49
|
+
|
|
50
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
51
|
+
const givenMatch = lines[index].match(GIVEN_REGEX);
|
|
52
|
+
if (!givenMatch) continue;
|
|
53
|
+
|
|
54
|
+
let cursor = index + 1;
|
|
55
|
+
while (cursor < lines.length && !lines[cursor].trim()) {
|
|
56
|
+
cursor += 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const whenMatch = cursor < lines.length ? lines[cursor].match(WHEN_REGEX) : null;
|
|
60
|
+
if (!whenMatch) continue;
|
|
61
|
+
|
|
62
|
+
cursor += 1;
|
|
63
|
+
while (cursor < lines.length && !lines[cursor].trim()) {
|
|
64
|
+
cursor += 1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const thenMatch = cursor < lines.length ? lines[cursor].match(THEN_REGEX) : null;
|
|
68
|
+
if (!thenMatch) continue;
|
|
69
|
+
|
|
70
|
+
const given = normalizeCriterion(givenMatch[1]);
|
|
71
|
+
const when = normalizeCriterion(whenMatch[1]);
|
|
72
|
+
const then = normalizeCriterion(thenMatch[1]);
|
|
73
|
+
const scenario = `Given ${given} When ${when} Then ${then}`;
|
|
74
|
+
|
|
75
|
+
criteria.push({
|
|
76
|
+
criterion: then,
|
|
77
|
+
type: detectType(scenario),
|
|
78
|
+
scenario,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
consumed.add(index);
|
|
82
|
+
consumed.add(cursor - 1);
|
|
83
|
+
consumed.add(cursor);
|
|
84
|
+
index = cursor;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
88
|
+
if (consumed.has(index)) continue;
|
|
89
|
+
|
|
90
|
+
const line = lines[index];
|
|
91
|
+
if (isIgnorableLine(line)) continue;
|
|
92
|
+
|
|
93
|
+
const checkboxMatch = line.match(CHECKBOX_REGEX);
|
|
94
|
+
if (checkboxMatch) {
|
|
95
|
+
const criterion = normalizeCriterion(checkboxMatch[1]);
|
|
96
|
+
criteria.push({
|
|
97
|
+
criterion,
|
|
98
|
+
type: detectType(criterion),
|
|
99
|
+
scenario: null,
|
|
100
|
+
});
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const shouldMatch = line.match(SHOULD_REGEX);
|
|
105
|
+
if (shouldMatch) {
|
|
106
|
+
const criterion = `should ${normalizeCriterion(shouldMatch[1])}`;
|
|
107
|
+
criteria.push({
|
|
108
|
+
criterion,
|
|
109
|
+
type: detectType(criterion),
|
|
110
|
+
scenario: null,
|
|
111
|
+
});
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const trimmed = line.trim();
|
|
116
|
+
if (/^(?:[-*]\s*)?(?:given|when|then)\b/i.test(trimmed)) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
criteria.push({
|
|
121
|
+
criterion: normalizeCriterion(trimmed.replace(/^[-*]\s*/, '')),
|
|
122
|
+
type: detectType(trimmed),
|
|
123
|
+
scenario: null,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return criteria;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = {
|
|
131
|
+
parseAcceptanceCriteria,
|
|
132
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, it } from 'vitest';
|
|
2
|
+
const assert = require('node:assert');
|
|
3
|
+
|
|
4
|
+
const { parseAcceptanceCriteria } = require('./acceptance-parser.js');
|
|
5
|
+
|
|
6
|
+
describe('parseAcceptanceCriteria', () => {
|
|
7
|
+
it('extracts checkbox criteria and infers types', () => {
|
|
8
|
+
const result = parseAcceptanceCriteria({
|
|
9
|
+
planContent: `
|
|
10
|
+
**Acceptance Criteria:**
|
|
11
|
+
- [ ] User can click the save button
|
|
12
|
+
- [x] GET /health endpoint returns 200
|
|
13
|
+
`,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
assert.deepStrictEqual(result, [
|
|
17
|
+
{
|
|
18
|
+
criterion: 'User can click the save button',
|
|
19
|
+
type: 'ui',
|
|
20
|
+
scenario: null,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
criterion: 'GET /health endpoint returns 200',
|
|
24
|
+
type: 'api',
|
|
25
|
+
scenario: null,
|
|
26
|
+
},
|
|
27
|
+
]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('extracts Given/When/Then scenarios', () => {
|
|
31
|
+
const result = parseAcceptanceCriteria({
|
|
32
|
+
planContent: `
|
|
33
|
+
Given the user is on the login page
|
|
34
|
+
When they click the sign in button
|
|
35
|
+
Then the dashboard page should load
|
|
36
|
+
`,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
assert.deepStrictEqual(result, [
|
|
40
|
+
{
|
|
41
|
+
criterion: 'the dashboard page should load',
|
|
42
|
+
type: 'ui',
|
|
43
|
+
scenario: 'Given the user is on the login page When they click the sign in button Then the dashboard page should load',
|
|
44
|
+
},
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('extracts should statements from bullets or plain text', () => {
|
|
49
|
+
const result = parseAcceptanceCriteria({
|
|
50
|
+
planContent: `
|
|
51
|
+
should call the API with the updated payload
|
|
52
|
+
- should show the success page
|
|
53
|
+
`,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
assert.deepStrictEqual(result, [
|
|
57
|
+
{
|
|
58
|
+
criterion: 'should call the API with the updated payload',
|
|
59
|
+
type: 'api',
|
|
60
|
+
scenario: null,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
criterion: 'should show the success page',
|
|
64
|
+
type: 'ui',
|
|
65
|
+
scenario: null,
|
|
66
|
+
},
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('falls back to free-text and marks ambiguous criteria as manual', () => {
|
|
71
|
+
const result = parseAcceptanceCriteria({
|
|
72
|
+
planContent: `
|
|
73
|
+
Acceptance Criteria:
|
|
74
|
+
Data is retained for 30 days.
|
|
75
|
+
`,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
assert.deepStrictEqual(result, [
|
|
79
|
+
{
|
|
80
|
+
criterion: 'Data is retained for 30 days.',
|
|
81
|
+
type: 'manual',
|
|
82
|
+
scenario: null,
|
|
83
|
+
},
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('ignores markdown structure around parsed lines', () => {
|
|
88
|
+
const result = parseAcceptanceCriteria({
|
|
89
|
+
planContent: `
|
|
90
|
+
## Task 4
|
|
91
|
+
|
|
92
|
+
**Acceptance Criteria:**
|
|
93
|
+
|
|
94
|
+
Given an admin user
|
|
95
|
+
When they POST /users
|
|
96
|
+
Then the API returns 201
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
`,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
assert.deepStrictEqual(result, [
|
|
103
|
+
{
|
|
104
|
+
criterion: 'the API returns 201',
|
|
105
|
+
type: 'api',
|
|
106
|
+
scenario: 'Given an admin user When they POST /users Then the API returns 201',
|
|
107
|
+
},
|
|
108
|
+
]);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const path = require('node:path');
|
|
2
|
+
|
|
3
|
+
function hasDependency(packageJson, dependencyName) {
|
|
4
|
+
const dependencyGroups = [
|
|
5
|
+
packageJson.dependencies,
|
|
6
|
+
packageJson.devDependencies,
|
|
7
|
+
packageJson.peerDependencies,
|
|
8
|
+
packageJson.optionalDependencies,
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
return dependencyGroups.some(
|
|
12
|
+
(dependencies) =>
|
|
13
|
+
dependencies &&
|
|
14
|
+
typeof dependencies === 'object' &&
|
|
15
|
+
Object.prototype.hasOwnProperty.call(dependencies, dependencyName)
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function readPackageJson(packageJsonPath, fs) {
|
|
20
|
+
try {
|
|
21
|
+
const packageJsonText = fs.readFileSync(packageJsonPath, 'utf8');
|
|
22
|
+
return JSON.parse(packageJsonText);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function detectE2eFramework({ projectDir, fs }) {
|
|
29
|
+
const packageJsonPath = path.join(projectDir, 'package.json');
|
|
30
|
+
const playwrightConfigPath = path.join(projectDir, 'playwright.config.ts');
|
|
31
|
+
const packageJson = readPackageJson(packageJsonPath, fs);
|
|
32
|
+
const hasPlaywrightConfig = fs.existsSync(playwrightConfigPath);
|
|
33
|
+
|
|
34
|
+
if (hasPlaywrightConfig) {
|
|
35
|
+
return { framework: 'playwright' };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (packageJson && hasDependency(packageJson, 'supertest')) {
|
|
39
|
+
return { framework: 'supertest' };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { framework: 'none' };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = {
|
|
46
|
+
detectE2eFramework,
|
|
47
|
+
};
|