@opensassi/opencode 0.1.3 → 0.1.4

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.
Files changed (37) hide show
  1. package/dashboard/dashboard.e2e.test.ts +247 -0
  2. package/dashboard/dist/index.d.ts +9 -0
  3. package/dashboard/dist/index.js +36 -0
  4. package/dashboard/dist/routes/api.d.ts +2 -0
  5. package/dashboard/dist/routes/api.js +215 -0
  6. package/dashboard/dist/services/cache.d.ts +13 -0
  7. package/dashboard/dist/services/cache.js +29 -0
  8. package/dashboard/dist/services/experiments.d.ts +11 -0
  9. package/dashboard/dist/services/experiments.js +108 -0
  10. package/dashboard/dist/services/git.d.ts +12 -0
  11. package/dashboard/dist/services/git.js +149 -0
  12. package/dashboard/dist/services/sessions.d.ts +25 -0
  13. package/dashboard/dist/services/sessions.js +208 -0
  14. package/dashboard/dist/services/specs.d.ts +9 -0
  15. package/dashboard/dist/services/specs.js +102 -0
  16. package/dashboard/dist/types.d.ts +173 -0
  17. package/dashboard/dist/types.js +1 -0
  18. package/dashboard/opencode.e2e.test.ts +100 -0
  19. package/dashboard/playwright.config.ts +11 -0
  20. package/dashboard/public/app.js +961 -0
  21. package/dashboard/public/index.html +29 -0
  22. package/dashboard/public/style.css +231 -0
  23. package/dashboard/src/index.ts +53 -0
  24. package/dashboard/src/routes/api.ts +235 -0
  25. package/dashboard/src/services/cache.ts +38 -0
  26. package/dashboard/src/services/experiments.ts +117 -0
  27. package/dashboard/src/services/git.ts +139 -0
  28. package/dashboard/src/services/sessions.ts +216 -0
  29. package/dashboard/src/services/specs.ts +95 -0
  30. package/dashboard/src/types.ts +168 -0
  31. package/dashboard/technical-specification.md +414 -0
  32. package/dashboard/test-api.sh +127 -0
  33. package/dashboard/tsconfig.json +16 -0
  34. package/lib/util/paths.js +9 -1
  35. package/package.json +9 -1
  36. package/scripts/dashboard.js +17 -0
  37. package/scripts/generate-daily-summaries.js +190 -0
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, readdirSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
3
+ import { join, resolve, basename } from 'node:path';
4
+ import { execFileSync } from 'node:child_process';
5
+
6
+ const SESSIONS_DIR = resolve(process.cwd(), 'sessions');
7
+
8
+ function parseMdField(content, label) {
9
+ const re = new RegExp(`\\*\\*${label}:\\*\\*\\s*(.+?)(?:\\n\\n|\\n(?=\\*\\*)|$)`, 's');
10
+ const m = content.match(re);
11
+ return m ? m[1].trim() : null;
12
+ }
13
+
14
+ function parseDurationMinutes(content) {
15
+ // Try **Duration:** HH<something>
16
+ const durMatch = content.match(/\*\*Duration:\*\*\s*(\d+)\s*min/);
17
+ if (durMatch) return parseInt(durMatch[1], 10);
18
+
19
+ // Fallback: from .json.bz2 (handled below)
20
+ return 0;
21
+ }
22
+
23
+ function parsePrompterMinutes(content) {
24
+ const totalMatch = content.match(/\*\*Total:\*\*\s*([\d.]+)\s*hours?/);
25
+ if (totalMatch) return Math.round(parseFloat(totalMatch[1]) * 60);
26
+ // also try: prompter active ≈ X hours
27
+ const activeMatch = content.match(/prompter active\s*[≈~]?\s*([\d.]+)\s*hours?/);
28
+ if (activeMatch) return Math.round(parseFloat(activeMatch[1]) * 60);
29
+ return 30;
30
+ }
31
+
32
+ function parseSmeHours(content) {
33
+ const smeMatch = content.match(/\*\*Model-Equivalent SME Time Estimate:\*\*\s*(?:~|≈)?\s*([\d.]+)\s*hours?/);
34
+ if (smeMatch) return Math.round(parseFloat(smeMatch[1]) * 60);
35
+ return 60;
36
+ }
37
+
38
+ function parseTags(content) {
39
+ const tagMatch = content.match(/\*\*Aggregation Tags:\*\*\s*(.+?)(?:\\n\\n|$)/s);
40
+ if (tagMatch) {
41
+ return tagMatch[1].split(',').map(t => t.trim()).filter(Boolean);
42
+ }
43
+ return [];
44
+ }
45
+
46
+ function parseSessionId(content) {
47
+ return parseMdField(content, 'Session ID') || '';
48
+ }
49
+
50
+ function parseDescription(content) {
51
+ return parseMdField(content, 'Top-Level Component') || '';
52
+ }
53
+
54
+ function parseConfidence(/*content*/) {
55
+ return 'medium';
56
+ }
57
+
58
+ function extractDate(sessionId, filename) {
59
+ if (sessionId) {
60
+ const m = sessionId.match(/^(\d{4}-\d{2}-\d{2})/);
61
+ if (m) return m[1];
62
+ }
63
+ const m = filename.match(/^(\d{4}-\d{2}-\d{2})/);
64
+ return m ? m[1] : null;
65
+ }
66
+
67
+ function getBz2Duration(sessionDir, sessionId) {
68
+ if (!sessionId) return null;
69
+ const files = readdirSync(sessionDir);
70
+ const match = files.find(f => f.startsWith(sessionId) && f.endsWith('.json.bz2'));
71
+ if (!match) return null;
72
+ try {
73
+ const buf = execFileSync('bzcat', [join(sessionDir, match)], {
74
+ encoding: 'utf-8', maxBuffer: 100 * 1024 * 1024, stdio: ['pipe', 'pipe', 'ignore'],
75
+ });
76
+ const d = JSON.parse(buf);
77
+ const info = d.info || {};
78
+ const t = info.time || {};
79
+ const created = t.created || 0;
80
+ const updated = t.updated || 0;
81
+ if (updated && created) return Math.round((updated - created) / 60000);
82
+ return null;
83
+ } catch { return null; }
84
+ }
85
+
86
+ async function main() {
87
+ if (!existsSync(SESSIONS_DIR)) {
88
+ console.error(`Sessions directory not found: ${SESSIONS_DIR}`);
89
+ process.exit(1);
90
+ }
91
+
92
+ const files = readdirSync(SESSIONS_DIR);
93
+ const mdFiles = files.filter(f => f.endsWith('.md') && !f.endsWith('.spec.md') && f !== 'README.md' && f !== 'export-session.sh');
94
+
95
+ // Group sessions by date
96
+ const byDate = {};
97
+
98
+ for (const mdFile of mdFiles) {
99
+ const content = readFileSync(join(SESSIONS_DIR, mdFile), 'utf-8');
100
+ const sessionId = parseSessionId(content);
101
+ const date = extractDate(sessionId, mdFile);
102
+ if (!date) { console.error(`Could not extract date from ${mdFile}`); continue; }
103
+
104
+ const prompterMin = parsePrompterMinutes(content);
105
+ const smeMin = parseSmeHours(content);
106
+ const bz2Duration = getBz2Duration(SESSIONS_DIR, sessionId);
107
+ const durationMin = bz2Duration || prompterMin || parseDurationMinutes(content);
108
+ const tags = parseTags(content);
109
+ const description = parseDescription(content);
110
+ const confidence = parseConfidence(content);
111
+
112
+ if (!byDate[date]) byDate[date] = [];
113
+ byDate[date].push({
114
+ session_id: sessionId || mdFile.replace(/\.md$/, ''),
115
+ duration_minutes: durationMin,
116
+ prompter_time_minutes: Math.min(prompterMin, durationMin || prompterMin),
117
+ sme_time_minutes: smeMin,
118
+ top_component_summary: description,
119
+ tags,
120
+ human_confidence: confidence,
121
+ });
122
+ }
123
+
124
+ // Generate daily files
125
+ const dailyDir = join(SESSIONS_DIR, 'daily');
126
+ if (!existsSync(dailyDir)) {
127
+ mkdirSync(dailyDir, { recursive: true });
128
+ }
129
+
130
+ for (const [date, sessions] of Object.entries(byDate).sort()) {
131
+ let totalPrompter = 0, totalSme = 0;
132
+ for (const s of sessions) {
133
+ totalPrompter += s.prompter_time_minutes;
134
+ totalSme += s.sme_time_minutes;
135
+ }
136
+ const totalPrompterHrs = Math.round((totalPrompter / 60) * 10) / 10;
137
+ const totalSmeHrs = Math.round((totalSme / 60) * 10) / 10;
138
+
139
+ // Compute per-tag aggregates
140
+ const tagMap = {};
141
+ for (const s of sessions) {
142
+ const perTagSme = s.sme_time_minutes / (s.tags.length || 1);
143
+ const perTagPrompter = s.prompter_time_minutes / (s.tags.length || 1);
144
+ for (const tag of s.tags) {
145
+ if (!tagMap[tag]) tagMap[tag] = { prompter: 0, sme: 0 };
146
+ tagMap[tag].prompter += perTagPrompter;
147
+ tagMap[tag].sme += perTagSme;
148
+ }
149
+ }
150
+ const topSubjectAreas = Object.entries(tagMap)
151
+ .map(([name, v]) => ({
152
+ name,
153
+ prompter_time_hours: Math.round((v.prompter / 60) * 100) / 100,
154
+ sme_time_hours: Math.round((v.sme / 60) * 100) / 100,
155
+ ai_multiplier: v.prompter > 0 ? Math.round((v.sme / v.prompter) * 10) / 10 : 0,
156
+ }))
157
+ .sort((a, b) => b.sme_time_hours - a.sme_time_hours);
158
+
159
+ const aiMultiplier = totalPrompterHrs > 0
160
+ ? Math.round((totalSmeHrs / totalPrompterHrs) * 10) / 10
161
+ : 0;
162
+
163
+ const daily = {
164
+ dashboard: {
165
+ metadata: {
166
+ generated_at: new Date().toISOString(),
167
+ audited: false,
168
+ audit_note: 'Auto-generated from session evaluation files',
169
+ },
170
+ daily_summary: {
171
+ date,
172
+ total_prompter_time_hours: totalPrompterHrs,
173
+ total_sme_time_hours: totalSmeHrs,
174
+ ai_multiplier: aiMultiplier,
175
+ total_sessions: sessions.length,
176
+ top_subject_areas: topSubjectAreas,
177
+ },
178
+ session_breakdown: sessions,
179
+ },
180
+ };
181
+
182
+ const outPath = join(dailyDir, `${date}.json`);
183
+ writeFileSync(outPath, JSON.stringify(daily, null, 2) + '\n');
184
+ console.log(`Wrote ${outPath} (${sessions.length} sessions, ${totalPrompterHrs}h prompter, ${totalSmeHrs}h SME)`);
185
+ }
186
+
187
+ console.log(`\nDone. Generated ${Object.keys(byDate).length} daily summaries.`);
188
+ }
189
+
190
+ main().catch(e => { console.error(e); process.exit(1); });