@opensassi/opencode 0.1.2 → 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.
- package/dashboard/dashboard.e2e.test.ts +247 -0
- package/dashboard/dist/index.d.ts +9 -0
- package/dashboard/dist/index.js +36 -0
- package/dashboard/dist/routes/api.d.ts +2 -0
- package/dashboard/dist/routes/api.js +215 -0
- package/dashboard/dist/services/cache.d.ts +13 -0
- package/dashboard/dist/services/cache.js +29 -0
- package/dashboard/dist/services/experiments.d.ts +11 -0
- package/dashboard/dist/services/experiments.js +108 -0
- package/dashboard/dist/services/git.d.ts +12 -0
- package/dashboard/dist/services/git.js +149 -0
- package/dashboard/dist/services/sessions.d.ts +25 -0
- package/dashboard/dist/services/sessions.js +208 -0
- package/dashboard/dist/services/specs.d.ts +9 -0
- package/dashboard/dist/services/specs.js +102 -0
- package/dashboard/dist/types.d.ts +173 -0
- package/dashboard/dist/types.js +1 -0
- package/dashboard/opencode.e2e.test.ts +100 -0
- package/dashboard/playwright.config.ts +11 -0
- package/dashboard/public/app.js +961 -0
- package/dashboard/public/index.html +29 -0
- package/dashboard/public/style.css +231 -0
- package/dashboard/src/index.ts +53 -0
- package/dashboard/src/routes/api.ts +235 -0
- package/dashboard/src/services/cache.ts +38 -0
- package/dashboard/src/services/experiments.ts +117 -0
- package/dashboard/src/services/git.ts +139 -0
- package/dashboard/src/services/sessions.ts +216 -0
- package/dashboard/src/services/specs.ts +95 -0
- package/dashboard/src/types.ts +168 -0
- package/dashboard/technical-specification.md +414 -0
- package/dashboard/test-api.sh +127 -0
- package/dashboard/tsconfig.json +16 -0
- package/lib/util/paths.js +9 -1
- package/package.json +9 -1
- package/scripts/dashboard.js +17 -0
- package/scripts/generate-daily-summaries.js +190 -0
- package/skills/opensassi/SKILL.md +150 -56
- package/skills/todo/SKILL.md +45 -63
- package/skills-index.json +10 -7
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import type { GitLogEntry, GitStats } from '../types.js';
|
|
3
|
+
|
|
4
|
+
export class GitService {
|
|
5
|
+
private repoDir: string;
|
|
6
|
+
private defaultSince: string | null;
|
|
7
|
+
|
|
8
|
+
constructor(repoDir: string, defaultSince?: string | null) {
|
|
9
|
+
this.repoDir = repoDir;
|
|
10
|
+
this.defaultSince = defaultSince ?? null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
detectForkPoint(): string | null {
|
|
14
|
+
const makeOpts = (mb: number) => ({
|
|
15
|
+
cwd: this.repoDir, encoding: 'utf-8' as const, maxBuffer: mb,
|
|
16
|
+
stdio: ['pipe', 'pipe', 'ignore'] as ['pipe', 'pipe', 'ignore'],
|
|
17
|
+
});
|
|
18
|
+
try {
|
|
19
|
+
const base = execFileSync('git', ['merge-base', 'HEAD', 'upstream/main'], makeOpts(1024 * 1024)).trim();
|
|
20
|
+
if (base) return base;
|
|
21
|
+
} catch {}
|
|
22
|
+
try {
|
|
23
|
+
const allFork = execFileSync('git', ['rev-list', '--reverse', '--author=opensassi', 'HEAD'], makeOpts(10 * 1024 * 1024)).trim().split('\n').filter(Boolean);
|
|
24
|
+
if (allFork.length > 0) {
|
|
25
|
+
const parent = execFileSync('git', ['rev-parse', allFork[0] + '^'], makeOpts(1024 * 1024)).trim();
|
|
26
|
+
if (parent) return parent;
|
|
27
|
+
}
|
|
28
|
+
} catch {}
|
|
29
|
+
try {
|
|
30
|
+
const allFork = execFileSync('git', ['rev-list', '--reverse', '--author=Ersun', 'HEAD'], makeOpts(10 * 1024 * 1024)).trim().split('\n').filter(Boolean);
|
|
31
|
+
if (allFork.length > 0) {
|
|
32
|
+
const parent = execFileSync('git', ['rev-parse', allFork[0] + '^'], makeOpts(1024 * 1024)).trim();
|
|
33
|
+
if (parent) return parent;
|
|
34
|
+
}
|
|
35
|
+
} catch {}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getRange(rangeBase: string | null): string | undefined {
|
|
40
|
+
if (!rangeBase) return undefined;
|
|
41
|
+
return rangeBase + '..HEAD';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
getLog(since?: string, until?: string, forkRange?: string | null): GitLogEntry[] {
|
|
45
|
+
const args = ['log', '--oneline', '--stat', '--no-merges', '--format=COMMIT%x1e%H%x1f%an%x1f%ai%x1f%s'];
|
|
46
|
+
if (forkRange) {
|
|
47
|
+
args.push(forkRange);
|
|
48
|
+
} else {
|
|
49
|
+
const s = since ?? this.defaultSince;
|
|
50
|
+
if (s) args.push('--after', s);
|
|
51
|
+
if (until) args.push('--before', until);
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const output = execFileSync('git', args, { cwd: this.repoDir, encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024, stdio: ['pipe', 'pipe', 'ignore'] as ['pipe', 'pipe', 'ignore'] });
|
|
55
|
+
return this.parseLogOutput(output);
|
|
56
|
+
} catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private parseLogOutput(output: string): GitLogEntry[] {
|
|
62
|
+
const entries: GitLogEntry[] = [];
|
|
63
|
+
const blocks = output.split('\x1e').filter(b => b.trim());
|
|
64
|
+
for (let bi = 0; bi < blocks.length; bi++) {
|
|
65
|
+
const block = blocks[bi];
|
|
66
|
+
const lines = block.trim().split('\n');
|
|
67
|
+
if (lines.length === 0) continue;
|
|
68
|
+
const header = lines[0];
|
|
69
|
+
const parts = header.split('\x1f');
|
|
70
|
+
// First block is just "COMMIT" (before first \x1e), skip it
|
|
71
|
+
if (bi === 0 && parts.length === 1 && parts[0] === 'COMMIT') continue;
|
|
72
|
+
// Subsequent blocks: hash\x1fauthor\x1fdate\x1fsubject
|
|
73
|
+
if (parts.length < 3) continue;
|
|
74
|
+
const commit = parts[0];
|
|
75
|
+
const author = parts[1];
|
|
76
|
+
const date = parts[2];
|
|
77
|
+
const message = parts.slice(3).join('\x1f');
|
|
78
|
+
let filesChanged = 0;
|
|
79
|
+
let insertions = 0;
|
|
80
|
+
let deletions = 0;
|
|
81
|
+
for (let i = 1; i < lines.length; i++) {
|
|
82
|
+
const statMatch = lines[i].match(/(\d+) file[s]? changed/);
|
|
83
|
+
if (statMatch) filesChanged = parseInt(statMatch[1], 10);
|
|
84
|
+
const insMatch = lines[i].match(/(\d+) insertion[s]?/);
|
|
85
|
+
if (insMatch) insertions = parseInt(insMatch[1], 10);
|
|
86
|
+
const delMatch = lines[i].match(/(\d+) deletion[s]?/);
|
|
87
|
+
if (delMatch) deletions = parseInt(delMatch[1], 10);
|
|
88
|
+
}
|
|
89
|
+
entries.push({
|
|
90
|
+
commit: commit ?? '',
|
|
91
|
+
author: author ?? '',
|
|
92
|
+
date: date ?? '',
|
|
93
|
+
message: message ?? '',
|
|
94
|
+
files_changed: filesChanged,
|
|
95
|
+
insertions,
|
|
96
|
+
deletions,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return entries;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
getStats(forkRange?: string | null): GitStats {
|
|
103
|
+
const allLog = this.getLog(undefined, undefined, forkRange);
|
|
104
|
+
const perDate: Record<string, { commits: number; insertions: number; deletions: number }> = {};
|
|
105
|
+
let totalFilesChanged = 0;
|
|
106
|
+
let totalInsertions = 0;
|
|
107
|
+
let totalDeletions = 0;
|
|
108
|
+
for (const entry of allLog) {
|
|
109
|
+
const day = entry.date.slice(0, 10);
|
|
110
|
+
if (!perDate[day]) perDate[day] = { commits: 0, insertions: 0, deletions: 0 };
|
|
111
|
+
perDate[day].commits++;
|
|
112
|
+
perDate[day].insertions += entry.insertions;
|
|
113
|
+
perDate[day].deletions += entry.deletions;
|
|
114
|
+
totalFilesChanged += entry.files_changed;
|
|
115
|
+
totalInsertions += entry.insertions;
|
|
116
|
+
totalDeletions += entry.deletions;
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
total_commits: allLog.length,
|
|
120
|
+
total_files_changed: totalFilesChanged,
|
|
121
|
+
total_insertions: totalInsertions,
|
|
122
|
+
total_deletions: totalDeletions,
|
|
123
|
+
per_date: perDate,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
getCommitDiff(hash: string): string {
|
|
128
|
+
try {
|
|
129
|
+
return execFileSync('git', ['show', hash, '--no-color'], {
|
|
130
|
+
cwd: this.repoDir,
|
|
131
|
+
encoding: 'utf-8',
|
|
132
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
133
|
+
stdio: ['pipe', 'pipe', 'ignore'] as ['pipe', 'pipe', 'ignore'],
|
|
134
|
+
});
|
|
135
|
+
} catch {
|
|
136
|
+
return '';
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { TTLCache } from './cache.js';
|
|
5
|
+
import type {
|
|
6
|
+
NormalizedDay, FormatA, FormatB,
|
|
7
|
+
SessionDetail, SessionEntry, SearchResult,
|
|
8
|
+
} from '../types.js';
|
|
9
|
+
|
|
10
|
+
function normalizeDay(raw: unknown, date: string): NormalizedDay {
|
|
11
|
+
const a = raw as FormatA;
|
|
12
|
+
if (a && typeof a === 'object' && 'dashboard' in a && a.dashboard) {
|
|
13
|
+
const ds = a.dashboard.daily_summary;
|
|
14
|
+
return {
|
|
15
|
+
date: ds.date,
|
|
16
|
+
metadata: a.dashboard.metadata,
|
|
17
|
+
total_prompter_time_hours: ds.total_prompter_time_hours,
|
|
18
|
+
total_sme_time_hours: ds.total_sme_time_hours,
|
|
19
|
+
ai_multiplier: ds.ai_multiplier,
|
|
20
|
+
total_sessions: ds.total_sessions,
|
|
21
|
+
top_subject_areas: ds.top_subject_areas,
|
|
22
|
+
session_breakdown: a.dashboard.session_breakdown,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const b = raw as FormatB;
|
|
26
|
+
if (b && typeof b === 'object' && 'date' in b && b.date) {
|
|
27
|
+
return {
|
|
28
|
+
date: b.date,
|
|
29
|
+
total_prompter_time_hours: b.total_prompter_time_hours,
|
|
30
|
+
total_sme_time_hours: b.total_sme_time_hours,
|
|
31
|
+
ai_multiplier: b.ai_multiplier,
|
|
32
|
+
total_sessions: b.total_sessions,
|
|
33
|
+
top_subject_areas: b.top_subject_areas,
|
|
34
|
+
session_breakdown: b.session_breakdown,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
date,
|
|
39
|
+
total_prompter_time_hours: 0,
|
|
40
|
+
total_sme_time_hours: 0,
|
|
41
|
+
ai_multiplier: 0,
|
|
42
|
+
total_sessions: 0,
|
|
43
|
+
top_subject_areas: [],
|
|
44
|
+
session_breakdown: [],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SessionsConfig {
|
|
49
|
+
sessionsDir: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class SessionsService {
|
|
53
|
+
private dailyCache = new TTLCache<NormalizedDay>(60_000);
|
|
54
|
+
private detailCache = new TTLCache<SessionDetail>(300_000);
|
|
55
|
+
private daysListCache = new TTLCache<string[]>(30_000);
|
|
56
|
+
private sessionsDir: string;
|
|
57
|
+
|
|
58
|
+
constructor(sessionsDir: string) {
|
|
59
|
+
this.sessionsDir = resolve(sessionsDir);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get dailyDir(): string {
|
|
63
|
+
return join(this.sessionsDir, 'daily');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
listDays(): string[] {
|
|
67
|
+
const cached = this.daysListCache.get('list');
|
|
68
|
+
if (cached) return cached;
|
|
69
|
+
const dir = this.dailyDir;
|
|
70
|
+
if (!existsSync(dir)) return [];
|
|
71
|
+
const files = readdirSync(dir).filter(f => f.endsWith('.json')).map(f => f.replace(/\.json$/, '')).sort();
|
|
72
|
+
this.daysListCache.set('list', files);
|
|
73
|
+
return files;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
listSessionsDirFiles(): string[] {
|
|
77
|
+
if (!existsSync(this.sessionsDir)) return [];
|
|
78
|
+
return readdirSync(this.sessionsDir).filter(f => f.endsWith('.json.bz2'));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
getDay(date: string): NormalizedDay {
|
|
82
|
+
const cached = this.dailyCache.get(date);
|
|
83
|
+
if (cached) return cached;
|
|
84
|
+
const path = join(this.dailyDir, `${date}.json`);
|
|
85
|
+
if (!existsSync(path)) {
|
|
86
|
+
const day: NormalizedDay = {
|
|
87
|
+
date,
|
|
88
|
+
total_prompter_time_hours: 0,
|
|
89
|
+
total_sme_time_hours: 0,
|
|
90
|
+
ai_multiplier: 0,
|
|
91
|
+
total_sessions: 0,
|
|
92
|
+
top_subject_areas: [],
|
|
93
|
+
session_breakdown: [],
|
|
94
|
+
};
|
|
95
|
+
this.dailyCache.set(date, day);
|
|
96
|
+
return day;
|
|
97
|
+
}
|
|
98
|
+
const raw = JSON.parse(readFileSync(path, 'utf-8'));
|
|
99
|
+
const day = normalizeDay(raw, date);
|
|
100
|
+
this.dailyCache.set(date, day);
|
|
101
|
+
return day;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
getAllSessionsFlat(): Array<{ date: string; entry: SessionEntry }> {
|
|
105
|
+
const days = this.listDays();
|
|
106
|
+
const result: Array<{ date: string; entry: SessionEntry }> = [];
|
|
107
|
+
for (const day of days) {
|
|
108
|
+
const normalized = this.getDay(day);
|
|
109
|
+
for (const entry of normalized.session_breakdown) {
|
|
110
|
+
result.push({ date: day, entry });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
getSessionDetail(sessionId: string): SessionDetail | null {
|
|
117
|
+
const cached = this.detailCache.get(sessionId);
|
|
118
|
+
if (cached) return cached;
|
|
119
|
+
const bz2File = this.resolveBz2File(sessionId);
|
|
120
|
+
if (!bz2File) return null;
|
|
121
|
+
try {
|
|
122
|
+
const json = execFileSync('bzcat', [bz2File], { encoding: 'utf-8', maxBuffer: 100 * 1024 * 1024 });
|
|
123
|
+
const detail = JSON.parse(json) as SessionDetail;
|
|
124
|
+
this.detailCache.set(sessionId, detail);
|
|
125
|
+
return detail;
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
getSessionSummary(sessionId: string): string | null {
|
|
132
|
+
const mdFile = this.resolveMdFile(sessionId);
|
|
133
|
+
if (!mdFile) return null;
|
|
134
|
+
return readFileSync(mdFile, 'utf-8');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private resolveBz2File(sessionId: string): string | null {
|
|
138
|
+
const exact = join(this.sessionsDir, `${sessionId}.json.bz2`);
|
|
139
|
+
if (existsSync(exact)) return exact;
|
|
140
|
+
const files = this.listSessionsDirFiles();
|
|
141
|
+
const match = files.find(f => f.startsWith(sessionId) && f.endsWith('.json.bz2'));
|
|
142
|
+
if (match) return join(this.sessionsDir, match);
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private resolveMdFile(sessionId: string): string | null {
|
|
147
|
+
const exact = join(this.sessionsDir, `${sessionId}.md`);
|
|
148
|
+
if (existsSync(exact)) return exact;
|
|
149
|
+
if (!existsSync(this.sessionsDir)) return null;
|
|
150
|
+
const files = readdirSync(this.sessionsDir).filter(f => f.endsWith('.md'));
|
|
151
|
+
const match = files.find(f => f.startsWith(sessionId));
|
|
152
|
+
if (match) return join(this.sessionsDir, match);
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
search(query: string): SearchResult[] {
|
|
157
|
+
const q = query.toLowerCase();
|
|
158
|
+
const results: SearchResult[] = [];
|
|
159
|
+
const days = this.listDays();
|
|
160
|
+
for (const day of days) {
|
|
161
|
+
const normalized = this.getDay(day);
|
|
162
|
+
for (const entry of normalized.session_breakdown) {
|
|
163
|
+
if (entry.top_component_summary.toLowerCase().includes(q)) {
|
|
164
|
+
results.push({
|
|
165
|
+
session_id: entry.session_id,
|
|
166
|
+
date: day,
|
|
167
|
+
summary: entry.top_component_summary,
|
|
168
|
+
tags: entry.tags,
|
|
169
|
+
match_type: 'summary',
|
|
170
|
+
match_snippet: entry.top_component_summary,
|
|
171
|
+
});
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const matchedTag = entry.tags.find(t => t.toLowerCase().includes(q));
|
|
175
|
+
if (matchedTag) {
|
|
176
|
+
results.push({
|
|
177
|
+
session_id: entry.session_id,
|
|
178
|
+
date: day,
|
|
179
|
+
summary: entry.top_component_summary,
|
|
180
|
+
tags: entry.tags,
|
|
181
|
+
match_type: 'tag',
|
|
182
|
+
match_snippet: matchedTag,
|
|
183
|
+
});
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const detail = this.getSessionDetail(entry.session_id);
|
|
187
|
+
if (detail) {
|
|
188
|
+
for (const msg of detail.messages) {
|
|
189
|
+
for (const part of msg.parts) {
|
|
190
|
+
if (part.text && part.text.toLowerCase().includes(q)) {
|
|
191
|
+
const snippet = part.text.slice(Math.max(0, part.text.toLowerCase().indexOf(q) - 40), part.text.toLowerCase().indexOf(q) + 80);
|
|
192
|
+
results.push({
|
|
193
|
+
session_id: entry.session_id,
|
|
194
|
+
date: day,
|
|
195
|
+
summary: entry.top_component_summary,
|
|
196
|
+
tags: entry.tags,
|
|
197
|
+
match_type: 'transcript',
|
|
198
|
+
match_snippet: snippet,
|
|
199
|
+
});
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (results.length > 0 && results[results.length - 1].session_id === entry.session_id && results[results.length - 1].match_type === 'transcript') break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return results;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
refresh(): void {
|
|
212
|
+
this.dailyCache.invalidateAll();
|
|
213
|
+
this.daysListCache.invalidateAll();
|
|
214
|
+
this.detailCache.invalidateAll();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
|
|
2
|
+
import { join, resolve, relative } from 'node:path';
|
|
3
|
+
import type { SpecNode } from '../types.js';
|
|
4
|
+
|
|
5
|
+
const SKIP_DIRS = new Set(['node_modules', 'thirdparty', 'external', '.git', 'build', 'build_debug', 'build_ml', 'build_ml_release', 'build_baseline', 'build_clean', 'build_clean_p3', 'build_clean_test', 'build_debug', 'build_final_clean', 'build_hw', 'build_hw_dbg', 'build_off', 'build_phase2', 'build_pipeline_baseline', 'build_release', 'build_sched', 'build_sched_full', 'build_sched_trace', 'lib', 'bin', 'install', 'pkgconfig', '.artifacts', '.github', '.playwright-mcp', '.profiler', 'ml-data', 'ml-models', 'ml-models-v2', 'logs', 'sessions', 'coverage', 'deps', 'cfg', 'docs', 'microbench', 'perf', 'test', 'thirdparty']);
|
|
6
|
+
|
|
7
|
+
export class TechSpecService {
|
|
8
|
+
private rootDir: string;
|
|
9
|
+
|
|
10
|
+
constructor(rootDir: string) {
|
|
11
|
+
this.rootDir = resolve(rootDir);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
getTree(): SpecNode {
|
|
15
|
+
return {
|
|
16
|
+
name: 'Technical Specifications',
|
|
17
|
+
path: '',
|
|
18
|
+
isDir: true,
|
|
19
|
+
children: this.scanDir(this.rootDir, 6),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
readSpec(specPath: string): string | null {
|
|
24
|
+
const fullPath = join(this.rootDir, specPath);
|
|
25
|
+
if (!existsSync(fullPath)) return null;
|
|
26
|
+
return readFileSync(fullPath, 'utf-8');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private scanDir(dir: string, maxDepth: number): SpecNode[] {
|
|
30
|
+
if (maxDepth <= 0) return [];
|
|
31
|
+
const entries: SpecNode[] = [];
|
|
32
|
+
try {
|
|
33
|
+
const items = readdirSync(dir);
|
|
34
|
+
const dirs: string[] = [];
|
|
35
|
+
const specFiles: SpecNode[] = [];
|
|
36
|
+
|
|
37
|
+
for (const item of items) {
|
|
38
|
+
if (SKIP_DIRS.has(item)) continue;
|
|
39
|
+
const fullPath = join(dir, item);
|
|
40
|
+
let stat;
|
|
41
|
+
try { stat = statSync(fullPath); } catch { continue; }
|
|
42
|
+
|
|
43
|
+
if (stat.isDirectory()) {
|
|
44
|
+
if (this.hasSpecs(fullPath)) {
|
|
45
|
+
dirs.push(fullPath);
|
|
46
|
+
}
|
|
47
|
+
} else if (item.endsWith('.spec.md') || item === 'technical-specification.md') {
|
|
48
|
+
specFiles.push({
|
|
49
|
+
name: item,
|
|
50
|
+
path: relative(this.rootDir, fullPath),
|
|
51
|
+
isDir: false,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const d of dirs.sort()) {
|
|
57
|
+
const relPath = relative(this.rootDir, d);
|
|
58
|
+
entries.push({
|
|
59
|
+
name: relPath,
|
|
60
|
+
path: relPath,
|
|
61
|
+
isDir: true,
|
|
62
|
+
children: this.scanDir(d, maxDepth - 1),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
entries.push(...specFiles);
|
|
67
|
+
entries.sort((a, b) => {
|
|
68
|
+
// Root spec first, then aggregate specs, then dirs, then other files
|
|
69
|
+
if (a.name === 'technical-specification.md') return -1;
|
|
70
|
+
if (b.name === 'technical-specification.md') return 1;
|
|
71
|
+
if (a.isDir && !b.isDir) return 1;
|
|
72
|
+
if (!a.isDir && b.isDir) return -1;
|
|
73
|
+
return a.name.localeCompare(b.name);
|
|
74
|
+
});
|
|
75
|
+
} catch {}
|
|
76
|
+
return entries;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private hasSpecs(dir: string): boolean {
|
|
80
|
+
try {
|
|
81
|
+
const items = readdirSync(dir);
|
|
82
|
+
for (const item of items) {
|
|
83
|
+
if (SKIP_DIRS.has(item)) continue;
|
|
84
|
+
if (item.endsWith('.spec.md') || item === 'technical-specification.md') return true;
|
|
85
|
+
const fullPath = join(dir, item);
|
|
86
|
+
try {
|
|
87
|
+
if (statSync(fullPath).isDirectory() && !SKIP_DIRS.has(item)) {
|
|
88
|
+
if (this.hasSpecs(fullPath)) return true;
|
|
89
|
+
}
|
|
90
|
+
} catch {}
|
|
91
|
+
}
|
|
92
|
+
} catch {}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
export interface SubjectArea {
|
|
2
|
+
name: string;
|
|
3
|
+
prompter_time_hours: number;
|
|
4
|
+
sme_time_hours: number;
|
|
5
|
+
ai_multiplier: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface SessionEntry {
|
|
9
|
+
session_id: string;
|
|
10
|
+
duration_minutes: number;
|
|
11
|
+
prompter_time_minutes: number;
|
|
12
|
+
sme_time_minutes: number;
|
|
13
|
+
top_component_summary: string;
|
|
14
|
+
tags: string[];
|
|
15
|
+
human_confidence: 'high' | 'medium' | 'low';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DailyMetadata {
|
|
19
|
+
generated_at: string;
|
|
20
|
+
audited: boolean;
|
|
21
|
+
audit_note: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface NormalizedDay {
|
|
25
|
+
date: string;
|
|
26
|
+
metadata?: DailyMetadata;
|
|
27
|
+
total_prompter_time_hours: number;
|
|
28
|
+
total_sme_time_hours: number;
|
|
29
|
+
ai_multiplier: number;
|
|
30
|
+
total_sessions: number;
|
|
31
|
+
top_subject_areas: SubjectArea[];
|
|
32
|
+
session_breakdown: SessionEntry[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface FormatA {
|
|
36
|
+
dashboard: {
|
|
37
|
+
metadata: DailyMetadata;
|
|
38
|
+
daily_summary: {
|
|
39
|
+
date: string;
|
|
40
|
+
total_prompter_time_hours: number;
|
|
41
|
+
total_sme_time_hours: number;
|
|
42
|
+
ai_multiplier: number;
|
|
43
|
+
total_sessions: number;
|
|
44
|
+
top_subject_areas: SubjectArea[];
|
|
45
|
+
};
|
|
46
|
+
session_breakdown: SessionEntry[];
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface FormatB {
|
|
51
|
+
date: string;
|
|
52
|
+
total_prompter_time_hours: number;
|
|
53
|
+
total_sme_time_hours: number;
|
|
54
|
+
ai_multiplier: number;
|
|
55
|
+
total_sessions: number;
|
|
56
|
+
top_subject_areas: SubjectArea[];
|
|
57
|
+
session_breakdown: SessionEntry[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface SessionInfo {
|
|
61
|
+
id: string;
|
|
62
|
+
slug: string;
|
|
63
|
+
title: string;
|
|
64
|
+
agent: string;
|
|
65
|
+
model: { id: string; providerID: string };
|
|
66
|
+
summary: { additions: number; deletions: number; files: number };
|
|
67
|
+
time: { created: number; updated: number };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface MessageInfo {
|
|
71
|
+
role: string;
|
|
72
|
+
time: { created: number };
|
|
73
|
+
agent: string;
|
|
74
|
+
model: { providerID: string; modelID: string };
|
|
75
|
+
summary: { diffs: Array<{ path: string; type: string; lines: Record<string, number> }> };
|
|
76
|
+
id: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface MessagePart {
|
|
80
|
+
type: string;
|
|
81
|
+
text?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface SessionMessage {
|
|
85
|
+
info: MessageInfo;
|
|
86
|
+
parts: MessagePart[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface SessionDetail {
|
|
90
|
+
info: SessionInfo;
|
|
91
|
+
messages: SessionMessage[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface GitLogEntry {
|
|
95
|
+
commit: string;
|
|
96
|
+
author: string;
|
|
97
|
+
date: string;
|
|
98
|
+
message: string;
|
|
99
|
+
files_changed: number;
|
|
100
|
+
insertions: number;
|
|
101
|
+
deletions: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface GitStats {
|
|
105
|
+
total_commits: number;
|
|
106
|
+
total_files_changed: number;
|
|
107
|
+
total_insertions: number;
|
|
108
|
+
total_deletions: number;
|
|
109
|
+
per_date: Record<string, { commits: number; insertions: number; deletions: number }>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface SearchResult {
|
|
113
|
+
session_id: string;
|
|
114
|
+
date: string;
|
|
115
|
+
summary: string;
|
|
116
|
+
tags: string[];
|
|
117
|
+
match_type: 'summary' | 'tag' | 'transcript';
|
|
118
|
+
match_snippet: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface CrossDayStats {
|
|
122
|
+
total_days: number;
|
|
123
|
+
total_sessions: number;
|
|
124
|
+
total_prompter_time_hours: number;
|
|
125
|
+
total_sme_time_hours: number;
|
|
126
|
+
avg_multiplier: number;
|
|
127
|
+
per_day: NormalizedDay[];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface SpecNode {
|
|
131
|
+
name: string;
|
|
132
|
+
path: string;
|
|
133
|
+
isDir: boolean;
|
|
134
|
+
children?: SpecNode[];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface HealthStatus {
|
|
138
|
+
status: 'ok' | 'error';
|
|
139
|
+
days_count: number;
|
|
140
|
+
sessions_count: number;
|
|
141
|
+
sessions_path: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface ExperimentEntry {
|
|
145
|
+
date: string;
|
|
146
|
+
directory: string;
|
|
147
|
+
description: string;
|
|
148
|
+
outcome: string;
|
|
149
|
+
agent: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface ExperimentFile {
|
|
153
|
+
path: string;
|
|
154
|
+
name: string;
|
|
155
|
+
size: number;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface ExperimentSubdir {
|
|
159
|
+
name: string;
|
|
160
|
+
files: ExperimentFile[];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface ExperimentDetail {
|
|
164
|
+
entry: ExperimentEntry;
|
|
165
|
+
readme: string | null;
|
|
166
|
+
subdirs: ExperimentSubdir[];
|
|
167
|
+
allFiles: ExperimentFile[];
|
|
168
|
+
}
|