@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.
- package/.agent/rules/freya/agents/coach.mdc +7 -16
- package/.agent/rules/freya/agents/ingestor.mdc +1 -89
- package/.agent/rules/freya/agents/master.mdc +3 -0
- package/.agent/rules/freya/agents/oracle.mdc +7 -23
- package/cli/web-ui.css +860 -182
- package/cli/web-ui.js +547 -175
- package/cli/web.js +690 -536
- package/package.json +6 -3
- package/scripts/build-vector-index.js +85 -0
- package/scripts/export-obsidian.js +6 -16
- package/scripts/generate-blockers-report.js +5 -17
- package/scripts/generate-daily-summary.js +25 -58
- package/scripts/generate-executive-report.js +22 -204
- package/scripts/generate-sm-weekly-report.js +27 -92
- package/scripts/lib/DataLayer.js +92 -0
- package/scripts/lib/DataManager.js +198 -0
- package/scripts/lib/Embedder.js +59 -0
- package/scripts/lib/schema.js +23 -0
- package/scripts/migrate-v1-v2.js +184 -0
- package/scripts/validate-data.js +48 -51
- package/scripts/validate-structure.js +12 -58
- package/templates/base/scripts/build-vector-index.js +85 -0
- package/templates/base/scripts/export-obsidian.js +143 -0
- package/templates/base/scripts/generate-daily-summary.js +25 -58
- package/templates/base/scripts/generate-executive-report.js +14 -225
- package/templates/base/scripts/generate-sm-weekly-report.js +9 -91
- package/templates/base/scripts/index/build-index.js +13 -0
- package/templates/base/scripts/index/update-index.js +15 -0
- package/templates/base/scripts/lib/DataLayer.js +92 -0
- package/templates/base/scripts/lib/DataManager.js +198 -0
- package/templates/base/scripts/lib/Embedder.js +59 -0
- package/templates/base/scripts/lib/index-utils.js +407 -0
- package/templates/base/scripts/lib/schema.js +23 -0
- package/templates/base/scripts/lib/search-utils.js +183 -0
- package/templates/base/scripts/migrate-v1-v2.js +184 -0
- package/templates/base/scripts/validate-data.js +48 -51
- 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
|
+
"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-
|
|
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
|
-
"
|
|
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
|
-
|
|
15
|
-
|
|
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
|
|
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 =
|
|
62
|
-
const 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
|
|
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
|
|
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
|
-
|
|
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
|
|
5
|
-
const { safeReadJson, quarantineCorruptedFile } = require('./lib/fs-utils');
|
|
4
|
+
const DataManager = require('./lib/DataManager');
|
|
6
5
|
|
|
7
|
-
const
|
|
8
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
31
|
-
const
|
|
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 =
|
|
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 (
|
|
54
|
-
summary +=
|
|
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
|
|
49
|
+
// 3. Bloqueios
|
|
50
|
+
const { open } = dm.getBlockers(start, now);
|
|
61
51
|
let blockersLine = "None";
|
|
62
|
-
if (
|
|
63
|
-
|
|
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 =
|
|
96
|
-
const time =
|
|
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,
|
|
13
|
-
const
|
|
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
|
|
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
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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.
|
|
350
|
-
md += `**Status Atual:** ${p.
|
|
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
|
});
|