vibe-splain 1.1.0 → 2.0.0
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/dist/index.js +1325 -302
- package/dist/mcp/server.js +60 -2
- package/dist/mcp/tools/get_file_context.d.ts +4 -0
- package/dist/mcp/tools/get_file_context.js +30 -27
- package/dist/mcp/tools/get_project_map.d.ts +15 -0
- package/dist/mcp/tools/get_project_map.js +41 -0
- package/dist/mcp/tools/mark_stale.js +8 -1
- package/dist/mcp/tools/scan_project.js +27 -31
- package/dist/mcp/tools/set_project_brief.d.ts +19 -0
- package/dist/mcp/tools/set_project_brief.js +27 -0
- package/dist/mcp/tools/write_decision_card.d.ts +29 -5
- package/dist/mcp/tools/write_decision_card.js +76 -67
- package/dist/ui/index.html +279 -278
- package/package.json +1 -1
|
@@ -1,118 +1,127 @@
|
|
|
1
|
-
import { readDossier, writeDossier, validateMermaidNodeCount } from '@vibe-splain/brain';
|
|
1
|
+
import { readDossier, writeDossier, validateMermaidNodeCount, readAnalysis } from '@vibe-splain/brain';
|
|
2
2
|
import { v4 as uuidv4 } from 'uuid';
|
|
3
3
|
import { createHash } from 'crypto';
|
|
4
4
|
import { readFile } from 'fs/promises';
|
|
5
5
|
import { join } from 'path';
|
|
6
|
+
const CATEGORIES = ['Bottleneck', 'Hack', 'Smart-Move', 'Risk', 'Convention', 'Dead-Weight'];
|
|
6
7
|
export const writeDecisionCardTool = {
|
|
7
8
|
name: 'write_decision_card',
|
|
8
|
-
description: 'Persists
|
|
9
|
+
description: 'Persists ONE Decision Card about ONE file. This is a hostile architecture review, not documentation. The thesis must be a VERDICT, not a description. The pillar MUST be one of the names from get_project_map (free-form is rejected). One card per file (duplicates rejected). Evidence must come from get_file_context hotSpans/smellSpans — never the header comment, never the whole file.',
|
|
9
10
|
inputSchema: {
|
|
10
11
|
type: 'object',
|
|
11
12
|
properties: {
|
|
12
|
-
projectRoot: {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
},
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
},
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
},
|
|
24
|
-
narrative: {
|
|
25
|
-
type: 'string',
|
|
26
|
-
description: '3-5 sentences explaining WHY this code exists',
|
|
27
|
-
},
|
|
13
|
+
projectRoot: { type: 'string' },
|
|
14
|
+
pillar: { type: 'string', description: 'MUST be one of the pillar names from get_project_map. Free-form values are rejected.' },
|
|
15
|
+
primaryFile: { type: 'string', description: 'The single file this card is about (relative path). Used to reject duplicate cards.' },
|
|
16
|
+
title: { type: 'string' },
|
|
17
|
+
thesis: { type: 'string', description: "ONE sharp sentence. A verdict, not a description. Take a position. Bad: 'This file implements a panel system.' Good: 'A 600-line god-component that owns drag, zoom, persistence AND the host bridge — the single highest-risk refactor in the app.'" },
|
|
18
|
+
category: { type: 'string', enum: CATEGORIES },
|
|
19
|
+
severity: { type: 'integer', minimum: 1, maximum: 5 },
|
|
20
|
+
narrative: { type: 'string', description: "3-5 sentences. WHY it exists and WHY it's built this way. Do NOT restate the file's header comments." },
|
|
21
|
+
tradeoff: { type: 'string', description: 'What was given up, or why the obvious approach was rejected. Null only if genuinely none.' },
|
|
22
|
+
blastRadius: { type: 'string', description: 'What breaks if this changes. Ground it in the fan-in (importedBy) from get_file_context.' },
|
|
23
|
+
confidence: { type: 'string', enum: ['low', 'medium', 'high'] },
|
|
28
24
|
evidence: {
|
|
29
25
|
type: 'array',
|
|
30
26
|
items: {
|
|
31
27
|
type: 'object',
|
|
32
28
|
properties: {
|
|
33
|
-
file: { type: 'string'
|
|
34
|
-
startLine: { type: 'number'
|
|
35
|
-
endLine: { type: 'number'
|
|
36
|
-
snippet: { type: 'string'
|
|
29
|
+
file: { type: 'string' },
|
|
30
|
+
startLine: { type: 'number' },
|
|
31
|
+
endLine: { type: 'number' },
|
|
32
|
+
snippet: { type: 'string' },
|
|
37
33
|
},
|
|
38
34
|
required: ['file', 'startLine', 'endLine', 'snippet'],
|
|
39
35
|
},
|
|
40
|
-
description: '
|
|
41
|
-
},
|
|
42
|
-
diagram: {
|
|
43
|
-
type: 'string',
|
|
44
|
-
description: 'Optional Mermaid diagram (stateDiagram-v2, flowchart TD, or linear style). Max 7 nodes.',
|
|
36
|
+
description: 'Use hotSpans/smellSpans from get_file_context. NEVER cite header comments or the whole file.',
|
|
45
37
|
},
|
|
38
|
+
diagram: { type: 'string', description: 'Optional. stateDiagram-v2 / flowchart TD / linear. Max 7 nodes.' },
|
|
46
39
|
},
|
|
47
|
-
required: ['projectRoot', 'pillar', 'title', 'narrative', 'evidence'],
|
|
40
|
+
required: ['projectRoot', 'pillar', 'primaryFile', 'title', 'thesis', 'category', 'severity', 'narrative', 'confidence', 'evidence'],
|
|
48
41
|
},
|
|
49
42
|
};
|
|
50
43
|
export async function handleWriteDecisionCard(args) {
|
|
51
44
|
const projectRoot = args.projectRoot;
|
|
52
45
|
const pillar = args.pillar;
|
|
46
|
+
const primaryFile = args.primaryFile;
|
|
53
47
|
const title = args.title;
|
|
48
|
+
const thesis = args.thesis;
|
|
49
|
+
const category = args.category;
|
|
50
|
+
const severity = args.severity;
|
|
54
51
|
const narrative = args.narrative;
|
|
52
|
+
const tradeoff = args.tradeoff || null;
|
|
53
|
+
const blastRadius = args.blastRadius || null;
|
|
54
|
+
const confidence = args.confidence || 'medium';
|
|
55
55
|
const evidence = args.evidence;
|
|
56
56
|
const diagram = args.diagram || null;
|
|
57
|
-
if (!projectRoot || !pillar || !title || !narrative || !evidence) {
|
|
58
|
-
throw new Error('projectRoot, pillar, title, narrative, and evidence are required');
|
|
57
|
+
if (!projectRoot || !pillar || !primaryFile || !title || !thesis || !category || !narrative || !evidence) {
|
|
58
|
+
throw new Error('projectRoot, pillar, primaryFile, title, thesis, category, narrative, and evidence are required');
|
|
59
|
+
}
|
|
60
|
+
if (!CATEGORIES.includes(category)) {
|
|
61
|
+
throw new Error(`Invalid category "${category}". Must be one of: ${CATEGORIES.join(', ')}`);
|
|
59
62
|
}
|
|
60
|
-
// Validate Mermaid diagram node count
|
|
61
63
|
if (diagram && !validateMermaidNodeCount(diagram)) {
|
|
62
64
|
throw new Error('Mermaid diagram exceeds maximum of 7 nodes. Simplify the diagram.');
|
|
63
65
|
}
|
|
64
|
-
|
|
66
|
+
const dossier = await readDossier(projectRoot);
|
|
67
|
+
if (!dossier || !dossier.map) {
|
|
68
|
+
throw new Error('No project map found. Run scan_project and set_project_brief before writing cards.');
|
|
69
|
+
}
|
|
70
|
+
// Enforce fixed pillar set.
|
|
71
|
+
const legalPillars = dossier.map.pillars.map(p => p.name);
|
|
72
|
+
if (!legalPillars.includes(pillar)) {
|
|
73
|
+
throw new Error(`Pillar "${pillar}" is not a legal pillar. Use one of: ${legalPillars.join(', ')}. Pillars are fixed by the scan — you may not invent new ones.`);
|
|
74
|
+
}
|
|
75
|
+
// Reject duplicate primaryFile — unless the existing card is stale (rewrite path).
|
|
76
|
+
const existing = [...dossier.pillars.flatMap(p => p.decisions), ...dossier.wildDiscoveries]
|
|
77
|
+
.find(c => c.primaryFile === primaryFile);
|
|
78
|
+
if (existing) {
|
|
79
|
+
if (existing.status === 'fresh') {
|
|
80
|
+
throw new Error(`A card already exists for "${primaryFile}". One card per file. To revise it, call mark_stale on this file and rewrite, or pick a different file.`);
|
|
81
|
+
}
|
|
82
|
+
// stale: drop the old card so the rewrite can replace it.
|
|
83
|
+
for (const p of dossier.pillars)
|
|
84
|
+
p.decisions = p.decisions.filter(c => c.id !== existing.id);
|
|
85
|
+
dossier.wildDiscoveries = dossier.wildDiscoveries.filter(c => c.id !== existing.id);
|
|
86
|
+
}
|
|
87
|
+
// Auto-carry gravity/heat from the scan.
|
|
88
|
+
const store = await readAnalysis(projectRoot);
|
|
89
|
+
const persisted = store?.files[primaryFile];
|
|
90
|
+
const gravity = persisted ? Math.round(persisted.gravity) : undefined;
|
|
91
|
+
const heat = persisted ? Math.round(persisted.heat) : undefined;
|
|
92
|
+
// Hash for staleness.
|
|
65
93
|
let combinedContent = '';
|
|
66
94
|
for (const e of evidence) {
|
|
67
95
|
try {
|
|
68
|
-
|
|
69
|
-
const content = await readFile(fullPath, 'utf8');
|
|
70
|
-
combinedContent += content;
|
|
96
|
+
combinedContent += await readFile(join(projectRoot, e.file), 'utf8');
|
|
71
97
|
}
|
|
72
98
|
catch {
|
|
73
|
-
// File might not exist, use snippet
|
|
74
99
|
combinedContent += e.snippet;
|
|
75
100
|
}
|
|
76
101
|
}
|
|
77
102
|
const hash = createHash('sha256').update(combinedContent).digest('hex');
|
|
78
103
|
const card = {
|
|
79
104
|
id: uuidv4(),
|
|
80
|
-
pillar,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
evidence,
|
|
84
|
-
diagram,
|
|
105
|
+
pillar, title, thesis, category, severity, narrative,
|
|
106
|
+
tradeoff, blastRadius, confidence, evidence, diagram,
|
|
107
|
+
gravity, heat, primaryFile,
|
|
85
108
|
status: 'fresh',
|
|
86
109
|
lastScannedHash: hash,
|
|
87
110
|
};
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
if (
|
|
91
|
-
dossier
|
|
92
|
-
version: '1.0.0',
|
|
93
|
-
scannedAt: new Date().toISOString(),
|
|
94
|
-
projectRoot,
|
|
95
|
-
pillars: [],
|
|
96
|
-
wildDiscoveries: [],
|
|
97
|
-
stalePaths: [],
|
|
98
|
-
};
|
|
111
|
+
// A high-heat card (severity >= 4) is also a Wild Discovery.
|
|
112
|
+
const isWild = severity >= 4 || (heat !== undefined && heat >= 60);
|
|
113
|
+
if (isWild) {
|
|
114
|
+
dossier.wildDiscoveries.push(card);
|
|
99
115
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
dossier.pillars.push(existingPillar);
|
|
116
|
+
let bucket = dossier.pillars.find(p => p.name === pillar);
|
|
117
|
+
if (!bucket) {
|
|
118
|
+
bucket = { name: pillar, cardCount: 0, decisions: [] };
|
|
119
|
+
dossier.pillars.push(bucket);
|
|
105
120
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
existingPillar.cardCount = existingPillar.decisions.length;
|
|
121
|
+
bucket.decisions.push(card);
|
|
122
|
+
bucket.cardCount = bucket.decisions.length;
|
|
109
123
|
await writeDossier(projectRoot, dossier);
|
|
110
|
-
console.error(`[vibe-splain]
|
|
111
|
-
return {
|
|
112
|
-
success: true,
|
|
113
|
-
cardId: card.id,
|
|
114
|
-
pillar,
|
|
115
|
-
title,
|
|
116
|
-
};
|
|
124
|
+
console.error(`[vibe-splain] Card written: "${title}" [${category} sev${severity}] in "${pillar}"${isWild ? ' (Wild Discovery)' : ''}`);
|
|
125
|
+
return { success: true, cardId: card.id, pillar, primaryFile, category, severity, wildDiscovery: isWild, gravity, heat };
|
|
117
126
|
}
|
|
118
127
|
//# sourceMappingURL=write_decision_card.js.map
|