@hbarefoot/engram 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/engram.js +11 -9
- package/dashboard/dist/assets/{index-C0aJJ5-D.js → index-DGlzKbuV.js} +25 -25
- package/dashboard/dist/assets/index-DOKHau-I.css +1 -0
- package/dashboard/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/memory/consolidate.js +147 -95
- package/src/memory/store.js +336 -0
- package/src/server/rest.js +112 -4
- package/dashboard/dist/assets/index-DFWnnKIv.css +0 -1
package/src/memory/store.js
CHANGED
|
@@ -130,6 +130,32 @@ function runMigrations(db) {
|
|
|
130
130
|
);
|
|
131
131
|
`);
|
|
132
132
|
|
|
133
|
+
// Contradictions table for detected memory conflicts
|
|
134
|
+
db.exec(`
|
|
135
|
+
CREATE TABLE IF NOT EXISTS contradictions (
|
|
136
|
+
id TEXT PRIMARY KEY,
|
|
137
|
+
memory1_id TEXT NOT NULL,
|
|
138
|
+
memory2_id TEXT NOT NULL,
|
|
139
|
+
confidence REAL NOT NULL DEFAULT 0.5,
|
|
140
|
+
reason TEXT,
|
|
141
|
+
category TEXT,
|
|
142
|
+
entity TEXT,
|
|
143
|
+
status TEXT NOT NULL DEFAULT 'unresolved',
|
|
144
|
+
detected_at INTEGER NOT NULL,
|
|
145
|
+
resolved_at INTEGER,
|
|
146
|
+
resolution_action TEXT,
|
|
147
|
+
FOREIGN KEY (memory1_id) REFERENCES memories(id) ON DELETE CASCADE,
|
|
148
|
+
FOREIGN KEY (memory2_id) REFERENCES memories(id) ON DELETE CASCADE
|
|
149
|
+
);
|
|
150
|
+
`);
|
|
151
|
+
|
|
152
|
+
db.exec(`
|
|
153
|
+
CREATE INDEX IF NOT EXISTS idx_contradictions_status ON contradictions(status);
|
|
154
|
+
CREATE INDEX IF NOT EXISTS idx_contradictions_memory1 ON contradictions(memory1_id);
|
|
155
|
+
CREATE INDEX IF NOT EXISTS idx_contradictions_memory2 ON contradictions(memory2_id);
|
|
156
|
+
CREATE INDEX IF NOT EXISTS idx_contradictions_detected_at ON contradictions(detected_at);
|
|
157
|
+
`);
|
|
158
|
+
|
|
133
159
|
logger.debug('Database migrations completed');
|
|
134
160
|
}
|
|
135
161
|
|
|
@@ -624,3 +650,313 @@ function deserializeMemory(row) {
|
|
|
624
650
|
|
|
625
651
|
return memory;
|
|
626
652
|
}
|
|
653
|
+
|
|
654
|
+
// --- Contradiction CRUD ---
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Create a contradiction record
|
|
658
|
+
* @param {Database} db
|
|
659
|
+
* @param {Object} contradiction
|
|
660
|
+
* @param {string} contradiction.memory1_id
|
|
661
|
+
* @param {string} contradiction.memory2_id
|
|
662
|
+
* @param {number} contradiction.confidence - 0.0 to 1.0
|
|
663
|
+
* @param {string} contradiction.reason
|
|
664
|
+
* @param {string} [contradiction.category]
|
|
665
|
+
* @param {string} [contradiction.entity]
|
|
666
|
+
* @returns {Object} Created contradiction record
|
|
667
|
+
*/
|
|
668
|
+
export function createContradiction(db, contradiction) {
|
|
669
|
+
const id = generateId();
|
|
670
|
+
const now = Date.now();
|
|
671
|
+
|
|
672
|
+
const stmt = db.prepare(`
|
|
673
|
+
INSERT INTO contradictions (id, memory1_id, memory2_id, confidence, reason, category, entity, status, detected_at)
|
|
674
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 'unresolved', ?)
|
|
675
|
+
`);
|
|
676
|
+
|
|
677
|
+
stmt.run(
|
|
678
|
+
id,
|
|
679
|
+
contradiction.memory1_id,
|
|
680
|
+
contradiction.memory2_id,
|
|
681
|
+
contradiction.confidence,
|
|
682
|
+
contradiction.reason || null,
|
|
683
|
+
contradiction.category || null,
|
|
684
|
+
contradiction.entity || null,
|
|
685
|
+
now
|
|
686
|
+
);
|
|
687
|
+
|
|
688
|
+
logger.debug('Contradiction created', { id, entity: contradiction.entity });
|
|
689
|
+
|
|
690
|
+
return getContradiction(db, id);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Get a single contradiction by ID with full memory details
|
|
695
|
+
* @param {Database} db
|
|
696
|
+
* @param {string} id
|
|
697
|
+
* @returns {Object|null}
|
|
698
|
+
*/
|
|
699
|
+
export function getContradiction(db, id) {
|
|
700
|
+
const stmt = db.prepare(`
|
|
701
|
+
SELECT c.*,
|
|
702
|
+
m1.content as m1_content, m1.category as m1_category, m1.entity as m1_entity,
|
|
703
|
+
m1.confidence as m1_confidence, m1.created_at as m1_created_at,
|
|
704
|
+
m1.namespace as m1_namespace, m1.tags as m1_tags, m1.source as m1_source,
|
|
705
|
+
m2.content as m2_content, m2.category as m2_category, m2.entity as m2_entity,
|
|
706
|
+
m2.confidence as m2_confidence, m2.created_at as m2_created_at,
|
|
707
|
+
m2.namespace as m2_namespace, m2.tags as m2_tags, m2.source as m2_source
|
|
708
|
+
FROM contradictions c
|
|
709
|
+
LEFT JOIN memories m1 ON c.memory1_id = m1.id
|
|
710
|
+
LEFT JOIN memories m2 ON c.memory2_id = m2.id
|
|
711
|
+
WHERE c.id = ?
|
|
712
|
+
`);
|
|
713
|
+
|
|
714
|
+
const row = stmt.get(id);
|
|
715
|
+
if (!row) return null;
|
|
716
|
+
|
|
717
|
+
return deserializeContradiction(row);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* List contradictions with optional filters
|
|
722
|
+
* @param {Database} db
|
|
723
|
+
* @param {Object} [options]
|
|
724
|
+
* @param {string} [options.status] - Filter by status
|
|
725
|
+
* @param {string} [options.category] - Filter by memory category
|
|
726
|
+
* @param {string} [options.sort='detected_at'] - Sort field
|
|
727
|
+
* @param {number} [options.limit=50]
|
|
728
|
+
* @param {number} [options.offset=0]
|
|
729
|
+
* @returns {{ items: Object[], total: number }}
|
|
730
|
+
*/
|
|
731
|
+
export function listContradictions(db, options = {}) {
|
|
732
|
+
const {
|
|
733
|
+
status,
|
|
734
|
+
category,
|
|
735
|
+
sort = 'detected_at',
|
|
736
|
+
limit = 50,
|
|
737
|
+
offset = 0
|
|
738
|
+
} = options;
|
|
739
|
+
|
|
740
|
+
const conditions = [];
|
|
741
|
+
const params = [];
|
|
742
|
+
|
|
743
|
+
if (status && status !== 'all') {
|
|
744
|
+
conditions.push('c.status = ?');
|
|
745
|
+
params.push(status);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if (category) {
|
|
749
|
+
conditions.push('(m1.category = ? OR m2.category = ?)');
|
|
750
|
+
params.push(category, category);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
754
|
+
|
|
755
|
+
// Validate sort to prevent injection
|
|
756
|
+
const sortMap = {
|
|
757
|
+
'detected_at': 'c.detected_at DESC',
|
|
758
|
+
'detected_at_asc': 'c.detected_at ASC',
|
|
759
|
+
'confidence': 'c.confidence DESC'
|
|
760
|
+
};
|
|
761
|
+
const orderBy = sortMap[sort] || 'c.detected_at DESC';
|
|
762
|
+
|
|
763
|
+
// Get total count
|
|
764
|
+
const countStmt = db.prepare(`
|
|
765
|
+
SELECT COUNT(*) as count
|
|
766
|
+
FROM contradictions c
|
|
767
|
+
LEFT JOIN memories m1 ON c.memory1_id = m1.id
|
|
768
|
+
LEFT JOIN memories m2 ON c.memory2_id = m2.id
|
|
769
|
+
${where}
|
|
770
|
+
`);
|
|
771
|
+
const total = countStmt.get(...params).count;
|
|
772
|
+
|
|
773
|
+
// Get items
|
|
774
|
+
const stmt = db.prepare(`
|
|
775
|
+
SELECT c.*,
|
|
776
|
+
m1.content as m1_content, m1.category as m1_category, m1.entity as m1_entity,
|
|
777
|
+
m1.confidence as m1_confidence, m1.created_at as m1_created_at,
|
|
778
|
+
m1.namespace as m1_namespace, m1.tags as m1_tags, m1.source as m1_source,
|
|
779
|
+
m2.content as m2_content, m2.category as m2_category, m2.entity as m2_entity,
|
|
780
|
+
m2.confidence as m2_confidence, m2.created_at as m2_created_at,
|
|
781
|
+
m2.namespace as m2_namespace, m2.tags as m2_tags, m2.source as m2_source
|
|
782
|
+
FROM contradictions c
|
|
783
|
+
LEFT JOIN memories m1 ON c.memory1_id = m1.id
|
|
784
|
+
LEFT JOIN memories m2 ON c.memory2_id = m2.id
|
|
785
|
+
${where}
|
|
786
|
+
ORDER BY ${orderBy}
|
|
787
|
+
LIMIT ? OFFSET ?
|
|
788
|
+
`);
|
|
789
|
+
|
|
790
|
+
const rows = stmt.all(...params, limit, offset);
|
|
791
|
+
|
|
792
|
+
return {
|
|
793
|
+
items: rows.map(deserializeContradiction),
|
|
794
|
+
total
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Resolve a contradiction
|
|
800
|
+
* @param {Database} db
|
|
801
|
+
* @param {string} id - Contradiction ID
|
|
802
|
+
* @param {string} action - 'keep_first' | 'keep_second' | 'keep_both' | 'dismiss'
|
|
803
|
+
* @returns {Object|null} Updated contradiction or null if not found
|
|
804
|
+
*/
|
|
805
|
+
export function resolveContradiction(db, id, action) {
|
|
806
|
+
const contradiction = getContradiction(db, id);
|
|
807
|
+
if (!contradiction) return null;
|
|
808
|
+
|
|
809
|
+
const now = Date.now();
|
|
810
|
+
|
|
811
|
+
// Perform side effects based on action
|
|
812
|
+
if (action === 'keep_first' && contradiction.memory2) {
|
|
813
|
+
deleteMemory(db, contradiction.memory2.id);
|
|
814
|
+
} else if (action === 'keep_second' && contradiction.memory1) {
|
|
815
|
+
deleteMemory(db, contradiction.memory1.id);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const newStatus = action === 'dismiss' ? 'dismissed' : 'resolved';
|
|
819
|
+
|
|
820
|
+
const stmt = db.prepare(`
|
|
821
|
+
UPDATE contradictions
|
|
822
|
+
SET status = ?, resolved_at = ?, resolution_action = ?
|
|
823
|
+
WHERE id = ?
|
|
824
|
+
`);
|
|
825
|
+
stmt.run(newStatus, now, action, id);
|
|
826
|
+
|
|
827
|
+
logger.info('Contradiction resolved', { id, action, status: newStatus });
|
|
828
|
+
|
|
829
|
+
return getContradiction(db, id);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Check if a contradiction already exists for a memory pair (unresolved only)
|
|
834
|
+
* @param {Database} db
|
|
835
|
+
* @param {string} memory1Id
|
|
836
|
+
* @param {string} memory2Id
|
|
837
|
+
* @returns {boolean}
|
|
838
|
+
*/
|
|
839
|
+
export function contradictionExists(db, memory1Id, memory2Id) {
|
|
840
|
+
const stmt = db.prepare(`
|
|
841
|
+
SELECT COUNT(*) as count FROM contradictions
|
|
842
|
+
WHERE status = 'unresolved'
|
|
843
|
+
AND ((memory1_id = ? AND memory2_id = ?) OR (memory1_id = ? AND memory2_id = ?))
|
|
844
|
+
`);
|
|
845
|
+
return stmt.get(memory1Id, memory2Id, memory2Id, memory1Id).count > 0;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Count unresolved contradictions
|
|
850
|
+
* @param {Database} db
|
|
851
|
+
* @returns {number}
|
|
852
|
+
*/
|
|
853
|
+
export function countUnresolvedContradictions(db) {
|
|
854
|
+
const stmt = db.prepare("SELECT COUNT(*) as count FROM contradictions WHERE status = 'unresolved'");
|
|
855
|
+
return stmt.get().count;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Migrate existing tag-based conflicts to contradictions table.
|
|
860
|
+
* Runs once (checks meta table for flag).
|
|
861
|
+
* @param {Database} db
|
|
862
|
+
* @returns {number} Number of contradictions migrated
|
|
863
|
+
*/
|
|
864
|
+
export function migrateTagConflicts(db) {
|
|
865
|
+
// Check if already migrated
|
|
866
|
+
const metaStmt = db.prepare('SELECT value FROM meta WHERE key = ?');
|
|
867
|
+
const migrated = metaStmt.get('contradictions_migrated');
|
|
868
|
+
if (migrated) return 0;
|
|
869
|
+
|
|
870
|
+
// Find all memories with conflict tags
|
|
871
|
+
const memories = listMemories(db, { limit: 10000 });
|
|
872
|
+
const conflicts = memories.filter(m =>
|
|
873
|
+
m.tags && m.tags.some(tag => tag.startsWith('conflict_'))
|
|
874
|
+
);
|
|
875
|
+
|
|
876
|
+
// Group by conflict ID
|
|
877
|
+
const grouped = new Map();
|
|
878
|
+
for (const memory of conflicts) {
|
|
879
|
+
const conflictTags = memory.tags.filter(tag => tag.startsWith('conflict_'));
|
|
880
|
+
for (const conflictId of conflictTags) {
|
|
881
|
+
if (!grouped.has(conflictId)) {
|
|
882
|
+
grouped.set(conflictId, []);
|
|
883
|
+
}
|
|
884
|
+
grouped.get(conflictId).push(memory);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
let count = 0;
|
|
889
|
+
for (const [, mems] of grouped.entries()) {
|
|
890
|
+
if (mems.length < 2) continue;
|
|
891
|
+
|
|
892
|
+
// Create pairwise contradictions
|
|
893
|
+
for (let i = 0; i < mems.length; i++) {
|
|
894
|
+
for (let j = i + 1; j < mems.length; j++) {
|
|
895
|
+
if (!contradictionExists(db, mems[i].id, mems[j].id)) {
|
|
896
|
+
createContradiction(db, {
|
|
897
|
+
memory1_id: mems[i].id,
|
|
898
|
+
memory2_id: mems[j].id,
|
|
899
|
+
confidence: 0.5,
|
|
900
|
+
reason: 'Legacy tag-based detection',
|
|
901
|
+
category: mems[i].category,
|
|
902
|
+
entity: mems[i].entity
|
|
903
|
+
});
|
|
904
|
+
count++;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Mark as migrated
|
|
911
|
+
db.prepare('INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)').run(
|
|
912
|
+
'contradictions_migrated',
|
|
913
|
+
new Date().toISOString()
|
|
914
|
+
);
|
|
915
|
+
|
|
916
|
+
if (count > 0) {
|
|
917
|
+
logger.info('Migrated tag-based conflicts to contradictions table', { count });
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
return count;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* Deserialize a contradiction row with joined memory data
|
|
925
|
+
* @param {Object} row
|
|
926
|
+
* @returns {Object}
|
|
927
|
+
*/
|
|
928
|
+
function deserializeContradiction(row) {
|
|
929
|
+
return {
|
|
930
|
+
id: row.id,
|
|
931
|
+
confidence: row.confidence,
|
|
932
|
+
reason: row.reason,
|
|
933
|
+
category: row.category,
|
|
934
|
+
entity: row.entity,
|
|
935
|
+
status: row.status,
|
|
936
|
+
detected_at: row.detected_at,
|
|
937
|
+
resolved_at: row.resolved_at,
|
|
938
|
+
resolution_action: row.resolution_action,
|
|
939
|
+
memory1: row.m1_content ? {
|
|
940
|
+
id: row.memory1_id,
|
|
941
|
+
content: row.m1_content,
|
|
942
|
+
category: row.m1_category,
|
|
943
|
+
entity: row.m1_entity,
|
|
944
|
+
confidence: row.m1_confidence,
|
|
945
|
+
created_at: row.m1_created_at,
|
|
946
|
+
namespace: row.m1_namespace,
|
|
947
|
+
tags: JSON.parse(row.m1_tags || '[]'),
|
|
948
|
+
source: row.m1_source
|
|
949
|
+
} : null,
|
|
950
|
+
memory2: row.m2_content ? {
|
|
951
|
+
id: row.memory2_id,
|
|
952
|
+
content: row.m2_content,
|
|
953
|
+
category: row.m2_category,
|
|
954
|
+
entity: row.m2_entity,
|
|
955
|
+
confidence: row.m2_confidence,
|
|
956
|
+
created_at: row.m2_created_at,
|
|
957
|
+
namespace: row.m2_namespace,
|
|
958
|
+
tags: JSON.parse(row.m2_tags || '[]'),
|
|
959
|
+
source: row.m2_source
|
|
960
|
+
} : null
|
|
961
|
+
};
|
|
962
|
+
}
|
package/src/server/rest.js
CHANGED
|
@@ -4,9 +4,9 @@ import fs from 'fs';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { loadConfig, getDatabasePath, getModelsPath } from '../config/index.js';
|
|
7
|
-
import { initDatabase, createMemory, getMemory, deleteMemory, listMemories, getStats } from '../memory/store.js';
|
|
7
|
+
import { initDatabase, createMemory, getMemory, deleteMemory, listMemories, getStats, listContradictions, resolveContradiction, countUnresolvedContradictions, migrateTagConflicts } from '../memory/store.js';
|
|
8
8
|
import { recallMemories } from '../memory/recall.js';
|
|
9
|
-
import { consolidate, getConflicts } from '../memory/consolidate.js';
|
|
9
|
+
import { consolidate, getConflicts, detectContradictionsForMemory } from '../memory/consolidate.js';
|
|
10
10
|
import { getOverview, getStaleMemories, getNeverRecalled, getDuplicateClusters, getTrends } from '../memory/analytics.js';
|
|
11
11
|
import { calculateHealthScore } from '../memory/health.js';
|
|
12
12
|
import { validateContent } from '../extract/secrets.js';
|
|
@@ -41,6 +41,22 @@ function sanitizePaths(paths) {
|
|
|
41
41
|
}
|
|
42
42
|
const __dirname = path.dirname(__filename);
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Get the Engram server version.
|
|
46
|
+
* In the esbuild sidecar bundle, process.env.ENGRAM_VERSION is replaced at build time.
|
|
47
|
+
* Otherwise, reads from package.json.
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
50
|
+
function getServerVersion() {
|
|
51
|
+
if (process.env.ENGRAM_VERSION) return process.env.ENGRAM_VERSION;
|
|
52
|
+
try {
|
|
53
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json'), 'utf-8'));
|
|
54
|
+
return pkg.version;
|
|
55
|
+
} catch {
|
|
56
|
+
return 'unknown';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
44
60
|
/**
|
|
45
61
|
* Create and configure the Fastify REST API server
|
|
46
62
|
* @param {Object} config - Engram configuration
|
|
@@ -56,6 +72,13 @@ export function createRESTServer(config) {
|
|
|
56
72
|
const db = initDatabase(getDatabasePath(config));
|
|
57
73
|
const modelsPath = getModelsPath(config);
|
|
58
74
|
|
|
75
|
+
// Migrate legacy tag-based conflicts to contradictions table
|
|
76
|
+
try {
|
|
77
|
+
migrateTagConflicts(db);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
logger.warn('Tag conflict migration failed', { error: error.message });
|
|
80
|
+
}
|
|
81
|
+
|
|
59
82
|
// CORS support
|
|
60
83
|
fastify.addHook('onRequest', async (request, reply) => {
|
|
61
84
|
reply.header('Access-Control-Allow-Origin', '*');
|
|
@@ -68,10 +91,13 @@ export function createRESTServer(config) {
|
|
|
68
91
|
reply.code(204).send();
|
|
69
92
|
});
|
|
70
93
|
|
|
94
|
+
const serverVersion = getServerVersion();
|
|
95
|
+
|
|
71
96
|
// Health check endpoint
|
|
72
97
|
fastify.get('/health', async (request, reply) => {
|
|
73
98
|
return {
|
|
74
99
|
status: 'healthy',
|
|
100
|
+
version: serverVersion,
|
|
75
101
|
timestamp: new Date().toISOString(),
|
|
76
102
|
uptime: process.uptime()
|
|
77
103
|
};
|
|
@@ -101,6 +127,7 @@ export function createRESTServer(config) {
|
|
|
101
127
|
|
|
102
128
|
return {
|
|
103
129
|
status: 'ok',
|
|
130
|
+
version: serverVersion,
|
|
104
131
|
memory: {
|
|
105
132
|
total: stats.total,
|
|
106
133
|
withEmbeddings: stats.withEmbeddings,
|
|
@@ -192,6 +219,13 @@ export function createRESTServer(config) {
|
|
|
192
219
|
|
|
193
220
|
logger.info('Memory created via API', { id: memory.id, category: memory.category });
|
|
194
221
|
|
|
222
|
+
// Proactive contradiction detection (fire-and-forget)
|
|
223
|
+
setImmediate(() => {
|
|
224
|
+
detectContradictionsForMemory(db, memory).catch(err => {
|
|
225
|
+
logger.warn('Proactive contradiction detection failed', { error: err.message });
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
195
229
|
return {
|
|
196
230
|
success: true,
|
|
197
231
|
memory: {
|
|
@@ -380,14 +414,16 @@ export function createRESTServer(config) {
|
|
|
380
414
|
detectDuplicates = true,
|
|
381
415
|
detectContradictions = true,
|
|
382
416
|
applyDecay = true,
|
|
383
|
-
cleanupStale = false
|
|
417
|
+
cleanupStale = false,
|
|
418
|
+
duplicateThreshold
|
|
384
419
|
} = request.body || {};
|
|
385
420
|
|
|
386
421
|
const results = await consolidate(db, {
|
|
387
422
|
detectDuplicates,
|
|
388
423
|
detectContradictions,
|
|
389
424
|
applyDecay,
|
|
390
|
-
cleanupStale
|
|
425
|
+
cleanupStale,
|
|
426
|
+
...(duplicateThreshold !== undefined && duplicateThreshold !== null && { duplicateThreshold })
|
|
391
427
|
});
|
|
392
428
|
|
|
393
429
|
logger.info('Consolidation completed via API', results);
|
|
@@ -435,6 +471,78 @@ export function createRESTServer(config) {
|
|
|
435
471
|
}
|
|
436
472
|
});
|
|
437
473
|
|
|
474
|
+
// --- Contradictions endpoints ---
|
|
475
|
+
|
|
476
|
+
// List contradictions with filtering
|
|
477
|
+
fastify.get('/api/contradictions', async (request, reply) => {
|
|
478
|
+
try {
|
|
479
|
+
const { status, category, sort, limit, offset } = request.query;
|
|
480
|
+
|
|
481
|
+
const result = listContradictions(db, {
|
|
482
|
+
status: status || undefined,
|
|
483
|
+
category: category || undefined,
|
|
484
|
+
sort: sort || 'detected_at',
|
|
485
|
+
limit: limit ? parseInt(limit) : 50,
|
|
486
|
+
offset: offset ? parseInt(offset) : 0
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
const unresolvedCount = countUnresolvedContradictions(db);
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
success: true,
|
|
493
|
+
contradictions: result.items,
|
|
494
|
+
unresolvedCount,
|
|
495
|
+
pagination: {
|
|
496
|
+
limit: limit ? parseInt(limit) : 50,
|
|
497
|
+
offset: offset ? parseInt(offset) : 0,
|
|
498
|
+
total: result.total
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
} catch (error) {
|
|
502
|
+
logger.error('Get contradictions error', { error: error.message });
|
|
503
|
+
reply.code(500);
|
|
504
|
+
return { error: error.message };
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
// Resolve a contradiction
|
|
509
|
+
fastify.post('/api/contradictions/:id/resolve', async (request, reply) => {
|
|
510
|
+
try {
|
|
511
|
+
const { id } = request.params;
|
|
512
|
+
const { action } = request.body || {};
|
|
513
|
+
|
|
514
|
+
const validActions = ['keep_first', 'keep_second', 'keep_both', 'dismiss'];
|
|
515
|
+
if (!action || !validActions.includes(action)) {
|
|
516
|
+
reply.code(400);
|
|
517
|
+
return { error: 'Invalid action. Must be: keep_first, keep_second, keep_both, or dismiss' };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const result = resolveContradiction(db, id, action);
|
|
521
|
+
if (!result) {
|
|
522
|
+
reply.code(404);
|
|
523
|
+
return { error: 'Contradiction not found' };
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return { success: true, contradiction: result };
|
|
527
|
+
} catch (error) {
|
|
528
|
+
logger.error('Resolve contradiction error', { error: error.message });
|
|
529
|
+
reply.code(500);
|
|
530
|
+
return { error: error.message };
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
// Get unresolved contradiction count (for badge)
|
|
535
|
+
fastify.get('/api/contradictions/count', async (request, reply) => {
|
|
536
|
+
try {
|
|
537
|
+
const count = countUnresolvedContradictions(db);
|
|
538
|
+
return { success: true, count };
|
|
539
|
+
} catch (error) {
|
|
540
|
+
logger.error('Contradiction count error', { error: error.message });
|
|
541
|
+
reply.code(500);
|
|
542
|
+
return { error: error.message };
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
|
|
438
546
|
// --- Analytics endpoints ---
|
|
439
547
|
|
|
440
548
|
fastify.get('/api/analytics/overview', async (request, reply) => {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.right-2{right:.5rem}.top-2{top:.5rem}.z-50{z-index:50}.mx-3{margin-left:.75rem;margin-right:.75rem}.mx-auto{margin-left:auto;margin-right:auto}.-mb-px{margin-bottom:-1px}.-ml-1{margin-left:-.25rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.ml-5{margin-left:1.25rem}.ml-auto{margin-left:auto}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.line-clamp-1{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:1}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-1{height:.25rem}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-2{height:.5rem}.h-3{height:.75rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-8{height:2rem}.h-px{height:1px}.max-h-\[500px\]{max-height:500px}.max-h-\[90vh\]{max-height:90vh}.min-h-screen{min-height:100vh}.w-0{width:0px}.w-10{width:2.5rem}.w-12{width:3rem}.w-16{width:4rem}.w-20{width:5rem}.w-3{width:.75rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-full{width:100%}.min-w-0{min-width:0px}.max-w-2xl{max-width:42rem}.max-w-4xl{max-width:56rem}.max-w-7xl{max-width:80rem}.max-w-lg{max-width:32rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.resize-none{resize:none}.list-inside{list-style-position:inside}.list-decimal{list-style-type:decimal}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-2\.5{gap:.625rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.75rem * var(--tw-space-x-reverse));margin-left:calc(.75rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(1rem * var(--tw-space-x-reverse));margin-left:calc(1rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(229 231 235 / var(--tw-divide-opacity, 1))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-0{border-width:0px}.border-2{border-width:2px}.border-4{border-width:4px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-t{border-top-width:1px}.border-amber-200{--tw-border-opacity: 1;border-color:rgb(253 230 138 / var(--tw-border-opacity, 1))}.border-blue-200{--tw-border-opacity: 1;border-color:rgb(191 219 254 / var(--tw-border-opacity, 1))}.border-gray-100{--tw-border-opacity: 1;border-color:rgb(243 244 246 / var(--tw-border-opacity, 1))}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity, 1))}.border-gray-300{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1))}.border-green-200{--tw-border-opacity: 1;border-color:rgb(187 247 208 / var(--tw-border-opacity, 1))}.border-orange-200{--tw-border-opacity: 1;border-color:rgb(254 215 170 / var(--tw-border-opacity, 1))}.border-primary-200{--tw-border-opacity: 1;border-color:rgb(186 230 253 / var(--tw-border-opacity, 1))}.border-primary-500{--tw-border-opacity: 1;border-color:rgb(14 165 233 / var(--tw-border-opacity, 1))}.border-primary-600{--tw-border-opacity: 1;border-color:rgb(2 132 199 / var(--tw-border-opacity, 1))}.border-red-200{--tw-border-opacity: 1;border-color:rgb(254 202 202 / var(--tw-border-opacity, 1))}.border-transparent{border-color:transparent}.border-yellow-200{--tw-border-opacity: 1;border-color:rgb(254 240 138 / var(--tw-border-opacity, 1))}.border-t-transparent{border-top-color:transparent}.bg-amber-100{--tw-bg-opacity: 1;background-color:rgb(254 243 199 / var(--tw-bg-opacity, 1))}.bg-amber-50{--tw-bg-opacity: 1;background-color:rgb(255 251 235 / var(--tw-bg-opacity, 1))}.bg-blue-100{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity, 1))}.bg-blue-50{--tw-bg-opacity: 1;background-color:rgb(239 246 255 / var(--tw-bg-opacity, 1))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.bg-gray-200{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.bg-gray-300{--tw-bg-opacity: 1;background-color:rgb(209 213 219 / var(--tw-bg-opacity, 1))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.bg-gray-500{--tw-bg-opacity: 1;background-color:rgb(107 114 128 / var(--tw-bg-opacity, 1))}.bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity, 1))}.bg-green-50{--tw-bg-opacity: 1;background-color:rgb(240 253 244 / var(--tw-bg-opacity, 1))}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-green-600{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity, 1))}.bg-orange-50{--tw-bg-opacity: 1;background-color:rgb(255 247 237 / var(--tw-bg-opacity, 1))}.bg-primary-100{--tw-bg-opacity: 1;background-color:rgb(224 242 254 / var(--tw-bg-opacity, 1))}.bg-primary-50{--tw-bg-opacity: 1;background-color:rgb(240 249 255 / var(--tw-bg-opacity, 1))}.bg-primary-600{--tw-bg-opacity: 1;background-color:rgb(2 132 199 / var(--tw-bg-opacity, 1))}.bg-purple-100{--tw-bg-opacity: 1;background-color:rgb(243 232 255 / var(--tw-bg-opacity, 1))}.bg-red-100{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity, 1))}.bg-red-50{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-yellow-100{--tw-bg-opacity: 1;background-color:rgb(254 249 195 / var(--tw-bg-opacity, 1))}.bg-yellow-50{--tw-bg-opacity: 1;background-color:rgb(254 252 232 / var(--tw-bg-opacity, 1))}.bg-yellow-600{--tw-bg-opacity: 1;background-color:rgb(202 138 4 / var(--tw-bg-opacity, 1))}.bg-opacity-75{--tw-bg-opacity: .75}.fill-gray-500{fill:#6b7280}.fill-gray-900{fill:#111827}.p-0{padding:0}.p-1{padding:.25rem}.p-12{padding:3rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-20{padding-top:5rem;padding-bottom:5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pt-1{padding-top:.25rem}.pt-4{padding-top:1rem}.pt-6{padding-top:1.5rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-6xl{font-size:3.75rem;line-height:1}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.tracking-wider{letter-spacing:.05em}.text-amber-800{--tw-text-opacity: 1;color:rgb(146 64 14 / var(--tw-text-opacity, 1))}.text-amber-900{--tw-text-opacity: 1;color:rgb(120 53 15 / var(--tw-text-opacity, 1))}.text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity, 1))}.text-blue-700{--tw-text-opacity: 1;color:rgb(29 78 216 / var(--tw-text-opacity, 1))}.text-blue-800{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity, 1))}.text-blue-900{--tw-text-opacity: 1;color:rgb(30 58 138 / var(--tw-text-opacity, 1))}.text-gray-100{--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity, 1))}.text-gray-200{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity, 1))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity, 1))}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-green-600{--tw-text-opacity: 1;color:rgb(22 163 74 / var(--tw-text-opacity, 1))}.text-green-700{--tw-text-opacity: 1;color:rgb(21 128 61 / var(--tw-text-opacity, 1))}.text-green-800{--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity, 1))}.text-green-900{--tw-text-opacity: 1;color:rgb(20 83 45 / var(--tw-text-opacity, 1))}.text-orange-700{--tw-text-opacity: 1;color:rgb(194 65 12 / var(--tw-text-opacity, 1))}.text-primary-600{--tw-text-opacity: 1;color:rgb(2 132 199 / var(--tw-text-opacity, 1))}.text-primary-700{--tw-text-opacity: 1;color:rgb(3 105 161 / var(--tw-text-opacity, 1))}.text-primary-800{--tw-text-opacity: 1;color:rgb(7 89 133 / var(--tw-text-opacity, 1))}.text-purple-800{--tw-text-opacity: 1;color:rgb(107 33 168 / var(--tw-text-opacity, 1))}.text-red-200{--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity, 1))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.text-red-700{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity, 1))}.text-red-800{--tw-text-opacity: 1;color:rgb(153 27 27 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-600{--tw-text-opacity: 1;color:rgb(202 138 4 / var(--tw-text-opacity, 1))}.text-yellow-700{--tw-text-opacity: 1;color:rgb(161 98 7 / var(--tw-text-opacity, 1))}.text-yellow-800{--tw-text-opacity: 1;color:rgb(133 77 14 / var(--tw-text-opacity, 1))}.text-yellow-900{--tw-text-opacity: 1;color:rgb(113 63 18 / var(--tw-text-opacity, 1))}.underline{text-decoration-line:underline}.placeholder-gray-400::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(156 163 175 / var(--tw-placeholder-opacity, 1))}.placeholder-gray-400::placeholder{--tw-placeholder-opacity: 1;color:rgb(156 163 175 / var(--tw-placeholder-opacity, 1))}.accent-primary-500{accent-color:#0ea5e9}.opacity-100{opacity:1}.opacity-25{opacity:.25}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-75{opacity:.75}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-shadow{transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}:root{font-family:Inter,system-ui,Avenir,Helvetica,Arial,sans-serif;line-height:1.5;font-weight:400;color-scheme:light dark;color:#ffffffde;background-color:#242424;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{margin:0;min-height:100vh}#root{min-height:100vh}@media(prefers-color-scheme:light){:root{color:#213547;background-color:#fff}}.hover\:border-gray-300:hover{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1))}.hover\:bg-gray-100:hover{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-300:hover{--tw-bg-opacity: 1;background-color:rgb(209 213 219 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-50:hover{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.hover\:bg-primary-700:hover{--tw-bg-opacity: 1;background-color:rgb(3 105 161 / var(--tw-bg-opacity, 1))}.hover\:bg-red-100:hover{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity, 1))}.hover\:bg-yellow-100:hover{--tw-bg-opacity: 1;background-color:rgb(254 249 195 / var(--tw-bg-opacity, 1))}.hover\:text-gray-500:hover{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.hover\:text-gray-700:hover{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.hover\:text-gray-900:hover{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity, 1))}.hover\:text-primary-700:hover{--tw-text-opacity: 1;color:rgb(3 105 161 / var(--tw-text-opacity, 1))}.hover\:text-red-500:hover{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.hover\:text-red-900:hover{--tw-text-opacity: 1;color:rgb(127 29 29 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.hover\:shadow-md:hover{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.focus\:border-primary-500:focus{--tw-border-opacity: 1;border-color:rgb(14 165 233 / var(--tw-border-opacity, 1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-0:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-primary-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(14 165 233 / var(--tw-ring-opacity, 1))}.focus\:ring-offset-2:focus{--tw-ring-offset-width: 2px}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-50:disabled{opacity:.5}@media(min-width:640px){.sm\:ml-6{margin-left:1.5rem}.sm\:flex{display:flex}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.sm\:space-x-8>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(2rem * var(--tw-space-x-reverse));margin-left:calc(2rem * calc(1 - var(--tw-space-x-reverse)))}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}}@media(min-width:768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media(min-width:1024px){.lg\:col-span-1{grid-column:span 1 / span 1}.lg\:col-span-4{grid-column:span 4 / span 4}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:px-8{padding-left:2rem;padding-right:2rem}}@media(prefers-color-scheme:dark){.dark\:divide-gray-700>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(55 65 81 / var(--tw-divide-opacity, 1))}.dark\:border-amber-800{--tw-border-opacity: 1;border-color:rgb(146 64 14 / var(--tw-border-opacity, 1))}.dark\:border-blue-800{--tw-border-opacity: 1;border-color:rgb(30 64 175 / var(--tw-border-opacity, 1))}.dark\:border-gray-600{--tw-border-opacity: 1;border-color:rgb(75 85 99 / var(--tw-border-opacity, 1))}.dark\:border-gray-700{--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity, 1))}.dark\:border-gray-700\/50{border-color:#37415180}.dark\:border-green-800{--tw-border-opacity: 1;border-color:rgb(22 101 52 / var(--tw-border-opacity, 1))}.dark\:border-orange-800{--tw-border-opacity: 1;border-color:rgb(154 52 18 / var(--tw-border-opacity, 1))}.dark\:border-primary-800{--tw-border-opacity: 1;border-color:rgb(7 89 133 / var(--tw-border-opacity, 1))}.dark\:border-red-800{--tw-border-opacity: 1;border-color:rgb(153 27 27 / var(--tw-border-opacity, 1))}.dark\:border-yellow-800{--tw-border-opacity: 1;border-color:rgb(133 77 14 / var(--tw-border-opacity, 1))}.dark\:bg-amber-900\/20{background-color:#78350f33}.dark\:bg-amber-900\/40{background-color:#78350f66}.dark\:bg-blue-900{--tw-bg-opacity: 1;background-color:rgb(30 58 138 / var(--tw-bg-opacity, 1))}.dark\:bg-blue-900\/20{background-color:#1e3a8a33}.dark\:bg-blue-900\/30{background-color:#1e3a8a4d}.dark\:bg-gray-600{--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity, 1))}.dark\:bg-gray-700{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.dark\:bg-gray-700\/50{background-color:#37415180}.dark\:bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.dark\:bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.dark\:bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.dark\:bg-green-900{--tw-bg-opacity: 1;background-color:rgb(20 83 45 / var(--tw-bg-opacity, 1))}.dark\:bg-green-900\/20{background-color:#14532d33}.dark\:bg-green-900\/30{background-color:#14532d4d}.dark\:bg-orange-900\/20{background-color:#7c2d1233}.dark\:bg-primary-500{--tw-bg-opacity: 1;background-color:rgb(14 165 233 / var(--tw-bg-opacity, 1))}.dark\:bg-primary-800{--tw-bg-opacity: 1;background-color:rgb(7 89 133 / var(--tw-bg-opacity, 1))}.dark\:bg-primary-900{--tw-bg-opacity: 1;background-color:rgb(12 74 110 / var(--tw-bg-opacity, 1))}.dark\:bg-primary-900\/20{background-color:#0c4a6e33}.dark\:bg-primary-900\/30{background-color:#0c4a6e4d}.dark\:bg-purple-900{--tw-bg-opacity: 1;background-color:rgb(88 28 135 / var(--tw-bg-opacity, 1))}.dark\:bg-red-900{--tw-bg-opacity: 1;background-color:rgb(127 29 29 / var(--tw-bg-opacity, 1))}.dark\:bg-red-900\/20{background-color:#7f1d1d33}.dark\:bg-yellow-900{--tw-bg-opacity: 1;background-color:rgb(113 63 18 / var(--tw-bg-opacity, 1))}.dark\:bg-yellow-900\/20{background-color:#713f1233}.dark\:fill-gray-400{fill:#9ca3af}.dark\:fill-white{fill:#fff}.dark\:text-amber-200{--tw-text-opacity: 1;color:rgb(253 230 138 / var(--tw-text-opacity, 1))}.dark\:text-amber-300{--tw-text-opacity: 1;color:rgb(252 211 77 / var(--tw-text-opacity, 1))}.dark\:text-blue-200{--tw-text-opacity: 1;color:rgb(191 219 254 / var(--tw-text-opacity, 1))}.dark\:text-blue-300{--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity, 1))}.dark\:text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.dark\:text-blue-500{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity, 1))}.dark\:text-gray-200{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.dark\:text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.dark\:text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.dark\:text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.dark\:text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.dark\:text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.dark\:text-green-200{--tw-text-opacity: 1;color:rgb(187 247 208 / var(--tw-text-opacity, 1))}.dark\:text-green-300{--tw-text-opacity: 1;color:rgb(134 239 172 / var(--tw-text-opacity, 1))}.dark\:text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.dark\:text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.dark\:text-orange-300{--tw-text-opacity: 1;color:rgb(253 186 116 / var(--tw-text-opacity, 1))}.dark\:text-primary-200{--tw-text-opacity: 1;color:rgb(186 230 253 / var(--tw-text-opacity, 1))}.dark\:text-primary-300{--tw-text-opacity: 1;color:rgb(125 211 252 / var(--tw-text-opacity, 1))}.dark\:text-primary-400{--tw-text-opacity: 1;color:rgb(56 189 248 / var(--tw-text-opacity, 1))}.dark\:text-purple-200{--tw-text-opacity: 1;color:rgb(233 213 255 / var(--tw-text-opacity, 1))}.dark\:text-purple-300{--tw-text-opacity: 1;color:rgb(216 180 254 / var(--tw-text-opacity, 1))}.dark\:text-red-200{--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity, 1))}.dark\:text-red-300{--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}.dark\:text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.dark\:text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.dark\:text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.dark\:text-yellow-200{--tw-text-opacity: 1;color:rgb(254 240 138 / var(--tw-text-opacity, 1))}.dark\:text-yellow-300{--tw-text-opacity: 1;color:rgb(253 224 71 / var(--tw-text-opacity, 1))}.dark\:text-yellow-400{--tw-text-opacity: 1;color:rgb(250 204 21 / var(--tw-text-opacity, 1))}.dark\:text-yellow-500{--tw-text-opacity: 1;color:rgb(234 179 8 / var(--tw-text-opacity, 1))}.dark\:hover\:border-gray-600:hover{--tw-border-opacity: 1;border-color:rgb(75 85 99 / var(--tw-border-opacity, 1))}.dark\:hover\:bg-gray-600:hover{--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity, 1))}.dark\:hover\:bg-gray-700:hover{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.dark\:hover\:bg-gray-700\/50:hover{background-color:#37415180}.dark\:hover\:bg-red-900\/40:hover{background-color:#7f1d1d66}.dark\:hover\:bg-yellow-900\/40:hover{background-color:#713f1266}.dark\:hover\:text-gray-300:hover{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.dark\:hover\:text-primary-300:hover{--tw-text-opacity: 1;color:rgb(125 211 252 / var(--tw-text-opacity, 1))}.dark\:hover\:text-red-300:hover{--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}.dark\:hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}}
|