@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cccarv82/freya",
3
- "version": "2.3.13",
3
+ "version": "2.4.1",
4
4
  "description": "Personal AI Assistant with local-first persistence",
5
5
  "scripts": {
6
6
  "health": "node scripts/validate-data.js && node scripts/validate-structure.js",
@@ -13,7 +13,7 @@
13
13
  "export-obsidian": "node scripts/export-obsidian.js",
14
14
  "build-index": "node scripts/index/build-index.js",
15
15
  "update-index": "node scripts/index/update-index.js",
16
- "test": "node tests/unit/test-package-config.js && node tests/unit/test-cli-init.js && node tests/unit/test-cli-web-help.js && node tests/unit/test-web-static-assets.js && node tests/unit/test-fs-utils.js && node tests/unit/test-search-utils.js && node tests/unit/test-index-utils.js && node tests/unit/test-task-schema.js && node tests/unit/test-daily-generation.js && node tests/unit/test-report-generation.js && node tests/unit/test-executive-report-logs.js && node tests/unit/test-oracle-retrieval.js && node tests/unit/test-task-completion.js && node tests/unit/test-migrate-data.js && node tests/unit/test-blockers-validation.js && node tests/unit/test-blockers-report.js && node tests/unit/test-sm-weekly-report.js && node tests/integration/test-ingestor-task.js && node tests/unit/test-structure-validation.js"
16
+ "test": "node tests/unit/test-package-config.js && node tests/unit/test-cli-init.js && node tests/unit/test-cli-web-help.js && node tests/unit/test-web-static-assets.js && node tests/unit/test-fs-utils.js && node tests/unit/test-search-utils.js && node tests/unit/test-index-utils.js && node tests/unit/test-daily-generation.js && node tests/unit/test-report-generation.js && node tests/unit/test-executive-report-logs.js && node tests/unit/test-oracle-retrieval.js && node tests/unit/test-task-completion.js && node tests/unit/test-migrate-data.js && node tests/unit/test-blockers-report.js && node tests/unit/test-sm-weekly-report.js && node tests/integration/test-ingestor-task.js && node tests/unit/test-structure-validation.js"
17
17
  },
18
18
  "keywords": [],
19
19
  "author": "",
@@ -31,6 +31,9 @@
31
31
  ],
32
32
  "preferGlobal": true,
33
33
  "dependencies": {
34
- "pdf-lib": "^1.17.1"
34
+ "@xenova/transformers": "^2.17.2",
35
+ "better-sqlite3": "^12.6.2",
36
+ "pdf-lib": "^1.17.1",
37
+ "sqlite3": "^5.1.7"
35
38
  }
36
39
  }
@@ -0,0 +1,85 @@
1
+ const { defaultInstance: dl } = require('./lib/DataLayer');
2
+ const { defaultEmbedder } = require('./lib/Embedder');
3
+
4
+ /**
5
+ * Splits markdown text into overlapping chunks of approx maximum length
6
+ */
7
+ function chunkText(text, maxChars = 800, overlap = 150) {
8
+ if (!text) return [];
9
+ const chunks = [];
10
+ let i = 0;
11
+ while (i < text.length) {
12
+ let end = i + maxChars;
13
+ if (end < text.length) {
14
+ // Find a newline or space to break at cleanly
15
+ const nextNewline = text.lastIndexOf('\n', end);
16
+ if (nextNewline > i + overlap) {
17
+ end = nextNewline;
18
+ } else {
19
+ const nextSpace = text.lastIndexOf(' ', end);
20
+ if (nextSpace > i + overlap) end = nextSpace;
21
+ }
22
+ }
23
+ chunks.push(text.slice(i, end).trim());
24
+ i = end - overlap;
25
+ }
26
+ return chunks;
27
+ }
28
+
29
+ async function buildVectorIndex() {
30
+ console.log('[RAG] Booting Embedding Engine...');
31
+ await defaultEmbedder.init();
32
+ console.log('[RAG] Model ready.');
33
+
34
+ // Find daily logs that haven't been indexed completely
35
+ const allLogs = dl.db.prepare('SELECT * FROM daily_logs').all();
36
+ let updatedCount = 0;
37
+
38
+ for (const log of allLogs) {
39
+ // Assume log is processed if we have *any* embedding for it.
40
+ // For total correctness, we would compare hash of raw_markdown,
41
+ // but skipping if exists is enough for initialization.
42
+ const existing = dl.db.prepare(
43
+ "SELECT count(*) as count FROM document_embeddings WHERE reference_type = 'daily_log' AND reference_id = ?"
44
+ ).get(log.date);
45
+
46
+ if (existing && existing.count > 0) {
47
+ continue;
48
+ }
49
+
50
+ console.log(`[RAG] Generating embeddings for Daily Log: ${log.date}`);
51
+ const chunks = chunkText(`Daily Log Date: ${log.date}\n\n${log.raw_markdown}`);
52
+
53
+ const insertStmt = dl.db.prepare(`
54
+ INSERT INTO document_embeddings
55
+ (reference_type, reference_id, chunk_index, text_chunk, embedding)
56
+ VALUES (?, ?, ?, ?, ?)
57
+ `);
58
+
59
+ // Use transaction for speed
60
+ const insertTx = dl.db.transaction((chunksArr) => {
61
+ for (let i = 0; i < chunksArr.length; i++) {
62
+ insertStmt.run('daily_log', log.date, i, chunksArr[i].text, chunksArr[i].buffer);
63
+ }
64
+ });
65
+
66
+ // Compute vectors asynchronously (since transformers is async) then insert
67
+ const preparedChunks = [];
68
+ for (let i = 0; i < chunks.length; i++) {
69
+ const vector = await defaultEmbedder.embedText(chunks[i]);
70
+ const buffer = defaultEmbedder.vectorToBuffer(vector);
71
+ preparedChunks.push({ text: chunks[i], buffer });
72
+ }
73
+
74
+ insertTx(preparedChunks);
75
+ updatedCount++;
76
+ }
77
+
78
+ console.log(`[RAG] Vector Index Built. Processed ${updatedCount} un-indexed logs.`);
79
+ }
80
+
81
+ if (require.main === module) {
82
+ buildVectorIndex().catch(console.error);
83
+ }
84
+
85
+ module.exports = { buildVectorIndex };
@@ -11,14 +11,8 @@ function ensureDir(p) {
11
11
  fs.mkdirSync(p, { recursive: true });
12
12
  }
13
13
 
14
- function readJsonOrDefault(p, def) {
15
- try {
16
- if (!exists(p)) return def;
17
- return JSON.parse(fs.readFileSync(p, 'utf8'));
18
- } catch {
19
- return def;
20
- }
21
- }
14
+ const { defaultInstance: dl } = require('./lib/DataLayer');
15
+ const DataManager = require('./lib/DataManager');
22
16
 
23
17
  function slugifyFileName(s) {
24
18
  return String(s || '')
@@ -52,14 +46,10 @@ function writeNote(baseDir, relPathNoExt, md) {
52
46
  function main() {
53
47
  const workspaceDir = path.resolve(process.cwd());
54
48
 
55
- const blockersFile = path.join(workspaceDir, 'data', 'blockers', 'blocker-log.json');
56
- const tasksFile = path.join(workspaceDir, 'data', 'tasks', 'task-log.json');
57
-
58
- const blockersLog = readJsonOrDefault(blockersFile, { schemaVersion: 1, blockers: [] });
59
- const tasksLog = readJsonOrDefault(tasksFile, { schemaVersion: 1, tasks: [] });
49
+ const dm = new DataManager(path.join(workspaceDir, 'data'), path.join(workspaceDir, 'logs'));
60
50
 
61
- const blockers = Array.isArray(blockersLog.blockers) ? blockersLog.blockers : [];
62
- const tasks = Array.isArray(tasksLog.tasks) ? tasksLog.tasks : [];
51
+ const blockers = dm.getBlockersRaw();
52
+ const tasks = dm.getTasksRaw();
63
53
 
64
54
  const notesRoot = path.join(workspaceDir, 'docs', 'notes');
65
55
  ensureDir(notesRoot);
@@ -106,7 +96,7 @@ function main() {
106
96
  '',
107
97
  '## Links',
108
98
  '- Related reports: see `docs/reports/`',
109
- '- Related tasks: see `data/tasks/task-log.json`',
99
+ '- Related tasks: see F.R.E.Y.A. SQLite tasks table',
110
100
  ''
111
101
  ].filter(Boolean).join('\n');
112
102
 
@@ -3,7 +3,7 @@ const path = require('path');
3
3
 
4
4
  const { toIsoDate, safeParseToMs, isWithinRange } = require('./lib/date-utils');
5
5
 
6
- const BLOCKERS_FILE = path.join(__dirname, '../data/blockers/blocker-log.json');
6
+ const DataManager = require('./lib/DataManager');
7
7
  const REPORT_DIR = path.join(__dirname, '../docs/reports');
8
8
 
9
9
  const SEVERITY_ORDER = {
@@ -95,26 +95,14 @@ function ensureReportDir() {
95
95
  }
96
96
  }
97
97
 
98
- function loadBlockers() {
99
- if (!fs.existsSync(BLOCKERS_FILE)) {
100
- return [];
101
- }
102
- try {
103
- const raw = fs.readFileSync(BLOCKERS_FILE, 'utf8');
104
- const data = JSON.parse(raw);
105
- return Array.isArray(data.blockers) ? data.blockers : [];
106
- } catch (err) {
107
- console.error(`Error reading blockers file: ${err.message}`);
108
- return [];
109
- }
110
- }
111
-
112
98
  function generateReport() {
113
99
  const now = new Date();
114
100
  const nowMs = now.getTime();
115
101
  const reportDate = toIsoDate(now);
116
- 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}`; })();
117
- const blockers = loadBlockers();
102
+ 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}`; })();
103
+
104
+ const dm = new DataManager();
105
+ const blockers = dm.getBlockersRaw();
118
106
 
119
107
  const statusCounts = new Map();
120
108
  blockers.forEach(blocker => {
@@ -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,10 +68,15 @@ 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();
71
+ function getBlockerTitle(blocker) {
72
+ return (
73
+ blocker.title ||
74
+ blocker.summary ||
75
+ blocker.description ||
76
+ blocker.content ||
77
+ blocker.text ||
78
+ 'Untitled blocker'
79
+ );
191
80
  }
192
81
 
193
82
  function normalizeSeverity(blocker) {
@@ -201,17 +90,6 @@ function normalizeSeverity(blocker) {
201
90
  return value;
202
91
  }
203
92
 
204
- function getBlockerTitle(blocker) {
205
- return (
206
- blocker.title ||
207
- blocker.summary ||
208
- blocker.description ||
209
- blocker.content ||
210
- blocker.text ||
211
- 'Untitled blocker'
212
- );
213
- }
214
-
215
93
  function getCreatedAt(blocker) {
216
94
  const candidates = [
217
95
  blocker.createdAt,
@@ -221,7 +99,7 @@ function getCreatedAt(blocker) {
221
99
  blocker.reportedAt,
222
100
  blocker.reported_at,
223
101
  blocker.date,
224
- blocker.loggedAt
102
+ blocker.loggedAt,
225
103
  ];
226
104
  for (const value of candidates) {
227
105
  const ms = safeParseToMs(value);
@@ -230,78 +108,18 @@ function getCreatedAt(blocker) {
230
108
  return NaN;
231
109
  }
232
110
 
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
111
  // --- Report Generation ---
294
112
 
295
113
  function generateReport(period) {
296
114
  const { start, end } = getDateRange(period);
297
115
  const dateStr = formatDate(new Date());
298
-
116
+
299
117
  console.log(`Generating ${period} report for ${formatDate(start)} to ${formatDate(end)}...`);
300
118
 
301
- const tasks = getTasks(start, end);
302
- const projects = getProjectUpdates(start, end);
303
- const logs = getDailyLogs(start, end);
304
- const blockers = getBlockers(start, end);
119
+ const tasks = dm.getTasks(start, end);
120
+ const projects = dm.getProjectUpdates(start, end);
121
+ const logs = dm.getDailyLogs(start, end);
122
+ const blockers = dm.getBlockers(start, end);
305
123
 
306
124
  let md = `# Relatório de Status Profissional - ${dateStr}\n`;
307
125
  md += `**Período:** ${formatDate(start)} a ${formatDate(end)}\n\n`;
@@ -346,9 +164,9 @@ function generateReport(period) {
346
164
  md += `*Sem atualizações de projeto recentes.*\n`;
347
165
  } else {
348
166
  projects.forEach(p => {
349
- md += `### ${p.client} / ${p.name}\n`;
350
- md += `**Status Atual:** ${p.status}\n`;
351
- if (p.events.length > 0) {
167
+ md += `### ${p.client} / ${p.project}\n`;
168
+ md += `**Status Atual:** ${p.currentStatus}\n`;
169
+ if (p.events && p.events.length > 0) {
352
170
  md += `**Atualizações Recentes:**\n`;
353
171
  p.events.forEach(e => {
354
172
  const typeIcon = e.type === 'Blocker' ? '🔴' : '🔹';
@@ -386,7 +204,7 @@ function generateReport(period) {
386
204
  md += `\n**Resolvidos no período:**\n`;
387
205
  blockers.resolvedRecent.forEach(blocker => {
388
206
  const title = getBlockerTitle(blocker);
389
- const resolvedAt = getResolvedAt(blocker);
207
+ const resolvedAt = dm.getResolvedAt(blocker);
390
208
  const resolvedDate = Number.isFinite(resolvedAt) ? toIsoDate(resolvedAt) : 'Unknown';
391
209
  md += `- ${title} (Resolvido: ${resolvedDate})\n`;
392
210
  });