@cccarv82/freya 3.1.0 → 3.2.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/cli/web-ui.js CHANGED
@@ -2498,6 +2498,15 @@
2498
2498
  msg += 'Contexto registrado no log diário. Nenhuma tarefa ou blocker identificado.\n';
2499
2499
  }
2500
2500
 
2501
+ // Show semantic duplicates detected
2502
+ if (summary && Array.isArray(summary.semanticDups) && summary.semanticDups.length > 0) {
2503
+ msg += '\n⚠️ **Duplicatas detectadas** (não criadas):\n';
2504
+ for (var di = 0; di < summary.semanticDups.length; di++) {
2505
+ var dup = summary.semanticDups[di];
2506
+ msg += '- "' + dup.newDesc + '" → já existe: "' + dup.existingDesc + '" (' + dup.similarity + ' similar)\n';
2507
+ }
2508
+ }
2509
+
2501
2510
  if (summary && Array.isArray(summary.reportsSuggested) && summary.reportsSuggested.length) {
2502
2511
  msg += '\n**Relatórios sugeridos:** ' + summary.reportsSuggested.join(', ');
2503
2512
  msg += '\n\nUse: **Rodar relatórios sugeridos** (barra lateral)';
package/cli/web.js CHANGED
@@ -3491,6 +3491,49 @@ async function cmdWeb({ port, dir, open, dev }) {
3491
3491
  const insertTask = dl.db.prepare(`INSERT INTO tasks (id, project_slug, description, category, status, metadata) VALUES (?, ?, ?, ?, ?, ?)`);
3492
3492
  const insertBlocker = dl.db.prepare(`INSERT INTO blockers (id, project_slug, title, severity, status, owner, next_action, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
3493
3493
 
3494
+ // ── Semantic deduplication (async, runs BEFORE the sync transaction) ──
3495
+ // Load pending tasks for cosine similarity comparison
3496
+ const pendingTasks = dl.db.prepare("SELECT id, description, project_slug FROM tasks WHERE status = 'PENDING'").all();
3497
+ let pendingEmbeddings = []; // [{ id, description, project_slug, vector }]
3498
+ try {
3499
+ const { defaultEmbedder } = require(path.join(workspaceDir, 'scripts', 'lib', 'Embedder.js'));
3500
+ for (const pt of pendingTasks) {
3501
+ try {
3502
+ const vec = await defaultEmbedder.embedText(pt.description);
3503
+ pendingEmbeddings.push({ id: pt.id, description: pt.description, project_slug: pt.project_slug, vector: vec });
3504
+ } catch { /* skip if embedding fails for individual task */ }
3505
+ }
3506
+ } catch (embErr) {
3507
+ // Embedder not available — fall back to exact-match only
3508
+ console.error('[dedup] Semantic dedup unavailable:', embErr.message);
3509
+ }
3510
+
3511
+ // Pre-compute semantic duplicates for each action
3512
+ const semanticDupMap = new Map(); // action index → existing task id (if duplicate)
3513
+ const SIMILARITY_THRESHOLD = 0.78;
3514
+ if (pendingEmbeddings.length > 0) {
3515
+ try {
3516
+ const { defaultEmbedder } = require(path.join(workspaceDir, 'scripts', 'lib', 'Embedder.js'));
3517
+ for (let ai = 0; ai < actions.length; ai++) {
3518
+ const a = actions[ai];
3519
+ if (!a || a.type !== 'create_task') continue;
3520
+ const desc = normalizeWhitespace(a.description);
3521
+ if (!desc) continue;
3522
+ try {
3523
+ const newVec = await defaultEmbedder.embedText(desc);
3524
+ let bestScore = 0, bestMatch = null;
3525
+ for (const pe of pendingEmbeddings) {
3526
+ const score = defaultEmbedder.cosineSimilarity(newVec, pe.vector);
3527
+ if (score > bestScore) { bestScore = score; bestMatch = pe; }
3528
+ }
3529
+ if (bestScore >= SIMILARITY_THRESHOLD && bestMatch) {
3530
+ semanticDupMap.set(ai, { existingId: bestMatch.id, existingDesc: bestMatch.description, score: bestScore });
3531
+ }
3532
+ } catch { /* skip */ }
3533
+ }
3534
+ } catch { /* embedder not available */ }
3535
+ }
3536
+
3494
3537
  // BUG-31: Move deduplication queries INSIDE the transaction to eliminate TOCTOU race
3495
3538
  const applyTx = dl.db.transaction((actionsToApply) => {
3496
3539
  // Query for existing keys inside the transaction for atomicity
@@ -3499,7 +3542,8 @@ async function cmdWeb({ port, dir, open, dev }) {
3499
3542
  const recentBlockers = dl.db.prepare("SELECT title FROM blockers WHERE created_at >= datetime('now', '-1 day')").all();
3500
3543
  const existingBlockerKeys24h = new Set(recentBlockers.map(b => sha1(normalizeTextForKey(b.title))));
3501
3544
 
3502
- for (const a of actionsToApply) {
3545
+ for (let ai = 0; ai < actionsToApply.length; ai++) {
3546
+ const a = actionsToApply[ai];
3503
3547
  if (!a || typeof a !== 'object') continue;
3504
3548
  const type = String(a.type || '').trim();
3505
3549
 
@@ -3509,8 +3553,25 @@ async function cmdWeb({ port, dir, open, dev }) {
3509
3553
  if (!description) continue;
3510
3554
  const projectSlug = String(a.projectSlug || '').trim() || inferProjectSlug(description, slugMap);
3511
3555
  const streamSlug = String(a.streamSlug || '').trim();
3556
+
3557
+ // Exact-match dedup (24h window)
3512
3558
  const key = sha1(normalizeTextForKey((projectSlug ? projectSlug + ' ' : '') + description));
3513
3559
  if (existingTaskKeys24h.has(key)) { applied.tasksSkipped++; continue; }
3560
+
3561
+ // Semantic dedup (all pending tasks)
3562
+ if (semanticDupMap.has(ai)) {
3563
+ const dup = semanticDupMap.get(ai);
3564
+ applied.tasksSkipped++;
3565
+ if (!applied.semanticDups) applied.semanticDups = [];
3566
+ applied.semanticDups.push({
3567
+ newDesc: description,
3568
+ existingId: dup.existingId,
3569
+ existingDesc: dup.existingDesc,
3570
+ similarity: Math.round(dup.score * 100) + '%'
3571
+ });
3572
+ continue;
3573
+ }
3574
+
3514
3575
  const category = validTaskCats.has(String(a.category || '').trim()) ? String(a.category).trim() : 'DO_NOW';
3515
3576
  const priority = normPriority(a.priority);
3516
3577
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cccarv82/freya",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Personal AI Assistant with local-first persistence",
5
5
  "scripts": {
6
6
  "health": "node scripts/validate-data.js && node scripts/validate-structure.js",