@cccarv82/freya 2.3.13 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agent/rules/freya/agents/coach.mdc +7 -16
- package/.agent/rules/freya/agents/ingestor.mdc +1 -89
- package/.agent/rules/freya/agents/master.mdc +3 -0
- package/.agent/rules/freya/agents/oracle.mdc +7 -23
- package/cli/web-ui.css +860 -182
- package/cli/web-ui.js +547 -175
- package/cli/web.js +690 -536
- package/package.json +6 -3
- package/scripts/build-vector-index.js +85 -0
- package/scripts/export-obsidian.js +6 -16
- package/scripts/generate-blockers-report.js +5 -17
- package/scripts/generate-daily-summary.js +25 -58
- package/scripts/generate-executive-report.js +22 -204
- package/scripts/generate-sm-weekly-report.js +27 -92
- package/scripts/lib/DataLayer.js +92 -0
- package/scripts/lib/DataManager.js +198 -0
- package/scripts/lib/Embedder.js +59 -0
- package/scripts/lib/schema.js +23 -0
- package/scripts/migrate-v1-v2.js +184 -0
- package/scripts/validate-data.js +48 -51
- package/scripts/validate-structure.js +12 -58
- package/templates/base/scripts/build-vector-index.js +85 -0
- package/templates/base/scripts/export-obsidian.js +143 -0
- package/templates/base/scripts/generate-daily-summary.js +25 -58
- package/templates/base/scripts/generate-executive-report.js +14 -225
- package/templates/base/scripts/generate-sm-weekly-report.js +9 -91
- package/templates/base/scripts/index/build-index.js +13 -0
- package/templates/base/scripts/index/update-index.js +15 -0
- package/templates/base/scripts/lib/DataLayer.js +92 -0
- package/templates/base/scripts/lib/DataManager.js +198 -0
- package/templates/base/scripts/lib/Embedder.js +59 -0
- package/templates/base/scripts/lib/index-utils.js +407 -0
- package/templates/base/scripts/lib/schema.js +23 -0
- package/templates/base/scripts/lib/search-utils.js +183 -0
- package/templates/base/scripts/migrate-v1-v2.js +184 -0
- package/templates/base/scripts/validate-data.js +48 -51
- package/templates/base/scripts/validate-structure.js +10 -32
|
@@ -1,61 +1,39 @@
|
|
|
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
|
|
41
|
-
const
|
|
42
|
-
if (!
|
|
43
|
-
|
|
44
|
-
|
|
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;
|
|
22
|
+
function normalizeStatus(blocker) {
|
|
23
|
+
const raw = blocker.status || blocker.state || blocker.currentStatus;
|
|
24
|
+
if (!raw) return 'UNKNOWN';
|
|
25
|
+
return String(raw).trim().toUpperCase();
|
|
26
|
+
}
|
|
53
27
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return
|
|
28
|
+
function normalizeSeverity(blocker) {
|
|
29
|
+
const raw = blocker.severity || blocker.priority || blocker.level;
|
|
30
|
+
if (!raw) return 'UNSPECIFIED';
|
|
31
|
+
const value = String(raw).trim().toUpperCase();
|
|
32
|
+
if (value.includes('CRIT')) return 'CRITICAL';
|
|
33
|
+
if (value.includes('HIGH')) return 'HIGH';
|
|
34
|
+
if (value.includes('MED')) return 'MEDIUM';
|
|
35
|
+
if (value.includes('LOW')) return 'LOW';
|
|
36
|
+
return value;
|
|
59
37
|
}
|
|
60
38
|
|
|
61
39
|
function generate() {
|
|
@@ -67,40 +45,15 @@ function generate() {
|
|
|
67
45
|
start.setDate(now.getDate() - 7);
|
|
68
46
|
|
|
69
47
|
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: [] });
|
|
48
|
+
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
49
|
|
|
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
|
-
});
|
|
50
|
+
const { completed: completedTasks, pending: pendingDoNow } = dm.getTasks(start, end);
|
|
51
|
+
const { open: activeBlockers, blockers } = dm.getBlockers(start, end);
|
|
98
52
|
|
|
99
53
|
const upcomingDueBlockers = blockers.filter(b => {
|
|
100
|
-
const st =
|
|
54
|
+
const st = normalizeStatus(b);
|
|
101
55
|
if (st !== 'OPEN') return false;
|
|
102
56
|
if (!b.dueDate) return false;
|
|
103
|
-
// dueDate is an ISO date; treat as UTC midnight
|
|
104
57
|
const dueMs = safeParseToMs(String(b.dueDate));
|
|
105
58
|
if (!Number.isFinite(dueMs)) return false;
|
|
106
59
|
const due = new Date(dueMs);
|
|
@@ -109,25 +62,7 @@ const reportTime = (() => { const d = new Date(); const hh = String(d.getHours()
|
|
|
109
62
|
return due >= now && due <= soonEnd;
|
|
110
63
|
});
|
|
111
64
|
|
|
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
|
-
}
|
|
65
|
+
const projectsWithUpdates = dm.getProjectUpdates(start, end);
|
|
131
66
|
|
|
132
67
|
// Markdown
|
|
133
68
|
let md = `# Scrum Master Weekly Report — ${reportDate}\n`;
|
|
@@ -153,13 +88,13 @@ const reportTime = (() => { const d = new Date(); const hh = String(d.getHours()
|
|
|
153
88
|
md += `None.\n\n`;
|
|
154
89
|
} else {
|
|
155
90
|
activeBlockers.forEach(b => {
|
|
156
|
-
const createdMs = safeParseToMs(b.createdAt) || Date.now();
|
|
91
|
+
const createdMs = safeParseToMs(b.created_at || b.createdAt) || Date.now();
|
|
157
92
|
const agingDays = daysBetweenMs(createdMs, Date.now());
|
|
158
|
-
const sev =
|
|
159
|
-
const st =
|
|
93
|
+
const sev = normalizeSeverity(b);
|
|
94
|
+
const st = normalizeStatus(b);
|
|
160
95
|
const owner = b.owner ? `; owner: ${b.owner}` : '';
|
|
161
96
|
const proj = b.projectSlug ? `; project: ${b.projectSlug}` : '';
|
|
162
|
-
const next = b.nextAction ? `; next: ${b.nextAction}` : '';
|
|
97
|
+
const next = b.next_action || b.nextAction ? `; next: ${b.next_action || b.nextAction}` : '';
|
|
163
98
|
md += `- [${sev}/${st}] ${b.title} (aging: ${agingDays}d${owner}${proj}${next})\n`;
|
|
164
99
|
});
|
|
165
100
|
md += `\n`;
|
|
@@ -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 };
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DataManager.js (V2 - SQLite Powered)
|
|
3
|
+
* Centralized data access layer for F.R.E.Y.A. reports and scripts.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { isWithinRange, formatDate } = require('./date-utils');
|
|
9
|
+
const SCHEMA = require('./schema');
|
|
10
|
+
const { defaultInstance: dl } = require('./DataLayer');
|
|
11
|
+
const { defaultEmbedder } = require('./Embedder');
|
|
12
|
+
|
|
13
|
+
class DataManager {
|
|
14
|
+
constructor(dataDir, logsDir) {
|
|
15
|
+
// Keeping args for backwards compatibility, though SQLite handles the storage now
|
|
16
|
+
this.dataDir = dataDir;
|
|
17
|
+
this.logsDir = logsDir;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// --- Helpers (Deprecated but kept for backwards compat if needed elsewhere) ---
|
|
21
|
+
readJsonOrQuarantine(filePath, fallback) {
|
|
22
|
+
return fallback;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// --- Tasks ---
|
|
26
|
+
getTasksRaw() {
|
|
27
|
+
return dl.db.prepare('SELECT * FROM tasks').all().map(t => {
|
|
28
|
+
const meta = t.metadata ? JSON.parse(t.metadata) : {};
|
|
29
|
+
return {
|
|
30
|
+
id: t.id,
|
|
31
|
+
projectSlug: t.project_slug,
|
|
32
|
+
description: t.description,
|
|
33
|
+
category: t.category,
|
|
34
|
+
status: t.status,
|
|
35
|
+
createdAt: t.created_at,
|
|
36
|
+
completedAt: t.completed_at,
|
|
37
|
+
priority: meta.priority,
|
|
38
|
+
streamSlug: meta.streamSlug
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
getTasks(start, end) {
|
|
44
|
+
// SQLite date func: use ISO format
|
|
45
|
+
const toIso = (d) => d ? (d instanceof Date ? d.toISOString() : new Date(d).toISOString()) : null;
|
|
46
|
+
const startIso = toIso(start);
|
|
47
|
+
const endIso = toIso(end);
|
|
48
|
+
|
|
49
|
+
const tasks = this.getTasksRaw();
|
|
50
|
+
const completed = dl.db.prepare(`
|
|
51
|
+
SELECT * FROM tasks
|
|
52
|
+
WHERE status = 'COMPLETED'
|
|
53
|
+
AND completed_at >= ? AND completed_at <= ?
|
|
54
|
+
`).all(startIso, endIso).map(t => ({ ...t, completedAt: t.completed_at, createdAt: t.created_at }));
|
|
55
|
+
|
|
56
|
+
const pending = dl.db.prepare(`
|
|
57
|
+
SELECT * FROM tasks
|
|
58
|
+
WHERE status = 'PENDING' AND category = 'DO_NOW'
|
|
59
|
+
`).all().map(t => ({ ...t, createdAt: t.created_at }));
|
|
60
|
+
|
|
61
|
+
return { tasks, completed, pending };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// --- Blockers ---
|
|
65
|
+
getBlockersRaw() {
|
|
66
|
+
return dl.db.prepare('SELECT * FROM blockers').all().map(b => {
|
|
67
|
+
const meta = b.metadata ? JSON.parse(b.metadata) : {};
|
|
68
|
+
return {
|
|
69
|
+
id: b.id,
|
|
70
|
+
projectSlug: b.project_slug,
|
|
71
|
+
title: b.title,
|
|
72
|
+
severity: b.severity,
|
|
73
|
+
status: b.status,
|
|
74
|
+
owner: b.owner,
|
|
75
|
+
nextAction: b.next_action,
|
|
76
|
+
createdAt: b.created_at,
|
|
77
|
+
resolvedAt: b.resolved_at,
|
|
78
|
+
streamSlug: meta.streamSlug
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
getBlockers(start, end) {
|
|
84
|
+
const toIso = (d) => d ? (d instanceof Date ? d.toISOString() : new Date(d).toISOString()) : null;
|
|
85
|
+
const startIso = toIso(start);
|
|
86
|
+
const endIso = toIso(end);
|
|
87
|
+
|
|
88
|
+
const blockers = this.getBlockersRaw();
|
|
89
|
+
|
|
90
|
+
const open = dl.db.prepare(`
|
|
91
|
+
SELECT * FROM blockers
|
|
92
|
+
WHERE status NOT IN ('RESOLVED', 'CLOSED', 'DONE', 'FIXED')
|
|
93
|
+
AND resolved_at IS NULL
|
|
94
|
+
ORDER BY
|
|
95
|
+
CASE severity
|
|
96
|
+
WHEN 'CRITICAL' THEN 0
|
|
97
|
+
WHEN 'HIGH' THEN 1
|
|
98
|
+
WHEN 'MEDIUM' THEN 2
|
|
99
|
+
WHEN 'LOW' THEN 3
|
|
100
|
+
ELSE 99
|
|
101
|
+
END ASC,
|
|
102
|
+
created_at ASC
|
|
103
|
+
`).all().map(b => ({ ...b, projectSlug: b.project_slug, createdAt: b.created_at }));
|
|
104
|
+
|
|
105
|
+
const openedRecent = dl.db.prepare(`
|
|
106
|
+
SELECT * FROM blockers
|
|
107
|
+
WHERE created_at >= ? AND created_at <= ?
|
|
108
|
+
`).all(startIso, endIso).map(b => ({ ...b, projectSlug: b.project_slug, createdAt: b.created_at }));
|
|
109
|
+
|
|
110
|
+
const resolvedRecent = dl.db.prepare(`
|
|
111
|
+
SELECT * FROM blockers
|
|
112
|
+
WHERE resolved_at >= ? AND resolved_at <= ?
|
|
113
|
+
`).all(startIso, endIso).map(b => ({ ...b, projectSlug: b.project_slug, resolvedAt: b.resolved_at }));
|
|
114
|
+
|
|
115
|
+
return { blockers, open, openedRecent, resolvedRecent };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// --- Project Updates ---
|
|
119
|
+
getProjectUpdates(start, end) {
|
|
120
|
+
const toIso = (d) => d ? (d instanceof Date ? d.toISOString() : new Date(d).toISOString()) : null;
|
|
121
|
+
const startIso = toIso(start);
|
|
122
|
+
const endIso = toIso(end);
|
|
123
|
+
|
|
124
|
+
const updates = [];
|
|
125
|
+
|
|
126
|
+
// Find active projects with recent history
|
|
127
|
+
const activeProjects = dl.db.prepare(`SELECT * FROM projects WHERE is_active = 1`).all();
|
|
128
|
+
|
|
129
|
+
for (const proj of activeProjects) {
|
|
130
|
+
const recentHistory = dl.db.prepare(`
|
|
131
|
+
SELECT status_text, date FROM project_status_history
|
|
132
|
+
WHERE project_id = ? AND date >= ? AND date <= ?
|
|
133
|
+
ORDER BY date DESC
|
|
134
|
+
`).all(proj.id, startIso, endIso).map(h => ({
|
|
135
|
+
content: h.status_text,
|
|
136
|
+
date: h.date
|
|
137
|
+
}));
|
|
138
|
+
|
|
139
|
+
// We determine currentStatus as the latest from history, or a placeholder if none
|
|
140
|
+
const latest = dl.db.prepare(`
|
|
141
|
+
SELECT status_text FROM project_status_history
|
|
142
|
+
WHERE project_id = ? ORDER BY date DESC LIMIT 1
|
|
143
|
+
`).get(proj.id);
|
|
144
|
+
|
|
145
|
+
const currentStatus = latest ? latest.status_text : "Initialized";
|
|
146
|
+
|
|
147
|
+
if (recentHistory.length > 0) {
|
|
148
|
+
updates.push({
|
|
149
|
+
client: proj.client,
|
|
150
|
+
project: proj.name,
|
|
151
|
+
slug: proj.slug,
|
|
152
|
+
currentStatus: currentStatus,
|
|
153
|
+
events: recentHistory
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return updates;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// --- Daily Logs ---
|
|
162
|
+
getDailyLogs(start, end) {
|
|
163
|
+
function toIso(d) { return (d instanceof Date ? d : new Date(d)).toISOString().slice(0, 10); }
|
|
164
|
+
const startIso = formatDate ? formatDate(start) : toIso(start);
|
|
165
|
+
const endIso = formatDate ? formatDate(end) : toIso(end);
|
|
166
|
+
|
|
167
|
+
return dl.db.prepare(`
|
|
168
|
+
SELECT date, raw_markdown as content FROM daily_logs
|
|
169
|
+
WHERE date >= ? AND date <= ?
|
|
170
|
+
ORDER BY date ASC
|
|
171
|
+
`).all(startIso, endIso);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// --- RAG (Vector Search) ---
|
|
175
|
+
async semanticSearch(query, topK = 10) {
|
|
176
|
+
const queryVector = await defaultEmbedder.embedText(query);
|
|
177
|
+
|
|
178
|
+
// Fetch all stored embeddings. For a local personal tool with < 100k chunks, in-memory cosine sim is perfectly fast.
|
|
179
|
+
const rows = dl.db.prepare(`
|
|
180
|
+
SELECT reference_type, reference_id, chunk_index, text_chunk, embedding
|
|
181
|
+
FROM document_embeddings
|
|
182
|
+
`).all();
|
|
183
|
+
|
|
184
|
+
const scored = [];
|
|
185
|
+
for (const row of rows) {
|
|
186
|
+
const chunkVector = defaultEmbedder.bufferToVector(row.embedding);
|
|
187
|
+
const score = defaultEmbedder.cosineSimilarity(queryVector, chunkVector);
|
|
188
|
+
scored.push({ ...row, score });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Sort pure descending by similarity score
|
|
192
|
+
scored.sort((a, b) => b.score - a.score);
|
|
193
|
+
|
|
194
|
+
return scored.slice(0, topK);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = DataManager;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const { pipeline } = require('@xenova/transformers');
|
|
2
|
+
|
|
3
|
+
class Embedder {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.modelName = 'Xenova/all-MiniLM-L6-v2';
|
|
6
|
+
this.extractorInfo = null;
|
|
7
|
+
this.initPromise = null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async init() {
|
|
11
|
+
if (this.extractorInfo) return;
|
|
12
|
+
if (!this.initPromise) {
|
|
13
|
+
this.initPromise = pipeline('feature-extraction', this.modelName, { quantized: true })
|
|
14
|
+
.then((ex) => {
|
|
15
|
+
this.extractorInfo = ex;
|
|
16
|
+
})
|
|
17
|
+
.catch((err) => {
|
|
18
|
+
this.initPromise = null;
|
|
19
|
+
throw err;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
await this.initPromise;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async embedText(text) {
|
|
26
|
+
await this.init();
|
|
27
|
+
const cleanText = String(text || '').trim();
|
|
28
|
+
if (!cleanText) return new Float32Array(384); // Empty zero vector for model
|
|
29
|
+
|
|
30
|
+
const output = await this.extractorInfo(cleanText, { pooling: 'mean', normalize: true });
|
|
31
|
+
return new Float32Array(output.data);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Prepares a Float32Array into a Node.js Buffer for SQLite BLOB storage
|
|
35
|
+
vectorToBuffer(float32Array) {
|
|
36
|
+
return Buffer.from(float32Array.buffer);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Parses a SQLite BLOB Buffer back into a Float32Array
|
|
40
|
+
bufferToVector(buffer) {
|
|
41
|
+
return new Float32Array(buffer.buffer, buffer.byteOffset, buffer.length / Float32Array.BYTES_PER_ELEMENT);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
cosineSimilarity(vecA, vecB) {
|
|
45
|
+
let dotProduct = 0;
|
|
46
|
+
let normA = 0;
|
|
47
|
+
let normB = 0;
|
|
48
|
+
for (let i = 0; i < vecA.length; i++) {
|
|
49
|
+
dotProduct += vecA[i] * vecB[i];
|
|
50
|
+
normA += vecA[i] * vecA[i];
|
|
51
|
+
normB += vecB[i] * vecB[i];
|
|
52
|
+
}
|
|
53
|
+
if (normA === 0 || normB === 0) return 0;
|
|
54
|
+
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const defaultEmbedder = new Embedder();
|
|
59
|
+
module.exports = { Embedder, defaultEmbedder };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* schema.js
|
|
3
|
+
* Centralized schema constants for F.R.E.Y.A. data structures.
|
|
4
|
+
* This is the single source of truth for validation and ingestion.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
TASK: {
|
|
9
|
+
CATEGORIES: ['DO_NOW', 'SCHEDULE', 'DELEGATE', 'IGNORE'],
|
|
10
|
+
STATUSES: ['PENDING', 'COMPLETED', 'ARCHIVED'],
|
|
11
|
+
PRIORITIES: ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']
|
|
12
|
+
},
|
|
13
|
+
BLOCKER: {
|
|
14
|
+
STATUSES: ['OPEN', 'MITIGATING', 'RESOLVED'],
|
|
15
|
+
SEVERITIES: ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']
|
|
16
|
+
},
|
|
17
|
+
CAREER: {
|
|
18
|
+
TYPES: ['Achievement', 'Feedback', 'Certification', 'Goal']
|
|
19
|
+
},
|
|
20
|
+
PROJECT: {
|
|
21
|
+
HISTORY_TYPES: ['Status', 'Decision', 'Risk', 'Achievement', 'Feedback', 'Goal', 'Blocker']
|
|
22
|
+
}
|
|
23
|
+
};
|