@girardmedia/bootspring 2.5.0 → 2.5.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/README.md +9 -403
- package/bin/bootspring.js +1 -96
- package/dist/cli/index.js +65134 -0
- package/dist/cli-launcher.js +92 -0
- package/dist/core/index.d.ts +2110 -5582
- package/dist/core/index.js +2 -0
- package/dist/core.js +21123 -5413
- package/dist/mcp/index.d.ts +357 -1
- package/dist/mcp/index.js +2 -0
- package/dist/mcp-server.js +51948 -1976
- package/package.json +27 -63
- package/scripts/postinstall.cjs +144 -0
- package/LICENSE +0 -29
- package/dist/cli/index.cjs +0 -20776
- package/generators/api-docs.js +0 -827
- package/generators/decisions.js +0 -655
- package/generators/generate.js +0 -595
- package/generators/health.js +0 -942
- package/generators/index.ts +0 -82
- package/generators/presets/full.js +0 -28
- package/generators/presets/index.js +0 -12
- package/generators/presets/minimal.js +0 -29
- package/generators/presets/standard.js +0 -28
- package/generators/questionnaire.js +0 -414
- package/generators/sections/advanced.js +0 -136
- package/generators/sections/ai.js +0 -106
- package/generators/sections/auth.js +0 -89
- package/generators/sections/backend.js +0 -146
- package/generators/sections/business.js +0 -118
- package/generators/sections/content.js +0 -300
- package/generators/sections/deployment.js +0 -139
- package/generators/sections/features.js +0 -122
- package/generators/sections/frontend.js +0 -118
- package/generators/sections/identity.js +0 -76
- package/generators/sections/index.js +0 -40
- package/generators/sections/instructions.js +0 -146
- package/generators/sections/payments.js +0 -104
- package/generators/sections/plugins.js +0 -142
- package/generators/sections/pre-build.js +0 -130
- package/generators/sections/security.js +0 -127
- package/generators/sections/technical.js +0 -171
- package/generators/sections/testing.js +0 -125
- package/generators/sections/workflow.js +0 -104
- package/generators/sprint.js +0 -675
- package/generators/templates/agents.template.js +0 -199
- package/generators/templates/assistant-context.template.js +0 -83
- package/generators/templates/build-planning.template.js +0 -708
- package/generators/templates/claude.template.js +0 -379
- package/generators/templates/content.template.js +0 -819
- package/generators/templates/index.js +0 -16
- package/generators/templates/planning.template.js +0 -515
- package/generators/templates/seed.template.js +0 -109
- package/generators/visual-doc-generator.js +0 -910
- package/scripts/postinstall.js +0 -197
- /package/{claude-commands → assets/claude-commands}/agent.md +0 -0
- /package/{claude-commands → assets/claude-commands}/bs.md +0 -0
- /package/{claude-commands → assets/claude-commands}/build.md +0 -0
- /package/{claude-commands → assets/claude-commands}/skill.md +0 -0
- /package/{claude-commands → assets/claude-commands}/todo.md +0 -0
package/generators/decisions.js
DELETED
|
@@ -1,655 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bootspring DECISIONS.md Generator
|
|
3
|
-
*
|
|
4
|
-
* Generates Architecture Decision Record (ADR) documentation
|
|
5
|
-
* with decisions, rationale, trade-offs, outcomes, and lessons learned.
|
|
6
|
-
*
|
|
7
|
-
* @package bootspring
|
|
8
|
-
* @module generators/decisions
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
const fs = require('fs');
|
|
12
|
-
const path = require('path');
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Decision status values
|
|
16
|
-
*/
|
|
17
|
-
const DECISION_STATUS = {
|
|
18
|
-
PROPOSED: 'proposed',
|
|
19
|
-
ACCEPTED: 'accepted',
|
|
20
|
-
DEPRECATED: 'deprecated',
|
|
21
|
-
SUPERSEDED: 'superseded',
|
|
22
|
-
REJECTED: 'rejected'
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Decision categories
|
|
27
|
-
*/
|
|
28
|
-
const DECISION_CATEGORIES = {
|
|
29
|
-
ARCHITECTURE: { label: 'Architecture', emoji: '🏗️' },
|
|
30
|
-
TECHNOLOGY: { label: 'Technology', emoji: '⚙️' },
|
|
31
|
-
DESIGN: { label: 'Design', emoji: '🎨' },
|
|
32
|
-
SECURITY: { label: 'Security', emoji: '🔐' },
|
|
33
|
-
PERFORMANCE: { label: 'Performance', emoji: '⚡' },
|
|
34
|
-
INFRASTRUCTURE: { label: 'Infrastructure', emoji: '☁️' },
|
|
35
|
-
PROCESS: { label: 'Process', emoji: '📋' },
|
|
36
|
-
API: { label: 'API', emoji: '🔌' },
|
|
37
|
-
DATA: { label: 'Data', emoji: '💾' },
|
|
38
|
-
UI_UX: { label: 'UI/UX', emoji: '🖥️' }
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Impact levels
|
|
43
|
-
*/
|
|
44
|
-
const IMPACT_LEVELS = {
|
|
45
|
-
HIGH: { label: 'High', emoji: '🔴', description: 'Affects core architecture or many components' },
|
|
46
|
-
MEDIUM: { label: 'Medium', emoji: '🟡', description: 'Affects specific features or modules' },
|
|
47
|
-
LOW: { label: 'Low', emoji: '🟢', description: 'Localized impact, easily reversible' }
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Decisions Document Generator
|
|
52
|
-
*/
|
|
53
|
-
class DecisionsGenerator {
|
|
54
|
-
constructor(options = {}) {
|
|
55
|
-
this.projectRoot = options.projectRoot || process.cwd();
|
|
56
|
-
this.decisionsPath = path.join(this.projectRoot, '.bootspring', 'decisions.json');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Load decisions data
|
|
61
|
-
*/
|
|
62
|
-
loadDecisions() {
|
|
63
|
-
try {
|
|
64
|
-
if (fs.existsSync(this.decisionsPath)) {
|
|
65
|
-
return JSON.parse(fs.readFileSync(this.decisionsPath, 'utf-8'));
|
|
66
|
-
}
|
|
67
|
-
} catch (_err) {
|
|
68
|
-
// Return default
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
project: this.getProjectName(),
|
|
73
|
-
decisions: [],
|
|
74
|
-
metadata: {
|
|
75
|
-
created: new Date().toISOString(),
|
|
76
|
-
lastUpdated: new Date().toISOString(),
|
|
77
|
-
totalDecisions: 0
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Save decisions data
|
|
84
|
-
*/
|
|
85
|
-
saveDecisions(data) {
|
|
86
|
-
data.metadata.lastUpdated = new Date().toISOString();
|
|
87
|
-
data.metadata.totalDecisions = data.decisions.length;
|
|
88
|
-
|
|
89
|
-
const dir = path.dirname(this.decisionsPath);
|
|
90
|
-
if (!fs.existsSync(dir)) {
|
|
91
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
92
|
-
}
|
|
93
|
-
fs.writeFileSync(this.decisionsPath, JSON.stringify(data, null, 2));
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Get project name from package.json
|
|
98
|
-
*/
|
|
99
|
-
getProjectName() {
|
|
100
|
-
try {
|
|
101
|
-
const pkgPath = path.join(this.projectRoot, 'package.json');
|
|
102
|
-
if (fs.existsSync(pkgPath)) {
|
|
103
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
104
|
-
return pkg.name || 'Project';
|
|
105
|
-
}
|
|
106
|
-
} catch (_err) {
|
|
107
|
-
// Default
|
|
108
|
-
}
|
|
109
|
-
return 'Project';
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Generate a new decision ID
|
|
114
|
-
*/
|
|
115
|
-
generateDecisionId(decisions) {
|
|
116
|
-
const maxId = decisions.reduce((max, d) => {
|
|
117
|
-
const num = parseInt(d.id.replace('ADR-', ''), 10);
|
|
118
|
-
return num > max ? num : max;
|
|
119
|
-
}, 0);
|
|
120
|
-
return `ADR-${String(maxId + 1).padStart(4, '0')}`;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Add a new decision
|
|
125
|
-
*/
|
|
126
|
-
addDecision(data, decision) {
|
|
127
|
-
const id = this.generateDecisionId(data.decisions);
|
|
128
|
-
|
|
129
|
-
const newDecision = {
|
|
130
|
-
id,
|
|
131
|
-
title: decision.title,
|
|
132
|
-
status: decision.status || DECISION_STATUS.PROPOSED,
|
|
133
|
-
category: decision.category || 'ARCHITECTURE',
|
|
134
|
-
impact: decision.impact || 'MEDIUM',
|
|
135
|
-
date: new Date().toISOString().split('T')[0],
|
|
136
|
-
|
|
137
|
-
// Context
|
|
138
|
-
context: decision.context || '',
|
|
139
|
-
problem: decision.problem || '',
|
|
140
|
-
|
|
141
|
-
// Decision
|
|
142
|
-
decision: decision.decision || '',
|
|
143
|
-
rationale: decision.rationale || '',
|
|
144
|
-
|
|
145
|
-
// Options considered
|
|
146
|
-
options: decision.options || [],
|
|
147
|
-
|
|
148
|
-
// Consequences
|
|
149
|
-
consequences: {
|
|
150
|
-
positive: decision.consequences?.positive || [],
|
|
151
|
-
negative: decision.consequences?.negative || [],
|
|
152
|
-
risks: decision.consequences?.risks || []
|
|
153
|
-
},
|
|
154
|
-
|
|
155
|
-
// Related
|
|
156
|
-
relatedDecisions: decision.relatedDecisions || [],
|
|
157
|
-
supersedes: decision.supersedes || null,
|
|
158
|
-
supersededBy: null,
|
|
159
|
-
|
|
160
|
-
// Tracking
|
|
161
|
-
outcome: null,
|
|
162
|
-
lessonsLearned: [],
|
|
163
|
-
reviewDate: decision.reviewDate || null,
|
|
164
|
-
|
|
165
|
-
// Metadata
|
|
166
|
-
authors: decision.authors || [],
|
|
167
|
-
tags: decision.tags || [],
|
|
168
|
-
links: decision.links || []
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
data.decisions.push(newDecision);
|
|
172
|
-
|
|
173
|
-
// If this supersedes another decision, update that one
|
|
174
|
-
if (decision.supersedes) {
|
|
175
|
-
const superseded = data.decisions.find(d => d.id === decision.supersedes);
|
|
176
|
-
if (superseded) {
|
|
177
|
-
superseded.status = DECISION_STATUS.SUPERSEDED;
|
|
178
|
-
superseded.supersededBy = id;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return newDecision;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Update decision status
|
|
187
|
-
*/
|
|
188
|
-
updateDecisionStatus(data, decisionId, status, notes = null) {
|
|
189
|
-
const decision = data.decisions.find(d => d.id === decisionId);
|
|
190
|
-
if (decision) {
|
|
191
|
-
decision.status = status;
|
|
192
|
-
if (notes) {
|
|
193
|
-
decision.statusNotes = notes;
|
|
194
|
-
}
|
|
195
|
-
decision.lastUpdated = new Date().toISOString();
|
|
196
|
-
}
|
|
197
|
-
return decision;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Record outcome for a decision
|
|
202
|
-
*/
|
|
203
|
-
recordOutcome(data, decisionId, outcome) {
|
|
204
|
-
const decision = data.decisions.find(d => d.id === decisionId);
|
|
205
|
-
if (decision) {
|
|
206
|
-
decision.outcome = {
|
|
207
|
-
status: outcome.status, // 'success', 'partial', 'failure'
|
|
208
|
-
description: outcome.description,
|
|
209
|
-
metrics: outcome.metrics || {},
|
|
210
|
-
date: new Date().toISOString().split('T')[0]
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
return decision;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Add lessons learned
|
|
218
|
-
*/
|
|
219
|
-
addLessonLearned(data, decisionId, lesson) {
|
|
220
|
-
const decision = data.decisions.find(d => d.id === decisionId);
|
|
221
|
-
if (decision) {
|
|
222
|
-
decision.lessonsLearned.push({
|
|
223
|
-
lesson,
|
|
224
|
-
date: new Date().toISOString().split('T')[0]
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
return decision;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Generate DECISIONS.md content
|
|
232
|
-
*/
|
|
233
|
-
generate(data = null) {
|
|
234
|
-
if (!data) {
|
|
235
|
-
data = this.loadDecisions();
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const sections = [];
|
|
239
|
-
const now = new Date().toISOString().split('T')[0];
|
|
240
|
-
|
|
241
|
-
// Header
|
|
242
|
-
sections.push(`# Architecture Decision Records
|
|
243
|
-
|
|
244
|
-
**Project:** ${data.project}
|
|
245
|
-
**Last Updated:** ${now}
|
|
246
|
-
**Total Decisions:** ${data.decisions.length}
|
|
247
|
-
|
|
248
|
-
---
|
|
249
|
-
|
|
250
|
-
## Overview
|
|
251
|
-
|
|
252
|
-
This document tracks significant architecture and design decisions made during the development of this project. Each decision is recorded as an Architecture Decision Record (ADR).
|
|
253
|
-
|
|
254
|
-
### Status Legend
|
|
255
|
-
|
|
256
|
-
| Status | Meaning |
|
|
257
|
-
|--------|---------|
|
|
258
|
-
| 📝 Proposed | Under discussion, not yet accepted |
|
|
259
|
-
| ✅ Accepted | Decision has been approved and is in effect |
|
|
260
|
-
| ⚠️ Deprecated | Decision is being phased out |
|
|
261
|
-
| 🔄 Superseded | Replaced by a newer decision |
|
|
262
|
-
| ❌ Rejected | Decision was considered but not adopted |
|
|
263
|
-
|
|
264
|
-
---`);
|
|
265
|
-
|
|
266
|
-
// Summary Table
|
|
267
|
-
const activeDecisions = data.decisions.filter(d =>
|
|
268
|
-
d.status === DECISION_STATUS.ACCEPTED || d.status === DECISION_STATUS.PROPOSED
|
|
269
|
-
);
|
|
270
|
-
|
|
271
|
-
if (data.decisions.length > 0) {
|
|
272
|
-
sections.push(`## Decision Index
|
|
273
|
-
|
|
274
|
-
| ID | Title | Category | Status | Impact | Date |
|
|
275
|
-
|----|-------|----------|--------|--------|------|
|
|
276
|
-
${data.decisions.map(d => {
|
|
277
|
-
const category = DECISION_CATEGORIES[d.category] || { emoji: '📋', label: d.category };
|
|
278
|
-
const impact = IMPACT_LEVELS[d.impact] || { emoji: '🟡', label: d.impact };
|
|
279
|
-
return `| [${d.id}](#${d.id.toLowerCase()}) | ${d.title} | ${category.emoji} ${category.label} | ${this.formatStatus(d.status)} | ${impact.emoji} ${impact.label} | ${d.date} |`;
|
|
280
|
-
}).join('\n')}
|
|
281
|
-
|
|
282
|
-
---`);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Individual Decisions
|
|
286
|
-
if (data.decisions.length > 0) {
|
|
287
|
-
sections.push('## Decisions\n');
|
|
288
|
-
|
|
289
|
-
for (const decision of data.decisions) {
|
|
290
|
-
sections.push(this.formatDecision(decision));
|
|
291
|
-
}
|
|
292
|
-
} else {
|
|
293
|
-
sections.push(`## Decisions
|
|
294
|
-
|
|
295
|
-
_No decisions recorded yet._
|
|
296
|
-
|
|
297
|
-
To add a decision, use:
|
|
298
|
-
\`\`\`bash
|
|
299
|
-
bootspring docs decisions add "Decision Title"
|
|
300
|
-
\`\`\`
|
|
301
|
-
|
|
302
|
-
---`);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Statistics
|
|
306
|
-
const stats = this.calculateStatistics(data);
|
|
307
|
-
sections.push(`## Statistics
|
|
308
|
-
|
|
309
|
-
| Metric | Value |
|
|
310
|
-
|--------|-------|
|
|
311
|
-
| Total Decisions | ${stats.total} |
|
|
312
|
-
| Accepted | ${stats.accepted} |
|
|
313
|
-
| Proposed | ${stats.proposed} |
|
|
314
|
-
| Superseded | ${stats.superseded} |
|
|
315
|
-
| With Outcomes | ${stats.withOutcomes} |
|
|
316
|
-
| Success Rate | ${stats.successRate}% |
|
|
317
|
-
|
|
318
|
-
### By Category
|
|
319
|
-
|
|
320
|
-
${Object.entries(stats.byCategory).map(([cat, count]) => {
|
|
321
|
-
const category = DECISION_CATEGORIES[cat] || { emoji: '📋', label: cat };
|
|
322
|
-
return `- ${category.emoji} **${category.label}:** ${count}`;
|
|
323
|
-
}).join('\n')}
|
|
324
|
-
|
|
325
|
-
### By Impact
|
|
326
|
-
|
|
327
|
-
${Object.entries(stats.byImpact).map(([imp, count]) => {
|
|
328
|
-
const impact = IMPACT_LEVELS[imp] || { emoji: '🟡', label: imp };
|
|
329
|
-
return `- ${impact.emoji} **${impact.label}:** ${count}`;
|
|
330
|
-
}).join('\n')}
|
|
331
|
-
|
|
332
|
-
---`);
|
|
333
|
-
|
|
334
|
-
// Lessons Learned Summary
|
|
335
|
-
const lessonsLearned = data.decisions
|
|
336
|
-
.filter(d => d.lessonsLearned && d.lessonsLearned.length > 0)
|
|
337
|
-
.flatMap(d => d.lessonsLearned.map(l => ({ ...l, decisionId: d.id, title: d.title })));
|
|
338
|
-
|
|
339
|
-
if (lessonsLearned.length > 0) {
|
|
340
|
-
sections.push(`## Lessons Learned
|
|
341
|
-
|
|
342
|
-
${lessonsLearned.slice(0, 10).map(l => `- **[${l.decisionId}]** ${l.lesson}
|
|
343
|
-
_From: ${l.title} (${l.date})_`).join('\n\n')}
|
|
344
|
-
|
|
345
|
-
---`);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Footer
|
|
349
|
-
sections.push(`---
|
|
350
|
-
|
|
351
|
-
*Generated by [Bootspring](https://bootspring.com) Decisions Generator*
|
|
352
|
-
|
|
353
|
-
### Adding New Decisions
|
|
354
|
-
|
|
355
|
-
\`\`\`bash
|
|
356
|
-
# Add a new decision interactively
|
|
357
|
-
bootspring docs decisions add
|
|
358
|
-
|
|
359
|
-
# Add with title
|
|
360
|
-
bootspring docs decisions add "Use PostgreSQL for Primary Database"
|
|
361
|
-
|
|
362
|
-
# View decision details
|
|
363
|
-
bootspring docs decisions show ADR-0001
|
|
364
|
-
\`\`\`
|
|
365
|
-
`);
|
|
366
|
-
|
|
367
|
-
return sections.join('\n\n');
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/**
|
|
371
|
-
* Format status with emoji
|
|
372
|
-
*/
|
|
373
|
-
formatStatus(status) {
|
|
374
|
-
const statusMap = {
|
|
375
|
-
[DECISION_STATUS.PROPOSED]: '📝 Proposed',
|
|
376
|
-
[DECISION_STATUS.ACCEPTED]: '✅ Accepted',
|
|
377
|
-
[DECISION_STATUS.DEPRECATED]: '⚠️ Deprecated',
|
|
378
|
-
[DECISION_STATUS.SUPERSEDED]: '🔄 Superseded',
|
|
379
|
-
[DECISION_STATUS.REJECTED]: '❌ Rejected'
|
|
380
|
-
};
|
|
381
|
-
return statusMap[status] || status;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* Format a single decision as markdown
|
|
386
|
-
*/
|
|
387
|
-
formatDecision(decision) {
|
|
388
|
-
const category = DECISION_CATEGORIES[decision.category] || { emoji: '📋', label: decision.category };
|
|
389
|
-
const impact = IMPACT_LEVELS[decision.impact] || { emoji: '🟡', label: decision.impact };
|
|
390
|
-
|
|
391
|
-
let md = `### ${decision.id}
|
|
392
|
-
|
|
393
|
-
# ${decision.title}
|
|
394
|
-
|
|
395
|
-
**Date:** ${decision.date}
|
|
396
|
-
**Status:** ${this.formatStatus(decision.status)}
|
|
397
|
-
**Category:** ${category.emoji} ${category.label}
|
|
398
|
-
**Impact:** ${impact.emoji} ${impact.label}
|
|
399
|
-
${decision.authors.length > 0 ? `**Authors:** ${decision.authors.join(', ')}` : ''}
|
|
400
|
-
${decision.tags.length > 0 ? `**Tags:** ${decision.tags.map(t => `\`${t}\``).join(' ')}` : ''}
|
|
401
|
-
|
|
402
|
-
#### Context
|
|
403
|
-
|
|
404
|
-
${decision.context || '_No context provided._'}
|
|
405
|
-
|
|
406
|
-
#### Problem
|
|
407
|
-
|
|
408
|
-
${decision.problem || '_No problem statement provided._'}
|
|
409
|
-
|
|
410
|
-
#### Decision
|
|
411
|
-
|
|
412
|
-
${decision.decision || '_No decision recorded._'}
|
|
413
|
-
|
|
414
|
-
#### Rationale
|
|
415
|
-
|
|
416
|
-
${decision.rationale || '_No rationale provided._'}
|
|
417
|
-
`;
|
|
418
|
-
|
|
419
|
-
// Options considered
|
|
420
|
-
if (decision.options && decision.options.length > 0) {
|
|
421
|
-
md += `
|
|
422
|
-
#### Options Considered
|
|
423
|
-
|
|
424
|
-
${decision.options.map((opt, i) => {
|
|
425
|
-
const chosen = opt.chosen ? ' ✅ **CHOSEN**' : '';
|
|
426
|
-
return `**Option ${i + 1}: ${opt.name}**${chosen}
|
|
427
|
-
|
|
428
|
-
${opt.description || ''}
|
|
429
|
-
|
|
430
|
-
- Pros: ${(opt.pros || []).join(', ') || 'None listed'}
|
|
431
|
-
- Cons: ${(opt.cons || []).join(', ') || 'None listed'}`;
|
|
432
|
-
}).join('\n\n')}
|
|
433
|
-
`;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// Consequences
|
|
437
|
-
md += `
|
|
438
|
-
#### Consequences
|
|
439
|
-
|
|
440
|
-
**Positive:**
|
|
441
|
-
${decision.consequences.positive.length > 0 ? decision.consequences.positive.map(c => `- ✅ ${c}`).join('\n') : '- _None identified_'}
|
|
442
|
-
|
|
443
|
-
**Negative:**
|
|
444
|
-
${decision.consequences.negative.length > 0 ? decision.consequences.negative.map(c => `- ⚠️ ${c}`).join('\n') : '- _None identified_'}
|
|
445
|
-
|
|
446
|
-
**Risks:**
|
|
447
|
-
${decision.consequences.risks.length > 0 ? decision.consequences.risks.map(c => `- 🔴 ${c}`).join('\n') : '- _None identified_'}
|
|
448
|
-
`;
|
|
449
|
-
|
|
450
|
-
// Related decisions
|
|
451
|
-
if (decision.relatedDecisions.length > 0) {
|
|
452
|
-
md += `
|
|
453
|
-
#### Related Decisions
|
|
454
|
-
|
|
455
|
-
${decision.relatedDecisions.map(r => `- [${r}](#${r.toLowerCase()})`).join('\n')}
|
|
456
|
-
`;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// Supersedes
|
|
460
|
-
if (decision.supersedes) {
|
|
461
|
-
md += `
|
|
462
|
-
#### Supersedes
|
|
463
|
-
|
|
464
|
-
This decision supersedes [${decision.supersedes}](#${decision.supersedes.toLowerCase()}).
|
|
465
|
-
`;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// Superseded by
|
|
469
|
-
if (decision.supersededBy) {
|
|
470
|
-
md += `
|
|
471
|
-
#### Superseded By
|
|
472
|
-
|
|
473
|
-
This decision has been superseded by [${decision.supersededBy}](#${decision.supersededBy.toLowerCase()}).
|
|
474
|
-
`;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// Outcome
|
|
478
|
-
if (decision.outcome) {
|
|
479
|
-
const outcomeEmoji = decision.outcome.status === 'success' ? '✅' :
|
|
480
|
-
decision.outcome.status === 'partial' ? '🟡' : '❌';
|
|
481
|
-
|
|
482
|
-
md += `
|
|
483
|
-
#### Outcome
|
|
484
|
-
|
|
485
|
-
**Status:** ${outcomeEmoji} ${decision.outcome.status.charAt(0).toUpperCase() + decision.outcome.status.slice(1)}
|
|
486
|
-
**Date:** ${decision.outcome.date}
|
|
487
|
-
|
|
488
|
-
${decision.outcome.description || ''}
|
|
489
|
-
`;
|
|
490
|
-
|
|
491
|
-
if (decision.outcome.metrics && Object.keys(decision.outcome.metrics).length > 0) {
|
|
492
|
-
md += `
|
|
493
|
-
**Metrics:**
|
|
494
|
-
${Object.entries(decision.outcome.metrics).map(([k, v]) => `- ${k}: ${v}`).join('\n')}
|
|
495
|
-
`;
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// Lessons learned
|
|
500
|
-
if (decision.lessonsLearned && decision.lessonsLearned.length > 0) {
|
|
501
|
-
md += `
|
|
502
|
-
#### Lessons Learned
|
|
503
|
-
|
|
504
|
-
${decision.lessonsLearned.map(l => `- ${l.lesson} _(${l.date})_`).join('\n')}
|
|
505
|
-
`;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// Links
|
|
509
|
-
if (decision.links && decision.links.length > 0) {
|
|
510
|
-
md += `
|
|
511
|
-
#### References
|
|
512
|
-
|
|
513
|
-
${decision.links.map(l => `- [${l.title || l.url}](${l.url})`).join('\n')}
|
|
514
|
-
`;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
md += '\n---\n';
|
|
518
|
-
|
|
519
|
-
return md;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/**
|
|
523
|
-
* Calculate statistics
|
|
524
|
-
*/
|
|
525
|
-
calculateStatistics(data) {
|
|
526
|
-
const decisions = data.decisions;
|
|
527
|
-
|
|
528
|
-
const stats = {
|
|
529
|
-
total: decisions.length,
|
|
530
|
-
accepted: decisions.filter(d => d.status === DECISION_STATUS.ACCEPTED).length,
|
|
531
|
-
proposed: decisions.filter(d => d.status === DECISION_STATUS.PROPOSED).length,
|
|
532
|
-
superseded: decisions.filter(d => d.status === DECISION_STATUS.SUPERSEDED).length,
|
|
533
|
-
deprecated: decisions.filter(d => d.status === DECISION_STATUS.DEPRECATED).length,
|
|
534
|
-
rejected: decisions.filter(d => d.status === DECISION_STATUS.REJECTED).length,
|
|
535
|
-
withOutcomes: decisions.filter(d => d.outcome).length,
|
|
536
|
-
byCategory: {},
|
|
537
|
-
byImpact: {}
|
|
538
|
-
};
|
|
539
|
-
|
|
540
|
-
// Success rate
|
|
541
|
-
const withOutcomes = decisions.filter(d => d.outcome);
|
|
542
|
-
const successful = withOutcomes.filter(d => d.outcome.status === 'success');
|
|
543
|
-
stats.successRate = withOutcomes.length > 0 ? Math.round((successful.length / withOutcomes.length) * 100) : 0;
|
|
544
|
-
|
|
545
|
-
// By category
|
|
546
|
-
for (const d of decisions) {
|
|
547
|
-
stats.byCategory[d.category] = (stats.byCategory[d.category] || 0) + 1;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
// By impact
|
|
551
|
-
for (const d of decisions) {
|
|
552
|
-
stats.byImpact[d.impact] = (stats.byImpact[d.impact] || 0) + 1;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
return stats;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
/**
|
|
559
|
-
* Import decisions from decision intelligence module
|
|
560
|
-
*/
|
|
561
|
-
async importFromDecisionIntelligence(data) {
|
|
562
|
-
try {
|
|
563
|
-
const { DecisionIntelligence } = require('../intelligence/decision-intelligence');
|
|
564
|
-
const di = new DecisionIntelligence(this.projectRoot);
|
|
565
|
-
const recentDecisions = await di.getRecentDecisions(50);
|
|
566
|
-
|
|
567
|
-
for (const d of recentDecisions) {
|
|
568
|
-
// Check if already exists
|
|
569
|
-
const exists = data.decisions.some(existing =>
|
|
570
|
-
existing.title.toLowerCase() === d.context?.toLowerCase() ||
|
|
571
|
-
existing.decision === d.chosen
|
|
572
|
-
);
|
|
573
|
-
|
|
574
|
-
if (!exists && d.type && d.chosen) {
|
|
575
|
-
this.addDecision(data, {
|
|
576
|
-
title: d.context || d.type,
|
|
577
|
-
category: this.mapTypeToCategory(d.type),
|
|
578
|
-
decision: d.chosen,
|
|
579
|
-
rationale: d.rationale || '',
|
|
580
|
-
status: DECISION_STATUS.ACCEPTED,
|
|
581
|
-
tags: d.tags || []
|
|
582
|
-
});
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
} catch (_err) {
|
|
586
|
-
// Decision intelligence not available
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
return data;
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
/**
|
|
593
|
-
* Map decision type to category
|
|
594
|
-
*/
|
|
595
|
-
mapTypeToCategory(type) {
|
|
596
|
-
const mapping = {
|
|
597
|
-
architecture: 'ARCHITECTURE',
|
|
598
|
-
technology: 'TECHNOLOGY',
|
|
599
|
-
database: 'DATA',
|
|
600
|
-
api: 'API',
|
|
601
|
-
security: 'SECURITY',
|
|
602
|
-
performance: 'PERFORMANCE',
|
|
603
|
-
ui: 'UI_UX',
|
|
604
|
-
deployment: 'INFRASTRUCTURE'
|
|
605
|
-
};
|
|
606
|
-
return mapping[type] || 'ARCHITECTURE';
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
/**
|
|
611
|
-
* Generate DECISIONS.md
|
|
612
|
-
*/
|
|
613
|
-
function generate(options = {}) {
|
|
614
|
-
const generator = new DecisionsGenerator(options);
|
|
615
|
-
const data = generator.loadDecisions();
|
|
616
|
-
return generator.generate(data);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
/**
|
|
620
|
-
* Load decisions data
|
|
621
|
-
*/
|
|
622
|
-
function loadDecisions(options = {}) {
|
|
623
|
-
const generator = new DecisionsGenerator(options);
|
|
624
|
-
return generator.loadDecisions();
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
/**
|
|
628
|
-
* Save decisions data
|
|
629
|
-
*/
|
|
630
|
-
function saveDecisions(data, options = {}) {
|
|
631
|
-
const generator = new DecisionsGenerator(options);
|
|
632
|
-
generator.saveDecisions(data);
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
/**
|
|
636
|
-
* Add a new decision
|
|
637
|
-
*/
|
|
638
|
-
function addDecision(decision, options = {}) {
|
|
639
|
-
const generator = new DecisionsGenerator(options);
|
|
640
|
-
const data = generator.loadDecisions();
|
|
641
|
-
const newDecision = generator.addDecision(data, decision);
|
|
642
|
-
generator.saveDecisions(data);
|
|
643
|
-
return newDecision;
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
module.exports = {
|
|
647
|
-
DecisionsGenerator,
|
|
648
|
-
generate,
|
|
649
|
-
loadDecisions,
|
|
650
|
-
saveDecisions,
|
|
651
|
-
addDecision,
|
|
652
|
-
DECISION_STATUS,
|
|
653
|
-
DECISION_CATEGORIES,
|
|
654
|
-
IMPACT_LEVELS
|
|
655
|
-
};
|