@cccarv82/freya 2.7.1 → 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 +48 -2
- 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 +51 -5
- 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
|
|
|
@@ -165,6 +165,15 @@ class SqlJsDatabase {
|
|
|
165
165
|
};
|
|
166
166
|
}
|
|
167
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
|
+
|
|
168
177
|
/**
|
|
169
178
|
* Close the database.
|
|
170
179
|
*/
|
|
@@ -181,7 +190,13 @@ class SqlJsDatabase {
|
|
|
181
190
|
class DataLayer {
|
|
182
191
|
constructor(dbPath = null) {
|
|
183
192
|
if (!dbPath) {
|
|
184
|
-
|
|
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');
|
|
185
200
|
if (!fs.existsSync(dataDir)) {
|
|
186
201
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
187
202
|
}
|
|
@@ -304,7 +319,7 @@ class DataLayer {
|
|
|
304
319
|
CREATE TABLE IF NOT EXISTS document_embeddings (
|
|
305
320
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
306
321
|
reference_type TEXT NOT NULL, /* 'daily_log', 'task', 'blocker' */
|
|
307
|
-
reference_id TEXT NOT NULL,
|
|
322
|
+
reference_id TEXT NOT NULL,
|
|
308
323
|
chunk_index INTEGER DEFAULT 0,
|
|
309
324
|
text_chunk TEXT NOT NULL,
|
|
310
325
|
embedding BLOB NOT NULL, /* Stored as Buffer of Float32Array */
|
|
@@ -332,4 +347,35 @@ const ready = defaultInstance.init().catch(err => {
|
|
|
332
347
|
process.exit(1);
|
|
333
348
|
});
|
|
334
349
|
|
|
335
|
-
|
|
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' },
|
|
@@ -4,7 +4,12 @@ const path = require('path');
|
|
|
4
4
|
const { safeReadJson, quarantineCorruptedFile } = require('./lib/fs-utils');
|
|
5
5
|
const SCHEMA = require('./lib/schema');
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
// BUG-30: use FREYA_WORKSPACE_DIR so the web server uses the correct workspace
|
|
8
|
+
const WORKSPACE_DIR = process.env.FREYA_WORKSPACE_DIR
|
|
9
|
+
? path.resolve(process.env.FREYA_WORKSPACE_DIR)
|
|
10
|
+
: path.join(__dirname, '..'); // fallback: scripts/ is one level below workspace root
|
|
11
|
+
|
|
12
|
+
const DATA_DIR = path.join(WORKSPACE_DIR, 'data');
|
|
8
13
|
|
|
9
14
|
// --- Validation Helpers ---
|
|
10
15
|
|
|
@@ -140,10 +145,10 @@ function walk(dir, fileList = []) {
|
|
|
140
145
|
}
|
|
141
146
|
|
|
142
147
|
function validateData() {
|
|
143
|
-
console.log('
|
|
148
|
+
console.log('Starting validation...');
|
|
144
149
|
try {
|
|
145
150
|
if (!fs.existsSync(DATA_DIR)) {
|
|
146
|
-
console.error('
|
|
151
|
+
console.error('Data directory not found:', DATA_DIR);
|
|
147
152
|
process.exit(1);
|
|
148
153
|
}
|
|
149
154
|
|
|
@@ -158,9 +163,9 @@ function validateData() {
|
|
|
158
163
|
if (!result.ok) {
|
|
159
164
|
if (result.error.type === 'parse') {
|
|
160
165
|
quarantineCorruptedFile(file, result.error.message);
|
|
161
|
-
console.warn(
|
|
166
|
+
console.warn(`[${relativePath}] JSON parse failed; quarantined to _corrupted.`);
|
|
162
167
|
} else {
|
|
163
|
-
console.error(
|
|
168
|
+
console.error(`[${relativePath}] Read failed: ${result.error.message}`);
|
|
164
169
|
}
|
|
165
170
|
errorCount++;
|
|
166
171
|
return;
|
|
@@ -173,29 +178,32 @@ function validateData() {
|
|
|
173
178
|
// Route validation based on filename/path
|
|
174
179
|
if (file.endsWith('career-log.json')) {
|
|
175
180
|
fileErrors = validateCareerLog(json, relativePath);
|
|
176
|
-
} else if (file.endsWith('task-log.json') || file.endsWith('
|
|
177
|
-
//
|
|
181
|
+
} else if (file.endsWith('task-log.json') || file.endsWith('blocker-log.json')) {
|
|
182
|
+
// BUG-06: These are legacy JSON files superseded by SQLite storage.
|
|
183
|
+
// They are no longer the source of truth; validation is skipped to avoid
|
|
184
|
+
// false positives on empty/stub files left behind after migration.
|
|
185
|
+
console.log(`[${relativePath}] Legacy file (superseded by SQLite). Skipping schema validation.`);
|
|
178
186
|
} else {
|
|
179
187
|
// Optional: warn about unknown files, or ignore
|
|
180
|
-
// console.warn(
|
|
188
|
+
// console.warn(`[${relativePath}] Unknown JSON file type. Skipping schema validation.`);
|
|
181
189
|
}
|
|
182
190
|
|
|
183
191
|
if (fileErrors.length > 0) {
|
|
184
|
-
console.error(
|
|
192
|
+
console.error(`[${relativePath}] Validation failed:`);
|
|
185
193
|
fileErrors.forEach(e => console.error(` - ${e}`));
|
|
186
194
|
errorCount++;
|
|
187
195
|
}
|
|
188
196
|
});
|
|
189
197
|
|
|
190
198
|
if (errorCount === 0) {
|
|
191
|
-
console.log('
|
|
199
|
+
console.log('All systems operational');
|
|
192
200
|
} else {
|
|
193
|
-
console.error(
|
|
201
|
+
console.error(`Validation completed with errors in ${errorCount} file(s).`);
|
|
194
202
|
process.exit(1);
|
|
195
203
|
}
|
|
196
204
|
|
|
197
205
|
} catch (err) {
|
|
198
|
-
console.error('
|
|
206
|
+
console.error('Fatal error:', err);
|
|
199
207
|
process.exit(1);
|
|
200
208
|
}
|
|
201
209
|
}
|