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.
@@ -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 a Decision Card you have synthesized to the project\'s dossier. The narrative should be 3–5 sentences explaining WHY this code exists. Evidence must reference specific line ranges from the actual source. Diagrams are optional but use only stateDiagram-v2, flowchart TD, or linear A-->B-->C style, max 7 nodes. Will reject diagrams with more than 7 nodes.',
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
- type: 'string',
14
- description: 'Absolute path to the project root',
15
- },
16
- pillar: {
17
- type: 'string',
18
- description: 'The pillar this card belongs to (e.g., Auth, Database, etc.)',
19
- },
20
- title: {
21
- type: 'string',
22
- description: 'Short title for the decision card',
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', description: 'Relative file path' },
34
- startLine: { type: 'number', description: 'Start line number' },
35
- endLine: { type: 'number', description: 'End line number' },
36
- snippet: { type: 'string', description: 'Code snippet from the file' },
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: 'Array of evidence items referencing specific code',
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
- // Compute hash from evidence files
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
- const fullPath = join(projectRoot, e.file);
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
- title,
82
- narrative,
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
- // Read existing dossier or create new one
89
- let dossier = await readDossier(projectRoot);
90
- if (!dossier) {
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
- // Find or create pillar
101
- let existingPillar = dossier.pillars.find(p => p.name === pillar);
102
- if (!existingPillar) {
103
- existingPillar = { name: pillar, cardCount: 0, decisions: [] };
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
- // Add card to pillar
107
- existingPillar.decisions.push(card);
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] Decision card written: "${title}" in pillar "${pillar}"`);
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