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.
@@ -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
- # Find task directory
9
- $taskDirs = Get-ChildItem -Filter "task-*-*" -Directory -ErrorAction SilentlyContinue
10
- if (-not $taskDirs) {
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 = Get-Content $taskFilePath -Raw
27
- $content = $content -replace '(?m)^(status:\s*)\S+', "`$1$Status"
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 = Get-Content $masterPath -Raw
76
+ $masterContent = [System.IO.File]::ReadAllText($masterPath, [System.Text.Encoding]::UTF8)
33
77
 
34
- # Update task table row status
35
- $pattern = "(\|\s*T-\d+\s*\|\s*[^\|]+\|\s*)\S+(\s*\|\s*[^\|]+\s*\|)"
36
- $replacement = '${1}' + $Status + '${2}'
37
- $masterContent = $masterContent -replace $pattern, $replacement
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 = Get-Content $f.FullName -Raw
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
- $config = Get-Content ".sdd/config.json" | ConvertFrom-Json
60
- if (-not $config.tasks) { $config | Add-Member -NotePropertyName "tasks" -NotePropertyValue ([ordered]@{}) }
61
- $config.tasks.$TaskId = $Status
62
- $config | ConvertTo-Json | Set-Content ".sdd/config.json"
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 "[OK] $TaskId -> $Status" -ForegroundColor Green
66
- Write-Host " Progress: $completed/$total completed, $inProgress in-progress"
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 " Next: pwsh commands/sdd-task-status.ps1 -TaskId $next -Status in-progress" -ForegroundColor Cyan
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 }