@lucieri/daxiom 0.2.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/README.md +122 -0
- package/bin/daxiom.js +4 -0
- package/package.json +47 -0
- package/src/agents.js +100 -0
- package/src/cli.js +423 -0
- package/src/connection.js +43 -0
- package/src/constants.js +91 -0
- package/src/embeddings.js +52 -0
- package/src/hooks/env.js +52 -0
- package/src/hooks/index.js +59 -0
- package/src/hooks/post-task.js +186 -0
- package/src/hooks/pre-compact.js +128 -0
- package/src/hooks/pre-task.js +97 -0
- package/src/hooks/session-start.js +108 -0
- package/src/hooks/summarize.js +141 -0
- package/src/index.js +67 -0
- package/src/patterns.js +339 -0
- package/src/quality-gates.js +183 -0
- package/src/self-manage.js +105 -0
- package/src/tiers.js +305 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
const MEMORY_DIR = path.join(os.homedir(), '.claude', 'memory');
|
|
8
|
+
const DEFAULT_MAX_AGE_DAYS = 30;
|
|
9
|
+
const SUMMARY_MAX_CHARS = 1500;
|
|
10
|
+
|
|
11
|
+
function getLogFiles() {
|
|
12
|
+
if (!fs.existsSync(MEMORY_DIR)) return [];
|
|
13
|
+
return fs.readdirSync(MEMORY_DIR)
|
|
14
|
+
.filter(f => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).sort();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseLogDate(filename) {
|
|
18
|
+
const match = filename.match(/^(\d{4})-(\d{2})-(\d{2})\.md$/);
|
|
19
|
+
if (!match) return null;
|
|
20
|
+
return new Date(parseInt(match[1]), parseInt(match[2]) - 1, parseInt(match[3]));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function daysBetween(d1, d2) {
|
|
24
|
+
return Math.floor((d2 - d1) / (1000 * 60 * 60 * 24));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function extractSummary(content) {
|
|
28
|
+
const lines = content.split('\n');
|
|
29
|
+
const summary = [];
|
|
30
|
+
let inCodeBlock = false;
|
|
31
|
+
for (const line of lines) {
|
|
32
|
+
if (line.startsWith('```')) { inCodeBlock = !inCodeBlock; continue; }
|
|
33
|
+
if (inCodeBlock) continue;
|
|
34
|
+
if (line.startsWith('#') || line.startsWith('**Time:') || line.startsWith('**Trigger:')) {
|
|
35
|
+
summary.push(line);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return summary.join('\n').substring(0, SUMMARY_MAX_CHARS);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseArgs(argv) {
|
|
42
|
+
const args = { prune: false, dryRun: false, maxAge: DEFAULT_MAX_AGE_DAYS };
|
|
43
|
+
for (let i = 0; i < argv.length; i++) {
|
|
44
|
+
if (argv[i] === '--prune') args.prune = true;
|
|
45
|
+
if (argv[i] === '--dry-run') args.dryRun = true;
|
|
46
|
+
if (argv[i] === '--max-age' && argv[i + 1]) {
|
|
47
|
+
args.maxAge = parseInt(argv[i + 1]) || DEFAULT_MAX_AGE_DAYS;
|
|
48
|
+
i++;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return args;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Summarize hook: process daily session logs, embed summaries to DAXIOM, optionally prune.
|
|
56
|
+
*
|
|
57
|
+
* @param {string[]} args - CLI args after "hooks summarize"
|
|
58
|
+
*/
|
|
59
|
+
async function run(args) {
|
|
60
|
+
const parsed = parseArgs(args);
|
|
61
|
+
const today = new Date();
|
|
62
|
+
today.setHours(0, 0, 0, 0);
|
|
63
|
+
|
|
64
|
+
const files = getLogFiles();
|
|
65
|
+
if (files.length === 0) {
|
|
66
|
+
console.log('No session logs found in', MEMORY_DIR);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log(`Found ${files.length} session log(s) in ${MEMORY_DIR}`);
|
|
71
|
+
|
|
72
|
+
let daxiom = null;
|
|
73
|
+
let KNOWN_CONTEXTS = [];
|
|
74
|
+
try {
|
|
75
|
+
daxiom = require('../index');
|
|
76
|
+
KNOWN_CONTEXTS = daxiom.KNOWN_CONTEXTS || [];
|
|
77
|
+
await daxiom.init();
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.error('DAXIOM unavailable:', err.message);
|
|
80
|
+
daxiom = null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let embedded = 0, pruned = 0, skipped = 0;
|
|
84
|
+
|
|
85
|
+
for (const file of files) {
|
|
86
|
+
const logDate = parseLogDate(file);
|
|
87
|
+
if (!logDate) continue;
|
|
88
|
+
const age = daysBetween(logDate, today);
|
|
89
|
+
|
|
90
|
+
if (age < 1) { skipped++; continue; }
|
|
91
|
+
|
|
92
|
+
const logPath = path.join(MEMORY_DIR, file);
|
|
93
|
+
const content = fs.readFileSync(logPath, 'utf8');
|
|
94
|
+
|
|
95
|
+
if (daxiom && content.trim().length > 50) {
|
|
96
|
+
const summary = extractSummary(content);
|
|
97
|
+
if (summary && !parsed.dryRun) {
|
|
98
|
+
try {
|
|
99
|
+
const lower = content.toLowerCase();
|
|
100
|
+
const detectedContexts = KNOWN_CONTEXTS.filter(ctx => lower.includes(ctx));
|
|
101
|
+
const result = await daxiom.storeLearnedPattern({
|
|
102
|
+
patternName: `session_summary_${file.replace('.md', '')}`,
|
|
103
|
+
description: `Session summary for ${file.replace('.md', '')}: ${summary.substring(0, 200)}`,
|
|
104
|
+
compactText: summary,
|
|
105
|
+
patternClass: 'session-summary',
|
|
106
|
+
namespace: 'session-memory',
|
|
107
|
+
category: 'daily-summary',
|
|
108
|
+
tier: age > 7 ? 'warm' : 'hot',
|
|
109
|
+
tags: ['session', 'summary', file.replace('.md', ''), ...detectedContexts],
|
|
110
|
+
metadata: {
|
|
111
|
+
logDate: logDate.toISOString(), ageDays: age,
|
|
112
|
+
charCount: content.length, contexts: detectedContexts,
|
|
113
|
+
summarizedAt: new Date().toISOString(),
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
embedded++;
|
|
117
|
+
console.log(` Embedded: ${file} (${result.action}, age: ${age}d)`);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.error(` Failed to embed ${file}: ${err.message}`);
|
|
120
|
+
}
|
|
121
|
+
} else if (parsed.dryRun) {
|
|
122
|
+
console.log(` [dry-run] Would embed: ${file} (age: ${age}d, ${content.length} chars)`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (parsed.prune && age > parsed.maxAge) {
|
|
127
|
+
if (!parsed.dryRun) {
|
|
128
|
+
fs.unlinkSync(logPath);
|
|
129
|
+
pruned++;
|
|
130
|
+
console.log(` Pruned: ${file} (age: ${age}d, exceeded ${parsed.maxAge}d max)`);
|
|
131
|
+
} else {
|
|
132
|
+
console.log(` [dry-run] Would prune: ${file} (age: ${age}d)`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (daxiom) await daxiom.close();
|
|
138
|
+
console.log(`\nDone: ${embedded} embedded, ${pruned} pruned, ${skipped} skipped (today's)`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = { run };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { init, close, getPool } = require('./connection');
|
|
4
|
+
const { getEmbedding } = require('./embeddings');
|
|
5
|
+
const {
|
|
6
|
+
checkCoherence, getConfidenceTier, checkSecurity, checkBehavioral,
|
|
7
|
+
checkResilience, checkContract, runQualityGates,
|
|
8
|
+
} = require('./quality-gates');
|
|
9
|
+
const {
|
|
10
|
+
searchPatterns, searchWithSalience, recordUsage, storeLearnedPattern, getPatternAction, getStats,
|
|
11
|
+
} = require('./patterns');
|
|
12
|
+
const {
|
|
13
|
+
updateTiers, getConfidenceTierDistribution, promotePatternConfidence,
|
|
14
|
+
demoteWithRollback, scanRollbackCandidates, decayPatterns, getDriftReport, previewDecay,
|
|
15
|
+
} = require('./tiers');
|
|
16
|
+
const { storeSelfManaged, listByClass, updateStatus } = require('./self-manage');
|
|
17
|
+
const { getAgentProfile, routeToAgent } = require('./agents');
|
|
18
|
+
const constants = require('./constants');
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
// Connection
|
|
22
|
+
init,
|
|
23
|
+
close,
|
|
24
|
+
getPool,
|
|
25
|
+
|
|
26
|
+
// Embeddings
|
|
27
|
+
getEmbedding,
|
|
28
|
+
|
|
29
|
+
// Quality gates
|
|
30
|
+
checkCoherence,
|
|
31
|
+
getConfidenceTier,
|
|
32
|
+
checkSecurity,
|
|
33
|
+
checkBehavioral,
|
|
34
|
+
checkResilience,
|
|
35
|
+
checkContract,
|
|
36
|
+
runQualityGates,
|
|
37
|
+
|
|
38
|
+
// Patterns
|
|
39
|
+
searchPatterns,
|
|
40
|
+
searchWithSalience,
|
|
41
|
+
recordUsage,
|
|
42
|
+
storeLearnedPattern,
|
|
43
|
+
getPatternAction,
|
|
44
|
+
getStats,
|
|
45
|
+
|
|
46
|
+
// Tiers
|
|
47
|
+
updateTiers,
|
|
48
|
+
getConfidenceTierDistribution,
|
|
49
|
+
promotePatternConfidence,
|
|
50
|
+
demoteWithRollback,
|
|
51
|
+
scanRollbackCandidates,
|
|
52
|
+
decayPatterns,
|
|
53
|
+
getDriftReport,
|
|
54
|
+
previewDecay,
|
|
55
|
+
|
|
56
|
+
// Self-management
|
|
57
|
+
storeSelfManaged,
|
|
58
|
+
listByClass,
|
|
59
|
+
updateStatus,
|
|
60
|
+
|
|
61
|
+
// Agents
|
|
62
|
+
getAgentProfile,
|
|
63
|
+
routeToAgent,
|
|
64
|
+
|
|
65
|
+
// Constants
|
|
66
|
+
...constants,
|
|
67
|
+
};
|
package/src/patterns.js
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { getPool } = require('./connection');
|
|
4
|
+
const { getEmbedding } = require('./embeddings');
|
|
5
|
+
const { checkCoherence, getConfidenceTier, runQualityGates } = require('./quality-gates');
|
|
6
|
+
const {
|
|
7
|
+
DEFAULT_SEARCH_LIMIT,
|
|
8
|
+
DEFAULT_THRESHOLD,
|
|
9
|
+
CONFIDENCE_SUCCESS_DELTA,
|
|
10
|
+
CONFIDENCE_FAILURE_DELTA,
|
|
11
|
+
EMBEDDING_DIM,
|
|
12
|
+
} = require('./constants');
|
|
13
|
+
|
|
14
|
+
// ── Semantic vector search ───────────────────────────────────────────────────
|
|
15
|
+
async function searchPatterns(query, options = {}) {
|
|
16
|
+
const {
|
|
17
|
+
limit = DEFAULT_SEARCH_LIMIT,
|
|
18
|
+
threshold = DEFAULT_THRESHOLD,
|
|
19
|
+
namespace = null,
|
|
20
|
+
} = options;
|
|
21
|
+
|
|
22
|
+
const p = getPool();
|
|
23
|
+
const client = await p.connect();
|
|
24
|
+
try {
|
|
25
|
+
await client.query('SET jit = off');
|
|
26
|
+
const embedding = await getEmbedding(query);
|
|
27
|
+
const vecStr = '[' + embedding.join(',') + ']';
|
|
28
|
+
|
|
29
|
+
let sql = `
|
|
30
|
+
SELECT id, pattern_name, description, namespace, pattern_class, category,
|
|
31
|
+
tier, confidence, usage_count, success_rate, tags, metadata,
|
|
32
|
+
ROUND((1 - (embedding <=> $1::ruvector(${EMBEDDING_DIM})))::numeric, 4) as similarity
|
|
33
|
+
FROM tribal_intelligence.patterns
|
|
34
|
+
WHERE embedding IS NOT NULL
|
|
35
|
+
`;
|
|
36
|
+
const params = [vecStr];
|
|
37
|
+
|
|
38
|
+
if (namespace) {
|
|
39
|
+
sql += ` AND namespace = $2`;
|
|
40
|
+
params.push(namespace);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
sql += ` ORDER BY embedding <=> $1::ruvector(${EMBEDDING_DIM}) LIMIT $${params.length + 1}`;
|
|
44
|
+
params.push(limit);
|
|
45
|
+
|
|
46
|
+
const res = await client.query(sql, params);
|
|
47
|
+
|
|
48
|
+
return res.rows
|
|
49
|
+
.filter(r => parseFloat(r.similarity) >= threshold)
|
|
50
|
+
.map(r => ({
|
|
51
|
+
id: r.id,
|
|
52
|
+
patternName: r.pattern_name,
|
|
53
|
+
description: r.description,
|
|
54
|
+
namespace: r.namespace,
|
|
55
|
+
patternClass: r.pattern_class,
|
|
56
|
+
category: r.category,
|
|
57
|
+
tier: r.tier,
|
|
58
|
+
confidence: r.confidence,
|
|
59
|
+
usageCount: r.usage_count,
|
|
60
|
+
successRate: r.success_rate,
|
|
61
|
+
similarity: parseFloat(r.similarity),
|
|
62
|
+
tags: r.tags,
|
|
63
|
+
metadata: r.metadata,
|
|
64
|
+
}));
|
|
65
|
+
} finally {
|
|
66
|
+
client.release();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Record pattern usage ─────────────────────────────────────────────────────
|
|
71
|
+
async function recordUsage(patternId, successful = true, agentId = null) {
|
|
72
|
+
const p = getPool();
|
|
73
|
+
const client = await p.connect();
|
|
74
|
+
try {
|
|
75
|
+
const delta = successful ? CONFIDENCE_SUCCESS_DELTA : CONFIDENCE_FAILURE_DELTA;
|
|
76
|
+
|
|
77
|
+
let sql = `
|
|
78
|
+
UPDATE tribal_intelligence.patterns
|
|
79
|
+
SET usage_count = usage_count + 1,
|
|
80
|
+
success_count = success_count + ${successful ? 1 : 0},
|
|
81
|
+
failure_count = failure_count + ${successful ? 0 : 1},
|
|
82
|
+
success_rate = (success_count + ${successful ? 1 : 0})::real / (usage_count + 1)::real,
|
|
83
|
+
confidence = LEAST(1.0, GREATEST(0.0, confidence + $2)),
|
|
84
|
+
last_used_at = NOW(),
|
|
85
|
+
updated_at = NOW()
|
|
86
|
+
`;
|
|
87
|
+
const params = [patternId, delta];
|
|
88
|
+
|
|
89
|
+
if (agentId) {
|
|
90
|
+
sql += `,
|
|
91
|
+
metadata = jsonb_set(
|
|
92
|
+
COALESCE(metadata, '{}'),
|
|
93
|
+
'{used_by_agents}',
|
|
94
|
+
COALESCE(metadata->'used_by_agents', '[]'::jsonb) || to_jsonb($3::text)
|
|
95
|
+
)`;
|
|
96
|
+
params.push(agentId);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
sql += ` WHERE id = $1 RETURNING confidence`;
|
|
100
|
+
const res = await client.query(sql, params);
|
|
101
|
+
|
|
102
|
+
const newConfidence = res.rows[0]?.confidence || 0;
|
|
103
|
+
const tier = getConfidenceTier(newConfidence);
|
|
104
|
+
return { confidence: newConfidence, tier: tier?.label || 'expired' };
|
|
105
|
+
} finally {
|
|
106
|
+
client.release();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── Store learned pattern ────────────────────────────────────────────────────
|
|
111
|
+
async function storeLearnedPattern({
|
|
112
|
+
patternName,
|
|
113
|
+
description,
|
|
114
|
+
compactText,
|
|
115
|
+
patternClass = 'learned',
|
|
116
|
+
namespace = 'learned',
|
|
117
|
+
category = 'agent-task',
|
|
118
|
+
tier = 'warm',
|
|
119
|
+
tags = [],
|
|
120
|
+
metadata = {},
|
|
121
|
+
skipCoherence = false,
|
|
122
|
+
agentId = null,
|
|
123
|
+
agentType = null,
|
|
124
|
+
}) {
|
|
125
|
+
if (agentId || agentType) {
|
|
126
|
+
metadata = {
|
|
127
|
+
...metadata,
|
|
128
|
+
agent_id: agentId || 'unknown',
|
|
129
|
+
agent_type: agentType || 'unknown',
|
|
130
|
+
agent_timestamp: new Date().toISOString(),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const p = getPool();
|
|
135
|
+
const client = await p.connect();
|
|
136
|
+
try {
|
|
137
|
+
const patternId = `${namespace}:${patternName}`;
|
|
138
|
+
const textToEmbed = compactText || description;
|
|
139
|
+
|
|
140
|
+
const existing = await client.query(
|
|
141
|
+
'SELECT id FROM tribal_intelligence.patterns WHERE pattern_id = $1',
|
|
142
|
+
[patternId]
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
if (existing.rows.length > 0) {
|
|
146
|
+
await client.query(`
|
|
147
|
+
UPDATE tribal_intelligence.patterns
|
|
148
|
+
SET usage_count = usage_count + 1,
|
|
149
|
+
confidence = LEAST(1.0, confidence + 0.01),
|
|
150
|
+
last_used_at = NOW(),
|
|
151
|
+
updated_at = NOW()
|
|
152
|
+
WHERE pattern_id = $1
|
|
153
|
+
`, [patternId]);
|
|
154
|
+
return { id: existing.rows[0].id, action: 'updated' };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const embedding = await getEmbedding(textToEmbed);
|
|
158
|
+
const vecStr = '[' + embedding.join(',') + ']';
|
|
159
|
+
|
|
160
|
+
// Coherence gate
|
|
161
|
+
if (!skipCoherence) {
|
|
162
|
+
const gate = await checkCoherence(client, vecStr, namespace, patternClass);
|
|
163
|
+
if (!gate.allowed) {
|
|
164
|
+
console.warn(`[COHERENCE GATE] REJECTED: "${patternName}" — ${gate.reason}`);
|
|
165
|
+
return { id: null, action: 'rejected', reason: gate.reason, neighbors: gate.neighbors };
|
|
166
|
+
}
|
|
167
|
+
if (gate.coherence_warning) {
|
|
168
|
+
console.warn(`[COHERENCE GATE] WARNING: "${patternName}" — ${gate.reason}`);
|
|
169
|
+
metadata = { ...metadata, coherence_warning: true, coherence_reason: gate.reason };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Quality gates 4-7
|
|
174
|
+
const gateResult = runQualityGates(description, namespace, patternClass, tags, metadata, {
|
|
175
|
+
skipSecurity: skipCoherence || false,
|
|
176
|
+
});
|
|
177
|
+
if (!gateResult.allowed) {
|
|
178
|
+
return { stored: false, reason: gateResult.summary, gates: gateResult.results };
|
|
179
|
+
}
|
|
180
|
+
if (gateResult.warned.length > 0) {
|
|
181
|
+
console.error(`[QUALITY GATE] WARNING: ${gateResult.summary}`);
|
|
182
|
+
metadata = { ...metadata, quality_warnings: gateResult.warned.map(w => w.reason) };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const res = await client.query(`
|
|
186
|
+
INSERT INTO tribal_intelligence.patterns (
|
|
187
|
+
pattern_id, pattern_name, description, embedding,
|
|
188
|
+
pattern_class, namespace, category, tier, tags, metadata,
|
|
189
|
+
confidence, usage_count
|
|
190
|
+
) VALUES ($1, $2, $3, $4::ruvector(${EMBEDDING_DIM}), $5, $6, $7, $8, $9, $10, 0.5, 1)
|
|
191
|
+
RETURNING id
|
|
192
|
+
`, [
|
|
193
|
+
patternId, patternName, description, vecStr,
|
|
194
|
+
patternClass, namespace, category, tier, tags,
|
|
195
|
+
JSON.stringify({ ...metadata, compact_text: compactText, learned_at: new Date().toISOString() }),
|
|
196
|
+
]);
|
|
197
|
+
|
|
198
|
+
return { id: res.rows[0].id, action: 'created' };
|
|
199
|
+
} finally {
|
|
200
|
+
client.release();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── Get pattern action (confidence-based routing) ────────────────────────────
|
|
205
|
+
async function getPatternAction(patternId) {
|
|
206
|
+
const p = getPool();
|
|
207
|
+
const client = await p.connect();
|
|
208
|
+
try {
|
|
209
|
+
let res = await client.query(`
|
|
210
|
+
SELECT id, pattern_name, confidence, metadata
|
|
211
|
+
FROM tribal_intelligence.patterns WHERE pattern_id = $1 LIMIT 1
|
|
212
|
+
`, [patternId]);
|
|
213
|
+
|
|
214
|
+
if (res.rows.length === 0) {
|
|
215
|
+
try {
|
|
216
|
+
res = await client.query(`
|
|
217
|
+
SELECT id, pattern_name, confidence, metadata
|
|
218
|
+
FROM tribal_intelligence.patterns WHERE id = $1::uuid LIMIT 1
|
|
219
|
+
`, [patternId]);
|
|
220
|
+
} catch (e) { return null; }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (res.rows.length === 0) return null;
|
|
224
|
+
|
|
225
|
+
const pattern = res.rows[0];
|
|
226
|
+
const confidence = pattern.confidence || 0;
|
|
227
|
+
const tier = getConfidenceTier(confidence);
|
|
228
|
+
|
|
229
|
+
if (!tier) {
|
|
230
|
+
return { confidence, tier: 'expired', action: 'revalidate', needsRevalidation: true };
|
|
231
|
+
}
|
|
232
|
+
return { confidence, tier: tier.label, action: tier.action, needsRevalidation: false };
|
|
233
|
+
} finally {
|
|
234
|
+
client.release();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── Stats ────────────────────────────────────────────────────────────────────
|
|
239
|
+
async function getStats() {
|
|
240
|
+
const p = getPool();
|
|
241
|
+
const client = await p.connect();
|
|
242
|
+
try {
|
|
243
|
+
const res = await client.query(`
|
|
244
|
+
SELECT namespace, tier, COUNT(*) as count, COUNT(embedding) as embedded,
|
|
245
|
+
ROUND(AVG(confidence)::numeric, 2) as avg_confidence,
|
|
246
|
+
SUM(usage_count) as total_usage,
|
|
247
|
+
ROUND(AVG(NULLIF(success_rate, 0))::numeric, 2) as avg_success_rate
|
|
248
|
+
FROM tribal_intelligence.patterns
|
|
249
|
+
GROUP BY namespace, tier ORDER BY namespace, tier
|
|
250
|
+
`);
|
|
251
|
+
return res.rows;
|
|
252
|
+
} finally {
|
|
253
|
+
client.release();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ── Salience-ranked search (similarity * recency * usage) ────────────────────
|
|
258
|
+
async function searchWithSalience(query, options = {}) {
|
|
259
|
+
const {
|
|
260
|
+
limit = DEFAULT_SEARCH_LIMIT,
|
|
261
|
+
threshold = DEFAULT_THRESHOLD,
|
|
262
|
+
namespace = null,
|
|
263
|
+
decay = 0.03,
|
|
264
|
+
candidatePool = 30,
|
|
265
|
+
} = options;
|
|
266
|
+
|
|
267
|
+
const p = getPool();
|
|
268
|
+
const client = await p.connect();
|
|
269
|
+
try {
|
|
270
|
+
await client.query('SET jit = off');
|
|
271
|
+
const embedding = await getEmbedding(query);
|
|
272
|
+
const vecStr = '[' + embedding.join(',') + ']';
|
|
273
|
+
|
|
274
|
+
let sql = `
|
|
275
|
+
SELECT id, pattern_name, description, namespace, pattern_class, category,
|
|
276
|
+
tier, confidence, usage_count, success_rate, tags, metadata,
|
|
277
|
+
last_used_at, created_at,
|
|
278
|
+
ROUND((1 - (embedding <=> $1::ruvector(${EMBEDDING_DIM})))::numeric, 4) as similarity
|
|
279
|
+
FROM tribal_intelligence.patterns
|
|
280
|
+
WHERE embedding IS NOT NULL
|
|
281
|
+
`;
|
|
282
|
+
const params = [vecStr];
|
|
283
|
+
|
|
284
|
+
if (namespace) {
|
|
285
|
+
sql += ` AND namespace = $2`;
|
|
286
|
+
params.push(namespace);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
sql += ` ORDER BY embedding <=> $1::ruvector(${EMBEDDING_DIM}) LIMIT $${params.length + 1}`;
|
|
290
|
+
params.push(candidatePool);
|
|
291
|
+
|
|
292
|
+
const res = await client.query(sql, params);
|
|
293
|
+
|
|
294
|
+
const now = Date.now();
|
|
295
|
+
return res.rows
|
|
296
|
+
.filter(r => parseFloat(r.similarity) >= threshold)
|
|
297
|
+
.map(r => {
|
|
298
|
+
const sim = parseFloat(r.similarity);
|
|
299
|
+
// Recency factor: exponential decay based on days since last use
|
|
300
|
+
const lastUsed = r.last_used_at ? new Date(r.last_used_at).getTime() : new Date(r.created_at).getTime();
|
|
301
|
+
const daysSinceUse = Math.max(0, (now - lastUsed) / (1000 * 60 * 60 * 24));
|
|
302
|
+
const recencyFactor = Math.exp(-decay * daysSinceUse);
|
|
303
|
+
// Usage factor: log-scaled usage count (diminishing returns)
|
|
304
|
+
const usageFactor = Math.log2(1 + (r.usage_count || 0)) / 10;
|
|
305
|
+
// Salience = similarity * (0.7 + 0.2*recency + 0.1*usage)
|
|
306
|
+
const salience = sim * (0.7 + 0.2 * recencyFactor + 0.1 * Math.min(usageFactor, 1));
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
id: r.id,
|
|
310
|
+
patternName: r.pattern_name,
|
|
311
|
+
description: r.description,
|
|
312
|
+
namespace: r.namespace,
|
|
313
|
+
patternClass: r.pattern_class,
|
|
314
|
+
category: r.category,
|
|
315
|
+
tier: r.tier,
|
|
316
|
+
confidence: r.confidence,
|
|
317
|
+
usageCount: r.usage_count,
|
|
318
|
+
successRate: r.success_rate,
|
|
319
|
+
similarity: sim,
|
|
320
|
+
salience: parseFloat(salience.toFixed(4)),
|
|
321
|
+
tags: r.tags,
|
|
322
|
+
metadata: r.metadata,
|
|
323
|
+
};
|
|
324
|
+
})
|
|
325
|
+
.sort((a, b) => b.salience - a.salience)
|
|
326
|
+
.slice(0, limit);
|
|
327
|
+
} finally {
|
|
328
|
+
client.release();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
module.exports = {
|
|
333
|
+
searchPatterns,
|
|
334
|
+
searchWithSalience,
|
|
335
|
+
recordUsage,
|
|
336
|
+
storeLearnedPattern,
|
|
337
|
+
getPatternAction,
|
|
338
|
+
getStats,
|
|
339
|
+
};
|