@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.
@@ -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, isWithinRange } = require('./lib/date-utils');
12
+ const { toIsoDate, safeParseToMs } = require('./lib/date-utils');
13
+ const DataManager = require('./lib/DataManager');
14
+ const { ready } = require('./lib/DataLayer');
5
15
 
6
- const DATA_DIR = path.join(__dirname, '../data');
7
- const REPORT_DIR = path.join(__dirname, '../docs/reports');
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 repo root
8
20
 
9
- // Ensure output dir exists
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 sevenDaysAgo = new Date(now.getTime() - (7 * oneDay));
20
- return isWithinRange(dateStr, sevenDaysAgo, now);
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
- // --- File Walking ---
35
- function walk(dir, fileList = []) {
36
- const files = fs.readdirSync(dir);
37
- files.forEach(file => {
38
- const filePath = path.join(dir, file);
39
- const stat = fs.statSync(filePath);
40
- if (stat.isDirectory()) {
41
- walk(filePath, fileList);
42
- } else {
43
- if (path.extname(file) === '.json') {
44
- fileList.push(filePath);
45
- }
46
- }
47
- });
48
- return fileList;
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 += "## 🚀 Project Updates\n";
84
- let hasProjectUpdates = false;
85
- projects.forEach(p => {
86
- if (p.history && Array.isArray(p.history)) {
87
- const recentUpdates = p.history.filter(h => isWithinWeek(h.date));
88
- if (recentUpdates.length > 0) {
89
- hasProjectUpdates = true;
90
- report += `### ${p.client} - ${p.project}\n`;
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
- } else {
110
- report += "No tasks completed this week.\n";
111
- }
79
+ report += '\n';
80
+ });
112
81
  } else {
113
- report += "No task log found.\n";
82
+ report += 'No project updates recorded this week.\n\n';
114
83
  }
115
- report += "\n";
116
-
117
- // Career
118
- report += "## 🌟 Career Highlights\n";
119
- if (careerLog.entries && Array.isArray(careerLog.entries)) {
120
- const recentCareer = careerLog.entries.filter(e => isWithinWeek(e.date));
121
- if (recentCareer.length > 0) {
122
- recentCareer.forEach(e => {
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 += "No career log found.\n";
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(`✅ Report generated at: ${outputPath}`);
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); });
@@ -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
- this._wrapper._save();
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._saveScheduled = false;
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._save();
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
- const dataDir = path.join(__dirname, '..', '..', 'data');
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
- // WAL mode may not work in WASM — we try, but it's silently ignored if unsupported
210
- this.db.pragma('journal_mode = WAL');
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 (
@@ -290,4 +347,35 @@ const ready = defaultInstance.init().catch(err => {
290
347
  process.exit(1);
291
348
  });
292
349
 
293
- module.exports = { defaultInstance, DataLayer, ready };
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
- const meta = t.metadata ? JSON.parse(t.metadata) : {};
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,
@@ -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
- const meta = b.metadata ? JSON.parse(b.metadata) : {};
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,
@@ -160,9 +172,17 @@ class DataManager {
160
172
 
161
173
  // --- Daily Logs ---
162
174
  getDailyLogs(start, end) {
163
- function toIso(d) { return (d instanceof Date ? d : new Date(d)).toISOString().slice(0, 10); }
164
- const startIso = formatDate ? formatDate(start) : toIso(start);
165
- const endIso = formatDate ? formatDate(end) : toIso(end);
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,6 +191,43 @@ 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);
@@ -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
- const TOKEN_RE = /[A-Za-z0-9_-]{3,}/g;
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
- : path.join(__dirname, '../data');
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
- const DATA_DIR = path.join(__dirname, '../data');
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 repo root
11
+
12
+ const DATA_DIR = path.join(WORKSPACE_DIR, 'data');
8
13
 
9
14
  // --- Validation Helpers ---
10
15
 
@@ -173,8 +178,11 @@ 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('status.json') || file.endsWith('blocker-log.json')) {
177
- // Obsoleted by SQLite, ignore
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
188
  // console.warn(`⚠️ [${relativePath}] Unknown JSON file type. Skipping schema validation.`);
@@ -5,7 +5,12 @@ const { toIsoDate, safeParseToMs, isWithinRange } = require('./lib/date-utils');
5
5
 
6
6
  const DataManager = require('./lib/DataManager');
7
7
  const { ready } = require('./lib/DataLayer');
8
- const REPORT_DIR = path.join(__dirname, '../docs/reports');
8
+ // BUG-30: use FREYA_WORKSPACE_DIR instead of __dirname
9
+ const WORKSPACE_DIR = process.env.FREYA_WORKSPACE_DIR
10
+ ? path.resolve(process.env.FREYA_WORKSPACE_DIR)
11
+ : path.join(__dirname, '..'); // fallback: scripts/ is one level below workspace root
12
+
13
+ const REPORT_DIR = path.join(WORKSPACE_DIR, 'docs', 'reports');
9
14
 
10
15
  const SEVERITY_ORDER = {
11
16
  CRITICAL: 0,
@@ -4,9 +4,14 @@ const path = require('path');
4
4
  const DataManager = require('./lib/DataManager');
5
5
  const { ready } = require('./lib/DataLayer');
6
6
 
7
- const DATA_DIR = path.join(__dirname, '../data');
8
- const LOGS_DIR = path.join(__dirname, '../logs/daily');
9
- const REPORT_DIR = path.join(__dirname, '../docs/reports');
7
+ // BUG-30: use FREYA_WORKSPACE_DIR instead of __dirname
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');
13
+ const LOGS_DIR = path.join(WORKSPACE_DIR, 'logs', 'daily');
14
+ const REPORT_DIR = path.join(WORKSPACE_DIR, 'docs', 'reports');
10
15
 
11
16
  const dm = new DataManager(DATA_DIR, LOGS_DIR);
12
17
 
@@ -24,9 +29,10 @@ async function generateDailySummary() {
24
29
  let summary = "";
25
30
 
26
31
  // 1. Ontem (Completed < 24h)
32
+ // BUG-09: use completedAt (not createdAt) to decide if a task appears in "yesterday"
27
33
  const completedRecently = tasks.filter(t => {
28
34
  if (t.status !== 'COMPLETED') return false;
29
- const ms = dm.getCreatedAt(t) || Date.parse(t.completedAt || t.completed_at || ''); // Use native since no safeParse helper here
35
+ const ms = dm.getResolvedAt(t);
30
36
  if (!Number.isFinite(ms)) return false;
31
37
  return ms >= ms24h && ms <= now.getTime();
32
38
  });
@@ -2,7 +2,7 @@
2
2
  * generate-executive-report.js
3
3
  * Generates a professional Markdown status report (Daily/Weekly)
4
4
  * aggregating Tasks, Project Updates, and Daily Logs.
5
- *
5
+ *
6
6
  * Usage: node scripts/generate-executive-report.js --period [daily|weekly]
7
7
  */
8
8
 
@@ -13,10 +13,14 @@ const { toIsoDate, safeParseToMs } = require('./lib/date-utils');
13
13
  const DataManager = require('./lib/DataManager');
14
14
  const { ready } = require('./lib/DataLayer');
15
15
 
16
- // --- Configuration ---
17
- const DATA_DIR = path.join(__dirname, '../data');
18
- const LOGS_DIR = path.join(__dirname, '../logs/daily');
19
- const OUTPUT_DIR = path.join(__dirname, '../docs/reports');
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
20
+
21
+ const DATA_DIR = path.join(WORKSPACE_DIR, 'data');
22
+ const LOGS_DIR = path.join(WORKSPACE_DIR, 'logs', 'daily');
23
+ const OUTPUT_DIR = path.join(WORKSPACE_DIR, 'docs', 'reports');
20
24
 
21
25
  const dm = new DataManager(DATA_DIR, LOGS_DIR);
22
26
 
@@ -5,9 +5,14 @@ const { toIsoDate, safeParseToMs } = require('./lib/date-utils');
5
5
  const DataManager = require('./lib/DataManager');
6
6
  const { ready } = require('./lib/DataLayer');
7
7
 
8
- const DATA_DIR = path.join(__dirname, '../data');
9
- const LOGS_DIR = path.join(__dirname, '../logs/daily');
10
- const REPORT_DIR = path.join(__dirname, '../docs/reports');
8
+ // BUG-30: use FREYA_WORKSPACE_DIR instead of __dirname
9
+ const WORKSPACE_DIR = process.env.FREYA_WORKSPACE_DIR
10
+ ? path.resolve(process.env.FREYA_WORKSPACE_DIR)
11
+ : path.join(__dirname, '..'); // fallback: scripts/ is one level below workspace root
12
+
13
+ const DATA_DIR = path.join(WORKSPACE_DIR, 'data');
14
+ const LOGS_DIR = path.join(WORKSPACE_DIR, 'logs', 'daily');
15
+ const REPORT_DIR = path.join(WORKSPACE_DIR, 'docs', 'reports');
11
16
 
12
17
  const dm = new DataManager(DATA_DIR, LOGS_DIR);
13
18
 
@@ -109,7 +114,8 @@ async function generate() {
109
114
  projectsWithUpdates.forEach(p => {
110
115
  md += `### ${p.client} / ${p.project}\n`;
111
116
  if (p.currentStatus) md += `**Status:** ${p.currentStatus}\n`;
112
- p.recent.forEach(e => {
117
+ const projectEvents = p.events || p.recent || [];
118
+ projectEvents.forEach(e => {
113
119
  md += `- [${e.date || e.timestamp}] ${e.content || ''}\n`;
114
120
  });
115
121
  md += `\n`;