@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.
@@ -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 workspace 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); });
@@ -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
- 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');
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
- 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,
@@ -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
- 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,
@@ -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
- 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,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
- 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 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('🔍 Starting validation...');
148
+ console.log('Starting validation...');
144
149
  try {
145
150
  if (!fs.existsSync(DATA_DIR)) {
146
- console.error('Data directory not found:', DATA_DIR);
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(`⚠️ [${relativePath}] JSON parse failed; quarantined to _corrupted.`);
166
+ console.warn(`[${relativePath}] JSON parse failed; quarantined to _corrupted.`);
162
167
  } else {
163
- console.error(`❌ [${relativePath}] Read failed: ${result.error.message}`);
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('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
- // console.warn(`⚠️ [${relativePath}] Unknown JSON file type. Skipping schema validation.`);
188
+ // console.warn(`[${relativePath}] Unknown JSON file type. Skipping schema validation.`);
181
189
  }
182
190
 
183
191
  if (fileErrors.length > 0) {
184
- console.error(`❌ [${relativePath}] Validation failed:`);
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('All systems operational');
199
+ console.log('All systems operational');
192
200
  } else {
193
- console.error(`❌ Validation completed with errors in ${errorCount} file(s).`);
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('Fatal error:', err);
206
+ console.error('Fatal error:', err);
199
207
  process.exit(1);
200
208
  }
201
209
  }