@boshu2/vibe-check 1.5.0 → 1.6.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/.agents/bundles/insight-mining-dashboard-research-2025-11-30.md +400 -0
- package/.agents/bundles/storage-enhancement-research-2025-11-30.md +292 -0
- package/.agents/bundles/timeline-feature-research-complete-2025-11-30.md +301 -0
- package/.agents/plans/insight-dashboard-plan-2025-11-30.md +1130 -0
- package/.agents/plans/json-storage-enhancement-plan.md +717 -0
- package/.agents/plans/storage-hardening-and-cache-plan.md +592 -0
- package/.agents/plans/test-coverage-gaps-plan.md +1117 -0
- package/.agents/plans/timeline-feature-plan.md +193 -0
- package/.agents/plans/vibe_timeline_research_findings.md +553 -0
- package/.claude/settings.local.json +1 -0
- package/.vibe-check/.gitignore +6 -0
- package/CHANGELOG.md +46 -0
- package/CLAUDE.md +24 -0
- package/CONTRIBUTING.md +227 -0
- package/README.md +200 -143
- package/claude-progress.json +191 -9
- package/claude-progress.txt +257 -0
- package/dashboard/app.js +75 -2
- package/dashboard/dashboard-data.json +653 -0
- package/dashboard/index.html +13 -0
- package/dashboard/styles.css +61 -0
- package/dist/analysis/cross-session-analysis.d.ts +68 -0
- package/dist/analysis/cross-session-analysis.d.ts.map +1 -0
- package/dist/analysis/cross-session-analysis.js +174 -0
- package/dist/analysis/cross-session-analysis.js.map +1 -0
- package/dist/analysis/index.d.ts +2 -0
- package/dist/analysis/index.d.ts.map +1 -0
- package/dist/analysis/index.js +12 -0
- package/dist/analysis/index.js.map +1 -0
- package/dist/cli.js +10 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/analyze.d.ts +2 -0
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +105 -2
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/cache.d.ts +6 -0
- package/dist/commands/cache.d.ts.map +1 -0
- package/dist/commands/cache.js +168 -0
- package/dist/commands/cache.js.map +1 -0
- package/dist/commands/dashboard.d.ts +8 -0
- package/dist/commands/dashboard.d.ts.map +1 -0
- package/dist/commands/dashboard.js +109 -0
- package/dist/commands/dashboard.js.map +1 -0
- package/dist/commands/index.d.ts +3 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +8 -1
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/timeline.d.ts +14 -0
- package/dist/commands/timeline.d.ts.map +1 -0
- package/dist/commands/timeline.js +462 -0
- package/dist/commands/timeline.js.map +1 -0
- package/dist/git.d.ts +24 -0
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +94 -0
- package/dist/git.js.map +1 -1
- package/dist/insights/generators.d.ts +44 -0
- package/dist/insights/generators.d.ts.map +1 -0
- package/dist/insights/generators.js +289 -0
- package/dist/insights/generators.js.map +1 -0
- package/dist/insights/index.d.ts +16 -0
- package/dist/insights/index.d.ts.map +1 -0
- package/dist/insights/index.js +171 -0
- package/dist/insights/index.js.map +1 -0
- package/dist/insights/types.d.ts +93 -0
- package/dist/insights/types.d.ts.map +1 -0
- package/dist/insights/types.js +6 -0
- package/dist/insights/types.js.map +1 -0
- package/dist/output/timeline-html.d.ts +6 -0
- package/dist/output/timeline-html.d.ts.map +1 -0
- package/dist/output/timeline-html.js +389 -0
- package/dist/output/timeline-html.js.map +1 -0
- package/dist/output/timeline-markdown.d.ts +6 -0
- package/dist/output/timeline-markdown.d.ts.map +1 -0
- package/dist/output/timeline-markdown.js +167 -0
- package/dist/output/timeline-markdown.js.map +1 -0
- package/dist/output/timeline.d.ts +9 -0
- package/dist/output/timeline.d.ts.map +1 -0
- package/dist/output/timeline.js +318 -0
- package/dist/output/timeline.js.map +1 -0
- package/dist/patterns/detour.d.ts +32 -0
- package/dist/patterns/detour.d.ts.map +1 -0
- package/dist/patterns/detour.js +137 -0
- package/dist/patterns/detour.js.map +1 -0
- package/dist/patterns/flow-state.d.ts +16 -0
- package/dist/patterns/flow-state.d.ts.map +1 -0
- package/dist/patterns/flow-state.js +40 -0
- package/dist/patterns/flow-state.js.map +1 -0
- package/dist/patterns/index.d.ts +8 -0
- package/dist/patterns/index.d.ts.map +1 -0
- package/dist/patterns/index.js +22 -0
- package/dist/patterns/index.js.map +1 -0
- package/dist/patterns/intervention-effectiveness.d.ts +42 -0
- package/dist/patterns/intervention-effectiveness.d.ts.map +1 -0
- package/dist/patterns/intervention-effectiveness.js +196 -0
- package/dist/patterns/intervention-effectiveness.js.map +1 -0
- package/dist/patterns/late-night.d.ts +30 -0
- package/dist/patterns/late-night.d.ts.map +1 -0
- package/dist/patterns/late-night.js +141 -0
- package/dist/patterns/late-night.js.map +1 -0
- package/dist/patterns/post-delete-sprint.d.ts +28 -0
- package/dist/patterns/post-delete-sprint.d.ts.map +1 -0
- package/dist/patterns/post-delete-sprint.js +85 -0
- package/dist/patterns/post-delete-sprint.js.map +1 -0
- package/dist/patterns/spiral-regression.d.ts +49 -0
- package/dist/patterns/spiral-regression.d.ts.map +1 -0
- package/dist/patterns/spiral-regression.js +219 -0
- package/dist/patterns/spiral-regression.js.map +1 -0
- package/dist/patterns/thrashing.d.ts +25 -0
- package/dist/patterns/thrashing.d.ts.map +1 -0
- package/dist/patterns/thrashing.js +111 -0
- package/dist/patterns/thrashing.js.map +1 -0
- package/dist/storage/atomic.d.ts +40 -0
- package/dist/storage/atomic.d.ts.map +1 -0
- package/dist/storage/atomic.js +155 -0
- package/dist/storage/atomic.js.map +1 -0
- package/dist/storage/commit-log.d.ts +35 -0
- package/dist/storage/commit-log.d.ts.map +1 -0
- package/dist/storage/commit-log.js +128 -0
- package/dist/storage/commit-log.js.map +1 -0
- package/dist/storage/index.d.ts +5 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +33 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/schema.d.ts +32 -0
- package/dist/storage/schema.d.ts.map +1 -0
- package/dist/storage/schema.js +37 -0
- package/dist/storage/schema.js.map +1 -0
- package/dist/storage/timeline-store.d.ts +117 -0
- package/dist/storage/timeline-store.d.ts.map +1 -0
- package/dist/storage/timeline-store.js +438 -0
- package/dist/storage/timeline-store.js.map +1 -0
- package/dist/types.d.ts +96 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/ARCHITECTURE.md +458 -0
- package/docs/DATA-ARCHITECTURE.md +565 -0
- package/docs/GAMIFICATION.md +564 -0
- package/docs/JSON-STORAGE-PATTERNS.md +512 -0
- package/docs/METRICS-EXPLAINED.md +394 -0
- package/docs/UNIFIED-ECOSYSTEM.md +560 -0
- package/docs/VIBE-ECOSYSTEM.md +406 -0
- package/feature-list.json +48 -0
- package/package.json +2 -1
- package/vitest.config.ts +1 -5
- package/.vibe-check/calibration.json +0 -38
- package/.vibe-check/latest.json +0 -114
- package/.vibe-check/sessions.json +0 -44
- package/PLAN-ultimate-game.md +0 -1362
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
# JSON Storage Patterns for CLI Tools
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
JSON is an excellent choice for CLI tool storage when done right. This document covers the patterns, anti-patterns, and best practices for building a robust JSON-based data layer.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Why JSON for CLI Tools?
|
|
10
|
+
|
|
11
|
+
| Advantage | Description |
|
|
12
|
+
|-----------|-------------|
|
|
13
|
+
| **Zero dependencies** | No database drivers to install |
|
|
14
|
+
| **Git-friendly** | Diffable, mergeable, human-readable |
|
|
15
|
+
| **Portable** | Works on any platform with Node.js |
|
|
16
|
+
| **Debuggable** | Open in any editor to inspect |
|
|
17
|
+
| **Offline-first** | No network required |
|
|
18
|
+
| **Embeddable** | Easy to include in reports/exports |
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## The 5 JSON Storage Patterns
|
|
23
|
+
|
|
24
|
+
### Pattern 1: Single Document Store
|
|
25
|
+
|
|
26
|
+
**When to use:** Small datasets (<1MB), simple read/write patterns
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
.vibe-check/
|
|
30
|
+
└── data.json # Everything in one file
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
interface Store {
|
|
35
|
+
version: string;
|
|
36
|
+
lastUpdated: string;
|
|
37
|
+
data: Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Read
|
|
41
|
+
const store = JSON.parse(fs.readFileSync('data.json', 'utf-8'));
|
|
42
|
+
|
|
43
|
+
// Write (atomic)
|
|
44
|
+
fs.writeFileSync('data.json', JSON.stringify(store, null, 2));
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Pros:** Simple, atomic writes
|
|
48
|
+
**Cons:** Load entire file for any operation
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
### Pattern 2: Collection Files
|
|
53
|
+
|
|
54
|
+
**When to use:** Multiple entity types, need to load subsets
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
.vibe-check/
|
|
58
|
+
├── sessions.json # TimelineSession[]
|
|
59
|
+
├── insights.json # StoredInsight[]
|
|
60
|
+
├── profile.json # User profile
|
|
61
|
+
└── settings.json # Configuration
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
// Load only what you need
|
|
66
|
+
const sessions = JSON.parse(fs.readFileSync('sessions.json', 'utf-8'));
|
|
67
|
+
|
|
68
|
+
// Update single collection
|
|
69
|
+
fs.writeFileSync('sessions.json', JSON.stringify(sessions, null, 2));
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Pros:** Selective loading, smaller writes
|
|
73
|
+
**Cons:** No cross-file transactions
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
### Pattern 3: NDJSON (Newline Delimited JSON)
|
|
78
|
+
|
|
79
|
+
**When to use:** Append-only logs, streaming data, large datasets
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
.vibe-check/
|
|
83
|
+
└── commits.ndjson # One JSON object per line
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```jsonl
|
|
87
|
+
{"hash":"abc1234","date":"2025-11-30T10:00:00Z","type":"feat"}
|
|
88
|
+
{"hash":"def5678","date":"2025-11-30T10:15:00Z","type":"fix"}
|
|
89
|
+
{"hash":"ghi9012","date":"2025-11-30T10:30:00Z","type":"feat"}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// Append (fast, no parse)
|
|
94
|
+
fs.appendFileSync('commits.ndjson', JSON.stringify(commit) + '\n');
|
|
95
|
+
|
|
96
|
+
// Read (stream for large files)
|
|
97
|
+
const lines = fs.readFileSync('commits.ndjson', 'utf-8').split('\n');
|
|
98
|
+
const commits = lines.filter(l => l).map(l => JSON.parse(l));
|
|
99
|
+
|
|
100
|
+
// Tail (last N entries)
|
|
101
|
+
// Use readline or stream for efficiency
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Pros:**
|
|
105
|
+
- O(1) appends
|
|
106
|
+
- Git diffs show only new lines
|
|
107
|
+
- Streamable for large files
|
|
108
|
+
- Natural event sourcing
|
|
109
|
+
|
|
110
|
+
**Cons:**
|
|
111
|
+
- No random access by key
|
|
112
|
+
- Need to read entire file for queries
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
### Pattern 4: Directory-per-Entity
|
|
117
|
+
|
|
118
|
+
**When to use:** Large individual records, need direct access by ID
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
.vibe-check/
|
|
122
|
+
├── sessions/
|
|
123
|
+
│ ├── 2025-11-28.json
|
|
124
|
+
│ ├── 2025-11-29.json
|
|
125
|
+
│ └── 2025-11-30.json
|
|
126
|
+
├── insights/
|
|
127
|
+
│ ├── late-night-pattern.json
|
|
128
|
+
│ └── peak-flow-hour.json
|
|
129
|
+
└── index.json # Metadata, pointers
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
// Load specific day
|
|
134
|
+
const daySessions = JSON.parse(
|
|
135
|
+
fs.readFileSync(`sessions/${date}.json`, 'utf-8')
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
// List all days
|
|
139
|
+
const days = fs.readdirSync('sessions').map(f => f.replace('.json', ''));
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Pros:**
|
|
143
|
+
- Fast access by key
|
|
144
|
+
- Parallel writes to different files
|
|
145
|
+
- Natural sharding
|
|
146
|
+
|
|
147
|
+
**Cons:**
|
|
148
|
+
- More filesystem overhead
|
|
149
|
+
- Need index for queries
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
### Pattern 5: Hybrid (Index + Data)
|
|
154
|
+
|
|
155
|
+
**When to use:** Need both queries and detail access
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
.vibe-check/
|
|
159
|
+
├── index.json # Lightweight index for queries
|
|
160
|
+
├── sessions.ndjson # Full session data (append-only)
|
|
161
|
+
└── cache/
|
|
162
|
+
├── weekly/ # Precomputed aggregates
|
|
163
|
+
└── monthly/
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
// index.json - small, frequently updated
|
|
168
|
+
interface Index {
|
|
169
|
+
sessionCount: number;
|
|
170
|
+
dateRange: { from: string; to: string };
|
|
171
|
+
sessionIndex: {
|
|
172
|
+
id: string;
|
|
173
|
+
date: string;
|
|
174
|
+
rating: string;
|
|
175
|
+
lineOffset: number; // Pointer into sessions.ndjson
|
|
176
|
+
}[];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Query the index
|
|
180
|
+
const eliteSessions = index.sessionIndex.filter(s => s.rating === 'ELITE');
|
|
181
|
+
|
|
182
|
+
// Load full session data only when needed
|
|
183
|
+
const session = readNdjsonLine('sessions.ndjson', offset);
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Pros:**
|
|
187
|
+
- Fast queries via index
|
|
188
|
+
- Full data accessible
|
|
189
|
+
- Scales well
|
|
190
|
+
|
|
191
|
+
**Cons:**
|
|
192
|
+
- Index must stay in sync
|
|
193
|
+
- More complex implementation
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Best Practices
|
|
198
|
+
|
|
199
|
+
### 1. Atomic Writes
|
|
200
|
+
|
|
201
|
+
Never write directly to the target file. Use write-rename pattern:
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
import { writeFileSync, renameSync } from 'fs';
|
|
205
|
+
import { randomBytes } from 'crypto';
|
|
206
|
+
|
|
207
|
+
function atomicWrite(path: string, data: string): void {
|
|
208
|
+
const tempPath = `${path}.${randomBytes(6).toString('hex')}.tmp`;
|
|
209
|
+
writeFileSync(tempPath, data, 'utf-8');
|
|
210
|
+
renameSync(tempPath, path); // Atomic on POSIX
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### 2. Schema Versioning
|
|
215
|
+
|
|
216
|
+
Always include version for migrations:
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
interface Store {
|
|
220
|
+
version: '1.0.0' | '1.1.0' | '2.0.0'; // Explicit versions
|
|
221
|
+
// ...data
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function migrateStore(store: Store): Store {
|
|
225
|
+
if (store.version === '1.0.0') {
|
|
226
|
+
// Migrate to 1.1.0
|
|
227
|
+
store = migrateV1ToV1_1(store);
|
|
228
|
+
}
|
|
229
|
+
if (store.version === '1.1.0') {
|
|
230
|
+
// Migrate to 2.0.0
|
|
231
|
+
store = migrateV1_1ToV2(store);
|
|
232
|
+
}
|
|
233
|
+
return store;
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### 3. Bounded Growth
|
|
238
|
+
|
|
239
|
+
Set limits to prevent unbounded file growth:
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
const MAX_SESSIONS = 500; // ~1 year of daily coding
|
|
243
|
+
const MAX_INSIGHTS = 20; // Top insights only
|
|
244
|
+
const MAX_WEEKS = 12; // 3 months of weekly trends
|
|
245
|
+
|
|
246
|
+
function pruneStore(store: TimelineStore): TimelineStore {
|
|
247
|
+
store.sessions = store.sessions.slice(-MAX_SESSIONS);
|
|
248
|
+
store.insights = store.insights.slice(0, MAX_INSIGHTS);
|
|
249
|
+
store.trends.weekly = store.trends.weekly.slice(-MAX_WEEKS);
|
|
250
|
+
return store;
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### 4. Deduplication
|
|
255
|
+
|
|
256
|
+
Prevent duplicate entries:
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
function sessionExists(store: TimelineStore, commitHashes: string[]): boolean {
|
|
260
|
+
const hashSet = new Set(commitHashes);
|
|
261
|
+
|
|
262
|
+
for (const session of store.sessions) {
|
|
263
|
+
// 80% overlap = duplicate
|
|
264
|
+
const matchCount = session.commitHashes.filter(h => hashSet.has(h)).length;
|
|
265
|
+
const overlapRatio = matchCount / Math.max(session.commitHashes.length, commitHashes.length);
|
|
266
|
+
if (overlapRatio > 0.8) return true;
|
|
267
|
+
}
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### 5. Incremental Sync
|
|
273
|
+
|
|
274
|
+
Track last known state for efficient updates:
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
interface Store {
|
|
278
|
+
lastCommitHash: string; // Resume point
|
|
279
|
+
lastUpdated: string; // Timestamp
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Only fetch new commits
|
|
283
|
+
const newCommits = await getCommitsSince(repoPath, store.lastCommitHash);
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### 6. Graceful Corruption Handling
|
|
287
|
+
|
|
288
|
+
Always handle parse failures:
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
function loadStore(path: string): Store {
|
|
292
|
+
if (!fs.existsSync(path)) {
|
|
293
|
+
return createInitialStore();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const data = fs.readFileSync(path, 'utf-8');
|
|
298
|
+
const store = JSON.parse(data);
|
|
299
|
+
return migrateStore(store);
|
|
300
|
+
} catch (error) {
|
|
301
|
+
console.warn(`Warning: Could not parse ${path}, creating fresh store`);
|
|
302
|
+
// Optionally backup corrupted file
|
|
303
|
+
fs.renameSync(path, `${path}.corrupted.${Date.now()}`);
|
|
304
|
+
return createInitialStore();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### 7. Compression for Large Data
|
|
310
|
+
|
|
311
|
+
For large historical data:
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
import { gzipSync, gunzipSync } from 'zlib';
|
|
315
|
+
|
|
316
|
+
// Write compressed
|
|
317
|
+
const data = JSON.stringify(store);
|
|
318
|
+
fs.writeFileSync('store.json.gz', gzipSync(data));
|
|
319
|
+
|
|
320
|
+
// Read compressed
|
|
321
|
+
const compressed = fs.readFileSync('store.json.gz');
|
|
322
|
+
const store = JSON.parse(gunzipSync(compressed).toString());
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## Recommended Architecture for vibe-check
|
|
328
|
+
|
|
329
|
+
Based on the data patterns identified:
|
|
330
|
+
|
|
331
|
+
```
|
|
332
|
+
.vibe-check/
|
|
333
|
+
├── timeline.json # Pattern 1: Single store for current state
|
|
334
|
+
│ # - Last 500 sessions (compressed)
|
|
335
|
+
│ # - Aggregated patterns/trends
|
|
336
|
+
│ # - Compounding insights
|
|
337
|
+
│
|
|
338
|
+
├── commits.ndjson # Pattern 3: Append-only commit log
|
|
339
|
+
│ # - Raw commit data (source of truth)
|
|
340
|
+
│ # - Git-friendly diffs
|
|
341
|
+
│ # - Regenerate timeline.json if needed
|
|
342
|
+
│
|
|
343
|
+
├── profile.json # Pattern 1: Single store for gamification
|
|
344
|
+
│ # - XP, streaks, achievements
|
|
345
|
+
│ # - Small, frequently updated
|
|
346
|
+
│
|
|
347
|
+
└── cache/ # Pattern 4: Precomputed views
|
|
348
|
+
├── weekly/
|
|
349
|
+
│ ├── 2025-W48.json
|
|
350
|
+
│ └── 2025-W49.json
|
|
351
|
+
└── monthly/
|
|
352
|
+
├── 2025-11.json
|
|
353
|
+
└── 2025-10.json
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### Data Flow
|
|
357
|
+
|
|
358
|
+
```
|
|
359
|
+
┌─────────────────────┐
|
|
360
|
+
│ Git Log │
|
|
361
|
+
└──────────┬──────────┘
|
|
362
|
+
│
|
|
363
|
+
▼
|
|
364
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
365
|
+
│ commits.ndjson │
|
|
366
|
+
│ (Append-Only Log) │
|
|
367
|
+
│ │
|
|
368
|
+
│ {"hash":"abc","date":"...","type":"feat",...} │
|
|
369
|
+
│ {"hash":"def","date":"...","type":"fix",...} │
|
|
370
|
+
│ {"hash":"ghi","date":"...","type":"feat",...} │
|
|
371
|
+
│ │
|
|
372
|
+
│ ✓ Source of truth │
|
|
373
|
+
│ ✓ Never modified, only appended │
|
|
374
|
+
│ ✓ Git diffs show only new commits │
|
|
375
|
+
└──────────────────────────────────────────────────────────────┘
|
|
376
|
+
│
|
|
377
|
+
Compute on demand
|
|
378
|
+
│
|
|
379
|
+
▼
|
|
380
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
381
|
+
│ timeline.json │
|
|
382
|
+
│ (Computed View) │
|
|
383
|
+
│ │
|
|
384
|
+
│ { │
|
|
385
|
+
│ "version": "2.0.0", │
|
|
386
|
+
│ "lastCommitHash": "ghi1234", │
|
|
387
|
+
│ "sessions": [...], // Derived from commits.ndjson │
|
|
388
|
+
│ "insights": [...], // Aggregated patterns │
|
|
389
|
+
│ "patterns": {...}, // Statistics │
|
|
390
|
+
│ "trends": {...} // Time-series │
|
|
391
|
+
│ } │
|
|
392
|
+
│ │
|
|
393
|
+
│ ✓ Regenerable from commits.ndjson │
|
|
394
|
+
│ ✓ Caches expensive computations │
|
|
395
|
+
│ ✓ Bounded size (500 sessions max) │
|
|
396
|
+
└──────────────────────────────────────────────────────────────┘
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Benefits of This Architecture
|
|
400
|
+
|
|
401
|
+
1. **Source of Truth Separation**
|
|
402
|
+
- `commits.ndjson` = immutable log (append-only)
|
|
403
|
+
- `timeline.json` = computed view (regenerable)
|
|
404
|
+
|
|
405
|
+
2. **Git-Friendly**
|
|
406
|
+
- Appending to NDJSON shows clean diffs
|
|
407
|
+
- Only new lines appear in git log
|
|
408
|
+
|
|
409
|
+
3. **Resilient**
|
|
410
|
+
- If `timeline.json` corrupts, regenerate from commits
|
|
411
|
+
- Append-only log is harder to corrupt
|
|
412
|
+
|
|
413
|
+
4. **Efficient**
|
|
414
|
+
- Cache computations in `timeline.json`
|
|
415
|
+
- Only recompute when commits change
|
|
416
|
+
|
|
417
|
+
5. **Queryable**
|
|
418
|
+
- Load `timeline.json` for most queries
|
|
419
|
+
- Stream `commits.ndjson` for historical analysis
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## Implementation Checklist
|
|
424
|
+
|
|
425
|
+
- [ ] Migrate to NDJSON for raw commit storage
|
|
426
|
+
- [ ] Keep JSON for computed views
|
|
427
|
+
- [ ] Add atomic write helper
|
|
428
|
+
- [ ] Implement schema versioning
|
|
429
|
+
- [ ] Add corruption recovery
|
|
430
|
+
- [ ] Set bounded growth limits
|
|
431
|
+
- [ ] Add deduplication checks
|
|
432
|
+
- [ ] Track last sync point
|
|
433
|
+
|
|
434
|
+
---
|
|
435
|
+
|
|
436
|
+
## Anti-Patterns to Avoid
|
|
437
|
+
|
|
438
|
+
### 1. Reading Entire File for Small Updates
|
|
439
|
+
```typescript
|
|
440
|
+
// BAD: Parse entire file to append one item
|
|
441
|
+
const data = JSON.parse(fs.readFileSync('data.json'));
|
|
442
|
+
data.items.push(newItem);
|
|
443
|
+
fs.writeFileSync('data.json', JSON.stringify(data));
|
|
444
|
+
|
|
445
|
+
// GOOD: Use NDJSON for append-only data
|
|
446
|
+
fs.appendFileSync('data.ndjson', JSON.stringify(newItem) + '\n');
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### 2. Storing Computed Values as Source of Truth
|
|
450
|
+
```typescript
|
|
451
|
+
// BAD: Store derived data without source
|
|
452
|
+
{ "trustPassRate": 85 } // How was this calculated?
|
|
453
|
+
|
|
454
|
+
// GOOD: Store source data, compute on read
|
|
455
|
+
{ "fixCommits": 3, "totalCommits": 20 } // Can verify: 17/20 = 85%
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### 3. Unbounded Arrays
|
|
459
|
+
```typescript
|
|
460
|
+
// BAD: Array grows forever
|
|
461
|
+
sessions.push(newSession);
|
|
462
|
+
|
|
463
|
+
// GOOD: Bound the array
|
|
464
|
+
sessions.push(newSession);
|
|
465
|
+
sessions = sessions.slice(-MAX_SESSIONS);
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### 4. No Version Field
|
|
469
|
+
```typescript
|
|
470
|
+
// BAD: No way to migrate
|
|
471
|
+
{ "data": [...] }
|
|
472
|
+
|
|
473
|
+
// GOOD: Always version
|
|
474
|
+
{ "version": "1.0.0", "data": [...] }
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### 5. Timestamps as Keys
|
|
478
|
+
```typescript
|
|
479
|
+
// BAD: Collisions, hard to query
|
|
480
|
+
{ "2025-11-30T10:00:00Z": {...} }
|
|
481
|
+
|
|
482
|
+
// GOOD: Stable IDs with timestamp field
|
|
483
|
+
{ "id": "session-123", "timestamp": "2025-11-30T10:00:00Z", ... }
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
## Performance Characteristics
|
|
489
|
+
|
|
490
|
+
| Operation | Pattern 1 (Single) | Pattern 3 (NDJSON) | Pattern 5 (Hybrid) |
|
|
491
|
+
|-----------|-------------------|-------------------|-------------------|
|
|
492
|
+
| Read all | O(n) | O(n) | O(n) |
|
|
493
|
+
| Read one | O(n) | O(n) | O(1) with index |
|
|
494
|
+
| Append | O(n) rewrite | O(1) | O(1) + index update |
|
|
495
|
+
| Query | O(n) scan | O(n) scan | O(log n) with index |
|
|
496
|
+
| Delete | O(n) | O(n) rewrite | O(1) soft delete |
|
|
497
|
+
| File size | Compact | Larger (no dedup) | Medium |
|
|
498
|
+
| Git diff | Full file | New lines only | Index + new lines |
|
|
499
|
+
|
|
500
|
+
---
|
|
501
|
+
|
|
502
|
+
## Conclusion
|
|
503
|
+
|
|
504
|
+
For vibe-check:
|
|
505
|
+
|
|
506
|
+
1. **Use NDJSON** for the raw commit log (append-only, git-friendly)
|
|
507
|
+
2. **Use JSON** for computed views (sessions, insights, trends)
|
|
508
|
+
3. **Implement bounds** to prevent unbounded growth
|
|
509
|
+
4. **Version everything** for future migrations
|
|
510
|
+
5. **Separate source from derived** for resilience
|
|
511
|
+
|
|
512
|
+
This gives you the simplicity of JSON with the scalability patterns of event sourcing.
|