@cccarv82/freya 2.7.0 → 2.8.2
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/cli/auto-update.js +2 -5
- package/cli/web.js +120 -87
- package/package.json +1 -1
- package/scripts/generate-blockers-report.js +6 -1
- package/scripts/generate-daily-summary.js +10 -4
- package/scripts/generate-executive-report.js +8 -4
- package/scripts/generate-sm-weekly-report.js +10 -4
- package/scripts/generate-weekly-report.js +86 -100
- package/scripts/lib/DataLayer.js +95 -7
- package/scripts/lib/DataManager.js +62 -5
- package/scripts/lib/index-utils.js +4 -2
- package/scripts/migrate-data.js +8 -1
- package/scripts/validate-data.js +11 -3
- package/templates/base/scripts/generate-blockers-report.js +6 -1
- package/templates/base/scripts/generate-daily-summary.js +10 -4
- package/templates/base/scripts/generate-executive-report.js +9 -5
- package/templates/base/scripts/generate-sm-weekly-report.js +10 -4
- package/templates/base/scripts/generate-weekly-report.js +86 -100
- package/templates/base/scripts/lib/DataLayer.js +98 -10
- package/templates/base/scripts/lib/DataManager.js +76 -19
- package/templates/base/scripts/lib/index-utils.js +4 -2
- package/templates/base/scripts/migrate-data.js +8 -1
- package/templates/base/scripts/validate-data.js +20 -12
|
@@ -1,23 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* generate-weekly-report.js
|
|
3
|
+
* Generates a weekly Markdown report aggregating Tasks, Blockers, Career entries,
|
|
4
|
+
* and Project Updates from the SQLite database.
|
|
5
|
+
*
|
|
6
|
+
* Usage: node scripts/generate-weekly-report.js
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
const fs = require('fs');
|
|
2
10
|
const path = require('path');
|
|
3
11
|
|
|
4
|
-
const { toIsoDate,
|
|
12
|
+
const { toIsoDate, safeParseToMs } = require('./lib/date-utils');
|
|
13
|
+
const DataManager = require('./lib/DataManager');
|
|
14
|
+
const { ready } = require('./lib/DataLayer');
|
|
5
15
|
|
|
6
|
-
|
|
7
|
-
const
|
|
16
|
+
// --- Configuration (BUG-30: use FREYA_WORKSPACE_DIR instead of __dirname) ---
|
|
17
|
+
const WORKSPACE_DIR = process.env.FREYA_WORKSPACE_DIR
|
|
18
|
+
? path.resolve(process.env.FREYA_WORKSPACE_DIR)
|
|
19
|
+
: path.join(__dirname, '..'); // fallback: scripts/ is one level below workspace root
|
|
8
20
|
|
|
9
|
-
|
|
10
|
-
if (!fs.existsSync(REPORT_DIR)) {
|
|
11
|
-
fs.mkdirSync(REPORT_DIR, { recursive: true });
|
|
12
|
-
}
|
|
21
|
+
const REPORT_DIR = path.join(WORKSPACE_DIR, 'docs', 'reports');
|
|
13
22
|
|
|
14
23
|
// --- Date Logic ---
|
|
15
24
|
const now = new Date();
|
|
16
25
|
const oneDay = 24 * 60 * 60 * 1000;
|
|
17
26
|
|
|
18
27
|
function isWithinWeek(dateStr) {
|
|
19
|
-
const
|
|
20
|
-
|
|
28
|
+
const ms = safeParseToMs(dateStr);
|
|
29
|
+
if (!Number.isFinite(ms)) return false;
|
|
30
|
+
const sevenDaysAgo = now.getTime() - (7 * oneDay);
|
|
31
|
+
return ms >= sevenDaysAgo && ms <= now.getTime();
|
|
21
32
|
}
|
|
22
33
|
|
|
23
34
|
function getFormattedDate() {
|
|
@@ -31,112 +42,87 @@ function getFormattedTime() {
|
|
|
31
42
|
return `${hh}${mm}${ss}`;
|
|
32
43
|
}
|
|
33
44
|
|
|
34
|
-
// ---
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
// --- Report Generation ---
|
|
46
|
+
async function generateWeeklyReport() {
|
|
47
|
+
await ready;
|
|
48
|
+
|
|
49
|
+
const start = new Date(now.getTime() - 7 * oneDay);
|
|
50
|
+
const end = now;
|
|
51
|
+
|
|
52
|
+
const dm = new DataManager();
|
|
53
|
+
|
|
54
|
+
// Fetch data from SQLite
|
|
55
|
+
const { completed: completedTasks } = dm.getTasks(start, end);
|
|
56
|
+
const { open: openBlockers, resolvedRecent } = dm.getBlockers(start, end);
|
|
57
|
+
const projectUpdates = dm.getProjectUpdates(start, end);
|
|
58
|
+
const careerEntries = dm.getCareerEntries ? dm.getCareerEntries(start, end) : [];
|
|
59
|
+
|
|
60
|
+
// Ensure output dir exists
|
|
61
|
+
if (!fs.existsSync(REPORT_DIR)) {
|
|
62
|
+
fs.mkdirSync(REPORT_DIR, { recursive: true });
|
|
63
|
+
}
|
|
50
64
|
|
|
51
|
-
// --- Aggregation ---
|
|
52
|
-
function generateWeeklyReport() {
|
|
53
|
-
const files = walk(DATA_DIR);
|
|
54
|
-
|
|
55
|
-
const projects = [];
|
|
56
|
-
let taskLog = { schemaVersion: 1, tasks: [] };
|
|
57
|
-
let careerLog = { entries: [] };
|
|
58
|
-
|
|
59
|
-
// 1. Collect Data
|
|
60
|
-
files.forEach(file => {
|
|
61
|
-
try {
|
|
62
|
-
const content = fs.readFileSync(file, 'utf8');
|
|
63
|
-
const json = JSON.parse(content);
|
|
64
|
-
|
|
65
|
-
if (file.endsWith('task-log.json')) {
|
|
66
|
-
taskLog = json;
|
|
67
|
-
} else if (file.endsWith('career-log.json')) {
|
|
68
|
-
careerLog = json;
|
|
69
|
-
} else if (file.endsWith('status.json')) {
|
|
70
|
-
projects.push(json);
|
|
71
|
-
}
|
|
72
|
-
} catch (err) {
|
|
73
|
-
console.error(`Error reading ${file}: ${err.message}`);
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// 2. Generate Content
|
|
78
65
|
const reportDate = getFormattedDate();
|
|
79
66
|
const reportTime = getFormattedTime();
|
|
80
67
|
let report = `# Weekly Report - ${reportDate}\n\n`;
|
|
81
68
|
|
|
82
69
|
// Projects
|
|
83
|
-
report +=
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
report +=
|
|
91
|
-
recentUpdates.forEach(u => {
|
|
92
|
-
const dateStr = u.date ? u.date.split('T')[0] : 'Unknown Date';
|
|
93
|
-
report += `- **${dateStr}**: ${u.content}\n`;
|
|
94
|
-
});
|
|
95
|
-
report += "\n";
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
if (!hasProjectUpdates) report += "No project updates recorded this week.\n\n";
|
|
100
|
-
|
|
101
|
-
// Tasks
|
|
102
|
-
report += "## ✅ Completed Tasks\n";
|
|
103
|
-
if (taskLog.tasks && Array.isArray(taskLog.tasks)) {
|
|
104
|
-
const recentTasks = taskLog.tasks.filter(t => t.status === "COMPLETED" && isWithinWeek(t.completedAt));
|
|
105
|
-
if (recentTasks.length > 0) {
|
|
106
|
-
recentTasks.forEach(t => {
|
|
107
|
-
report += `- ${t.description}\n`;
|
|
70
|
+
report += '## Project Updates\n';
|
|
71
|
+
if (projectUpdates.length > 0) {
|
|
72
|
+
projectUpdates.forEach(p => {
|
|
73
|
+
report += `### ${p.client || 'Unknown'} - ${p.project || p.slug || 'Unknown'}\n`;
|
|
74
|
+
const events = Array.isArray(p.events) ? p.events : [];
|
|
75
|
+
events.forEach(e => {
|
|
76
|
+
const dateStr = e.date ? String(e.date).slice(0, 10) : 'Unknown Date';
|
|
77
|
+
report += `- **${dateStr}**: ${e.content || ''}\n`;
|
|
108
78
|
});
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
79
|
+
report += '\n';
|
|
80
|
+
});
|
|
112
81
|
} else {
|
|
113
|
-
report +=
|
|
82
|
+
report += 'No project updates recorded this week.\n\n';
|
|
114
83
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
report += `- **[${e.type}]**: ${e.description}\n`;
|
|
124
|
-
});
|
|
125
|
-
} else {
|
|
126
|
-
report += "No career updates this week.\n";
|
|
127
|
-
}
|
|
84
|
+
|
|
85
|
+
// Completed Tasks
|
|
86
|
+
report += '## Completed Tasks\n';
|
|
87
|
+
if (completedTasks.length > 0) {
|
|
88
|
+
completedTasks.forEach(t => {
|
|
89
|
+
const projectTag = t.projectSlug || t.project_slug ? `[${t.projectSlug || t.project_slug}] ` : '';
|
|
90
|
+
report += `- ${projectTag}${t.description}\n`;
|
|
91
|
+
});
|
|
128
92
|
} else {
|
|
129
|
-
report +=
|
|
93
|
+
report += 'No tasks completed this week.\n';
|
|
94
|
+
}
|
|
95
|
+
report += '\n';
|
|
96
|
+
|
|
97
|
+
// Open Blockers
|
|
98
|
+
report += '## Open Blockers\n';
|
|
99
|
+
if (openBlockers.length > 0) {
|
|
100
|
+
openBlockers.forEach(b => {
|
|
101
|
+
const sev = b.severity ? `[${b.severity}] ` : '';
|
|
102
|
+
report += `- ${sev}${b.title}\n`;
|
|
103
|
+
});
|
|
104
|
+
} else {
|
|
105
|
+
report += 'No open blockers.\n';
|
|
106
|
+
}
|
|
107
|
+
report += '\n';
|
|
108
|
+
|
|
109
|
+
// Career entries (if DataManager supports it)
|
|
110
|
+
if (Array.isArray(careerEntries) && careerEntries.length > 0) {
|
|
111
|
+
report += '## Career Highlights\n';
|
|
112
|
+
careerEntries.forEach(e => {
|
|
113
|
+
report += `- **[${e.type || 'Note'}]**: ${e.description || e.content || ''}\n`;
|
|
114
|
+
});
|
|
115
|
+
report += '\n';
|
|
130
116
|
}
|
|
131
117
|
|
|
132
118
|
// 3. Save and Output
|
|
133
119
|
const outputPath = path.join(REPORT_DIR, `weekly-${reportDate}-${reportTime}.md`);
|
|
134
120
|
fs.writeFileSync(outputPath, report);
|
|
135
|
-
|
|
136
|
-
console.log(
|
|
137
|
-
console.log(
|
|
121
|
+
|
|
122
|
+
console.log(`Report generated at: ${outputPath}`);
|
|
123
|
+
console.log('---------------------------------------------------');
|
|
138
124
|
console.log(report);
|
|
139
|
-
console.log(
|
|
125
|
+
console.log('---------------------------------------------------');
|
|
140
126
|
}
|
|
141
127
|
|
|
142
|
-
generateWeeklyReport();
|
|
128
|
+
generateWeeklyReport().catch(err => { console.error(err); process.exit(1); });
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DataLayer.js (V2.1 - sql.js powered, no native compilation needed)
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Drop-in replacement for better-sqlite3 using sql.js (WebAssembly SQLite).
|
|
5
5
|
* Exposes the same API surface: .prepare().all(), .prepare().get(), .prepare().run(),
|
|
6
6
|
* .exec(), .pragma(), .transaction(), .close()
|
|
7
|
-
*
|
|
7
|
+
*
|
|
8
8
|
* Auto-persists to disk after every write operation.
|
|
9
9
|
*/
|
|
10
10
|
|
|
@@ -72,7 +72,10 @@ class PreparedStatement {
|
|
|
72
72
|
const db = this._wrapper._db;
|
|
73
73
|
db.run(this._sql, this._flattenParams(params));
|
|
74
74
|
const changes = db.getRowsModified();
|
|
75
|
-
|
|
75
|
+
// Only save to disk if NOT inside a transaction (save happens at COMMIT)
|
|
76
|
+
if (!this._wrapper._inTransaction) {
|
|
77
|
+
this._wrapper._save();
|
|
78
|
+
}
|
|
76
79
|
return { changes };
|
|
77
80
|
}
|
|
78
81
|
|
|
@@ -92,7 +95,7 @@ class SqlJsDatabase {
|
|
|
92
95
|
constructor(sqlJsDb, filePath) {
|
|
93
96
|
this._db = sqlJsDb;
|
|
94
97
|
this._filePath = filePath;
|
|
95
|
-
this.
|
|
98
|
+
this._inTransaction = false;
|
|
96
99
|
}
|
|
97
100
|
|
|
98
101
|
/**
|
|
@@ -126,7 +129,9 @@ class SqlJsDatabase {
|
|
|
126
129
|
*/
|
|
127
130
|
exec(sql) {
|
|
128
131
|
this._db.run(sql);
|
|
129
|
-
this.
|
|
132
|
+
if (!this._inTransaction) {
|
|
133
|
+
this._save();
|
|
134
|
+
}
|
|
130
135
|
}
|
|
131
136
|
|
|
132
137
|
/**
|
|
@@ -144,19 +149,31 @@ class SqlJsDatabase {
|
|
|
144
149
|
transaction(fn) {
|
|
145
150
|
const self = this;
|
|
146
151
|
return function (...args) {
|
|
152
|
+
self._inTransaction = true;
|
|
147
153
|
self._db.run('BEGIN TRANSACTION');
|
|
148
154
|
try {
|
|
149
155
|
const result = fn(...args);
|
|
150
156
|
self._db.run('COMMIT');
|
|
157
|
+
self._inTransaction = false;
|
|
151
158
|
self._save();
|
|
152
159
|
return result;
|
|
153
160
|
} catch (e) {
|
|
161
|
+
self._inTransaction = false;
|
|
154
162
|
try { self._db.run('ROLLBACK'); } catch { /* already rolled back */ }
|
|
155
163
|
throw e;
|
|
156
164
|
}
|
|
157
165
|
};
|
|
158
166
|
}
|
|
159
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Public save — explicitly flush the in-memory database to disk.
|
|
170
|
+
* Use this when you need to ensure data is persisted before spawning a
|
|
171
|
+
* child process that will read the same SQLite file (e.g. in tests).
|
|
172
|
+
*/
|
|
173
|
+
save() {
|
|
174
|
+
this._save();
|
|
175
|
+
}
|
|
176
|
+
|
|
160
177
|
/**
|
|
161
178
|
* Close the database.
|
|
162
179
|
*/
|
|
@@ -173,7 +190,13 @@ class SqlJsDatabase {
|
|
|
173
190
|
class DataLayer {
|
|
174
191
|
constructor(dbPath = null) {
|
|
175
192
|
if (!dbPath) {
|
|
176
|
-
|
|
193
|
+
// Prefer FREYA_WORKSPACE_DIR env var so that the web server and report
|
|
194
|
+
// child processes always share the same SQLite file regardless of where
|
|
195
|
+
// each DataLayer.js lives on disk.
|
|
196
|
+
const workspaceDir = process.env.FREYA_WORKSPACE_DIR
|
|
197
|
+
? path.resolve(process.env.FREYA_WORKSPACE_DIR)
|
|
198
|
+
: path.join(__dirname, '..', '..');
|
|
199
|
+
const dataDir = path.join(workspaceDir, 'data');
|
|
177
200
|
if (!fs.existsSync(dataDir)) {
|
|
178
201
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
179
202
|
}
|
|
@@ -192,6 +215,40 @@ class DataLayer {
|
|
|
192
215
|
const initSqlJs = require('sql.js');
|
|
193
216
|
const SQL = await initSqlJs();
|
|
194
217
|
|
|
218
|
+
// --- WAL consolidation (migration from better-sqlite3) ---
|
|
219
|
+
// better-sqlite3 used WAL mode, which may leave a -wal file with uncommitted data.
|
|
220
|
+
// sql.js cannot read WAL files directly, so we need to checkpoint first.
|
|
221
|
+
const walPath = this._dbPath + '-wal';
|
|
222
|
+
const shmPath = this._dbPath + '-shm';
|
|
223
|
+
if (fs.existsSync(this._dbPath) && fs.existsSync(walPath)) {
|
|
224
|
+
try {
|
|
225
|
+
console.log('[DataLayer] Consolidating WAL file from previous better-sqlite3 usage...');
|
|
226
|
+
// Open the database including the WAL data
|
|
227
|
+
const fileBuffer = fs.readFileSync(this._dbPath);
|
|
228
|
+
const tempDb = new SQL.Database(fileBuffer);
|
|
229
|
+
|
|
230
|
+
// Try to checkpoint the WAL (merge pending writes into main file)
|
|
231
|
+
try { tempDb.run('PRAGMA wal_checkpoint(TRUNCATE)'); } catch { /* may not work in WASM */ }
|
|
232
|
+
|
|
233
|
+
// Switch to DELETE journal mode (fully compatible with sql.js)
|
|
234
|
+
try { tempDb.run('PRAGMA journal_mode = DELETE'); } catch { /* ignore */ }
|
|
235
|
+
|
|
236
|
+
// Export the consolidated database and save it
|
|
237
|
+
const consolidated = tempDb.export();
|
|
238
|
+
fs.writeFileSync(this._dbPath, Buffer.from(consolidated));
|
|
239
|
+
tempDb.close();
|
|
240
|
+
|
|
241
|
+
// Clean up WAL/SHM files
|
|
242
|
+
try { fs.unlinkSync(walPath); } catch { /* ignore */ }
|
|
243
|
+
try { fs.unlinkSync(shmPath); } catch { /* ignore */ }
|
|
244
|
+
|
|
245
|
+
console.log('[DataLayer] WAL consolidation complete.');
|
|
246
|
+
} catch (err) {
|
|
247
|
+
console.error('[DataLayer] Warning: WAL consolidation failed:', err.message);
|
|
248
|
+
// Continue anyway — the main .sqlite file may still be usable
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
195
252
|
// Load existing database from disk, or create new one
|
|
196
253
|
let sqlJsDb;
|
|
197
254
|
if (fs.existsSync(this._dbPath)) {
|
|
@@ -206,8 +263,8 @@ class DataLayer {
|
|
|
206
263
|
}
|
|
207
264
|
|
|
208
265
|
_initSchema() {
|
|
209
|
-
//
|
|
210
|
-
this.db.pragma('journal_mode =
|
|
266
|
+
// Use DELETE journal mode (compatible with sql.js WASM)
|
|
267
|
+
this.db.pragma('journal_mode = DELETE');
|
|
211
268
|
|
|
212
269
|
this.db.exec(`
|
|
213
270
|
CREATE TABLE IF NOT EXISTS projects (
|
|
@@ -262,7 +319,7 @@ class DataLayer {
|
|
|
262
319
|
CREATE TABLE IF NOT EXISTS document_embeddings (
|
|
263
320
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
264
321
|
reference_type TEXT NOT NULL, /* 'daily_log', 'task', 'blocker' */
|
|
265
|
-
reference_id TEXT NOT NULL,
|
|
322
|
+
reference_id TEXT NOT NULL,
|
|
266
323
|
chunk_index INTEGER DEFAULT 0,
|
|
267
324
|
text_chunk TEXT NOT NULL,
|
|
268
325
|
embedding BLOB NOT NULL, /* Stored as Buffer of Float32Array */
|
|
@@ -290,4 +347,35 @@ const ready = defaultInstance.init().catch(err => {
|
|
|
290
347
|
process.exit(1);
|
|
291
348
|
});
|
|
292
349
|
|
|
293
|
-
|
|
350
|
+
/**
|
|
351
|
+
* Configure the singleton to use a specific workspace directory.
|
|
352
|
+
* Call this before performing any DB operations against the target workspace.
|
|
353
|
+
* Closes the current in-memory DB (if open) and reinitializes from the new path.
|
|
354
|
+
*
|
|
355
|
+
* This is used by the web server to ensure it uses the same SQLite file as
|
|
356
|
+
* report child processes (both pointed at {workspaceDir}/data/freya.sqlite).
|
|
357
|
+
*
|
|
358
|
+
* @param {string} workspaceDir Absolute path to the Freya workspace directory.
|
|
359
|
+
* @returns {Promise<void>} Resolves when the new DB is ready.
|
|
360
|
+
*/
|
|
361
|
+
async function configure(workspaceDir) {
|
|
362
|
+
const newDbPath = path.join(path.resolve(workspaceDir), 'data', 'freya.sqlite');
|
|
363
|
+
if (defaultInstance._dbPath === newDbPath && defaultInstance.db) {
|
|
364
|
+
// Already pointing at the right file.
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
// Close existing connection if open.
|
|
368
|
+
if (defaultInstance.db) {
|
|
369
|
+
try { defaultInstance.db.close(); } catch { /* ignore */ }
|
|
370
|
+
defaultInstance.db = null;
|
|
371
|
+
}
|
|
372
|
+
// Set the new path and reinitialize.
|
|
373
|
+
defaultInstance._dbPath = newDbPath;
|
|
374
|
+
const dataDir = path.dirname(newDbPath);
|
|
375
|
+
if (!fs.existsSync(dataDir)) {
|
|
376
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
377
|
+
}
|
|
378
|
+
await defaultInstance.init();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
module.exports = { defaultInstance, DataLayer, ready, configure };
|
|
@@ -25,7 +25,13 @@ class DataManager {
|
|
|
25
25
|
// --- Tasks ---
|
|
26
26
|
getTasksRaw() {
|
|
27
27
|
return dl.db.prepare('SELECT * FROM tasks').all().map(t => {
|
|
28
|
-
|
|
28
|
+
// BUG-17: JSON.parse can throw on corrupted metadata; default to {}
|
|
29
|
+
let meta = {};
|
|
30
|
+
try {
|
|
31
|
+
meta = t.metadata ? JSON.parse(t.metadata) : {};
|
|
32
|
+
} catch {
|
|
33
|
+
meta = {};
|
|
34
|
+
}
|
|
29
35
|
return {
|
|
30
36
|
id: t.id,
|
|
31
37
|
projectSlug: t.project_slug,
|
|
@@ -48,13 +54,13 @@ class DataManager {
|
|
|
48
54
|
|
|
49
55
|
const tasks = this.getTasksRaw();
|
|
50
56
|
const completed = dl.db.prepare(`
|
|
51
|
-
SELECT * FROM tasks
|
|
52
|
-
WHERE status = 'COMPLETED'
|
|
57
|
+
SELECT * FROM tasks
|
|
58
|
+
WHERE status = 'COMPLETED'
|
|
53
59
|
AND completed_at >= ? AND completed_at <= ?
|
|
54
60
|
`).all(startIso, endIso).map(t => ({ ...t, completedAt: t.completed_at, createdAt: t.created_at }));
|
|
55
61
|
|
|
56
62
|
const pending = dl.db.prepare(`
|
|
57
|
-
SELECT * FROM tasks
|
|
63
|
+
SELECT * FROM tasks
|
|
58
64
|
WHERE status = 'PENDING' AND category = 'DO_NOW'
|
|
59
65
|
`).all().map(t => ({ ...t, createdAt: t.created_at }));
|
|
60
66
|
|
|
@@ -64,7 +70,13 @@ class DataManager {
|
|
|
64
70
|
// --- Blockers ---
|
|
65
71
|
getBlockersRaw() {
|
|
66
72
|
return dl.db.prepare('SELECT * FROM blockers').all().map(b => {
|
|
67
|
-
|
|
73
|
+
// BUG-17: JSON.parse can throw on corrupted metadata; default to {}
|
|
74
|
+
let meta = {};
|
|
75
|
+
try {
|
|
76
|
+
meta = b.metadata ? JSON.parse(b.metadata) : {};
|
|
77
|
+
} catch {
|
|
78
|
+
meta = {};
|
|
79
|
+
}
|
|
68
80
|
return {
|
|
69
81
|
id: b.id,
|
|
70
82
|
projectSlug: b.project_slug,
|
|
@@ -88,27 +100,27 @@ class DataManager {
|
|
|
88
100
|
const blockers = this.getBlockersRaw();
|
|
89
101
|
|
|
90
102
|
const open = dl.db.prepare(`
|
|
91
|
-
SELECT * FROM blockers
|
|
103
|
+
SELECT * FROM blockers
|
|
92
104
|
WHERE status NOT IN ('RESOLVED', 'CLOSED', 'DONE', 'FIXED')
|
|
93
105
|
AND resolved_at IS NULL
|
|
94
|
-
ORDER BY
|
|
95
|
-
CASE severity
|
|
96
|
-
WHEN 'CRITICAL' THEN 0
|
|
97
|
-
WHEN 'HIGH' THEN 1
|
|
98
|
-
WHEN 'MEDIUM' THEN 2
|
|
99
|
-
WHEN 'LOW' THEN 3
|
|
100
|
-
ELSE 99
|
|
106
|
+
ORDER BY
|
|
107
|
+
CASE severity
|
|
108
|
+
WHEN 'CRITICAL' THEN 0
|
|
109
|
+
WHEN 'HIGH' THEN 1
|
|
110
|
+
WHEN 'MEDIUM' THEN 2
|
|
111
|
+
WHEN 'LOW' THEN 3
|
|
112
|
+
ELSE 99
|
|
101
113
|
END ASC,
|
|
102
114
|
created_at ASC
|
|
103
115
|
`).all().map(b => ({ ...b, projectSlug: b.project_slug, createdAt: b.created_at }));
|
|
104
116
|
|
|
105
117
|
const openedRecent = dl.db.prepare(`
|
|
106
|
-
SELECT * FROM blockers
|
|
118
|
+
SELECT * FROM blockers
|
|
107
119
|
WHERE created_at >= ? AND created_at <= ?
|
|
108
120
|
`).all(startIso, endIso).map(b => ({ ...b, projectSlug: b.project_slug, createdAt: b.created_at }));
|
|
109
121
|
|
|
110
122
|
const resolvedRecent = dl.db.prepare(`
|
|
111
|
-
SELECT * FROM blockers
|
|
123
|
+
SELECT * FROM blockers
|
|
112
124
|
WHERE resolved_at >= ? AND resolved_at <= ?
|
|
113
125
|
`).all(startIso, endIso).map(b => ({ ...b, projectSlug: b.project_slug, resolvedAt: b.resolved_at }));
|
|
114
126
|
|
|
@@ -160,9 +172,17 @@ class DataManager {
|
|
|
160
172
|
|
|
161
173
|
// --- Daily Logs ---
|
|
162
174
|
getDailyLogs(start, end) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
175
|
+
// BUG-41: use local date parts instead of toISOString().slice(0,10) to avoid
|
|
176
|
+
// timezone drift (UTC midnight may differ from local midnight)
|
|
177
|
+
function toLocalDate(d) {
|
|
178
|
+
const dt = d instanceof Date ? d : new Date(d);
|
|
179
|
+
const yyyy = dt.getFullYear();
|
|
180
|
+
const mm = String(dt.getMonth() + 1).padStart(2, '0');
|
|
181
|
+
const dd = String(dt.getDate()).padStart(2, '0');
|
|
182
|
+
return `${yyyy}-${mm}-${dd}`;
|
|
183
|
+
}
|
|
184
|
+
const startIso = formatDate ? formatDate(start) : toLocalDate(start);
|
|
185
|
+
const endIso = formatDate ? formatDate(end) : toLocalDate(end);
|
|
166
186
|
|
|
167
187
|
return dl.db.prepare(`
|
|
168
188
|
SELECT date, raw_markdown as content FROM daily_logs
|
|
@@ -171,13 +191,50 @@ class DataManager {
|
|
|
171
191
|
`).all(startIso, endIso);
|
|
172
192
|
}
|
|
173
193
|
|
|
194
|
+
// --- Timestamp helpers ---
|
|
195
|
+
// These accept a record (task or blocker) and return milliseconds since epoch, or NaN.
|
|
196
|
+
|
|
197
|
+
getCreatedAt(record) {
|
|
198
|
+
const { safeParseToMs } = require('./date-utils');
|
|
199
|
+
const candidates = [
|
|
200
|
+
record.createdAt,
|
|
201
|
+
record.created_at,
|
|
202
|
+
record.openedAt,
|
|
203
|
+
record.opened_at,
|
|
204
|
+
record.date,
|
|
205
|
+
record.loggedAt,
|
|
206
|
+
];
|
|
207
|
+
for (const value of candidates) {
|
|
208
|
+
const ms = safeParseToMs(value);
|
|
209
|
+
if (Number.isFinite(ms)) return ms;
|
|
210
|
+
}
|
|
211
|
+
return NaN;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
getResolvedAt(record) {
|
|
215
|
+
const { safeParseToMs } = require('./date-utils');
|
|
216
|
+
const candidates = [
|
|
217
|
+
record.resolvedAt,
|
|
218
|
+
record.resolved_at,
|
|
219
|
+
record.closedAt,
|
|
220
|
+
record.closed_at,
|
|
221
|
+
record.completedAt,
|
|
222
|
+
record.completed_at,
|
|
223
|
+
];
|
|
224
|
+
for (const value of candidates) {
|
|
225
|
+
const ms = safeParseToMs(value);
|
|
226
|
+
if (Number.isFinite(ms)) return ms;
|
|
227
|
+
}
|
|
228
|
+
return NaN;
|
|
229
|
+
}
|
|
230
|
+
|
|
174
231
|
// --- RAG (Vector Search) ---
|
|
175
232
|
async semanticSearch(query, topK = 10) {
|
|
176
233
|
const queryVector = await defaultEmbedder.embedText(query);
|
|
177
234
|
|
|
178
235
|
// Fetch all stored embeddings. For a local personal tool with < 100k chunks, in-memory cosine sim is perfectly fast.
|
|
179
236
|
const rows = dl.db.prepare(`
|
|
180
|
-
SELECT reference_type, reference_id, chunk_index, text_chunk, embedding
|
|
237
|
+
SELECT reference_type, reference_id, chunk_index, text_chunk, embedding
|
|
181
238
|
FROM document_embeddings
|
|
182
239
|
`).all();
|
|
183
240
|
|
|
@@ -8,7 +8,8 @@ const ID_PATTERNS = [
|
|
|
8
8
|
];
|
|
9
9
|
|
|
10
10
|
const TEXT_EXTS = new Set(['.md', '.txt', '.log', '.json', '.yaml', '.yml']);
|
|
11
|
-
|
|
11
|
+
// BUG-46: TOKEN_RE was a module-level global with /g flag — its lastIndex would
|
|
12
|
+
// persist between calls causing random skipped tokens. Moved inside the function.
|
|
12
13
|
|
|
13
14
|
const DEFAULT_MAX_SIZE = 2 * 1024 * 1024;
|
|
14
15
|
const DEFAULT_TOKEN_LIMIT = 500;
|
|
@@ -142,6 +143,8 @@ function extractIdMatches(text) {
|
|
|
142
143
|
}
|
|
143
144
|
|
|
144
145
|
function extractKeywordIndexMap(textLower, tokenLimit) {
|
|
146
|
+
// BUG-46: create a fresh regex each call — module-level /g regex retains lastIndex
|
|
147
|
+
const TOKEN_RE = /[A-Za-z0-9_-]{3,}/g;
|
|
145
148
|
const map = new Map();
|
|
146
149
|
let m;
|
|
147
150
|
while ((m = TOKEN_RE.exec(textLower)) !== null) {
|
|
@@ -151,7 +154,6 @@ function extractKeywordIndexMap(textLower, tokenLimit) {
|
|
|
151
154
|
if (map.size >= tokenLimit) break;
|
|
152
155
|
}
|
|
153
156
|
}
|
|
154
|
-
TOKEN_RE.lastIndex = 0;
|
|
155
157
|
return map;
|
|
156
158
|
}
|
|
157
159
|
|
|
@@ -3,9 +3,16 @@ const path = require('path');
|
|
|
3
3
|
|
|
4
4
|
const { safeReadJson, quarantineCorruptedFile } = require('./lib/fs-utils');
|
|
5
5
|
|
|
6
|
+
// BUG-43: support FREYA_WORKSPACE_DIR (used by web server) in addition to DATA_DIR
|
|
7
|
+
const WORKSPACE_DIR = process.env.FREYA_WORKSPACE_DIR
|
|
8
|
+
? path.resolve(process.env.FREYA_WORKSPACE_DIR)
|
|
9
|
+
: null;
|
|
10
|
+
|
|
6
11
|
const DATA_DIR = process.env.DATA_DIR
|
|
7
12
|
? path.resolve(process.env.DATA_DIR)
|
|
8
|
-
:
|
|
13
|
+
: WORKSPACE_DIR
|
|
14
|
+
? path.join(WORKSPACE_DIR, 'data')
|
|
15
|
+
: path.join(__dirname, '../data');
|
|
9
16
|
|
|
10
17
|
const KNOWN_FILES = [
|
|
11
18
|
{ relPath: path.join('tasks', 'task-log.json'), label: 'tasks/task-log.json' },
|