@cccarv82/freya 1.0.52 → 1.0.54
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/oracle.mdc +13 -1
- package/README.md +1 -1
- package/cli/web.js +83 -19
- package/package.json +2 -2
- package/scripts/generate-executive-report.js +195 -6
- package/templates/base/.agent/rules/freya/agents/oracle.mdc +14 -1
- package/templates/base/README.md +61 -2
- package/templates/base/USER_GUIDE.md +38 -1
- package/templates/base/scripts/generate-executive-report.js +195 -6
|
@@ -25,6 +25,12 @@ Do not invent status updates.
|
|
|
25
25
|
* **Keywords:** "Tarefa", "Task", "To-Do", "Fazer", "Agenda", "Delegado".
|
|
26
26
|
* **Target File:** `data/tasks/task-log.json`.
|
|
27
27
|
* **Action:** Read file directly.
|
|
28
|
+
* **If Daily Log Query:**
|
|
29
|
+
* **Keywords:** "log diário", "diário", "daily log", "anotações", "o que anotei", "ontem", "hoje", "semana passada".
|
|
30
|
+
* **Target Folder:** `logs/daily/`.
|
|
31
|
+
* **If date provided:** Read `logs/daily/YYYY-MM-DD.md`.
|
|
32
|
+
* **If date is relative (hoje/ontem):** Resolve to an exact date and read the matching file.
|
|
33
|
+
* **If no specific date or file missing:** Route to **Search** across `logs/daily/` (or ask the user to refine the date). If Search is unavailable, say you have no records for that date and offer to list available log dates.
|
|
28
34
|
* **If Project Query:**
|
|
29
35
|
* Proceed to Project Lookup (Glob search).
|
|
30
36
|
* **Strategy:** Search recursively in `data/Clients`.
|
|
@@ -46,7 +52,13 @@ Do not invent status updates.
|
|
|
46
52
|
* **Context:** "Aqui estão suas tarefas pendentes [{Category}]:"
|
|
47
53
|
* **List:** Bullet points.
|
|
48
54
|
* Format: `* [ID-Short] {Description} ({Priority})`
|
|
49
|
-
|
|
55
|
+
* **Empty:** "Você não tem tarefas pendentes nesta categoria."
|
|
56
|
+
|
|
57
|
+
* **Daily Log Logic:**
|
|
58
|
+
* Read the Markdown file from `logs/daily/YYYY-MM-DD.md`.
|
|
59
|
+
* Return a concise excerpt (first few meaningful lines or bullet points).
|
|
60
|
+
* If the file is empty, say: "Log registrado sem detalhes para essa data."
|
|
61
|
+
* If the file does not exist: "Não encontrei log diário para esta data."
|
|
50
62
|
|
|
51
63
|
* **Project Logic:**
|
|
52
64
|
* If matches found: Read the `status.json` file(s).
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ F.R.E.Y.A. é um sistema de agentes de IA projetado para organizar seu trabalho,
|
|
|
8
8
|
|
|
9
9
|
* **Ingestão Universal:** Registre updates, blockers e notas mentais em linguagem natural.
|
|
10
10
|
* **Gestão de Tarefas:** Crie, liste e conclua tarefas ("Lembre-me de fazer X", "Minhas tarefas", "Terminei X").
|
|
11
|
-
* **Oráculo:** Pergunte sobre o status de qualquer projeto ("Como está o projeto X?").
|
|
11
|
+
* **Oráculo:** Pergunte sobre o status de qualquer projeto ("Como está o projeto X?") e recupere logs diários ("O que anotei ontem?").
|
|
12
12
|
* **Career Coach:** Gere "Brag Sheets" automáticas para suas avaliações de desempenho.
|
|
13
13
|
* **Relatórios Automatizados:** Gere resumos semanais, dailies, relatório de Scrum Master e relatórios executivos.
|
|
14
14
|
* **Blockers & Riscos:** Gere um relatório rápido de blockers priorizados por severidade.
|
package/cli/web.js
CHANGED
|
@@ -30,6 +30,12 @@ function escapeHtml(str) {
|
|
|
30
30
|
|
|
31
31
|
const APP_VERSION = readAppVersion();
|
|
32
32
|
|
|
33
|
+
const CHAT_ID_PATTERNS = [
|
|
34
|
+
/\bPTI\d{4,}-\d+\b/gi,
|
|
35
|
+
/\bINC\d+\b/gi,
|
|
36
|
+
/\bCHG\d+\b/gi
|
|
37
|
+
];
|
|
38
|
+
|
|
33
39
|
function guessNpmCmd() {
|
|
34
40
|
// We'll execute via cmd.exe on Windows for reliability.
|
|
35
41
|
return process.platform === 'win32' ? 'npm' : 'npm';
|
|
@@ -637,6 +643,61 @@ function isAllowedChatSearchPath(relPath) {
|
|
|
637
643
|
return relPath.startsWith('data/') || relPath.startsWith('logs/') || relPath.startsWith('docs/');
|
|
638
644
|
}
|
|
639
645
|
|
|
646
|
+
function extractChatIds(text) {
|
|
647
|
+
const tokens = new Set();
|
|
648
|
+
const q = String(text || '');
|
|
649
|
+
for (const re of CHAT_ID_PATTERNS) {
|
|
650
|
+
const matches = q.match(re);
|
|
651
|
+
if (matches) {
|
|
652
|
+
for (const m of matches) tokens.add(m.toUpperCase());
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return Array.from(tokens);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function extractProjectToken(text) {
|
|
659
|
+
const raw = String(text || '');
|
|
660
|
+
const m = raw.match(/project\s*[:=]\s*([A-Za-z0-9_\/-]+)/i);
|
|
661
|
+
if (m && m[1]) return m[1].trim();
|
|
662
|
+
const m2 = raw.match(/project\s*\(([^)]+)\)/i);
|
|
663
|
+
if (m2 && m2[1]) return m2[1].trim();
|
|
664
|
+
return '';
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function projectFromPath(relPath) {
|
|
668
|
+
const p = String(relPath || '');
|
|
669
|
+
const m = p.match(/data\/Clients\/([^/]+)\/([^/]+)/i);
|
|
670
|
+
if (m && m[1] && m[2]) return `${m[1]}/${m[2]}`;
|
|
671
|
+
return '';
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function matchKey(m) {
|
|
675
|
+
const ids = extractChatIds(`${m.file || ''} ${m.snippet || ''}`);
|
|
676
|
+
if (ids.length) return `id:${ids[0]}`;
|
|
677
|
+
const proj = projectFromPath(m.file) || extractProjectToken(`${m.file || ''} ${m.snippet || ''}`);
|
|
678
|
+
if (proj) return `proj:${proj.toLowerCase()}`;
|
|
679
|
+
return `file:${m.file || ''}`;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function mergeMatches(primary, secondary, limit = 8) {
|
|
683
|
+
const list = [];
|
|
684
|
+
const seen = new Set();
|
|
685
|
+
const push = (m) => {
|
|
686
|
+
if (!m || !m.file || !isAllowedChatSearchPath(m.file)) return;
|
|
687
|
+
const key = matchKey(m);
|
|
688
|
+
if (seen.has(key)) return;
|
|
689
|
+
seen.add(key);
|
|
690
|
+
list.push(m);
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
(primary || []).forEach(push);
|
|
694
|
+
(secondary || []).forEach(push);
|
|
695
|
+
|
|
696
|
+
const total = list.length;
|
|
697
|
+
const trimmed = list.slice(0, Math.max(1, Math.min(20, limit)));
|
|
698
|
+
return { matches: trimmed, total };
|
|
699
|
+
}
|
|
700
|
+
|
|
640
701
|
async function copilotSearch(workspaceDir, query, opts = {}) {
|
|
641
702
|
const q = String(query || '').trim();
|
|
642
703
|
if (!q) return { ok: false, error: 'Missing query' };
|
|
@@ -723,8 +784,8 @@ async function copilotSearch(workspaceDir, query, opts = {}) {
|
|
|
723
784
|
return { ok: true, answer, matches, evidence };
|
|
724
785
|
}
|
|
725
786
|
|
|
726
|
-
function buildChatAnswer(query, matches, summary, evidence, answer) {
|
|
727
|
-
const count = matches.length;
|
|
787
|
+
function buildChatAnswer(query, matches, summary, evidence, answer, totalCount) {
|
|
788
|
+
const count = typeof totalCount === 'number' ? totalCount : matches.length;
|
|
728
789
|
let summaryText = String(summary || '').trim();
|
|
729
790
|
let answerText = String(answer || '').trim();
|
|
730
791
|
if (!answerText) {
|
|
@@ -740,7 +801,7 @@ function buildChatAnswer(query, matches, summary, evidence, answer) {
|
|
|
740
801
|
|
|
741
802
|
const lines = [];
|
|
742
803
|
lines.push(`Encontrei ${count} registro(s).`);
|
|
743
|
-
lines.push(`
|
|
804
|
+
lines.push(`Resposta curta: ${answerText}`);
|
|
744
805
|
|
|
745
806
|
const evidences = Array.isArray(evidence) && evidence.length
|
|
746
807
|
? evidence
|
|
@@ -749,15 +810,15 @@ function buildChatAnswer(query, matches, summary, evidence, answer) {
|
|
|
749
810
|
if (!evidences.length) return lines.join('\n');
|
|
750
811
|
|
|
751
812
|
lines.push('');
|
|
752
|
-
lines.push('
|
|
813
|
+
lines.push('Detalhes:');
|
|
753
814
|
for (const m of evidences.slice(0, 5)) {
|
|
754
|
-
const parts = [];
|
|
755
|
-
if (m.date) parts.push(`**${m.date}**`);
|
|
756
|
-
if (m.file) parts.push('`' + m.file + '`');
|
|
757
|
-
const prefix = parts.length ? parts.join(' — ') + ':' : '';
|
|
758
815
|
const detail = (m.detail || m.snippet || '').toString().trim();
|
|
759
816
|
if (!detail) continue;
|
|
760
|
-
|
|
817
|
+
const meta = [];
|
|
818
|
+
if (m.file) meta.push(m.file);
|
|
819
|
+
if (m.date) meta.push(m.date);
|
|
820
|
+
const suffix = meta.length ? ` (${meta.join(' · ')})` : '';
|
|
821
|
+
lines.push(`- ${detail}${suffix}`);
|
|
761
822
|
}
|
|
762
823
|
|
|
763
824
|
return lines.join('\n');
|
|
@@ -1781,24 +1842,27 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
1781
1842
|
if (!query) return safeJson(res, 400, { error: 'Missing query' });
|
|
1782
1843
|
|
|
1783
1844
|
const copilotResult = await copilotSearch(workspaceDir, query, { limit: 8 });
|
|
1845
|
+
const indexMatches = searchIndex(workspaceDir, query, { limit: 12 });
|
|
1846
|
+
const baseMatches = indexMatches.length
|
|
1847
|
+
? indexMatches
|
|
1848
|
+
: searchWorkspace(workspaceDir, query, { limit: 12 });
|
|
1849
|
+
|
|
1784
1850
|
if (copilotResult.ok) {
|
|
1785
|
-
const
|
|
1851
|
+
const merged = mergeMatches(copilotResult.matches || [], baseMatches, 8);
|
|
1786
1852
|
const answer = buildChatAnswer(
|
|
1787
1853
|
query,
|
|
1788
|
-
matches,
|
|
1854
|
+
merged.matches,
|
|
1789
1855
|
copilotResult.summary,
|
|
1790
1856
|
copilotResult.evidence,
|
|
1791
|
-
copilotResult.answer
|
|
1857
|
+
copilotResult.answer,
|
|
1858
|
+
merged.total
|
|
1792
1859
|
);
|
|
1793
|
-
return safeJson(res, 200, { ok: true, sessionId, answer, matches });
|
|
1860
|
+
return safeJson(res, 200, { ok: true, sessionId, answer, matches: merged.matches });
|
|
1794
1861
|
}
|
|
1795
1862
|
|
|
1796
|
-
const
|
|
1797
|
-
const
|
|
1798
|
-
|
|
1799
|
-
: searchWorkspace(workspaceDir, query, { limit: 8 });
|
|
1800
|
-
const answer = buildChatAnswer(query, matches, '');
|
|
1801
|
-
return safeJson(res, 200, { ok: true, sessionId, answer, matches });
|
|
1863
|
+
const merged = mergeMatches(baseMatches, [], 8);
|
|
1864
|
+
const answer = buildChatAnswer(query, merged.matches, '', [], '', merged.total);
|
|
1865
|
+
return safeJson(res, 200, { ok: true, sessionId, answer, matches: merged.matches });
|
|
1802
1866
|
}
|
|
1803
1867
|
|
|
1804
1868
|
// Chat persistence (per session)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cccarv82/freya",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.54",
|
|
4
4
|
"description": "Personal AI Assistant with local-first persistence",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"health": "node scripts/validate-data.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-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"
|
|
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"
|
|
17
17
|
},
|
|
18
18
|
"keywords": [],
|
|
19
19
|
"author": "",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
|
|
12
|
-
const { toIsoDate, isWithinRange } = require('./lib/date-utils');
|
|
12
|
+
const { toIsoDate, isWithinRange, safeParseToMs } = require('./lib/date-utils');
|
|
13
13
|
const { safeReadJson, quarantineCorruptedFile } = require('./lib/fs-utils');
|
|
14
14
|
|
|
15
15
|
// --- Configuration ---
|
|
@@ -17,6 +17,15 @@ 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
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
|
+
|
|
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
29
|
|
|
21
30
|
// --- Helpers ---
|
|
22
31
|
function getDateRange(period) {
|
|
@@ -152,6 +161,135 @@ function getDailyLogs(start, end) {
|
|
|
152
161
|
return relevantLogs;
|
|
153
162
|
}
|
|
154
163
|
|
|
164
|
+
function summarizeLogContent(content, maxLines = 3, maxChars = 280) {
|
|
165
|
+
if (!content) return null;
|
|
166
|
+
const lines = content
|
|
167
|
+
.split('\n')
|
|
168
|
+
.map(line => line.trim())
|
|
169
|
+
.filter(line => line && !line.startsWith('#'));
|
|
170
|
+
|
|
171
|
+
const highlights = [];
|
|
172
|
+
for (const line of lines) {
|
|
173
|
+
const cleaned = line.replace(/^[-*]\s+/, '').replace(/^>\s+/, '').trim();
|
|
174
|
+
if (!cleaned) continue;
|
|
175
|
+
highlights.push(cleaned);
|
|
176
|
+
if (highlights.length >= maxLines) break;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (highlights.length === 0) return null;
|
|
180
|
+
let summary = highlights.join('; ');
|
|
181
|
+
if (summary.length > maxChars) {
|
|
182
|
+
summary = summary.slice(0, Math.max(0, maxChars - 3)).trim() + '...';
|
|
183
|
+
}
|
|
184
|
+
return summary;
|
|
185
|
+
}
|
|
186
|
+
|
|
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
|
+
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
|
+
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
|
+
|
|
155
293
|
// --- Report Generation ---
|
|
156
294
|
|
|
157
295
|
function generateReport(period) {
|
|
@@ -162,7 +300,8 @@ function generateReport(period) {
|
|
|
162
300
|
|
|
163
301
|
const tasks = getTasks(start, end);
|
|
164
302
|
const projects = getProjectUpdates(start, end);
|
|
165
|
-
|
|
303
|
+
const logs = getDailyLogs(start, end);
|
|
304
|
+
const blockers = getBlockers(start, end);
|
|
166
305
|
|
|
167
306
|
let md = `# Relatório de Status Profissional - ${dateStr}\n`;
|
|
168
307
|
md += `**Período:** ${formatDate(start)} a ${formatDate(end)}\n\n`;
|
|
@@ -171,9 +310,25 @@ function generateReport(period) {
|
|
|
171
310
|
md += `## 📋 Resumo Executivo\n`;
|
|
172
311
|
const totalDone = tasks.completed.length;
|
|
173
312
|
const activeProjects = projects.length;
|
|
174
|
-
|
|
313
|
+
const logCount = logs.length;
|
|
314
|
+
const openBlockers = blockers.open.length;
|
|
315
|
+
md += `Neste período, foram concluídas **${totalDone}** entregas focais. Atualmente há **${activeProjects}** projetos com atualizações recentes. Foram registrados **${logCount}** logs diários e existem **${openBlockers}** blockers em aberto.\n\n`;
|
|
316
|
+
|
|
317
|
+
// 2. Contexto dos Logs Diários
|
|
318
|
+
md += `## 📝 Contexto dos Logs Diários\n`;
|
|
319
|
+
if (logs.length === 0) {
|
|
320
|
+
md += `*Sem logs diários no período.*\n\n`;
|
|
321
|
+
} else {
|
|
322
|
+
logs
|
|
323
|
+
.sort((a, b) => a.date.localeCompare(b.date))
|
|
324
|
+
.forEach(log => {
|
|
325
|
+
const summary = summarizeLogContent(log.content) || 'Log registrado sem destaques.';
|
|
326
|
+
md += `- **${log.date}:** ${summary}\n`;
|
|
327
|
+
});
|
|
328
|
+
md += `\n`;
|
|
329
|
+
}
|
|
175
330
|
|
|
176
|
-
//
|
|
331
|
+
// 3. Principais Entregas
|
|
177
332
|
md += `## ✅ Principais Entregas\n`;
|
|
178
333
|
if (tasks.completed.length === 0) {
|
|
179
334
|
md += `*Nenhuma entrega registrada no período.*\n`;
|
|
@@ -185,7 +340,7 @@ function generateReport(period) {
|
|
|
185
340
|
}
|
|
186
341
|
md += `\n`;
|
|
187
342
|
|
|
188
|
-
//
|
|
343
|
+
// 4. Status dos Projetos
|
|
189
344
|
md += `## 🏗️ Status dos Projetos\n`;
|
|
190
345
|
if (projects.length === 0) {
|
|
191
346
|
md += `*Sem atualizações de projeto recentes.*\n`;
|
|
@@ -204,7 +359,41 @@ function generateReport(period) {
|
|
|
204
359
|
});
|
|
205
360
|
}
|
|
206
361
|
|
|
207
|
-
//
|
|
362
|
+
// 5. Bloqueios
|
|
363
|
+
md += `## 🚧 Bloqueios\n`;
|
|
364
|
+
if (blockers.open.length === 0) {
|
|
365
|
+
md += `*Nenhum blocker em aberto registrado.*\n`;
|
|
366
|
+
} else {
|
|
367
|
+
md += `**Em aberto:**\n`;
|
|
368
|
+
blockers.open.forEach(blocker => {
|
|
369
|
+
const title = getBlockerTitle(blocker);
|
|
370
|
+
const severity = normalizeSeverity(blocker);
|
|
371
|
+
const createdAt = getCreatedAt(blocker);
|
|
372
|
+
const createdDate = Number.isFinite(createdAt) ? toIsoDate(createdAt) : 'Unknown';
|
|
373
|
+
const project = blocker.project || blocker.projectName || blocker.projectSlug;
|
|
374
|
+
const client = blocker.client || blocker.clientName || blocker.clientSlug;
|
|
375
|
+
const metaParts = [
|
|
376
|
+
`Severidade: ${severity}`,
|
|
377
|
+
project ? `Projeto: ${project}` : null,
|
|
378
|
+
client ? `Cliente: ${client}` : null,
|
|
379
|
+
`Aberto: ${createdDate}`
|
|
380
|
+
].filter(Boolean);
|
|
381
|
+
md += `- ${title} (${metaParts.join('; ')})\n`;
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (blockers.resolvedRecent.length > 0) {
|
|
386
|
+
md += `\n**Resolvidos no período:**\n`;
|
|
387
|
+
blockers.resolvedRecent.forEach(blocker => {
|
|
388
|
+
const title = getBlockerTitle(blocker);
|
|
389
|
+
const resolvedAt = getResolvedAt(blocker);
|
|
390
|
+
const resolvedDate = Number.isFinite(resolvedAt) ? toIsoDate(resolvedAt) : 'Unknown';
|
|
391
|
+
md += `- ${title} (Resolvido: ${resolvedDate})\n`;
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
md += `\n\n`;
|
|
395
|
+
|
|
396
|
+
// 6. Próximos Passos
|
|
208
397
|
md += `## 🚀 Próximos Passos\n`;
|
|
209
398
|
if (tasks.pending.length === 0) {
|
|
210
399
|
md += `*Sem itens prioritários na fila.*\n`;
|
|
@@ -25,6 +25,12 @@ Do not invent status updates.
|
|
|
25
25
|
* **Keywords:** "Tarefa", "Task", "To-Do", "Fazer", "Agenda", "Delegado".
|
|
26
26
|
* **Target File:** `data/tasks/task-log.json`.
|
|
27
27
|
* **Action:** Read file directly.
|
|
28
|
+
* **If Daily Log Query:**
|
|
29
|
+
* **Keywords:** "log diário", "diário", "daily log", "anotações", "o que anotei", "ontem", "hoje", "semana passada".
|
|
30
|
+
* **Target Folder:** `logs/daily/`.
|
|
31
|
+
* **If date provided:** Read `logs/daily/YYYY-MM-DD.md`.
|
|
32
|
+
* **If date is relative (hoje/ontem):** Resolve to an exact date and read the matching file.
|
|
33
|
+
* **If no specific date or file missing:** Route to **Search** across `logs/daily/` (or ask the user to refine the date). If Search is unavailable, say you have no records for that date and offer to list available log dates.
|
|
28
34
|
* **If Project Query:**
|
|
29
35
|
* Proceed to Project Lookup (Glob search).
|
|
30
36
|
* **Strategy:** Search recursively in `data/Clients`.
|
|
@@ -46,7 +52,13 @@ Do not invent status updates.
|
|
|
46
52
|
* **Context:** "Aqui estão suas tarefas pendentes [{Category}]:"
|
|
47
53
|
* **List:** Bullet points.
|
|
48
54
|
* Format: `* [ID-Short] {Description} ({Priority})`
|
|
49
|
-
|
|
55
|
+
* **Empty:** "Você não tem tarefas pendentes nesta categoria."
|
|
56
|
+
|
|
57
|
+
* **Daily Log Logic:**
|
|
58
|
+
* Read the Markdown file from `logs/daily/YYYY-MM-DD.md`.
|
|
59
|
+
* Return a concise excerpt (first few meaningful lines or bullet points).
|
|
60
|
+
* If the file is empty, say: "Log registrado sem detalhes para essa data."
|
|
61
|
+
* If the file does not exist: "Não encontrei log diário para esta data."
|
|
50
62
|
|
|
51
63
|
* **Project Logic:**
|
|
52
64
|
* If matches found: Read the `status.json` file(s).
|
|
@@ -96,6 +108,7 @@ Em atraso devido a condições climáticas.
|
|
|
96
108
|
<persona>
|
|
97
109
|
Maintain the F.R.E.Y.A. persona defined in `master.mdc`.
|
|
98
110
|
Tone: Analytical, Precise, Data-Driven.
|
|
111
|
+
Obsidian: Quando citar projetos/notas, prefira nomes e links em formato Obsidian (wikilinks `[[...]]`) quando aplicável.
|
|
99
112
|
Signature:
|
|
100
113
|
— FREYA
|
|
101
114
|
Assistente Responsiva com Otimização Aprimorada
|
package/templates/base/README.md
CHANGED
|
@@ -8,7 +8,7 @@ F.R.E.Y.A. é um sistema de agentes de IA projetado para organizar seu trabalho,
|
|
|
8
8
|
|
|
9
9
|
* **Ingestão Universal:** Registre updates, blockers e notas mentais em linguagem natural.
|
|
10
10
|
* **Gestão de Tarefas:** Crie, liste e conclua tarefas ("Lembre-me de fazer X", "Minhas tarefas", "Terminei X").
|
|
11
|
-
* **Oráculo:** Pergunte sobre o status de qualquer projeto ("Como está o projeto X?").
|
|
11
|
+
* **Oráculo:** Pergunte sobre o status de qualquer projeto ("Como está o projeto X?") e recupere logs diários ("O que anotei ontem?").
|
|
12
12
|
* **Career Coach:** Gere "Brag Sheets" automáticas para suas avaliações de desempenho.
|
|
13
13
|
* **Relatórios Automatizados:** Gere resumos semanais, dailies, relatório de Scrum Master e relatórios executivos.
|
|
14
14
|
* **Blockers & Riscos:** Gere um relatório rápido de blockers priorizados por severidade.
|
|
@@ -16,9 +16,68 @@ F.R.E.Y.A. é um sistema de agentes de IA projetado para organizar seu trabalho,
|
|
|
16
16
|
* **Git Automation:** Gere commits inteligentes automaticamente. A Freya analisa suas mudanças e escreve a mensagem para você.
|
|
17
17
|
* **Privacidade Total:** Seus dados (JSON e Markdown) ficam 100% locais na sua máquina.
|
|
18
18
|
|
|
19
|
+
## 📦 Instalação (CLI)
|
|
20
|
+
|
|
21
|
+
Você pode usar a FREYA como um CLI para **inicializar uma workspace** completa (agents + scripts + data) em qualquer diretório.
|
|
22
|
+
|
|
23
|
+
## 🚢 Publicação no npm (maintainers)
|
|
24
|
+
|
|
25
|
+
Este repositório suporta publicação automática via GitHub Actions.
|
|
26
|
+
|
|
27
|
+
### Pré-requisitos
|
|
28
|
+
1) Ter permissão de publish no pacote `@cccarv82/freya` no npm.
|
|
29
|
+
2) Criar o secret no GitHub: `NPM_TOKEN` (Automation token do npm com permissão de publish).
|
|
30
|
+
|
|
31
|
+
### Como publicar
|
|
32
|
+
1) Atualize a versão e crie uma tag `vX.Y.Z`:
|
|
33
|
+
```bash
|
|
34
|
+
npm version patch
|
|
35
|
+
# ou minor/major
|
|
36
|
+
|
|
37
|
+
git push --follow-tags
|
|
38
|
+
```
|
|
39
|
+
2) A Action `npm-publish` roda no push da tag e executa `npm publish --access public`.
|
|
40
|
+
|
|
41
|
+
### Via npx (recomendado)
|
|
42
|
+
```bash
|
|
43
|
+
npx @cccarv82/freya init
|
|
44
|
+
# cria ./freya
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Via instalação global
|
|
48
|
+
```bash
|
|
49
|
+
npm i -g @cccarv82/freya
|
|
50
|
+
freya init
|
|
51
|
+
# cria ./freya
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Modos do `init`
|
|
55
|
+
```bash
|
|
56
|
+
freya init # cria ./freya
|
|
57
|
+
freya init meu-projeto # cria ./meu-projeto
|
|
58
|
+
freya init --here # instala no diretório atual
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Atualizar uma workspace existente (sem perder dados)
|
|
62
|
+
Por padrão, ao rodar `init` em uma pasta existente, o CLI **preserva**:
|
|
63
|
+
- `data/**`
|
|
64
|
+
- `logs/**`
|
|
65
|
+
|
|
66
|
+
E atualiza/instala normalmente:
|
|
67
|
+
- `.agent/**`
|
|
68
|
+
- `scripts/**`
|
|
69
|
+
- `README.md`, `USER_GUIDE.md`
|
|
70
|
+
- `package.json` (merge de scripts)
|
|
71
|
+
|
|
72
|
+
Flags (use com cuidado):
|
|
73
|
+
```bash
|
|
74
|
+
freya init --here --force-data # permite sobrescrever data/
|
|
75
|
+
freya init --here --force-logs # permite sobrescrever logs/
|
|
76
|
+
```
|
|
77
|
+
|
|
19
78
|
## 🚀 Como Usar
|
|
20
79
|
|
|
21
|
-
1. Abra
|
|
80
|
+
1. Abra a pasta da workspace gerada (ex.: `./freya`) na **sua IDE**.
|
|
22
81
|
2. No chat da IDE (ex: Ctrl+L / Cmd+L), digite:
|
|
23
82
|
> `@freya Ajuda`
|
|
24
83
|
3. Siga as instruções da assistente.
|
|
@@ -5,6 +5,39 @@ Este sistema foi projetado para ser seu assistente pessoal de produtividade, ope
|
|
|
5
5
|
|
|
6
6
|
## 🚀 Como Iniciar
|
|
7
7
|
|
|
8
|
+
### 1) Criar uma workspace (CLI)
|
|
9
|
+
Você pode inicializar uma workspace completa (agents + scripts + data) em qualquer diretório.
|
|
10
|
+
|
|
11
|
+
**Via npx (recomendado):**
|
|
12
|
+
```bash
|
|
13
|
+
npx @cccarv82/freya init
|
|
14
|
+
# cria ./freya
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**Via instalação global:**
|
|
18
|
+
```bash
|
|
19
|
+
npm i -g @cccarv82/freya
|
|
20
|
+
freya init
|
|
21
|
+
# cria ./freya
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Modos do init:**
|
|
25
|
+
```bash
|
|
26
|
+
freya init # cria ./freya
|
|
27
|
+
freya init meu-projeto # cria ./meu-projeto
|
|
28
|
+
freya init --here # instala no diretório atual
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Upgrade sem perder dados (recomendado):**
|
|
32
|
+
Ao rodar `init` em uma pasta já existente, o CLI **preserva automaticamente** `data/**` e `logs/**` (se não estiverem vazios) e atualiza o restante.
|
|
33
|
+
|
|
34
|
+
Se você quiser sobrescrever explicitamente:
|
|
35
|
+
```bash
|
|
36
|
+
freya init --here --force-data
|
|
37
|
+
freya init --here --force-logs
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 2) Interagir no chat da IDE
|
|
8
41
|
Para interagir com a assistente, basta chamá-la no chat da sua IDE:
|
|
9
42
|
|
|
10
43
|
> `@freya [sua mensagem]`
|
|
@@ -39,6 +72,10 @@ Recupere o contexto de qualquer projeto instantaneamente.
|
|
|
39
72
|
> "Como está o projeto Vivo 5G?"
|
|
40
73
|
* *Resultado:* Resumo executivo do status atual e das últimas 3 atualizações.
|
|
41
74
|
|
|
75
|
+
* **Consulta de Logs Diários:**
|
|
76
|
+
> "O que anotei ontem?"
|
|
77
|
+
* *Resultado:* Retorna um trecho do log diário em `logs/daily/YYYY-MM-DD.md` (ou direciona para busca quando a data não estiver clara).
|
|
78
|
+
|
|
42
79
|
* **Anti-Alucinação:**
|
|
43
80
|
A FREYA sempre citará a fonte da informação (ex: `(Source: data/Clients/vivo/5g/status.json)`). Se ela não souber, ela dirá explicitamente.
|
|
44
81
|
|
|
@@ -80,7 +117,7 @@ Transforme seus logs em relatórios úteis sem esforço. Peça à FREYA no chat
|
|
|
80
117
|
|
|
81
118
|
* **Relatório de Status Profissional (Executivo):**
|
|
82
119
|
> "Gerar status report", "Relatório Executivo"
|
|
83
|
-
* *Resultado:* Gera um relatório Markdown completo com Resumo Executivo, Entregas, Status de Projetos e Bloqueios. Ideal para enviar stakeholders.
|
|
120
|
+
* *Resultado:* Gera um relatório Markdown completo com Resumo Executivo, Contexto dos Logs Diários, Entregas, Status de Projetos e Bloqueios. Ideal para enviar stakeholders.
|
|
84
121
|
* *Manual:* `npm run status -- --period [daily|weekly]`
|
|
85
122
|
|
|
86
123
|
* **Relatório Scrum Master (Semanal):**
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
|
|
12
|
-
const { toIsoDate, isWithinRange } = require('./lib/date-utils');
|
|
12
|
+
const { toIsoDate, isWithinRange, safeParseToMs } = require('./lib/date-utils');
|
|
13
13
|
const { safeReadJson, quarantineCorruptedFile } = require('./lib/fs-utils');
|
|
14
14
|
|
|
15
15
|
// --- Configuration ---
|
|
@@ -17,6 +17,15 @@ 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
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
|
+
|
|
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
29
|
|
|
21
30
|
// --- Helpers ---
|
|
22
31
|
function getDateRange(period) {
|
|
@@ -152,6 +161,135 @@ function getDailyLogs(start, end) {
|
|
|
152
161
|
return relevantLogs;
|
|
153
162
|
}
|
|
154
163
|
|
|
164
|
+
function summarizeLogContent(content, maxLines = 3, maxChars = 280) {
|
|
165
|
+
if (!content) return null;
|
|
166
|
+
const lines = content
|
|
167
|
+
.split('\n')
|
|
168
|
+
.map(line => line.trim())
|
|
169
|
+
.filter(line => line && !line.startsWith('#'));
|
|
170
|
+
|
|
171
|
+
const highlights = [];
|
|
172
|
+
for (const line of lines) {
|
|
173
|
+
const cleaned = line.replace(/^[-*]\s+/, '').replace(/^>\s+/, '').trim();
|
|
174
|
+
if (!cleaned) continue;
|
|
175
|
+
highlights.push(cleaned);
|
|
176
|
+
if (highlights.length >= maxLines) break;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (highlights.length === 0) return null;
|
|
180
|
+
let summary = highlights.join('; ');
|
|
181
|
+
if (summary.length > maxChars) {
|
|
182
|
+
summary = summary.slice(0, Math.max(0, maxChars - 3)).trim() + '...';
|
|
183
|
+
}
|
|
184
|
+
return summary;
|
|
185
|
+
}
|
|
186
|
+
|
|
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
|
+
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
|
+
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
|
+
|
|
155
293
|
// --- Report Generation ---
|
|
156
294
|
|
|
157
295
|
function generateReport(period) {
|
|
@@ -162,7 +300,8 @@ function generateReport(period) {
|
|
|
162
300
|
|
|
163
301
|
const tasks = getTasks(start, end);
|
|
164
302
|
const projects = getProjectUpdates(start, end);
|
|
165
|
-
|
|
303
|
+
const logs = getDailyLogs(start, end);
|
|
304
|
+
const blockers = getBlockers(start, end);
|
|
166
305
|
|
|
167
306
|
let md = `# Relatório de Status Profissional - ${dateStr}\n`;
|
|
168
307
|
md += `**Período:** ${formatDate(start)} a ${formatDate(end)}\n\n`;
|
|
@@ -171,9 +310,25 @@ function generateReport(period) {
|
|
|
171
310
|
md += `## 📋 Resumo Executivo\n`;
|
|
172
311
|
const totalDone = tasks.completed.length;
|
|
173
312
|
const activeProjects = projects.length;
|
|
174
|
-
|
|
313
|
+
const logCount = logs.length;
|
|
314
|
+
const openBlockers = blockers.open.length;
|
|
315
|
+
md += `Neste período, foram concluídas **${totalDone}** entregas focais. Atualmente há **${activeProjects}** projetos com atualizações recentes. Foram registrados **${logCount}** logs diários e existem **${openBlockers}** blockers em aberto.\n\n`;
|
|
316
|
+
|
|
317
|
+
// 2. Contexto dos Logs Diários
|
|
318
|
+
md += `## 📝 Contexto dos Logs Diários\n`;
|
|
319
|
+
if (logs.length === 0) {
|
|
320
|
+
md += `*Sem logs diários no período.*\n\n`;
|
|
321
|
+
} else {
|
|
322
|
+
logs
|
|
323
|
+
.sort((a, b) => a.date.localeCompare(b.date))
|
|
324
|
+
.forEach(log => {
|
|
325
|
+
const summary = summarizeLogContent(log.content) || 'Log registrado sem destaques.';
|
|
326
|
+
md += `- **${log.date}:** ${summary}\n`;
|
|
327
|
+
});
|
|
328
|
+
md += `\n`;
|
|
329
|
+
}
|
|
175
330
|
|
|
176
|
-
//
|
|
331
|
+
// 3. Principais Entregas
|
|
177
332
|
md += `## ✅ Principais Entregas\n`;
|
|
178
333
|
if (tasks.completed.length === 0) {
|
|
179
334
|
md += `*Nenhuma entrega registrada no período.*\n`;
|
|
@@ -185,7 +340,7 @@ function generateReport(period) {
|
|
|
185
340
|
}
|
|
186
341
|
md += `\n`;
|
|
187
342
|
|
|
188
|
-
//
|
|
343
|
+
// 4. Status dos Projetos
|
|
189
344
|
md += `## 🏗️ Status dos Projetos\n`;
|
|
190
345
|
if (projects.length === 0) {
|
|
191
346
|
md += `*Sem atualizações de projeto recentes.*\n`;
|
|
@@ -204,7 +359,41 @@ function generateReport(period) {
|
|
|
204
359
|
});
|
|
205
360
|
}
|
|
206
361
|
|
|
207
|
-
//
|
|
362
|
+
// 5. Bloqueios
|
|
363
|
+
md += `## 🚧 Bloqueios\n`;
|
|
364
|
+
if (blockers.open.length === 0) {
|
|
365
|
+
md += `*Nenhum blocker em aberto registrado.*\n`;
|
|
366
|
+
} else {
|
|
367
|
+
md += `**Em aberto:**\n`;
|
|
368
|
+
blockers.open.forEach(blocker => {
|
|
369
|
+
const title = getBlockerTitle(blocker);
|
|
370
|
+
const severity = normalizeSeverity(blocker);
|
|
371
|
+
const createdAt = getCreatedAt(blocker);
|
|
372
|
+
const createdDate = Number.isFinite(createdAt) ? toIsoDate(createdAt) : 'Unknown';
|
|
373
|
+
const project = blocker.project || blocker.projectName || blocker.projectSlug;
|
|
374
|
+
const client = blocker.client || blocker.clientName || blocker.clientSlug;
|
|
375
|
+
const metaParts = [
|
|
376
|
+
`Severidade: ${severity}`,
|
|
377
|
+
project ? `Projeto: ${project}` : null,
|
|
378
|
+
client ? `Cliente: ${client}` : null,
|
|
379
|
+
`Aberto: ${createdDate}`
|
|
380
|
+
].filter(Boolean);
|
|
381
|
+
md += `- ${title} (${metaParts.join('; ')})\n`;
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (blockers.resolvedRecent.length > 0) {
|
|
386
|
+
md += `\n**Resolvidos no período:**\n`;
|
|
387
|
+
blockers.resolvedRecent.forEach(blocker => {
|
|
388
|
+
const title = getBlockerTitle(blocker);
|
|
389
|
+
const resolvedAt = getResolvedAt(blocker);
|
|
390
|
+
const resolvedDate = Number.isFinite(resolvedAt) ? toIsoDate(resolvedAt) : 'Unknown';
|
|
391
|
+
md += `- ${title} (Resolvido: ${resolvedDate})\n`;
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
md += `\n\n`;
|
|
395
|
+
|
|
396
|
+
// 6. Próximos Passos
|
|
208
397
|
md += `## 🚀 Próximos Passos\n`;
|
|
209
398
|
if (tasks.pending.length === 0) {
|
|
210
399
|
md += `*Sem itens prioritários na fila.*\n`;
|