sdd-pipeline 1.2.4 → 1.2.7
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/bin/sdd.cjs +201 -0
- package/lib/bmad/index.js +20 -3
- package/lib/bundle/.claude/commands/sdd-check.md +153 -0
- package/lib/bundle/.claude/commands/sdd-cook.md +149 -167
- package/lib/bundle/.claude/commands/sdd-task-status.md +97 -27
- package/lib/bundle/bin/sdd.cjs +201 -0
- package/lib/bundle/bin/sdd.js +159 -0
- package/lib/bundle/commands/sdd-task-status.ps1 +82 -22
- package/lib/bundle/lib/bmad/confidence.js +129 -0
- package/lib/bundle/lib/bmad/index.js +157 -0
- package/lib/bundle/lib/bmad/problem-first.js +91 -0
- package/lib/bundle/lib/bmad/questions.js +110 -0
- package/lib/bundle/lib/bmad/synthesis.js +109 -0
- package/lib/bundle/lib/config.js +35 -0
- package/lib/bundle/lib/init.js +204 -0
- package/lib/bundle/lib/sanitize.js +10 -0
- package/lib/bundle/run-converge.ps1 +288 -42
- package/lib/bundle/skills/sdd-tasks/SKILL.md +259 -0
- package/lib/init.js +108 -32
- package/package.json +8 -8
- package/bin/sdd.js +0 -70
- package/lib/bmad.js +0 -54
|
@@ -1,20 +1,43 @@
|
|
|
1
|
-
param(
|
|
1
|
+
param(
|
|
2
2
|
[Parameter(Mandatory)][string]$TaskId,
|
|
3
3
|
[Parameter(Mandatory)][ValidateSet("pending", "in-progress", "completed")][string]$Status
|
|
4
4
|
)
|
|
5
5
|
|
|
6
6
|
$ErrorActionPreference = "Stop"
|
|
7
7
|
|
|
8
|
-
#
|
|
9
|
-
$
|
|
10
|
-
|
|
8
|
+
# Remove UTF-8 BOM if present
|
|
9
|
+
$PSCommandPath = $MyInvocation.MyCommand.Path
|
|
10
|
+
$content = [System.IO.File]::ReadAllText($PSCommandPath, [System.Text.Encoding]::UTF8)
|
|
11
|
+
[System.IO.File]::WriteAllText($PSCommandPath, $content, [System.Text.Encoding]::UTF8)
|
|
12
|
+
|
|
13
|
+
# Find task directory - check sdd/tasks/ first, then current directory
|
|
14
|
+
$taskDir = $null
|
|
15
|
+
$SDD_TASKS_DIR = "sdd/tasks"
|
|
16
|
+
|
|
17
|
+
# Check sdd/tasks/ (new location)
|
|
18
|
+
if (Test-Path $SDD_TASKS_DIR) {
|
|
19
|
+
$taskDirs = Get-ChildItem -Path $SDD_TASKS_DIR -Filter "task-*-*" -Directory -ErrorAction SilentlyContinue
|
|
20
|
+
if ($taskDirs) {
|
|
21
|
+
$taskDir = Join-Path $SDD_TASKS_DIR ($taskDirs | Select-Object -First 1).Name
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Fallback to current directory (legacy)
|
|
26
|
+
if (-not $taskDir) {
|
|
27
|
+
$taskDirs = Get-ChildItem -Filter "task-*-*" -Directory -ErrorAction SilentlyContinue
|
|
28
|
+
if ($taskDirs) {
|
|
29
|
+
$taskDir = ($taskDirs | Select-Object -First 1).FullName
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (-not $taskDir) {
|
|
11
34
|
Write-Host "ERROR: No task directory found. Run /sdd-tasks first." -ForegroundColor Red
|
|
12
35
|
exit 1
|
|
13
36
|
}
|
|
14
|
-
$taskDir = ($taskDirs | Select-Object -First 1).Name
|
|
15
37
|
|
|
16
38
|
# Find task file
|
|
17
39
|
$taskFiles = Get-ChildItem -Path $taskDir -Filter "${TaskId}-*.md" -ErrorAction SilentlyContinue
|
|
40
|
+
$taskFiles = $taskFiles | Where-Object { $_.Name -ne "MASTER-TASKS.md" }
|
|
18
41
|
$taskFilePath = ($taskFiles | Select-Object -First 1).FullName
|
|
19
42
|
|
|
20
43
|
if (-not $taskFilePath) {
|
|
@@ -23,31 +46,63 @@ if (-not $taskFilePath) {
|
|
|
23
46
|
}
|
|
24
47
|
|
|
25
48
|
# Update task file status in frontmatter
|
|
26
|
-
$content =
|
|
27
|
-
|
|
49
|
+
$content = [System.IO.File]::ReadAllText($taskFilePath, [System.Text.Encoding]::UTF8)
|
|
50
|
+
|
|
51
|
+
# Check if status exists and replace, or add it
|
|
52
|
+
if ($content -match '(?m)^status:\s*\S+') {
|
|
53
|
+
# Replace existing status
|
|
54
|
+
$content = $content -replace '(?m)^(status:\s*)\S+', "`$1$Status"
|
|
55
|
+
} else {
|
|
56
|
+
# Add status after description line (or first line of frontmatter)
|
|
57
|
+
if ($content -match '(?m)^(description:\s*"[^"]+")') {
|
|
58
|
+
$content = $content -replace '(?m)^(description:\s*"[^"]+")', "`$1`nstatus: $Status"
|
|
59
|
+
} elseif ($content -match '(?m)^(name:\s*[^\n]+)') {
|
|
60
|
+
$content = $content -replace '(?m)^(name:\s*[^\n]+)', "`$1`nstatus: $Status"
|
|
61
|
+
} else {
|
|
62
|
+
# Fallback: add after opening ---
|
|
63
|
+
$content = $content -replace '^(---)$', "`$1`nstatus: $Status"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
28
66
|
Set-Content $taskFilePath $content -Encoding UTF8
|
|
29
67
|
|
|
30
|
-
# Update MASTER-TASKS.md
|
|
68
|
+
# Update MASTER-TASKS.md - map status to display format
|
|
69
|
+
$displayStatus = switch ($Status) {
|
|
70
|
+
"completed" { "[x]" }
|
|
71
|
+
"in-progress" { "[>]" }
|
|
72
|
+
default { "[ ]" }
|
|
73
|
+
}
|
|
74
|
+
|
|
31
75
|
$masterPath = Join-Path $taskDir "MASTER-TASKS.md"
|
|
32
|
-
$masterContent =
|
|
76
|
+
$masterContent = [System.IO.File]::ReadAllText($masterPath, [System.Text.Encoding]::UTF8)
|
|
33
77
|
|
|
34
|
-
# Update task table row status
|
|
35
|
-
|
|
36
|
-
$
|
|
37
|
-
$
|
|
78
|
+
# Update task table row status - handle T-XXX pattern
|
|
79
|
+
# Pattern: | T-XXX | Name | Estimate | [status] |
|
|
80
|
+
$escapedTaskId = [regex]::Escape($TaskId)
|
|
81
|
+
$pattern = "(\|\s*$escapedTaskId\s*\|[^|]*\|[^|]*\|\s*)\[[^\]]\](\s*\|)"
|
|
82
|
+
$masterContent = $masterContent -replace $pattern, "`$1$displayStatus`$2"
|
|
38
83
|
|
|
39
|
-
# Update progress counters
|
|
84
|
+
# Update progress counters - check both frontmatter AND MASTER-TASKS table
|
|
40
85
|
$allTaskFiles = Get-ChildItem -Path $taskDir -Filter "T-*.md" | Where-Object { $_.Name -ne "MASTER-TASKS.md" }
|
|
41
86
|
$total = $allTaskFiles.Count
|
|
42
87
|
$completed = 0
|
|
43
88
|
$inProgress = 0
|
|
44
89
|
|
|
90
|
+
# Count from frontmatter
|
|
45
91
|
foreach ($f in $allTaskFiles) {
|
|
46
|
-
$c =
|
|
92
|
+
$c = [System.IO.File]::ReadAllText($f.FullName, [System.Text.Encoding]::UTF8)
|
|
47
93
|
if ($c -match '(?m)^status:\s*completed') { $completed++ }
|
|
48
94
|
elseif ($c -match '(?m)^status:\s*in-progress') { $inProgress++ }
|
|
49
95
|
}
|
|
50
96
|
|
|
97
|
+
# If frontmatter count is 0, check MASTER-TASKS table
|
|
98
|
+
if ($completed -eq 0 -and $inProgress -eq 0) {
|
|
99
|
+
$masterContent = [System.IO.File]::ReadAllText($masterPath, [System.Text.Encoding]::UTF8)
|
|
100
|
+
# Count [x] in table
|
|
101
|
+
$completed = ([regex]::Matches($masterContent, '\[x\]')).Count
|
|
102
|
+
# Count [>] in table
|
|
103
|
+
$inProgress = ([regex]::Matches($masterContent, '\[>\]')).Count
|
|
104
|
+
}
|
|
105
|
+
|
|
51
106
|
$masterContent = $masterContent -replace 'Completed: \d+ / \d+', "Completed: $completed / $total"
|
|
52
107
|
$masterContent = $masterContent -replace 'In Progress: \d+', "In Progress: $inProgress"
|
|
53
108
|
$masterContent = $masterContent -replace 'Pending: \d+', "Pending: $($total - $completed - $inProgress)"
|
|
@@ -56,14 +111,18 @@ Set-Content $masterPath $masterContent -Encoding UTF8
|
|
|
56
111
|
|
|
57
112
|
# Update .sdd/config.json
|
|
58
113
|
if (Test-Path ".sdd/config.json") {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
114
|
+
try {
|
|
115
|
+
$config = Get-Content ".sdd/config.json" -Raw | ConvertFrom-Json
|
|
116
|
+
if (-not $config.tasks) { $config | Add-Member -NotePropertyName "tasks" -NotePropertyValue ([ordered]@{}) }
|
|
117
|
+
$config.tasks.$TaskId = $Status
|
|
118
|
+
$config | ConvertTo-Json -Depth 10 | Set-Content ".sdd/config.json"
|
|
119
|
+
} catch {}
|
|
63
120
|
}
|
|
64
121
|
|
|
65
|
-
Write-Host "
|
|
66
|
-
Write-Host "
|
|
122
|
+
Write-Host ""
|
|
123
|
+
Write-Host "[OK] $TaskId -> $displayStatus" -ForegroundColor Green
|
|
124
|
+
Write-Host " Progress: $completed/$total completed, $inProgress in-progress" -ForegroundColor Gray
|
|
125
|
+
Write-Host " Task file: $taskFilePath" -ForegroundColor Gray
|
|
67
126
|
|
|
68
127
|
# Suggest next task
|
|
69
128
|
if ($Status -eq "completed") {
|
|
@@ -73,6 +132,7 @@ if ($Status -eq "completed") {
|
|
|
73
132
|
}
|
|
74
133
|
if ($pending) {
|
|
75
134
|
$next = ($pending | Select-Object -First 1).Name -replace '\.md$', ''
|
|
76
|
-
Write-Host "
|
|
135
|
+
Write-Host ""
|
|
136
|
+
Write-Host "Next: pwsh commands/sdd-task-status.ps1 -TaskId $next -Status in-progress" -ForegroundColor Cyan
|
|
77
137
|
}
|
|
78
138
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BMAD Confidence Score Calculator
|
|
3
|
+
* Computes a 0-100 score from interview answers and user input.
|
|
4
|
+
* Thresholds: None <20 | Weak <40 | Medium <70 | Strong ≥70
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const THRESHOLDS = {
|
|
8
|
+
NONE: 20,
|
|
9
|
+
WEAK: 40,
|
|
10
|
+
MEDIUM: 70,
|
|
11
|
+
// Strong: ≥70
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Calculate confidence score from interview state.
|
|
16
|
+
* @param {object} state - BMAD interview state
|
|
17
|
+
* @returns {{ score: number, level: string, factors: object }}
|
|
18
|
+
*/
|
|
19
|
+
function calculate(state) {
|
|
20
|
+
let score = 0
|
|
21
|
+
const factors = {}
|
|
22
|
+
|
|
23
|
+
// --- Factor 1: Input quality (0-30 points) ---
|
|
24
|
+
const inputLength = (state.input || '').trim().length
|
|
25
|
+
if (inputLength === 0) {
|
|
26
|
+
factors.input = { points: 0, note: 'No input provided' }
|
|
27
|
+
} else if (inputLength < 20) {
|
|
28
|
+
factors.input = { points: 5, note: 'Very brief input' }
|
|
29
|
+
} else if (inputLength < 80) {
|
|
30
|
+
factors.input = { points: 15, note: 'Brief input — interview fills gaps' }
|
|
31
|
+
} else if (inputLength < 300) {
|
|
32
|
+
factors.input = { points: 25, note: 'Good input detail' }
|
|
33
|
+
} else {
|
|
34
|
+
factors.input = { points: 30, note: 'Detailed input' }
|
|
35
|
+
}
|
|
36
|
+
score += factors.input.points
|
|
37
|
+
|
|
38
|
+
// --- Factor 2: Interview completion (0-30 points) ---
|
|
39
|
+
const totalQuestions = 9
|
|
40
|
+
const answeredCount = Object.keys(state.answers || {}).length
|
|
41
|
+
const completionRatio = answeredCount / totalQuestions
|
|
42
|
+
|
|
43
|
+
if (answeredCount === 0) {
|
|
44
|
+
factors.interview = { points: 0, note: 'No questions answered' }
|
|
45
|
+
} else if (completionRatio < 0.4) {
|
|
46
|
+
factors.interview = { points: 5, note: `${answeredCount}/${totalQuestions} answered` }
|
|
47
|
+
} else if (completionRatio < 0.7) {
|
|
48
|
+
factors.interview = { points: 15, note: `${answeredCount}/${totalQuestions} answered` }
|
|
49
|
+
} else if (completionRatio < 1) {
|
|
50
|
+
factors.interview = { points: 25, note: `${answeredCount}/${totalQuestions} answered` }
|
|
51
|
+
} else {
|
|
52
|
+
factors.interview = { points: 30, note: 'All 9 questions answered' }
|
|
53
|
+
}
|
|
54
|
+
score += factors.interview.points
|
|
55
|
+
|
|
56
|
+
// --- Factor 3: Problem clarity (0-20 points) ---
|
|
57
|
+
const problemAnswer = state.answers?.q1 || state.input || ''
|
|
58
|
+
const hasProblem = (problemAnswer || '').trim().length > 20
|
|
59
|
+
const problemSpecific =
|
|
60
|
+
/\d+%|\$\d+|\d+x/i.test(problemAnswer)
|
|
61
|
+
|
|
62
|
+
if (!hasProblem) {
|
|
63
|
+
factors.problemClarity = { points: 0, note: 'No problem statement' }
|
|
64
|
+
} else if (problemSpecific) {
|
|
65
|
+
factors.problemClarity = { points: 20, note: 'Specific, measurable problem' }
|
|
66
|
+
} else if (hasProblem) {
|
|
67
|
+
factors.problemClarity = { points: 10, note: 'Problem stated but generic' }
|
|
68
|
+
}
|
|
69
|
+
score += factors.problemClarity.points
|
|
70
|
+
|
|
71
|
+
// --- Factor 4: Technical awareness (0-20 points) ---
|
|
72
|
+
const techAnswer = state.answers?.q4 || ''
|
|
73
|
+
const techTerms = /database|api|frontend|backend|auth|sql|json|server|deploy/i.test(
|
|
74
|
+
techAnswer
|
|
75
|
+
)
|
|
76
|
+
const integrationAnswer = state.answers?.q5 || ''
|
|
77
|
+
const hasIntegration = integrationAnswer.trim().length > 10
|
|
78
|
+
|
|
79
|
+
if (!techAnswer && !integrationAnswer) {
|
|
80
|
+
factors.technicalAwareness = { points: 0, note: 'No technical detail' }
|
|
81
|
+
} else if (techTerms && hasIntegration) {
|
|
82
|
+
factors.technicalAwareness = { points: 20, note: 'Good technical awareness' }
|
|
83
|
+
} else if (techTerms || hasIntegration) {
|
|
84
|
+
factors.technicalAwareness = { points: 10, note: 'Some technical detail' }
|
|
85
|
+
} else {
|
|
86
|
+
factors.technicalAwareness = { points: 5, note: 'Minimal technical detail' }
|
|
87
|
+
}
|
|
88
|
+
score += factors.technicalAwareness.points
|
|
89
|
+
|
|
90
|
+
// Cap at 100
|
|
91
|
+
score = Math.min(100, Math.max(0, score))
|
|
92
|
+
|
|
93
|
+
// Determine level
|
|
94
|
+
let level
|
|
95
|
+
if (score < THRESHOLDS.NONE) {
|
|
96
|
+
level = 'None'
|
|
97
|
+
} else if (score < THRESHOLDS.WEAK) {
|
|
98
|
+
level = 'Weak'
|
|
99
|
+
} else if (score < THRESHOLDS.MEDIUM) {
|
|
100
|
+
level = 'Medium'
|
|
101
|
+
} else {
|
|
102
|
+
level = 'Strong'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { score, level, factors }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function getLevel(score) {
|
|
109
|
+
if (score < THRESHOLDS.NONE) return 'None'
|
|
110
|
+
if (score < THRESHOLDS.WEAK) return 'Weak'
|
|
111
|
+
if (score < THRESHOLDS.MEDIUM) return 'Medium'
|
|
112
|
+
return 'Strong'
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function canProceed(score) {
|
|
116
|
+
return score >= THRESHOLDS.NONE
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getWarning(score) {
|
|
120
|
+
if (score < THRESHOLDS.WEAK) {
|
|
121
|
+
return '⚠️ Confidence is Weak — consider answering more questions before proceeding.'
|
|
122
|
+
}
|
|
123
|
+
if (score < THRESHOLDS.MEDIUM) {
|
|
124
|
+
return '⚠️ Confidence is Medium — spec may need iteration during implementation.'
|
|
125
|
+
}
|
|
126
|
+
return ''
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = { calculate, getLevel, canProceed, getWarning, THRESHOLDS }
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BMAD Orchestrator — lib/bmad/index.js
|
|
3
|
+
* Main entry point for BMAD interactive system.
|
|
4
|
+
*
|
|
5
|
+
* Coordinates:
|
|
6
|
+
* - Problem-first inversion
|
|
7
|
+
* - 9-question interview framework
|
|
8
|
+
* - 3-agent synthesis (single-pass LLM mode)
|
|
9
|
+
* - Confidence scoring
|
|
10
|
+
* - Brief output to sdd/ directory
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs')
|
|
14
|
+
const path = require('path')
|
|
15
|
+
|
|
16
|
+
const questions = require('./questions')
|
|
17
|
+
const problemFirst = require('./problem-first')
|
|
18
|
+
const synthesis = require('./synthesis')
|
|
19
|
+
const confidenceCalc = require('./confidence')
|
|
20
|
+
|
|
21
|
+
// Output directory - can be overridden via environment
|
|
22
|
+
const OUTPUT_BASE = process.env.SDD_OUTPUT_BASE || 'sdd'
|
|
23
|
+
const OUTPUT_FILE = path.join(OUTPUT_BASE, 'brief.md')
|
|
24
|
+
const TEMPLATE_DATE = new Date().toISOString().split('T')[0]
|
|
25
|
+
const TEMPLATE_EXPIRY = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
|
|
26
|
+
.toISOString()
|
|
27
|
+
.split('T')[0]
|
|
28
|
+
|
|
29
|
+
// In-memory interview state (resumable)
|
|
30
|
+
let state = {
|
|
31
|
+
input: '',
|
|
32
|
+
answers: {},
|
|
33
|
+
inversion: {},
|
|
34
|
+
startedAt: null,
|
|
35
|
+
currentTier: 0,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Ensure output directory exists
|
|
40
|
+
*/
|
|
41
|
+
function ensureOutputDir() {
|
|
42
|
+
if (!fs.existsSync(OUTPUT_BASE)) {
|
|
43
|
+
fs.mkdirSync(OUTPUT_BASE, { recursive: true })
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Initialize a new BMAD interview session.
|
|
49
|
+
* @param {string} rawInput - Raw user feature description
|
|
50
|
+
* @returns {{ sessionId: string, tier: number, questions: array, inversion: object }}
|
|
51
|
+
*/
|
|
52
|
+
function init(rawInput) {
|
|
53
|
+
state = {
|
|
54
|
+
input: rawInput.trim(),
|
|
55
|
+
answers: {},
|
|
56
|
+
inversion: problemFirst.invert(rawInput),
|
|
57
|
+
startedAt: new Date().toISOString(),
|
|
58
|
+
currentTier: 1,
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
tier: 1,
|
|
62
|
+
inversion: state.inversion,
|
|
63
|
+
questions: questions.getAllQuestions().filter((q) => q.tier === 1),
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Record an answer to a question.
|
|
69
|
+
* @param {string} questionId - e.g. 'q1'
|
|
70
|
+
* @param {string} answer - User's answer
|
|
71
|
+
*/
|
|
72
|
+
function answer(questionId, answer) {
|
|
73
|
+
state.answers[questionId] = answer
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get the current confidence score.
|
|
78
|
+
* @returns {{ score: number, level: string, factors: object }}
|
|
79
|
+
*/
|
|
80
|
+
function score() {
|
|
81
|
+
return confidenceCalc.calculate(state)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check if the user can proceed to /sdd-spec.
|
|
86
|
+
* @returns {{ canProceed: boolean, level: string, message: string }}
|
|
87
|
+
*/
|
|
88
|
+
function canProceed() {
|
|
89
|
+
const c = confidenceCalc.calculate(state)
|
|
90
|
+
return {
|
|
91
|
+
canProceed: confidenceCalc.canProceed(c.score),
|
|
92
|
+
level: c.level,
|
|
93
|
+
score: c.score,
|
|
94
|
+
message: confidenceCalc.getWarning(c.score),
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Generate the sdd/brief.md file.
|
|
100
|
+
* @returns {{ filePath: string, confidence: object }}
|
|
101
|
+
*/
|
|
102
|
+
function generateBrief() {
|
|
103
|
+
const { brief, confidence } = synthesis.synthesize(state)
|
|
104
|
+
|
|
105
|
+
const header = `# BMAD Brief — [Project Name]
|
|
106
|
+
|
|
107
|
+
> Generated: ${TEMPLATE_DATE}. Expires: ${TEMPLATE_EXPIRY}.
|
|
108
|
+
> Confidence: **${confidence.score}/100** — ${confidence.level}
|
|
109
|
+
|
|
110
|
+
## Input
|
|
111
|
+
> ${state.input}
|
|
112
|
+
`
|
|
113
|
+
|
|
114
|
+
const fullContent = header + brief
|
|
115
|
+
|
|
116
|
+
// Ensure output directory exists
|
|
117
|
+
ensureOutputDir()
|
|
118
|
+
|
|
119
|
+
// Write to sdd/brief.md
|
|
120
|
+
fs.writeFileSync(OUTPUT_FILE, fullContent, 'utf8')
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
filePath: path.resolve(OUTPUT_FILE),
|
|
124
|
+
confidence,
|
|
125
|
+
outputDir: OUTPUT_BASE,
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get current interview state (for resume/debug).
|
|
131
|
+
*/
|
|
132
|
+
function getState() {
|
|
133
|
+
return { ...state }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get all questions with current answer status.
|
|
138
|
+
*/
|
|
139
|
+
function getQuestionsWithStatus() {
|
|
140
|
+
return questions.getAllQuestions().map((q) => ({
|
|
141
|
+
...q,
|
|
142
|
+
answered: !!state.answers[q.id],
|
|
143
|
+
answer: state.answers[q.id] || null,
|
|
144
|
+
}))
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = {
|
|
148
|
+
init,
|
|
149
|
+
answer,
|
|
150
|
+
score,
|
|
151
|
+
canProceed,
|
|
152
|
+
generateBrief,
|
|
153
|
+
getState,
|
|
154
|
+
getQuestionsWithStatus,
|
|
155
|
+
OUTPUT_FILE,
|
|
156
|
+
OUTPUT_BASE,
|
|
157
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Problem-First Inversion
|
|
3
|
+
* Transforms a solution-first description into a problem-first brief.
|
|
4
|
+
* Rule: Always state the PROBLEM before describing the solution.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Apply problem-first inversion to user input.
|
|
9
|
+
* @param {string} input - Raw user description (often solution-first)
|
|
10
|
+
* @returns {{ problem: string, solution: string, inversionNote: string }}
|
|
11
|
+
*/
|
|
12
|
+
function invert(input) {
|
|
13
|
+
// Common solution-first patterns to detect
|
|
14
|
+
const SOLUTION_PATTERNS = [
|
|
15
|
+
/\bbuild\b/i,
|
|
16
|
+
/\bcreate\b/i,
|
|
17
|
+
/\bimplement\b/i,
|
|
18
|
+
/\bmake\b/i,
|
|
19
|
+
/\bput\b/i,
|
|
20
|
+
/\badd\b/i,
|
|
21
|
+
/\blanding page\b/i,
|
|
22
|
+
/\bdashboard\b/i,
|
|
23
|
+
/\bfeature\b/i,
|
|
24
|
+
/\bfunctionality\b/i,
|
|
25
|
+
/\bbutton\b/i,
|
|
26
|
+
/\bform\b/i,
|
|
27
|
+
/\bapp\b/i,
|
|
28
|
+
/\bwebsite\b/i,
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
const hasSolutionFocus = SOLUTION_PATTERNS.some((p) => p.test(input))
|
|
32
|
+
|
|
33
|
+
if (!hasSolutionFocus) {
|
|
34
|
+
return {
|
|
35
|
+
problem: input,
|
|
36
|
+
solution: '',
|
|
37
|
+
inversionNote: 'Input already problem-focused.',
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Extract what they think they want (the solution)
|
|
42
|
+
const solutionFragment = input
|
|
43
|
+
|
|
44
|
+
// Infer the likely problem (best effort heuristic)
|
|
45
|
+
const problem = inferProblem(input)
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
problem,
|
|
49
|
+
solution: solutionFragment,
|
|
50
|
+
inversionNote:
|
|
51
|
+
'⚠️ INVERSION APPLIED: Your request described a solution. The problem it addresses is stated first above.',
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Best-effort problem inference from solution-first input.
|
|
57
|
+
* This is heuristic — the user corrects it during the interview.
|
|
58
|
+
*/
|
|
59
|
+
function inferProblem(input) {
|
|
60
|
+
const lower = input.toLowerCase()
|
|
61
|
+
|
|
62
|
+
if (lower.includes('landing page')) {
|
|
63
|
+
return 'Users are not converting or not finding us credible — we need a way to communicate value and capture interest quickly.'
|
|
64
|
+
}
|
|
65
|
+
if (lower.includes('dashboard')) {
|
|
66
|
+
return 'Users lack visibility into key metrics or workflows — decisions are made blind or based on incomplete data.'
|
|
67
|
+
}
|
|
68
|
+
if (lower.includes('login') || lower.includes('auth')) {
|
|
69
|
+
return 'Users cannot securely access their accounts or data — engagement and retention are blocked.'
|
|
70
|
+
}
|
|
71
|
+
if (lower.includes('notification') || lower.includes('email')) {
|
|
72
|
+
return 'Users are missing important information or actions — engagement and retention suffer.'
|
|
73
|
+
}
|
|
74
|
+
if (lower.includes('search')) {
|
|
75
|
+
return 'Users cannot find what they need — they leave frustrated or submit support requests.'
|
|
76
|
+
}
|
|
77
|
+
if (lower.includes('export') || lower.includes('report')) {
|
|
78
|
+
return 'Users cannot act on their data — business decisions are slowed or made without full information.'
|
|
79
|
+
}
|
|
80
|
+
if (lower.includes('todo') || lower.includes('task')) {
|
|
81
|
+
return 'Users forget tasks or lack a system to track progress — important items slip through the cracks.'
|
|
82
|
+
}
|
|
83
|
+
if (lower.includes('contact form') || lower.includes('form')) {
|
|
84
|
+
return 'Users cannot easily reach out or submit information — communication barriers reduce engagement.'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Generic fallback
|
|
88
|
+
return 'Users face a friction point that prevents them from completing their goal — specific symptoms and impact are being confirmed in the interview.'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = { invert }
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BMAD 9-Question Framework
|
|
3
|
+
* Tier 1 (3) → Tier 2 (3) → Tier 3 (3) = 9 questions total
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const TIERS = {
|
|
7
|
+
TIER1: {
|
|
8
|
+
name: 'Problem Definition',
|
|
9
|
+
questions: [
|
|
10
|
+
{
|
|
11
|
+
id: 'q1',
|
|
12
|
+
tier: 1,
|
|
13
|
+
question: 'What specific problem does this solve for the user?',
|
|
14
|
+
hint: 'Be specific — not "improve UX" but "reduce checkout abandonment by 40%"',
|
|
15
|
+
perspective: 'pm',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'q2',
|
|
19
|
+
tier: 1,
|
|
20
|
+
question: 'Who is the target user, and what is their current workaround?',
|
|
21
|
+
hint: 'Describe the person and what they do today without this feature',
|
|
22
|
+
perspective: 'pm',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'q3',
|
|
26
|
+
tier: 1,
|
|
27
|
+
question: 'What does success look like in 90 days? (measurable outcome)',
|
|
28
|
+
hint: 'Specific metric: "X% conversion", "N fewer support tickets", "$Y revenue"',
|
|
29
|
+
perspective: 'pm',
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
TIER2: {
|
|
34
|
+
name: 'Technical & Feasibility',
|
|
35
|
+
questions: [
|
|
36
|
+
{
|
|
37
|
+
id: 'q4',
|
|
38
|
+
tier: 2,
|
|
39
|
+
question: 'What is the minimum viable version of this — one paragraph?',
|
|
40
|
+
hint: 'Strip everything optional. What remains is the core?',
|
|
41
|
+
perspective: 'architect',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'q5',
|
|
45
|
+
tier: 2,
|
|
46
|
+
question: 'What existing systems or data does this need to connect to?',
|
|
47
|
+
hint: 'List integrations, APIs, databases, or services required',
|
|
48
|
+
perspective: 'architect',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'q6',
|
|
52
|
+
tier: 2,
|
|
53
|
+
question: 'What are the top 2 technical risks, and how do we mitigate them?',
|
|
54
|
+
hint: 'Think: data loss, downtime, security, scale, third-party dependency',
|
|
55
|
+
perspective: 'architect',
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
TIER3: {
|
|
60
|
+
name: 'UX & Polish',
|
|
61
|
+
questions: [
|
|
62
|
+
{
|
|
63
|
+
id: 'q7',
|
|
64
|
+
tier: 3,
|
|
65
|
+
question: 'What is the first thing the user sees and does? (first impression)',
|
|
66
|
+
hint: 'Describe the screen — what appears, what can they click?',
|
|
67
|
+
perspective: 'ux',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: 'q8',
|
|
71
|
+
tier: 3,
|
|
72
|
+
question: 'What microcopy is essential? (labels, buttons, errors, empty states)',
|
|
73
|
+
hint: 'List specific phrases that must appear in the UI',
|
|
74
|
+
perspective: 'ux',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'q9',
|
|
78
|
+
tier: 3,
|
|
79
|
+
question: 'What could go wrong that we need to handle gracefully?',
|
|
80
|
+
hint: 'Network failure, empty data, permission denied, slow loading',
|
|
81
|
+
perspective: 'ux',
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getAllQuestions() {
|
|
88
|
+
return [
|
|
89
|
+
...TIERS.TIER1.questions,
|
|
90
|
+
...TIERS.TIER2.questions,
|
|
91
|
+
...TIERS.TIER3.questions,
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getQuestionById(id) {
|
|
96
|
+
for (const tier of Object.values(TIERS)) {
|
|
97
|
+
const found = tier.questions.find((q) => q.id === id)
|
|
98
|
+
if (found) return found
|
|
99
|
+
}
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getQuestionsByTier(tier) {
|
|
104
|
+
for (const t of Object.values(TIERS)) {
|
|
105
|
+
if (t.questions[0]?.tier === tier) return t.questions
|
|
106
|
+
}
|
|
107
|
+
return []
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = { TIERS, getAllQuestions, getQuestionById, getQuestionsByTier }
|