@cccarv82/freya 1.0.3
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/README.md +92 -0
- package/bin/freya.js +11 -0
- package/cli/index.js +62 -0
- package/cli/init.js +133 -0
- package/package.json +27 -0
- package/templates/base/.agent/rules/freya/agents/coach.mdc +72 -0
- package/templates/base/.agent/rules/freya/agents/ingestor.mdc +183 -0
- package/templates/base/.agent/rules/freya/agents/master.mdc +93 -0
- package/templates/base/.agent/rules/freya/agents/oracle.mdc +102 -0
- package/templates/base/.agent/rules/freya/freya.mdc +31 -0
- package/templates/base/README.md +50 -0
- package/templates/base/USER_GUIDE.md +160 -0
- package/templates/base/data/blockers/blocker-log.json +4 -0
- package/templates/base/data/career/career-log.json +4 -0
- package/templates/base/data/schemas.md +66 -0
- package/templates/base/data/tasks/task-log.json +4 -0
- package/templates/base/scripts/generate-blockers-report.js +215 -0
- package/templates/base/scripts/generate-daily-summary.js +96 -0
- package/templates/base/scripts/generate-executive-report.js +240 -0
- package/templates/base/scripts/generate-sm-weekly-report.js +207 -0
- package/templates/base/scripts/generate-weekly-report.js +134 -0
- package/templates/base/scripts/lib/date-utils.js +37 -0
- package/templates/base/scripts/lib/fs-utils.js +61 -0
- package/templates/base/scripts/migrate-data.js +80 -0
- package/templates/base/scripts/validate-data.js +206 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const { safeParseToMs } = require('./lib/date-utils');
|
|
5
|
+
const { safeReadJson, quarantineCorruptedFile } = require('./lib/fs-utils');
|
|
6
|
+
|
|
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');
|
|
9
|
+
|
|
10
|
+
// --- Helper Logic ---
|
|
11
|
+
const now = new Date();
|
|
12
|
+
const oneDay = 24 * 60 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
function isRecentlyCompleted(dateStr) {
|
|
15
|
+
const ms = safeParseToMs(dateStr);
|
|
16
|
+
if (!Number.isFinite(ms)) return false;
|
|
17
|
+
const diff = now.getTime() - ms;
|
|
18
|
+
// Consider "Yesterday" as anything completed in the last 24 hours for daily sync purposes
|
|
19
|
+
return diff >= 0 && diff <= oneDay;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function generateDailySummary() {
|
|
23
|
+
try {
|
|
24
|
+
if (!fs.existsSync(TASK_LOG_PATH)) {
|
|
25
|
+
console.log("**Ontem:** No task log found.\n**Hoje:** Set up task log.\n**Bloqueios:** None");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const content = fs.readFileSync(TASK_LOG_PATH, 'utf8');
|
|
30
|
+
const json = JSON.parse(content);
|
|
31
|
+
|
|
32
|
+
if (!json.tasks) {
|
|
33
|
+
console.log("**Ontem:** Invalid task log.\n**Hoje:** Fix task log.\n**Bloqueios:** None");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let summary = "";
|
|
38
|
+
|
|
39
|
+
// 1. Ontem (Completed < 24h)
|
|
40
|
+
const completedRecently = json.tasks.filter(t => t.status === "COMPLETED" && isRecentlyCompleted(t.completedAt));
|
|
41
|
+
summary += "**Ontem:** ";
|
|
42
|
+
if (completedRecently.length > 0) {
|
|
43
|
+
summary += completedRecently.map(t => t.description).join(", ");
|
|
44
|
+
} else {
|
|
45
|
+
summary += "Nothing recorded";
|
|
46
|
+
}
|
|
47
|
+
summary += "\n";
|
|
48
|
+
|
|
49
|
+
// 2. Hoje (DO_NOW + PENDING)
|
|
50
|
+
const doNow = json.tasks.filter(t => t.status === "PENDING" && t.category === "DO_NOW");
|
|
51
|
+
summary += "**Hoje:** ";
|
|
52
|
+
if (doNow.length > 0) {
|
|
53
|
+
summary += doNow.map(t => t.description).join(", ");
|
|
54
|
+
} else {
|
|
55
|
+
summary += "Nothing planned";
|
|
56
|
+
}
|
|
57
|
+
summary += "\n";
|
|
58
|
+
|
|
59
|
+
// 3. Bloqueios (from blocker-log.json)
|
|
60
|
+
let blockersLine = "None";
|
|
61
|
+
if (fs.existsSync(BLOCKERS_LOG_PATH)) {
|
|
62
|
+
const res = safeReadJson(BLOCKERS_LOG_PATH);
|
|
63
|
+
if (!res.ok) {
|
|
64
|
+
if (res.error.type === 'parse') {
|
|
65
|
+
quarantineCorruptedFile(BLOCKERS_LOG_PATH, res.error.message);
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
const blockers = (res.json.blockers || []).filter(b => {
|
|
69
|
+
const st = (b.status || '').toUpperCase();
|
|
70
|
+
return st === 'OPEN' || st === 'MITIGATING';
|
|
71
|
+
});
|
|
72
|
+
const sevOrder = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
|
|
73
|
+
blockers.sort((a, b) => {
|
|
74
|
+
const sa = sevOrder[(a.severity || '').toUpperCase()] ?? 99;
|
|
75
|
+
const sb = sevOrder[(b.severity || '').toUpperCase()] ?? 99;
|
|
76
|
+
if (sa !== sb) return sa - sb;
|
|
77
|
+
const ta = safeParseToMs(a.createdAt) || 0;
|
|
78
|
+
const tb = safeParseToMs(b.createdAt) || 0;
|
|
79
|
+
return ta - tb; // older first
|
|
80
|
+
});
|
|
81
|
+
if (blockers.length > 0) {
|
|
82
|
+
blockersLine = blockers.slice(0, 3).map(b => b.title || b.description || b.id).join(", ");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
summary += `**Bloqueios:** ${blockersLine}`;
|
|
88
|
+
|
|
89
|
+
console.log(summary);
|
|
90
|
+
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.error("Error generating daily:", err.message);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
generateDailySummary();
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* generate-executive-report.js
|
|
3
|
+
* Generates a professional Markdown status report (Daily/Weekly)
|
|
4
|
+
* aggregating Tasks, Project Updates, and Daily Logs.
|
|
5
|
+
*
|
|
6
|
+
* Usage: node scripts/generate-executive-report.js --period [daily|weekly]
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const { toIsoDate, isWithinRange } = require('./lib/date-utils');
|
|
13
|
+
const { safeReadJson, quarantineCorruptedFile } = require('./lib/fs-utils');
|
|
14
|
+
|
|
15
|
+
// --- Configuration ---
|
|
16
|
+
const DATA_DIR = path.join(__dirname, '../data');
|
|
17
|
+
const LOGS_DIR = path.join(__dirname, '../logs/daily');
|
|
18
|
+
const OUTPUT_DIR = path.join(__dirname, '../docs/reports');
|
|
19
|
+
const TASKS_FILE = path.join(DATA_DIR, 'tasks/task-log.json');
|
|
20
|
+
|
|
21
|
+
// --- Helpers ---
|
|
22
|
+
function getDateRange(period) {
|
|
23
|
+
const today = new Date();
|
|
24
|
+
const end = new Date(today);
|
|
25
|
+
const start = new Date(today);
|
|
26
|
+
|
|
27
|
+
if (period === 'weekly') {
|
|
28
|
+
// Last 7 days
|
|
29
|
+
start.setDate(today.getDate() - 7);
|
|
30
|
+
} else {
|
|
31
|
+
// Daily: Start of today (00:00 local time) to now.
|
|
32
|
+
start.setHours(0, 0, 0, 0);
|
|
33
|
+
}
|
|
34
|
+
return { start, end };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function formatDate(date) {
|
|
38
|
+
return toIsoDate(date);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function ensureDir(dir) {
|
|
42
|
+
if (!fs.existsSync(dir)) {
|
|
43
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// --- Data Fetching ---
|
|
48
|
+
|
|
49
|
+
function getTasks(start, end) {
|
|
50
|
+
if (!fs.existsSync(TASKS_FILE)) return { completed: [], pending: [], blockers: [] };
|
|
51
|
+
|
|
52
|
+
const result = safeReadJson(TASKS_FILE);
|
|
53
|
+
if (!result.ok) {
|
|
54
|
+
const relativePath = path.relative(DATA_DIR, TASKS_FILE);
|
|
55
|
+
if (result.error.type === 'parse') {
|
|
56
|
+
quarantineCorruptedFile(TASKS_FILE, result.error.message);
|
|
57
|
+
console.warn(`⚠️ [${relativePath}] JSON parse failed; quarantined to _corrupted.`);
|
|
58
|
+
} else {
|
|
59
|
+
console.error(`Error reading tasks: ${result.error.message}`);
|
|
60
|
+
}
|
|
61
|
+
return { completed: [], pending: [] };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const tasks = result.json.tasks || [];
|
|
65
|
+
|
|
66
|
+
const completed = tasks.filter(t => {
|
|
67
|
+
if (t.status !== 'COMPLETED' || !t.completedAt) return false;
|
|
68
|
+
return isWithinRange(t.completedAt, start, end);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const pending = tasks.filter(t => t.status === 'PENDING' && t.category === 'DO_NOW');
|
|
72
|
+
|
|
73
|
+
// "Blockers" could be tasks tagged as such or implicit in status?
|
|
74
|
+
// For now, we don't have explicit blocker task type, but we can look for high priority or specific keywords if we wanted.
|
|
75
|
+
// Let's stick to simple PENDING DO_NOW for Next Steps.
|
|
76
|
+
|
|
77
|
+
return { completed, pending };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getProjectUpdates(start, end) {
|
|
81
|
+
const clientsDir = path.join(DATA_DIR, 'Clients');
|
|
82
|
+
if (!fs.existsSync(clientsDir)) return [];
|
|
83
|
+
|
|
84
|
+
const updates = [];
|
|
85
|
+
|
|
86
|
+
function scan(dir) {
|
|
87
|
+
const files = fs.readdirSync(dir);
|
|
88
|
+
for (const file of files) {
|
|
89
|
+
const fullPath = path.join(dir, file);
|
|
90
|
+
const stat = fs.statSync(fullPath);
|
|
91
|
+
|
|
92
|
+
if (stat.isDirectory()) {
|
|
93
|
+
if (file === '_corrupted') {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
scan(fullPath);
|
|
97
|
+
} else if (file === 'status.json') {
|
|
98
|
+
const result = safeReadJson(fullPath);
|
|
99
|
+
if (!result.ok) {
|
|
100
|
+
const relativePath = path.relative(DATA_DIR, fullPath);
|
|
101
|
+
if (result.error.type === 'parse') {
|
|
102
|
+
quarantineCorruptedFile(fullPath, result.error.message);
|
|
103
|
+
console.warn(`⚠️ [${relativePath}] JSON parse failed; quarantined to _corrupted.`);
|
|
104
|
+
} else {
|
|
105
|
+
console.error(`Error reading ${relativePath}: ${result.error.message}`);
|
|
106
|
+
}
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const project = result.json;
|
|
111
|
+
|
|
112
|
+
// Filter history
|
|
113
|
+
const recentEvents = (project.history || []).filter(h => {
|
|
114
|
+
return isWithinRange(h.date || h.timestamp, start, end); // Support both formats if they vary
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (recentEvents.length > 0 || project.currentStatus) {
|
|
118
|
+
updates.push({
|
|
119
|
+
name: project.project || path.basename(path.dirname(fullPath)),
|
|
120
|
+
client: project.client || path.basename(path.dirname(path.dirname(fullPath))),
|
|
121
|
+
status: project.currentStatus,
|
|
122
|
+
events: recentEvents
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
scan(clientsDir);
|
|
130
|
+
return updates;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getDailyLogs(start, end) {
|
|
134
|
+
if (!fs.existsSync(LOGS_DIR)) return [];
|
|
135
|
+
|
|
136
|
+
const relevantLogs = [];
|
|
137
|
+
const files = fs.readdirSync(LOGS_DIR);
|
|
138
|
+
|
|
139
|
+
for (const file of files) {
|
|
140
|
+
if (!file.endsWith('.md')) continue;
|
|
141
|
+
const dateStr = file.replace('.md', '');
|
|
142
|
+
// The filenames are YYYY-MM-DD. Compare as ISO dates to avoid timezone parsing drift.
|
|
143
|
+
const startIso = formatDate(start);
|
|
144
|
+
const endIso = formatDate(end);
|
|
145
|
+
if (dateStr >= startIso && dateStr <= endIso) {
|
|
146
|
+
try {
|
|
147
|
+
const content = fs.readFileSync(path.join(LOGS_DIR, file), 'utf8');
|
|
148
|
+
relevantLogs.push({ date: dateStr, content });
|
|
149
|
+
} catch (e) {}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return relevantLogs;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// --- Report Generation ---
|
|
156
|
+
|
|
157
|
+
function generateReport(period) {
|
|
158
|
+
const { start, end } = getDateRange(period);
|
|
159
|
+
const dateStr = formatDate(new Date());
|
|
160
|
+
|
|
161
|
+
console.log(`Generating ${period} report for ${formatDate(start)} to ${formatDate(end)}...`);
|
|
162
|
+
|
|
163
|
+
const tasks = getTasks(start, end);
|
|
164
|
+
const projects = getProjectUpdates(start, end);
|
|
165
|
+
// const logs = getDailyLogs(start, end); // Maybe too verbose to include raw logs, let's stick to summarized data
|
|
166
|
+
|
|
167
|
+
let md = `# Relatório de Status Profissional - ${dateStr}\n`;
|
|
168
|
+
md += `**Período:** ${formatDate(start)} a ${formatDate(end)}\n\n`;
|
|
169
|
+
|
|
170
|
+
// 1. Resumo Executivo (Placeholder logic)
|
|
171
|
+
md += `## 📋 Resumo Executivo\n`;
|
|
172
|
+
const totalDone = tasks.completed.length;
|
|
173
|
+
const activeProjects = projects.length;
|
|
174
|
+
md += `Neste período, foram concluídas **${totalDone}** entregas focais. Atualmente há **${activeProjects}** projetos com atualizações recentes.\n\n`;
|
|
175
|
+
|
|
176
|
+
// 2. Principais Entregas
|
|
177
|
+
md += `## ✅ Principais Entregas\n`;
|
|
178
|
+
if (tasks.completed.length === 0) {
|
|
179
|
+
md += `*Nenhuma entrega registrada no período.*\n`;
|
|
180
|
+
} else {
|
|
181
|
+
tasks.completed.forEach(t => {
|
|
182
|
+
const projectTag = t.projectSlug ? `\`[${t.projectSlug}]\`` : '';
|
|
183
|
+
md += `- ${projectTag} ${t.description}\n`;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
md += `\n`;
|
|
187
|
+
|
|
188
|
+
// 3. Status dos Projetos
|
|
189
|
+
md += `## 🏗️ Status dos Projetos\n`;
|
|
190
|
+
if (projects.length === 0) {
|
|
191
|
+
md += `*Sem atualizações de projeto recentes.*\n`;
|
|
192
|
+
} else {
|
|
193
|
+
projects.forEach(p => {
|
|
194
|
+
md += `### ${p.client} / ${p.name}\n`;
|
|
195
|
+
md += `**Status Atual:** ${p.status}\n`;
|
|
196
|
+
if (p.events.length > 0) {
|
|
197
|
+
md += `**Atualizações Recentes:**\n`;
|
|
198
|
+
p.events.forEach(e => {
|
|
199
|
+
const typeIcon = e.type === 'Blocker' ? '🔴' : '🔹';
|
|
200
|
+
md += `- ${typeIcon} [${e.date}] ${e.content}\n`;
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
md += `\n`;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 4. Próximos Passos
|
|
208
|
+
md += `## 🚀 Próximos Passos\n`;
|
|
209
|
+
if (tasks.pending.length === 0) {
|
|
210
|
+
md += `*Sem itens prioritários na fila.*\n`;
|
|
211
|
+
} else {
|
|
212
|
+
tasks.pending.forEach(t => {
|
|
213
|
+
const projectTag = t.projectSlug ? `\`[${t.projectSlug}]\`` : '';
|
|
214
|
+
md += `- [ ] ${projectTag} ${t.description}\n`;
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
md += `\n`;
|
|
218
|
+
|
|
219
|
+
// Save
|
|
220
|
+
ensureDir(OUTPUT_DIR);
|
|
221
|
+
const filename = `report-${period}-${dateStr}.md`;
|
|
222
|
+
const outputPath = path.join(OUTPUT_DIR, filename);
|
|
223
|
+
fs.writeFileSync(outputPath, md, 'utf8');
|
|
224
|
+
|
|
225
|
+
console.log(`Report generated successfully: ${outputPath}`);
|
|
226
|
+
console.log(`\n--- SUMMARY PREVIEW ---\n`);
|
|
227
|
+
console.log(md.substring(0, 500) + "...");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// --- Main ---
|
|
231
|
+
const args = process.argv.slice(2);
|
|
232
|
+
const periodIdx = args.indexOf('--period');
|
|
233
|
+
const period = periodIdx !== -1 ? args[periodIdx + 1] : 'daily';
|
|
234
|
+
|
|
235
|
+
if (!['daily', 'weekly'].includes(period)) {
|
|
236
|
+
console.error("Invalid period. Use 'daily' or 'weekly'.");
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
generateReport(period);
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const { toIsoDate, safeParseToMs, isWithinRange } = require('./lib/date-utils');
|
|
5
|
+
const { safeReadJson, quarantineCorruptedFile } = require('./lib/fs-utils');
|
|
6
|
+
|
|
7
|
+
const DATA_DIR = path.join(__dirname, '../data');
|
|
8
|
+
const REPORT_DIR = path.join(__dirname, '../docs/reports');
|
|
9
|
+
|
|
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 };
|
|
15
|
+
|
|
16
|
+
function ensureDir(dir) {
|
|
17
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
|
|
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
|
+
function daysBetweenMs(aMs, bMs) {
|
|
36
|
+
const diff = Math.max(0, bMs - aMs);
|
|
37
|
+
return Math.floor(diff / (24 * 60 * 60 * 1000));
|
|
38
|
+
}
|
|
39
|
+
|
|
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
|
+
function generate() {
|
|
62
|
+
ensureDir(REPORT_DIR);
|
|
63
|
+
|
|
64
|
+
const now = new Date();
|
|
65
|
+
const end = now;
|
|
66
|
+
const start = new Date(now);
|
|
67
|
+
start.setDate(now.getDate() - 7);
|
|
68
|
+
|
|
69
|
+
const reportDate = toIsoDate(now);
|
|
70
|
+
|
|
71
|
+
const taskLog = readJsonOrQuarantine(TASKS_FILE, { schemaVersion: 1, tasks: [] });
|
|
72
|
+
const blockersLog = readJsonOrQuarantine(BLOCKERS_FILE, { schemaVersion: 1, blockers: [] });
|
|
73
|
+
|
|
74
|
+
const tasks = taskLog.tasks || [];
|
|
75
|
+
const blockers = blockersLog.blockers || [];
|
|
76
|
+
|
|
77
|
+
const completedTasks = tasks.filter(t => {
|
|
78
|
+
if (t.status !== 'COMPLETED') return false;
|
|
79
|
+
return isWithinRange(t.completedAt || t.completed_at, start, end);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const pendingDoNow = tasks.filter(t => t.status === 'PENDING' && t.category === 'DO_NOW');
|
|
83
|
+
|
|
84
|
+
const activeBlockers = blockers.filter(b => {
|
|
85
|
+
const st = String(b.status || '').toUpperCase();
|
|
86
|
+
return st === 'OPEN' || st === 'MITIGATING';
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
activeBlockers.sort((a, b) => {
|
|
90
|
+
const sa = SEV_ORDER[String(a.severity || '').toUpperCase()] ?? 99;
|
|
91
|
+
const sb = SEV_ORDER[String(b.severity || '').toUpperCase()] ?? 99;
|
|
92
|
+
if (sa !== sb) return sa - sb;
|
|
93
|
+
const ta = safeParseToMs(a.createdAt) || 0;
|
|
94
|
+
const tb = safeParseToMs(b.createdAt) || 0;
|
|
95
|
+
return ta - tb; // older first
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const upcomingDueBlockers = blockers.filter(b => {
|
|
99
|
+
const st = String(b.status || '').toUpperCase();
|
|
100
|
+
if (st !== 'OPEN') return false;
|
|
101
|
+
if (!b.dueDate) return false;
|
|
102
|
+
// dueDate is an ISO date; treat as UTC midnight
|
|
103
|
+
const dueMs = safeParseToMs(String(b.dueDate));
|
|
104
|
+
if (!Number.isFinite(dueMs)) return false;
|
|
105
|
+
const due = new Date(dueMs);
|
|
106
|
+
const soonEnd = new Date(now);
|
|
107
|
+
soonEnd.setDate(now.getDate() + 7);
|
|
108
|
+
return due >= now && due <= soonEnd;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Projects
|
|
112
|
+
const statusFiles = scanProjectStatusFiles();
|
|
113
|
+
const projectsWithUpdates = [];
|
|
114
|
+
|
|
115
|
+
for (const statusPath of statusFiles) {
|
|
116
|
+
const proj = readJsonOrQuarantine(statusPath, null);
|
|
117
|
+
if (!proj) continue;
|
|
118
|
+
|
|
119
|
+
const history = Array.isArray(proj.history) ? proj.history : [];
|
|
120
|
+
const recent = history.filter(h => isWithinRange(h.date || h.timestamp, start, end));
|
|
121
|
+
if (recent.length === 0) continue;
|
|
122
|
+
|
|
123
|
+
projectsWithUpdates.push({
|
|
124
|
+
client: proj.client || path.basename(path.dirname(path.dirname(statusPath))),
|
|
125
|
+
project: proj.project || path.basename(path.dirname(statusPath)),
|
|
126
|
+
currentStatus: proj.currentStatus,
|
|
127
|
+
recent
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Markdown
|
|
132
|
+
let md = `# Scrum Master Weekly Report — ${reportDate}\n`;
|
|
133
|
+
md += `**Período:** ${toIsoDate(start)} a ${toIsoDate(end)}\n\n`;
|
|
134
|
+
|
|
135
|
+
md += `## Summary\n`;
|
|
136
|
+
md += `- Completed tasks (7d): **${completedTasks.length}**\n`;
|
|
137
|
+
md += `- Active blockers (OPEN/MITIGATING): **${activeBlockers.length}**\n`;
|
|
138
|
+
md += `- Projects updated (7d): **${projectsWithUpdates.length}**\n\n`;
|
|
139
|
+
|
|
140
|
+
md += `## Wins\n`;
|
|
141
|
+
if (completedTasks.length === 0) {
|
|
142
|
+
md += `No completed tasks recorded in the last 7 days.\n\n`;
|
|
143
|
+
} else {
|
|
144
|
+
completedTasks.forEach(t => {
|
|
145
|
+
md += `- ${t.description}\n`;
|
|
146
|
+
});
|
|
147
|
+
md += `\n`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
md += `## Blockers & Risks\n`;
|
|
151
|
+
if (activeBlockers.length === 0) {
|
|
152
|
+
md += `None.\n\n`;
|
|
153
|
+
} else {
|
|
154
|
+
activeBlockers.forEach(b => {
|
|
155
|
+
const createdMs = safeParseToMs(b.createdAt) || Date.now();
|
|
156
|
+
const agingDays = daysBetweenMs(createdMs, Date.now());
|
|
157
|
+
const sev = String(b.severity || '').toUpperCase();
|
|
158
|
+
const st = String(b.status || '').toUpperCase();
|
|
159
|
+
const owner = b.owner ? `; owner: ${b.owner}` : '';
|
|
160
|
+
const proj = b.projectSlug ? `; project: ${b.projectSlug}` : '';
|
|
161
|
+
const next = b.nextAction ? `; next: ${b.nextAction}` : '';
|
|
162
|
+
md += `- [${sev}/${st}] ${b.title} (aging: ${agingDays}d${owner}${proj}${next})\n`;
|
|
163
|
+
});
|
|
164
|
+
md += `\n`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
md += `## Project Updates\n`;
|
|
168
|
+
if (projectsWithUpdates.length === 0) {
|
|
169
|
+
md += `No project updates found in the last 7 days.\n\n`;
|
|
170
|
+
} else {
|
|
171
|
+
projectsWithUpdates.forEach(p => {
|
|
172
|
+
md += `### ${p.client} / ${p.project}\n`;
|
|
173
|
+
if (p.currentStatus) md += `**Status:** ${p.currentStatus}\n`;
|
|
174
|
+
p.recent.forEach(e => {
|
|
175
|
+
md += `- [${e.date || e.timestamp}] ${e.content || ''}\n`;
|
|
176
|
+
});
|
|
177
|
+
md += `\n`;
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
md += `## Next Week Focus\n`;
|
|
182
|
+
if (pendingDoNow.length === 0 && upcomingDueBlockers.length === 0) {
|
|
183
|
+
md += `No DO_NOW tasks or due-soon blockers found.\n`;
|
|
184
|
+
} else {
|
|
185
|
+
if (pendingDoNow.length > 0) {
|
|
186
|
+
md += `### DO_NOW Tasks\n`;
|
|
187
|
+
pendingDoNow.forEach(t => {
|
|
188
|
+
md += `- [ ] ${t.description}\n`;
|
|
189
|
+
});
|
|
190
|
+
md += `\n`;
|
|
191
|
+
}
|
|
192
|
+
if (upcomingDueBlockers.length > 0) {
|
|
193
|
+
md += `### Blockers due soon (next 7 days)\n`;
|
|
194
|
+
upcomingDueBlockers.forEach(b => {
|
|
195
|
+
md += `- [${String(b.severity || '').toUpperCase()}] ${b.title} (due: ${b.dueDate})\n`;
|
|
196
|
+
});
|
|
197
|
+
md += `\n`;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const outPath = path.join(REPORT_DIR, `sm-weekly-${reportDate}.md`);
|
|
202
|
+
fs.writeFileSync(outPath, md, 'utf8');
|
|
203
|
+
console.log(md);
|
|
204
|
+
console.log(`\nSaved: ${outPath}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
generate();
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const { toIsoDate, isWithinRange } = require('./lib/date-utils');
|
|
5
|
+
|
|
6
|
+
const DATA_DIR = path.join(__dirname, '../data');
|
|
7
|
+
const REPORT_DIR = path.join(__dirname, '../docs/reports');
|
|
8
|
+
|
|
9
|
+
// Ensure output dir exists
|
|
10
|
+
if (!fs.existsSync(REPORT_DIR)) {
|
|
11
|
+
fs.mkdirSync(REPORT_DIR, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// --- Date Logic ---
|
|
15
|
+
const now = new Date();
|
|
16
|
+
const oneDay = 24 * 60 * 60 * 1000;
|
|
17
|
+
|
|
18
|
+
function isWithinWeek(dateStr) {
|
|
19
|
+
const sevenDaysAgo = new Date(now.getTime() - (7 * oneDay));
|
|
20
|
+
return isWithinRange(dateStr, sevenDaysAgo, now);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getFormattedDate() {
|
|
24
|
+
return toIsoDate(now);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// --- File Walking ---
|
|
28
|
+
function walk(dir, fileList = []) {
|
|
29
|
+
const files = fs.readdirSync(dir);
|
|
30
|
+
files.forEach(file => {
|
|
31
|
+
const filePath = path.join(dir, file);
|
|
32
|
+
const stat = fs.statSync(filePath);
|
|
33
|
+
if (stat.isDirectory()) {
|
|
34
|
+
walk(filePath, fileList);
|
|
35
|
+
} else {
|
|
36
|
+
if (path.extname(file) === '.json') {
|
|
37
|
+
fileList.push(filePath);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
return fileList;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// --- Aggregation ---
|
|
45
|
+
function generateWeeklyReport() {
|
|
46
|
+
const files = walk(DATA_DIR);
|
|
47
|
+
|
|
48
|
+
const projects = [];
|
|
49
|
+
let taskLog = { schemaVersion: 1, tasks: [] };
|
|
50
|
+
let careerLog = { entries: [] };
|
|
51
|
+
|
|
52
|
+
// 1. Collect Data
|
|
53
|
+
files.forEach(file => {
|
|
54
|
+
try {
|
|
55
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
56
|
+
const json = JSON.parse(content);
|
|
57
|
+
|
|
58
|
+
if (file.endsWith('task-log.json')) {
|
|
59
|
+
taskLog = json;
|
|
60
|
+
} else if (file.endsWith('career-log.json')) {
|
|
61
|
+
careerLog = json;
|
|
62
|
+
} else if (file.endsWith('status.json')) {
|
|
63
|
+
projects.push(json);
|
|
64
|
+
}
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error(`Error reading ${file}: ${err.message}`);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// 2. Generate Content
|
|
71
|
+
const reportDate = getFormattedDate();
|
|
72
|
+
let report = `# Weekly Report - ${reportDate}\n\n`;
|
|
73
|
+
|
|
74
|
+
// Projects
|
|
75
|
+
report += "## 🚀 Project Updates\n";
|
|
76
|
+
let hasProjectUpdates = false;
|
|
77
|
+
projects.forEach(p => {
|
|
78
|
+
if (p.history && Array.isArray(p.history)) {
|
|
79
|
+
const recentUpdates = p.history.filter(h => isWithinWeek(h.date));
|
|
80
|
+
if (recentUpdates.length > 0) {
|
|
81
|
+
hasProjectUpdates = true;
|
|
82
|
+
report += `### ${p.client} - ${p.project}\n`;
|
|
83
|
+
recentUpdates.forEach(u => {
|
|
84
|
+
const dateStr = u.date ? u.date.split('T')[0] : 'Unknown Date';
|
|
85
|
+
report += `- **${dateStr}**: ${u.content}\n`;
|
|
86
|
+
});
|
|
87
|
+
report += "\n";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
if (!hasProjectUpdates) report += "No project updates recorded this week.\n\n";
|
|
92
|
+
|
|
93
|
+
// Tasks
|
|
94
|
+
report += "## ✅ Completed Tasks\n";
|
|
95
|
+
if (taskLog.tasks && Array.isArray(taskLog.tasks)) {
|
|
96
|
+
const recentTasks = taskLog.tasks.filter(t => t.status === "COMPLETED" && isWithinWeek(t.completedAt));
|
|
97
|
+
if (recentTasks.length > 0) {
|
|
98
|
+
recentTasks.forEach(t => {
|
|
99
|
+
report += `- ${t.description}\n`;
|
|
100
|
+
});
|
|
101
|
+
} else {
|
|
102
|
+
report += "No tasks completed this week.\n";
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
report += "No task log found.\n";
|
|
106
|
+
}
|
|
107
|
+
report += "\n";
|
|
108
|
+
|
|
109
|
+
// Career
|
|
110
|
+
report += "## 🌟 Career Highlights\n";
|
|
111
|
+
if (careerLog.entries && Array.isArray(careerLog.entries)) {
|
|
112
|
+
const recentCareer = careerLog.entries.filter(e => isWithinWeek(e.date));
|
|
113
|
+
if (recentCareer.length > 0) {
|
|
114
|
+
recentCareer.forEach(e => {
|
|
115
|
+
report += `- **[${e.type}]**: ${e.description}\n`;
|
|
116
|
+
});
|
|
117
|
+
} else {
|
|
118
|
+
report += "No career updates this week.\n";
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
report += "No career log found.\n";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 3. Save and Output
|
|
125
|
+
const outputPath = path.join(REPORT_DIR, `weekly-${reportDate}.md`);
|
|
126
|
+
fs.writeFileSync(outputPath, report);
|
|
127
|
+
|
|
128
|
+
console.log(`✅ Report generated at: ${outputPath}`);
|
|
129
|
+
console.log("---------------------------------------------------");
|
|
130
|
+
console.log(report);
|
|
131
|
+
console.log("---------------------------------------------------");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
generateWeeklyReport();
|