@cccarv82/freya 2.3.13 → 2.5.0
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 +965 -182
- package/cli/web-ui.js +551 -173
- package/cli/web.js +863 -536
- package/package.json +7 -4
- 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
|
@@ -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,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.
|
|
350
|
-
md += `**Status Atual:** ${p.
|
|
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
|
|
5
|
-
const
|
|
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
|
|
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
|
|
76
|
-
const 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 =
|
|
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
|
-
|
|
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 };
|