@cccarv82/freya 2.3.13 → 2.4.1

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.
Files changed (37) hide show
  1. package/.agent/rules/freya/agents/coach.mdc +7 -16
  2. package/.agent/rules/freya/agents/ingestor.mdc +1 -89
  3. package/.agent/rules/freya/agents/master.mdc +3 -0
  4. package/.agent/rules/freya/agents/oracle.mdc +7 -23
  5. package/cli/web-ui.css +860 -182
  6. package/cli/web-ui.js +547 -175
  7. package/cli/web.js +690 -536
  8. package/package.json +6 -3
  9. package/scripts/build-vector-index.js +85 -0
  10. package/scripts/export-obsidian.js +6 -16
  11. package/scripts/generate-blockers-report.js +5 -17
  12. package/scripts/generate-daily-summary.js +25 -58
  13. package/scripts/generate-executive-report.js +22 -204
  14. package/scripts/generate-sm-weekly-report.js +27 -92
  15. package/scripts/lib/DataLayer.js +92 -0
  16. package/scripts/lib/DataManager.js +198 -0
  17. package/scripts/lib/Embedder.js +59 -0
  18. package/scripts/lib/schema.js +23 -0
  19. package/scripts/migrate-v1-v2.js +184 -0
  20. package/scripts/validate-data.js +48 -51
  21. package/scripts/validate-structure.js +12 -58
  22. package/templates/base/scripts/build-vector-index.js +85 -0
  23. package/templates/base/scripts/export-obsidian.js +143 -0
  24. package/templates/base/scripts/generate-daily-summary.js +25 -58
  25. package/templates/base/scripts/generate-executive-report.js +14 -225
  26. package/templates/base/scripts/generate-sm-weekly-report.js +9 -91
  27. package/templates/base/scripts/index/build-index.js +13 -0
  28. package/templates/base/scripts/index/update-index.js +15 -0
  29. package/templates/base/scripts/lib/DataLayer.js +92 -0
  30. package/templates/base/scripts/lib/DataManager.js +198 -0
  31. package/templates/base/scripts/lib/Embedder.js +59 -0
  32. package/templates/base/scripts/lib/index-utils.js +407 -0
  33. package/templates/base/scripts/lib/schema.js +23 -0
  34. package/templates/base/scripts/lib/search-utils.js +183 -0
  35. package/templates/base/scripts/migrate-v1-v2.js +184 -0
  36. package/templates/base/scripts/validate-data.js +48 -51
  37. package/templates/base/scripts/validate-structure.js +10 -32
@@ -1,44 +1,34 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
 
4
- const { safeParseToMs } = require('./lib/date-utils');
5
- const { safeReadJson, quarantineCorruptedFile } = require('./lib/fs-utils');
4
+ const DataManager = require('./lib/DataManager');
6
5
 
7
- const TASK_LOG_PATH = path.join(__dirname, '../data/tasks/task-log.json');
8
- const BLOCKERS_LOG_PATH = path.join(__dirname, '../data/blockers/blocker-log.json');
6
+ const DATA_DIR = path.join(__dirname, '../data');
7
+ const LOGS_DIR = path.join(__dirname, '../logs/daily');
9
8
  const REPORT_DIR = path.join(__dirname, '../docs/reports');
10
9
 
11
- // --- Helper Logic ---
12
- const now = new Date();
13
- const oneDay = 24 * 60 * 60 * 1000;
14
-
15
- function isRecentlyCompleted(dateStr) {
16
- const ms = safeParseToMs(dateStr);
17
- if (!Number.isFinite(ms)) return false;
18
- const diff = now.getTime() - ms;
19
- // Consider "Yesterday" as anything completed in the last 24 hours for daily sync purposes
20
- return diff >= 0 && diff <= oneDay;
21
- }
10
+ const dm = new DataManager(DATA_DIR, LOGS_DIR);
22
11
 
23
12
  function generateDailySummary() {
24
13
  try {
25
- if (!fs.existsSync(TASK_LOG_PATH)) {
26
- console.log("**Ontem:** No task log found.\n**Hoje:** Set up task log.\n**Bloqueios:** None");
27
- return;
28
- }
14
+ const now = new Date();
15
+ const start = new Date(now);
16
+ start.setHours(0, 0, 0, 0); // Today 00:00
29
17
 
30
- const content = fs.readFileSync(TASK_LOG_PATH, 'utf8');
31
- const json = JSON.parse(content);
32
-
33
- if (!json.tasks) {
34
- console.log("**Ontem:** Invalid task log.\n**Hoje:** Fix task log.\n**Bloqueios:** None");
35
- return;
36
- }
18
+ // Custom logic for "ontem" (completed in the last 24h)
19
+ const ms24h = now.getTime() - (24 * 60 * 60 * 1000);
20
+ const { tasks, pending } = dm.getTasks(new Date(ms24h), now); // Not using the strict start boundary for recently completed
37
21
 
38
22
  let summary = "";
39
23
 
40
24
  // 1. Ontem (Completed < 24h)
41
- const completedRecently = json.tasks.filter(t => t.status === "COMPLETED" && isRecentlyCompleted(t.completedAt));
25
+ const completedRecently = tasks.filter(t => {
26
+ if (t.status !== 'COMPLETED') return false;
27
+ const ms = dm.getCreatedAt(t) || Date.parse(t.completedAt || t.completed_at || ''); // Use native since no safeParse helper here
28
+ if (!Number.isFinite(ms)) return false;
29
+ return ms >= ms24h && ms <= now.getTime();
30
+ });
31
+
42
32
  summary += "**Ontem:** ";
43
33
  if (completedRecently.length > 0) {
44
34
  summary += completedRecently.map(t => t.description).join(", ");
@@ -48,52 +38,29 @@ function generateDailySummary() {
48
38
  summary += "\n";
49
39
 
50
40
  // 2. Hoje (DO_NOW + PENDING)
51
- const doNow = json.tasks.filter(t => t.status === "PENDING" && t.category === "DO_NOW");
52
41
  summary += "**Hoje:** ";
53
- if (doNow.length > 0) {
54
- summary += doNow.map(t => t.description).join(", ");
42
+ if (pending.length > 0) {
43
+ summary += pending.map(t => t.description).join(", ");
55
44
  } else {
56
45
  summary += "Nothing planned";
57
46
  }
58
47
  summary += "\n";
59
48
 
60
- // 3. Bloqueios (from blocker-log.json)
49
+ // 3. Bloqueios
50
+ const { open } = dm.getBlockers(start, now);
61
51
  let blockersLine = "None";
62
- if (fs.existsSync(BLOCKERS_LOG_PATH)) {
63
- const res = safeReadJson(BLOCKERS_LOG_PATH);
64
- if (!res.ok) {
65
- if (res.error.type === 'parse') {
66
- quarantineCorruptedFile(BLOCKERS_LOG_PATH, res.error.message);
67
- }
68
- } else {
69
- const blockers = (res.json.blockers || []).filter(b => {
70
- const st = (b.status || '').toUpperCase();
71
- return st === 'OPEN' || st === 'MITIGATING';
72
- });
73
- const sevOrder = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
74
- blockers.sort((a, b) => {
75
- const sa = sevOrder[(a.severity || '').toUpperCase()] ?? 99;
76
- const sb = sevOrder[(b.severity || '').toUpperCase()] ?? 99;
77
- if (sa !== sb) return sa - sb;
78
- const ta = safeParseToMs(a.createdAt) || 0;
79
- const tb = safeParseToMs(b.createdAt) || 0;
80
- return ta - tb; // older first
81
- });
82
- if (blockers.length > 0) {
83
- blockersLine = blockers.slice(0, 3).map(b => b.title || b.description || b.id).join(", ");
84
- }
85
- }
52
+ if (open.length > 0) {
53
+ blockersLine = open.slice(0, 3).map(b => b.title || b.description || b.id).join(", ");
86
54
  }
87
55
 
88
56
  summary += `**Bloqueios:** ${blockersLine}`;
89
57
 
90
58
  console.log(summary);
91
59
 
92
- // Write report file for UI (optional, but helps preview/history)
93
60
  try {
94
61
  fs.mkdirSync(REPORT_DIR, { recursive: true });
95
- const date = new Date().toISOString().slice(0, 10);
96
- const time = new Date().toTimeString().slice(0, 8).replace(/:/g, '');
62
+ const date = now.toISOString().slice(0, 10);
63
+ const time = now.toTimeString().slice(0, 8).replace(/:/g, '');
97
64
  const outPath = path.join(REPORT_DIR, `daily-${date}-${time}.md`);
98
65
  fs.writeFileSync(outPath, `# Daily Summary — ${date}\n\n${summary}\n`, 'utf8');
99
66
  } catch (e) {
@@ -9,23 +9,15 @@
9
9
  const fs = require('fs');
10
10
  const path = require('path');
11
11
 
12
- const { toIsoDate, isWithinRange, safeParseToMs } = require('./lib/date-utils');
13
- const { safeReadJson, quarantineCorruptedFile } = require('./lib/fs-utils');
12
+ const { toIsoDate, safeParseToMs } = require('./lib/date-utils');
13
+ const DataManager = require('./lib/DataManager');
14
14
 
15
15
  // --- Configuration ---
16
16
  const DATA_DIR = path.join(__dirname, '../data');
17
17
  const LOGS_DIR = path.join(__dirname, '../logs/daily');
18
18
  const OUTPUT_DIR = path.join(__dirname, '../docs/reports');
19
- const TASKS_FILE = path.join(DATA_DIR, 'tasks/task-log.json');
20
- const BLOCKERS_FILE = path.join(DATA_DIR, 'blockers/blocker-log.json');
21
19
 
22
- const RESOLVED_STATUSES = new Set(['RESOLVED', 'CLOSED', 'DONE', 'FIXED']);
23
- const SEVERITY_ORDER = {
24
- CRITICAL: 0,
25
- HIGH: 1,
26
- MEDIUM: 2,
27
- LOW: 3
28
- };
20
+ const dm = new DataManager(DATA_DIR, LOGS_DIR);
29
21
 
30
22
  // --- Helpers ---
31
23
  function getDateRange(period) {
@@ -53,114 +45,6 @@ function ensureDir(dir) {
53
45
  }
54
46
  }
55
47
 
56
- // --- Data Fetching ---
57
-
58
- function getTasks(start, end) {
59
- if (!fs.existsSync(TASKS_FILE)) return { completed: [], pending: [], blockers: [] };
60
-
61
- const result = safeReadJson(TASKS_FILE);
62
- if (!result.ok) {
63
- const relativePath = path.relative(DATA_DIR, TASKS_FILE);
64
- if (result.error.type === 'parse') {
65
- quarantineCorruptedFile(TASKS_FILE, result.error.message);
66
- console.warn(`⚠️ [${relativePath}] JSON parse failed; quarantined to _corrupted.`);
67
- } else {
68
- console.error(`Error reading tasks: ${result.error.message}`);
69
- }
70
- return { completed: [], pending: [] };
71
- }
72
-
73
- const tasks = result.json.tasks || [];
74
-
75
- const completed = tasks.filter(t => {
76
- if (t.status !== 'COMPLETED' || !t.completedAt) return false;
77
- return isWithinRange(t.completedAt, start, end);
78
- });
79
-
80
- const pending = tasks.filter(t => t.status === 'PENDING' && t.category === 'DO_NOW');
81
-
82
- // "Blockers" could be tasks tagged as such or implicit in status?
83
- // For now, we don't have explicit blocker task type, but we can look for high priority or specific keywords if we wanted.
84
- // Let's stick to simple PENDING DO_NOW for Next Steps.
85
-
86
- return { completed, pending };
87
- }
88
-
89
- function getProjectUpdates(start, end) {
90
- const clientsDir = path.join(DATA_DIR, 'Clients');
91
- if (!fs.existsSync(clientsDir)) return [];
92
-
93
- const updates = [];
94
-
95
- function scan(dir) {
96
- const files = fs.readdirSync(dir);
97
- for (const file of files) {
98
- const fullPath = path.join(dir, file);
99
- const stat = fs.statSync(fullPath);
100
-
101
- if (stat.isDirectory()) {
102
- if (file === '_corrupted') {
103
- continue;
104
- }
105
- scan(fullPath);
106
- } else if (file === 'status.json') {
107
- const result = safeReadJson(fullPath);
108
- if (!result.ok) {
109
- const relativePath = path.relative(DATA_DIR, fullPath);
110
- if (result.error.type === 'parse') {
111
- quarantineCorruptedFile(fullPath, result.error.message);
112
- console.warn(`⚠️ [${relativePath}] JSON parse failed; quarantined to _corrupted.`);
113
- } else {
114
- console.error(`Error reading ${relativePath}: ${result.error.message}`);
115
- }
116
- continue;
117
- }
118
-
119
- const project = result.json;
120
-
121
- // Filter history
122
- const recentEvents = (project.history || []).filter(h => {
123
- return isWithinRange(h.date || h.timestamp, start, end); // Support both formats if they vary
124
- });
125
-
126
- if (recentEvents.length > 0 || project.currentStatus) {
127
- updates.push({
128
- name: project.project || path.basename(path.dirname(fullPath)),
129
- client: project.client || path.basename(path.dirname(path.dirname(fullPath))),
130
- status: project.currentStatus,
131
- events: recentEvents
132
- });
133
- }
134
- }
135
- }
136
- }
137
-
138
- scan(clientsDir);
139
- return updates;
140
- }
141
-
142
- function getDailyLogs(start, end) {
143
- if (!fs.existsSync(LOGS_DIR)) return [];
144
-
145
- const relevantLogs = [];
146
- const files = fs.readdirSync(LOGS_DIR);
147
-
148
- for (const file of files) {
149
- if (!file.endsWith('.md')) continue;
150
- const dateStr = file.replace('.md', '');
151
- // The filenames are YYYY-MM-DD. Compare as ISO dates to avoid timezone parsing drift.
152
- const startIso = formatDate(start);
153
- const endIso = formatDate(end);
154
- if (dateStr >= startIso && dateStr <= endIso) {
155
- try {
156
- const content = fs.readFileSync(path.join(LOGS_DIR, file), 'utf8');
157
- relevantLogs.push({ date: dateStr, content });
158
- } catch (e) {}
159
- }
160
- }
161
- return relevantLogs;
162
- }
163
-
164
48
  function summarizeLogContent(content, maxLines = 3, maxChars = 280) {
165
49
  if (!content) return null;
166
50
  const lines = content
@@ -184,23 +68,6 @@ function summarizeLogContent(content, maxLines = 3, maxChars = 280) {
184
68
  return summary;
185
69
  }
186
70
 
187
- function normalizeStatus(blocker) {
188
- const raw = blocker.status || blocker.state || blocker.currentStatus;
189
- if (!raw) return 'UNKNOWN';
190
- return String(raw).trim().toUpperCase();
191
- }
192
-
193
- function normalizeSeverity(blocker) {
194
- const raw = blocker.severity || blocker.priority || blocker.level;
195
- if (!raw) return 'UNSPECIFIED';
196
- const value = String(raw).trim().toUpperCase();
197
- if (value.includes('CRIT')) return 'CRITICAL';
198
- if (value.includes('HIGH')) return 'HIGH';
199
- if (value.includes('MED')) return 'MEDIUM';
200
- if (value.includes('LOW')) return 'LOW';
201
- return value;
202
- }
203
-
204
71
  function getBlockerTitle(blocker) {
205
72
  return (
206
73
  blocker.title ||
@@ -212,96 +79,18 @@ function getBlockerTitle(blocker) {
212
79
  );
213
80
  }
214
81
 
215
- function getCreatedAt(blocker) {
216
- const candidates = [
217
- blocker.createdAt,
218
- blocker.created_at,
219
- blocker.openedAt,
220
- blocker.opened_at,
221
- blocker.reportedAt,
222
- blocker.reported_at,
223
- blocker.date,
224
- blocker.loggedAt
225
- ];
226
- for (const value of candidates) {
227
- const ms = safeParseToMs(value);
228
- if (Number.isFinite(ms)) return ms;
229
- }
230
- return NaN;
231
- }
232
-
233
- function getResolvedAt(blocker) {
234
- const candidates = [
235
- blocker.resolvedAt,
236
- blocker.resolved_at,
237
- blocker.closedAt,
238
- blocker.closed_at,
239
- blocker.completedAt
240
- ];
241
- for (const value of candidates) {
242
- const ms = safeParseToMs(value);
243
- if (Number.isFinite(ms)) return ms;
244
- }
245
- return NaN;
246
- }
247
-
248
- function isOpen(blocker) {
249
- const status = normalizeStatus(blocker);
250
- if (RESOLVED_STATUSES.has(status)) return false;
251
- const resolvedAt = getResolvedAt(blocker);
252
- return !Number.isFinite(resolvedAt);
253
- }
254
-
255
- function loadBlockers() {
256
- if (!fs.existsSync(BLOCKERS_FILE)) return [];
257
- const result = safeReadJson(BLOCKERS_FILE);
258
- if (!result.ok) {
259
- const relativePath = path.relative(DATA_DIR, BLOCKERS_FILE);
260
- if (result.error.type === 'parse') {
261
- quarantineCorruptedFile(BLOCKERS_FILE, result.error.message);
262
- console.warn(`⚠️ [${relativePath}] JSON parse failed; quarantined to _corrupted.`);
263
- } else {
264
- console.error(`Error reading blockers: ${result.error.message}`);
265
- }
266
- return [];
267
- }
268
- return Array.isArray(result.json.blockers) ? result.json.blockers : [];
269
- }
270
-
271
- function getBlockers(start, end) {
272
- const blockers = loadBlockers();
273
- const open = blockers.filter(isOpen);
274
- open.sort((a, b) => {
275
- const severityA = normalizeSeverity(a);
276
- const severityB = normalizeSeverity(b);
277
- const rankA = SEVERITY_ORDER[severityA] ?? 99;
278
- const rankB = SEVERITY_ORDER[severityB] ?? 99;
279
- if (rankA !== rankB) return rankA - rankB;
280
- const createdA = getCreatedAt(a);
281
- const createdB = getCreatedAt(b);
282
- const msA = Number.isFinite(createdA) ? createdA : Number.MAX_SAFE_INTEGER;
283
- const msB = Number.isFinite(createdB) ? createdB : Number.MAX_SAFE_INTEGER;
284
- return msA - msB;
285
- });
286
-
287
- const openedRecent = blockers.filter(blocker => isWithinRange(getCreatedAt(blocker), start, end));
288
- const resolvedRecent = blockers.filter(blocker => isWithinRange(getResolvedAt(blocker), start, end));
289
-
290
- return { open, openedRecent, resolvedRecent };
291
- }
292
-
293
82
  // --- Report Generation ---
294
83
 
295
84
  function generateReport(period) {
296
85
  const { start, end } = getDateRange(period);
297
86
  const dateStr = formatDate(new Date());
298
-
87
+
299
88
  console.log(`Generating ${period} report for ${formatDate(start)} to ${formatDate(end)}...`);
300
89
 
301
- const tasks = getTasks(start, end);
302
- const projects = getProjectUpdates(start, end);
303
- const logs = getDailyLogs(start, end);
304
- const blockers = getBlockers(start, end);
90
+ const tasks = dm.getTasks(start, end);
91
+ const projects = dm.getProjectUpdates(start, end);
92
+ const logs = dm.getDailyLogs(start, end);
93
+ const blockers = dm.getBlockers(start, end);
305
94
 
306
95
  let md = `# Relatório de Status Profissional - ${dateStr}\n`;
307
96
  md += `**Período:** ${formatDate(start)} a ${formatDate(end)}\n\n`;
@@ -346,9 +135,9 @@ function generateReport(period) {
346
135
  md += `*Sem atualizações de projeto recentes.*\n`;
347
136
  } else {
348
137
  projects.forEach(p => {
349
- md += `### ${p.client} / ${p.name}\n`;
350
- md += `**Status Atual:** ${p.status}\n`;
351
- if (p.events.length > 0) {
138
+ md += `### ${p.client} / ${p.project}\n`;
139
+ md += `**Status Atual:** ${p.currentStatus}\n`;
140
+ if (p.events && p.events.length > 0) {
352
141
  md += `**Atualizações Recentes:**\n`;
353
142
  p.events.forEach(e => {
354
143
  const typeIcon = e.type === 'Blocker' ? '🔴' : '🔹';
@@ -367,8 +156,8 @@ function generateReport(period) {
367
156
  md += `**Em aberto:**\n`;
368
157
  blockers.open.forEach(blocker => {
369
158
  const title = getBlockerTitle(blocker);
370
- const severity = normalizeSeverity(blocker);
371
- const createdAt = getCreatedAt(blocker);
159
+ const severity = dm.normalizeSeverity(blocker);
160
+ const createdAt = dm.getCreatedAt(blocker);
372
161
  const createdDate = Number.isFinite(createdAt) ? toIsoDate(createdAt) : 'Unknown';
373
162
  const project = blocker.project || blocker.projectName || blocker.projectSlug;
374
163
  const client = blocker.client || blocker.clientName || blocker.clientSlug;
@@ -386,7 +175,7 @@ function generateReport(period) {
386
175
  md += `\n**Resolvidos no período:**\n`;
387
176
  blockers.resolvedRecent.forEach(blocker => {
388
177
  const title = getBlockerTitle(blocker);
389
- const resolvedAt = getResolvedAt(blocker);
178
+ const resolvedAt = dm.getResolvedAt(blocker);
390
179
  const resolvedDate = Number.isFinite(resolvedAt) ? toIsoDate(resolvedAt) : 'Unknown';
391
180
  md += `- ${title} (Resolvido: ${resolvedDate})\n`;
392
181
  });
@@ -1,63 +1,24 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
 
4
- const { toIsoDate, safeParseToMs, isWithinRange } = require('./lib/date-utils');
5
- const { safeReadJson, quarantineCorruptedFile } = require('./lib/fs-utils');
4
+ const { toIsoDate, safeParseToMs } = require('./lib/date-utils');
5
+ const DataManager = require('./lib/DataManager');
6
6
 
7
7
  const DATA_DIR = path.join(__dirname, '../data');
8
+ const LOGS_DIR = path.join(__dirname, '../logs/daily');
8
9
  const REPORT_DIR = path.join(__dirname, '../docs/reports');
9
10
 
10
- const TASKS_FILE = path.join(DATA_DIR, 'tasks/task-log.json');
11
- const BLOCKERS_FILE = path.join(DATA_DIR, 'blockers/blocker-log.json');
12
- const CLIENTS_DIR = path.join(DATA_DIR, 'Clients');
13
-
14
- const SEV_ORDER = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
11
+ const dm = new DataManager(DATA_DIR, LOGS_DIR);
15
12
 
16
13
  function ensureDir(dir) {
17
14
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
18
15
  }
19
16
 
20
- function readJsonOrQuarantine(filePath, fallback) {
21
- if (!fs.existsSync(filePath)) return fallback;
22
- const res = safeReadJson(filePath);
23
- if (!res.ok) {
24
- if (res.error.type === 'parse') {
25
- quarantineCorruptedFile(filePath, res.error.message);
26
- console.warn(`⚠️ JSON parse failed; quarantined: ${filePath}`);
27
- } else {
28
- console.warn(`⚠️ JSON read failed: ${filePath}: ${res.error.message}`);
29
- }
30
- return fallback;
31
- }
32
- return res.json;
33
- }
34
-
35
17
  function daysBetweenMs(aMs, bMs) {
36
18
  const diff = Math.max(0, bMs - aMs);
37
19
  return Math.floor(diff / (24 * 60 * 60 * 1000));
38
20
  }
39
21
 
40
- function scanProjectStatusFiles() {
41
- const results = [];
42
- if (!fs.existsSync(CLIENTS_DIR)) return results;
43
-
44
- const clients = fs.readdirSync(CLIENTS_DIR);
45
- for (const clientSlug of clients) {
46
- const clientPath = path.join(CLIENTS_DIR, clientSlug);
47
- if (!fs.statSync(clientPath).isDirectory()) continue;
48
-
49
- const projects = fs.readdirSync(clientPath);
50
- for (const projectSlug of projects) {
51
- const projectPath = path.join(clientPath, projectSlug);
52
- if (!fs.statSync(projectPath).isDirectory()) continue;
53
-
54
- const statusPath = path.join(projectPath, 'status.json');
55
- if (fs.existsSync(statusPath)) results.push(statusPath);
56
- }
57
- }
58
- return results;
59
- }
60
-
61
22
  function generate() {
62
23
  ensureDir(REPORT_DIR);
63
24
 
@@ -67,40 +28,15 @@ function generate() {
67
28
  start.setDate(now.getDate() - 7);
68
29
 
69
30
  const reportDate = toIsoDate(now);
70
- const reportTime = (() => { const d = new Date(); const hh = String(d.getHours()).padStart(2,'0'); const mm = String(d.getMinutes()).padStart(2,'0'); const ss = String(d.getSeconds()).padStart(2,'0'); return `${hh}${mm}${ss}`; })();
71
-
72
- const taskLog = readJsonOrQuarantine(TASKS_FILE, { schemaVersion: 1, tasks: [] });
73
- const blockersLog = readJsonOrQuarantine(BLOCKERS_FILE, { schemaVersion: 1, blockers: [] });
31
+ const reportTime = (() => { const d = new Date(); const hh = String(d.getHours()).padStart(2, '0'); const mm = String(d.getMinutes()).padStart(2, '0'); const ss = String(d.getSeconds()).padStart(2, '0'); return `${hh}${mm}${ss}`; })();
74
32
 
75
- const tasks = taskLog.tasks || [];
76
- const blockers = blockersLog.blockers || [];
77
-
78
- const completedTasks = tasks.filter(t => {
79
- if (t.status !== 'COMPLETED') return false;
80
- return isWithinRange(t.completedAt || t.completed_at, start, end);
81
- });
82
-
83
- const pendingDoNow = tasks.filter(t => t.status === 'PENDING' && t.category === 'DO_NOW');
84
-
85
- const activeBlockers = blockers.filter(b => {
86
- const st = String(b.status || '').toUpperCase();
87
- return st === 'OPEN' || st === 'MITIGATING';
88
- });
89
-
90
- activeBlockers.sort((a, b) => {
91
- const sa = SEV_ORDER[String(a.severity || '').toUpperCase()] ?? 99;
92
- const sb = SEV_ORDER[String(b.severity || '').toUpperCase()] ?? 99;
93
- if (sa !== sb) return sa - sb;
94
- const ta = safeParseToMs(a.createdAt) || 0;
95
- const tb = safeParseToMs(b.createdAt) || 0;
96
- return ta - tb; // older first
97
- });
33
+ const { completed: completedTasks, pending: pendingDoNow } = dm.getTasks(start, end);
34
+ const { open: activeBlockers, blockers } = dm.getBlockers(start, end);
98
35
 
99
36
  const upcomingDueBlockers = blockers.filter(b => {
100
- const st = String(b.status || '').toUpperCase();
37
+ const st = dm.normalizeStatus(b);
101
38
  if (st !== 'OPEN') return false;
102
39
  if (!b.dueDate) return false;
103
- // dueDate is an ISO date; treat as UTC midnight
104
40
  const dueMs = safeParseToMs(String(b.dueDate));
105
41
  if (!Number.isFinite(dueMs)) return false;
106
42
  const due = new Date(dueMs);
@@ -109,25 +45,7 @@ const reportTime = (() => { const d = new Date(); const hh = String(d.getHours()
109
45
  return due >= now && due <= soonEnd;
110
46
  });
111
47
 
112
- // Projects
113
- const statusFiles = scanProjectStatusFiles();
114
- const projectsWithUpdates = [];
115
-
116
- for (const statusPath of statusFiles) {
117
- const proj = readJsonOrQuarantine(statusPath, null);
118
- if (!proj) continue;
119
-
120
- const history = Array.isArray(proj.history) ? proj.history : [];
121
- const recent = history.filter(h => isWithinRange(h.date || h.timestamp, start, end));
122
- if (recent.length === 0) continue;
123
-
124
- projectsWithUpdates.push({
125
- client: proj.client || path.basename(path.dirname(path.dirname(statusPath))),
126
- project: proj.project || path.basename(path.dirname(statusPath)),
127
- currentStatus: proj.currentStatus,
128
- recent
129
- });
130
- }
48
+ const projectsWithUpdates = dm.getProjectUpdates(start, end);
131
49
 
132
50
  // Markdown
133
51
  let md = `# Scrum Master Weekly Report — ${reportDate}\n`;
@@ -0,0 +1,13 @@
1
+ const path = require('path');
2
+ const { buildIndex } = require('../lib/index-utils');
3
+
4
+ function main() {
5
+ const workspaceDir = process.cwd();
6
+ const result = buildIndex(workspaceDir);
7
+ const rel = path.relative(workspaceDir, result.indexPath).replace(/\\/g, '/');
8
+ console.log(`Index rebuilt: ${rel}`);
9
+ console.log(`Files: ${result.fileCount}`);
10
+ console.log(`Keys: ${result.keyCount}`);
11
+ }
12
+
13
+ main();
@@ -0,0 +1,15 @@
1
+ const path = require('path');
2
+ const { updateIndex } = require('../lib/index-utils');
3
+
4
+ function main() {
5
+ const workspaceDir = process.cwd();
6
+ const result = updateIndex(workspaceDir);
7
+ const rel = path.relative(workspaceDir, result.indexPath).replace(/\\/g, '/');
8
+ console.log(`Index updated: ${rel}`);
9
+ console.log(`Files: ${result.fileCount}`);
10
+ console.log(`Keys: ${result.keyCount}`);
11
+ if (typeof result.changed === 'number') console.log(`Changed: ${result.changed}`);
12
+ if (typeof result.removed === 'number') console.log(`Removed: ${result.removed}`);
13
+ }
14
+
15
+ main();
@@ -0,0 +1,92 @@
1
+ const Database = require('better-sqlite3');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+
5
+ class DataLayer {
6
+ constructor(dbPath = null) {
7
+ if (!dbPath) {
8
+ const dataDir = path.join(__dirname, '..', '..', 'data');
9
+ if (!fs.existsSync(dataDir)) {
10
+ fs.mkdirSync(dataDir, { recursive: true });
11
+ }
12
+ dbPath = path.join(dataDir, 'freya.sqlite');
13
+ }
14
+ this.db = new Database(dbPath, { verbose: console.log });
15
+ this.initSchema();
16
+ }
17
+
18
+ initSchema() {
19
+ // Enable Write-Ahead Logging for better concurrent performance
20
+ this.db.pragma('journal_mode = WAL');
21
+
22
+ this.db.exec(`
23
+ CREATE TABLE IF NOT EXISTS projects (
24
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
25
+ slug TEXT UNIQUE NOT NULL,
26
+ client TEXT,
27
+ name TEXT,
28
+ is_active BOOLEAN DEFAULT 1,
29
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
30
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
31
+ );
32
+
33
+ CREATE TABLE IF NOT EXISTS project_status_history (
34
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
35
+ project_id INTEGER NOT NULL,
36
+ status_text TEXT NOT NULL,
37
+ date DATETIME DEFAULT CURRENT_TIMESTAMP,
38
+ FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE
39
+ );
40
+
41
+ CREATE TABLE IF NOT EXISTS tasks (
42
+ id TEXT PRIMARY KEY, /* UUID */
43
+ project_slug TEXT, /* Can be null, or match projects.slug */
44
+ description TEXT NOT NULL,
45
+ category TEXT NOT NULL, /* DO_NOW, SCHEDULE, DELEGATE, IGNORE */
46
+ status TEXT NOT NULL, /* PENDING, COMPLETED, ARCHIVED */
47
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
48
+ completed_at DATETIME,
49
+ metadata TEXT /* JSON string for extra fields like priority, streamSlug */
50
+ );
51
+
52
+ CREATE TABLE IF NOT EXISTS blockers (
53
+ id TEXT PRIMARY KEY, /* UUID */
54
+ project_slug TEXT,
55
+ title TEXT NOT NULL,
56
+ severity TEXT NOT NULL, /* CRITICAL, HIGH, MEDIUM, LOW */
57
+ status TEXT NOT NULL, /* OPEN, MITIGATING, RESOLVED, ARCHIVED */
58
+ owner TEXT,
59
+ next_action TEXT,
60
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
61
+ resolved_at DATETIME,
62
+ metadata TEXT /* JSON string */
63
+ );
64
+
65
+ CREATE TABLE IF NOT EXISTS daily_logs (
66
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
67
+ date TEXT UNIQUE NOT NULL, /* YYYY-MM-DD */
68
+ raw_markdown TEXT NOT NULL,
69
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
70
+ );
71
+
72
+ CREATE TABLE IF NOT EXISTS document_embeddings (
73
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
74
+ reference_type TEXT NOT NULL, /* 'daily_log', 'task', 'blocker' */
75
+ reference_id TEXT NOT NULL,
76
+ chunk_index INTEGER DEFAULT 0,
77
+ text_chunk TEXT NOT NULL,
78
+ embedding BLOB NOT NULL, /* Stored as Buffer of Float32Array */
79
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
80
+ );
81
+ `);
82
+ }
83
+
84
+ // Helper close method
85
+ close() {
86
+ this.db.close();
87
+ }
88
+ }
89
+
90
+ // Export a singleton instance by default, or the class for testing
91
+ const defaultInstance = new DataLayer();
92
+ module.exports = { defaultInstance, DataLayer };