@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.
@@ -0,0 +1,183 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ COHERENCE_DUPLICATE_THRESHOLD,
5
+ COHERENCE_CONTRADICTION_THRESHOLD,
6
+ CONFIDENCE_TIERS,
7
+ CONFIDENCE_EXPIRED_THRESHOLD,
8
+ KNOWN_CLASSES,
9
+ KNOWN_NAMESPACES,
10
+ SECURITY_PATTERNS,
11
+ CONTENT_MIN_LENGTH,
12
+ CONTENT_MAX_LENGTH,
13
+ } = require('./constants');
14
+
15
+ // ── Gate 1: Coherence (hallucination prevention) ─────────────────────────────
16
+ async function checkCoherence(client, embeddingVecStr, candidateNamespace, candidateClass) {
17
+ const sql = `
18
+ SELECT pattern_name, description, namespace, pattern_class,
19
+ ROUND((1 - (embedding <=> $1::ruvector(1536)))::numeric, 4) as similarity
20
+ FROM tribal_intelligence.patterns
21
+ WHERE embedding IS NOT NULL
22
+ ORDER BY embedding <=> $1::ruvector(1536)
23
+ LIMIT 5
24
+ `;
25
+ const res = await client.query(sql, [embeddingVecStr]);
26
+ const neighbors = res.rows.map(r => ({
27
+ pattern_name: r.pattern_name,
28
+ similarity: parseFloat(r.similarity),
29
+ namespace: r.namespace,
30
+ pattern_class: r.pattern_class,
31
+ }));
32
+
33
+ for (const n of neighbors) {
34
+ if (n.similarity > COHERENCE_DUPLICATE_THRESHOLD) {
35
+ return {
36
+ allowed: false,
37
+ action: 'reject',
38
+ reason: `Near-duplicate detected: "${n.pattern_name}" (${n.namespace}/${n.pattern_class}) with similarity ${n.similarity}`,
39
+ neighbors,
40
+ coherence_warning: false,
41
+ };
42
+ }
43
+ }
44
+
45
+ for (const n of neighbors) {
46
+ if (
47
+ n.similarity > COHERENCE_CONTRADICTION_THRESHOLD &&
48
+ n.pattern_class === candidateClass &&
49
+ n.namespace === candidateNamespace
50
+ ) {
51
+ return {
52
+ allowed: true,
53
+ action: 'warn',
54
+ reason: `Potential contradiction with "${n.pattern_name}" (similarity ${n.similarity}, same ${candidateNamespace}/${candidateClass})`,
55
+ neighbors,
56
+ coherence_warning: true,
57
+ };
58
+ }
59
+ }
60
+
61
+ return { allowed: true, action: 'allow', reason: null, neighbors, coherence_warning: false };
62
+ }
63
+
64
+ // ── Gate 2: Confidence Tier ──────────────────────────────────────────────────
65
+ function getConfidenceTier(confidence) {
66
+ if (confidence < CONFIDENCE_EXPIRED_THRESHOLD) return null;
67
+ if (confidence >= CONFIDENCE_TIERS.PLATINUM.min) return CONFIDENCE_TIERS.PLATINUM;
68
+ if (confidence >= CONFIDENCE_TIERS.GOLD.min) return CONFIDENCE_TIERS.GOLD;
69
+ if (confidence >= CONFIDENCE_TIERS.SILVER.min) return CONFIDENCE_TIERS.SILVER;
70
+ if (confidence >= CONFIDENCE_TIERS.BRONZE.min) return CONFIDENCE_TIERS.BRONZE;
71
+ return null;
72
+ }
73
+
74
+ // ── Gate 4: Security — Detects secrets, PII, credentials ────────────────────
75
+ function checkSecurity(content, metadata) {
76
+ const corpus = content + (metadata ? ' ' + JSON.stringify(metadata) : '');
77
+ for (const pattern of SECURITY_PATTERNS) {
78
+ if (pattern.test(corpus)) {
79
+ return {
80
+ pass: false,
81
+ level: 'block',
82
+ gate: 'security',
83
+ reason: `Security violation: content matches pattern ${pattern.source.slice(0, 40)}`,
84
+ };
85
+ }
86
+ }
87
+ return { pass: true, level: 'pass', gate: 'security', reason: 'No secrets or PII detected' };
88
+ }
89
+
90
+ // ── Gate 5: Behavioral — Validates pattern class and namespace ───────────────
91
+ function checkBehavioral(patternClass, namespace) {
92
+ const warnings = [];
93
+ if (!KNOWN_CLASSES.includes(patternClass)) {
94
+ warnings.push(`Unknown pattern class "${patternClass}"`);
95
+ }
96
+ if (!KNOWN_NAMESPACES.includes(namespace)) {
97
+ warnings.push(`Unknown namespace "${namespace}"`);
98
+ }
99
+ if (warnings.length > 0) {
100
+ return { pass: true, level: 'warn', gate: 'behavioral', reason: warnings.join('; ') };
101
+ }
102
+ return { pass: true, level: 'pass', gate: 'behavioral', reason: 'Class and namespace recognized' };
103
+ }
104
+
105
+ // ── Gate 6: Resilience — Validates content length bounds ─────────────────────
106
+ function checkResilience(content) {
107
+ if (typeof content !== 'string') {
108
+ return { pass: false, level: 'block', gate: 'resilience', reason: 'Content is not a string' };
109
+ }
110
+ if (content.length < CONTENT_MIN_LENGTH) {
111
+ return {
112
+ pass: false, level: 'block', gate: 'resilience',
113
+ reason: `Content too short (${content.length} chars, min ${CONTENT_MIN_LENGTH})`,
114
+ };
115
+ }
116
+ if (content.length > CONTENT_MAX_LENGTH) {
117
+ return {
118
+ pass: false, level: 'block', gate: 'resilience',
119
+ reason: `Content too long (${content.length} chars, max ${CONTENT_MAX_LENGTH})`,
120
+ };
121
+ }
122
+ return { pass: true, level: 'pass', gate: 'resilience', reason: 'Content length within bounds' };
123
+ }
124
+
125
+ // ── Gate 7: Contract — Validates required fields and types ───────────────────
126
+ function checkContract(content, namespace, patternClass, tags, metadata) {
127
+ const violations = [];
128
+ if (typeof content !== 'string' || content.length === 0) {
129
+ violations.push('content must be a non-empty string');
130
+ }
131
+ if (typeof namespace !== 'string' || namespace.length === 0) {
132
+ violations.push('namespace must be a non-empty string');
133
+ }
134
+ if (typeof patternClass !== 'string' || patternClass.length === 0) {
135
+ violations.push('patternClass must be a non-empty string');
136
+ }
137
+ if (tags != null && !Array.isArray(tags)) {
138
+ violations.push('tags must be an array or null/undefined');
139
+ }
140
+ if (metadata != null && (typeof metadata !== 'object' || Array.isArray(metadata))) {
141
+ violations.push('metadata must be a plain object or null/undefined');
142
+ }
143
+ if (violations.length > 0) {
144
+ return { pass: false, level: 'block', gate: 'contract', reason: violations.join('; ') };
145
+ }
146
+ return { pass: true, level: 'pass', gate: 'contract', reason: 'All fields valid' };
147
+ }
148
+
149
+ // ── Aggregator — Runs gates 4-7 and returns combined verdict ─────────────────
150
+ function runQualityGates(content, namespace, patternClass, tags, metadata, options = {}) {
151
+ const results = [];
152
+ if (!options.skipSecurity) {
153
+ results.push(checkSecurity(content, metadata));
154
+ }
155
+ results.push(checkBehavioral(patternClass, namespace));
156
+ results.push(checkResilience(content));
157
+ results.push(checkContract(content, namespace, patternClass, tags, metadata));
158
+
159
+ const blocked = results.filter(r => r.level === 'block');
160
+ const warned = results.filter(r => r.level === 'warn');
161
+
162
+ return {
163
+ allowed: blocked.length === 0,
164
+ results,
165
+ blocked,
166
+ warned,
167
+ summary: blocked.length > 0
168
+ ? `BLOCKED by ${blocked.map(b => b.gate).join(', ')}: ${blocked.map(b => b.reason).join('; ')}`
169
+ : warned.length > 0
170
+ ? `PASSED with warnings: ${warned.map(w => w.reason).join('; ')}`
171
+ : 'All gates passed',
172
+ };
173
+ }
174
+
175
+ module.exports = {
176
+ checkCoherence,
177
+ getConfidenceTier,
178
+ checkSecurity,
179
+ checkBehavioral,
180
+ checkResilience,
181
+ checkContract,
182
+ runQualityGates,
183
+ };
@@ -0,0 +1,105 @@
1
+ 'use strict';
2
+
3
+ const { getPool } = require('./connection');
4
+ const { storeLearnedPattern } = require('./patterns');
5
+ const { SELF_MGMT_CLASSES } = require('./constants');
6
+
7
+ // ── Store structured project artifacts ───────────────────────────────────────
8
+ async function storeSelfManaged({
9
+ patternClass,
10
+ name,
11
+ description,
12
+ namespace = 'daxiom',
13
+ category = null,
14
+ tags = [],
15
+ status = null,
16
+ repo = null,
17
+ priority = null,
18
+ skipCoherence = false,
19
+ }) {
20
+ if (!SELF_MGMT_CLASSES.includes(patternClass)) {
21
+ throw new Error(`Invalid class: ${patternClass}. Must be one of: ${SELF_MGMT_CLASSES.join(', ')}`);
22
+ }
23
+
24
+ const meta = { stored_at: new Date().toISOString() };
25
+ if (status) meta.status = status;
26
+ if (repo) meta.repo = repo;
27
+ if (priority) meta.priority = priority;
28
+
29
+ return storeLearnedPattern({
30
+ patternName: name,
31
+ description,
32
+ compactText: description,
33
+ patternClass,
34
+ namespace,
35
+ category: category || patternClass,
36
+ tier: patternClass === 'todo' ? 'hot' : 'warm',
37
+ tags,
38
+ metadata: meta,
39
+ skipCoherence,
40
+ });
41
+ }
42
+
43
+ // ── List patterns by class ───────────────────────────────────────────────────
44
+ async function listByClass(patternClass, options = {}) {
45
+ const { namespace = null, limit = 20, status = null } = options;
46
+ const p = getPool();
47
+ const client = await p.connect();
48
+ try {
49
+ let sql = `
50
+ SELECT id, pattern_id, pattern_name, description, pattern_class, namespace,
51
+ category, tier, confidence, usage_count, tags, metadata,
52
+ created_at, updated_at, last_used_at
53
+ FROM tribal_intelligence.patterns WHERE pattern_class = $1
54
+ `;
55
+ const params = [patternClass];
56
+
57
+ if (namespace) {
58
+ sql += ` AND namespace = $${params.length + 1}`;
59
+ params.push(namespace);
60
+ }
61
+ if (status) {
62
+ sql += ` AND metadata->>'status' = $${params.length + 1}`;
63
+ params.push(status);
64
+ }
65
+
66
+ sql += ` ORDER BY created_at DESC LIMIT $${params.length + 1}`;
67
+ params.push(limit);
68
+
69
+ const res = await client.query(sql, params);
70
+ return res.rows;
71
+ } finally {
72
+ client.release();
73
+ }
74
+ }
75
+
76
+ // ── Update pattern status ────────────────────────────────────────────────────
77
+ async function updateStatus(patternId, newStatus) {
78
+ const p = getPool();
79
+ const client = await p.connect();
80
+ try {
81
+ const res = await client.query(`
82
+ UPDATE tribal_intelligence.patterns
83
+ SET metadata = jsonb_set(COALESCE(metadata, '{}'), '{status}', to_jsonb($2::text)),
84
+ updated_at = NOW(), last_used_at = NOW()
85
+ WHERE pattern_id = $1
86
+ RETURNING id, pattern_name, metadata->>'status' as status
87
+ `, [patternId, newStatus]);
88
+
89
+ if (res.rows.length === 0) {
90
+ const res2 = await client.query(`
91
+ UPDATE tribal_intelligence.patterns
92
+ SET metadata = jsonb_set(COALESCE(metadata, '{}'), '{status}', to_jsonb($2::text)),
93
+ updated_at = NOW(), last_used_at = NOW()
94
+ WHERE id = $1::uuid
95
+ RETURNING id, pattern_name, metadata->>'status' as status
96
+ `, [patternId, newStatus]);
97
+ return res2.rows[0] || null;
98
+ }
99
+ return res.rows[0];
100
+ } finally {
101
+ client.release();
102
+ }
103
+ }
104
+
105
+ module.exports = { storeSelfManaged, listByClass, updateStatus };
package/src/tiers.js ADDED
@@ -0,0 +1,305 @@
1
+ 'use strict';
2
+
3
+ const { getPool } = require('./connection');
4
+ const { getConfidenceTier } = require('./quality-gates');
5
+ const { CONFIDENCE_SUCCESS_DELTA } = require('./constants');
6
+
7
+ // ── Promote/demote tiers based on usage ──────────────────────────────────────
8
+ async function updateTiers() {
9
+ const p = getPool();
10
+ const client = await p.connect();
11
+ try {
12
+ const promoted = await client.query(`
13
+ UPDATE tribal_intelligence.patterns SET tier = 'hot', updated_at = NOW()
14
+ WHERE tier = 'warm' AND usage_count >= 10 AND success_rate >= 0.7
15
+ RETURNING id, pattern_name
16
+ `);
17
+
18
+ const demoted = await client.query(`
19
+ UPDATE tribal_intelligence.patterns SET tier = 'warm', updated_at = NOW()
20
+ WHERE tier = 'hot' AND last_used_at < NOW() - INTERVAL '30 days'
21
+ RETURNING id, pattern_name
22
+ `);
23
+
24
+ const cooled = await client.query(`
25
+ UPDATE tribal_intelligence.patterns SET tier = 'cold', updated_at = NOW()
26
+ WHERE tier = 'warm' AND last_used_at IS NOT NULL
27
+ AND last_used_at < NOW() - INTERVAL '90 days'
28
+ RETURNING id, pattern_name
29
+ `);
30
+
31
+ await client.query(`
32
+ UPDATE tribal_intelligence.patterns
33
+ SET metadata = jsonb_set(
34
+ jsonb_set(
35
+ COALESCE(metadata, '{}'),
36
+ '{confidence_tier}',
37
+ to_jsonb(
38
+ CASE
39
+ WHEN confidence >= 0.95::real THEN 'platinum'
40
+ WHEN confidence >= 0.85::real THEN 'gold'
41
+ WHEN confidence >= 0.75::real THEN 'silver'
42
+ WHEN confidence >= 0.70::real THEN 'bronze'
43
+ ELSE 'expired'
44
+ END
45
+ )
46
+ ),
47
+ '{needs_revalidation}',
48
+ to_jsonb(confidence < 0.70::real)
49
+ ),
50
+ updated_at = NOW()
51
+ WHERE confidence IS NOT NULL
52
+ `);
53
+
54
+ return {
55
+ promoted: promoted.rows.length,
56
+ demotedToWarm: demoted.rows.length,
57
+ demotedToCold: cooled.rows.length,
58
+ };
59
+ } finally {
60
+ client.release();
61
+ }
62
+ }
63
+
64
+ // ── Confidence tier distribution ─────────────────────────────────────────────
65
+ async function getConfidenceTierDistribution() {
66
+ const p = getPool();
67
+ const client = await p.connect();
68
+ try {
69
+ const res = await client.query(`
70
+ SELECT
71
+ COUNT(*) FILTER (WHERE confidence >= 0.95) as platinum,
72
+ COUNT(*) FILTER (WHERE confidence >= 0.85 AND confidence < 0.95) as gold,
73
+ COUNT(*) FILTER (WHERE confidence >= 0.75 AND confidence < 0.85) as silver,
74
+ COUNT(*) FILTER (WHERE confidence >= 0.70 AND confidence < 0.75) as bronze,
75
+ COUNT(*) FILTER (WHERE confidence < 0.70) as expired
76
+ FROM tribal_intelligence.patterns WHERE confidence IS NOT NULL
77
+ `);
78
+ return res.rows[0];
79
+ } finally {
80
+ client.release();
81
+ }
82
+ }
83
+
84
+ // ── Force-promote pattern confidence ─────────────────────────────────────────
85
+ async function promotePatternConfidence(patternId) {
86
+ const p = getPool();
87
+ const client = await p.connect();
88
+ try {
89
+ let res = await client.query(`
90
+ UPDATE tribal_intelligence.patterns
91
+ SET confidence = LEAST(1.0, confidence + $2), updated_at = NOW()
92
+ WHERE pattern_id = $1 RETURNING confidence, pattern_name
93
+ `, [patternId, CONFIDENCE_SUCCESS_DELTA]);
94
+
95
+ if (res.rows.length === 0) {
96
+ try {
97
+ res = await client.query(`
98
+ UPDATE tribal_intelligence.patterns
99
+ SET confidence = LEAST(1.0, confidence + $2), updated_at = NOW()
100
+ WHERE id = $1::uuid RETURNING confidence, pattern_name
101
+ `, [patternId, CONFIDENCE_SUCCESS_DELTA]);
102
+ } catch (e) { return { success: false, error: 'Pattern not found' }; }
103
+ }
104
+
105
+ if (res.rows.length === 0) return { success: false, error: 'Pattern not found' };
106
+
107
+ const newConfidence = res.rows[0].confidence;
108
+ const tier = getConfidenceTier(newConfidence);
109
+ return { success: true, patternName: res.rows[0].pattern_name, newConfidence, tier: tier?.label || 'expired' };
110
+ } finally {
111
+ client.release();
112
+ }
113
+ }
114
+
115
+ // ── Demote with rollback ─────────────────────────────────────────────────────
116
+ async function demoteWithRollback(patternId, options = {}) {
117
+ const amount = options.amount ?? 0.1;
118
+ const threshold = options.threshold ?? 0.3;
119
+ const reason = options.reason ?? 'failure';
120
+
121
+ const p = getPool();
122
+ const client = await p.connect();
123
+ try {
124
+ let res = await client.query(`
125
+ UPDATE tribal_intelligence.patterns
126
+ SET confidence = GREATEST(0.0, confidence - $2),
127
+ failure_count = failure_count + 1, usage_count = usage_count + 1,
128
+ metadata = jsonb_set(
129
+ jsonb_set(COALESCE(metadata, '{}'), '{last_demotion_reason}', to_jsonb($3::text)),
130
+ '{last_demotion_at}', to_jsonb(NOW()::text)
131
+ ), updated_at = NOW()
132
+ WHERE pattern_id = $1
133
+ RETURNING id, pattern_id, pattern_name, confidence, (confidence + $2) as old_confidence
134
+ `, [patternId, amount, reason]);
135
+
136
+ if (res.rows.length === 0) {
137
+ try {
138
+ res = await client.query(`
139
+ UPDATE tribal_intelligence.patterns
140
+ SET confidence = GREATEST(0.0, confidence - $2),
141
+ failure_count = failure_count + 1, usage_count = usage_count + 1,
142
+ metadata = jsonb_set(
143
+ jsonb_set(COALESCE(metadata, '{}'), '{last_demotion_reason}', to_jsonb($3::text)),
144
+ '{last_demotion_at}', to_jsonb(NOW()::text)
145
+ ), updated_at = NOW()
146
+ WHERE id = $1::uuid
147
+ RETURNING id, pattern_id, pattern_name, confidence, (confidence + $2) as old_confidence
148
+ `, [patternId, amount, reason]);
149
+ } catch (e) { return { success: false, error: 'Pattern not found' }; }
150
+ }
151
+
152
+ if (res.rows.length === 0) return { success: false, error: 'Pattern not found' };
153
+
154
+ const row = res.rows[0];
155
+ const newConfidence = parseFloat(row.confidence);
156
+ const oldConfidence = parseFloat(row.old_confidence);
157
+ const tier = getConfidenceTier(newConfidence);
158
+ const rollbackTriggered = newConfidence < threshold;
159
+ let rollbackEvent = null;
160
+
161
+ if (rollbackTriggered) {
162
+ await client.query(`
163
+ UPDATE tribal_intelligence.patterns
164
+ SET tier = 'cold',
165
+ metadata = jsonb_set(
166
+ jsonb_set(
167
+ jsonb_set(COALESCE(metadata, '{}'), '{archived}', 'true'::jsonb),
168
+ '{archived_at}', to_jsonb(NOW()::text)
169
+ ), '{rollback_reason}', to_jsonb($2::text)
170
+ ), updated_at = NOW()
171
+ WHERE id = $1
172
+ `, [row.id, `Confidence ${newConfidence.toFixed(2)} below threshold ${threshold}`]);
173
+
174
+ rollbackEvent = {
175
+ patternId: row.pattern_id, patternName: row.pattern_name,
176
+ previousConfidence: oldConfidence, triggeredAtConfidence: newConfidence,
177
+ threshold, action: 'archived', timestamp: new Date().toISOString(), reason,
178
+ };
179
+ }
180
+
181
+ return { success: true, patternName: row.pattern_name, oldConfidence, newConfidence, tier: tier?.label || 'expired', rollbackTriggered, rollbackEvent };
182
+ } finally {
183
+ client.release();
184
+ }
185
+ }
186
+
187
+ // ── Scan rollback candidates ─────────────────────────────────────────────────
188
+ async function scanRollbackCandidates(options = {}) {
189
+ const threshold = options.threshold ?? 0.3;
190
+ const limit = options.limit ?? 50;
191
+ const p = getPool();
192
+ const client = await p.connect();
193
+ try {
194
+ const res = await client.query(`
195
+ SELECT pattern_id, pattern_name, namespace, confidence, tier,
196
+ COALESCE((metadata->>'archived')::boolean, false) as archived,
197
+ metadata->>'rollback_reason' as rollback_reason
198
+ FROM tribal_intelligence.patterns
199
+ WHERE confidence IS NOT NULL AND confidence < $1
200
+ ORDER BY confidence ASC LIMIT $2
201
+ `, [threshold, limit]);
202
+
203
+ return res.rows.map(r => ({
204
+ patternId: r.pattern_id, patternName: r.pattern_name, namespace: r.namespace,
205
+ confidence: parseFloat(r.confidence), tier: r.tier,
206
+ archived: r.archived || false, rollbackReason: r.rollback_reason || null,
207
+ }));
208
+ } finally {
209
+ client.release();
210
+ }
211
+ }
212
+
213
+ // ── Temporal decay ───────────────────────────────────────────────────────────
214
+ async function decayPatterns(options = {}) {
215
+ const { baseRate = 0.02, minIdleDays = 1.0, floor = 0.0 } = options;
216
+ const p = getPool();
217
+ const client = await p.connect();
218
+ try {
219
+ const res = await client.query(
220
+ `SELECT * FROM tribal_intelligence.decay_pattern_confidence($1, $2, $3)`,
221
+ [baseRate, minIdleDays, floor]
222
+ );
223
+ const row = res.rows[0] || {};
224
+ return {
225
+ patternsDecayed: parseInt(row.patterns_decayed) || 0,
226
+ avgNewConfidence: parseFloat(row.avg_new_confidence) || 0,
227
+ tierChanges: parseInt(row.tier_changes) || 0,
228
+ };
229
+ } finally {
230
+ client.release();
231
+ }
232
+ }
233
+
234
+ // ── Drift report ─────────────────────────────────────────────────────────────
235
+ async function getDriftReport(options = {}) {
236
+ const { limit = 20, minIdleDays = 1.0 } = options;
237
+ const p = getPool();
238
+ const client = await p.connect();
239
+ try {
240
+ await client.query('SET jit = off');
241
+ const res = await client.query(
242
+ `SELECT * FROM tribal_intelligence.get_drift_report($1, $2)`,
243
+ [limit, minIdleDays]
244
+ );
245
+ return res.rows.map(r => ({
246
+ patternId: r.pattern_id, patternName: r.pattern_name, namespace: r.namespace,
247
+ patternClass: r.pattern_class, confidence: parseFloat(r.confidence) || 0,
248
+ daysIdle: parseFloat(r.days_idle) || 0, driftScore: parseFloat(r.drift_score) || 0,
249
+ tier: r.tier,
250
+ }));
251
+ } finally {
252
+ client.release();
253
+ }
254
+ }
255
+
256
+ // ── Preview decay ────────────────────────────────────────────────────────────
257
+ async function previewDecay(patternId, options = {}) {
258
+ const { baseRate = 0.02 } = options;
259
+ const p = getPool();
260
+ const client = await p.connect();
261
+ try {
262
+ let res;
263
+ try {
264
+ res = await client.query(
265
+ `SELECT * FROM tribal_intelligence.preview_pattern_decay($1::uuid, $2)`,
266
+ [patternId, baseRate]
267
+ );
268
+ } catch (e) {
269
+ const lookup = await client.query(
270
+ `SELECT id FROM tribal_intelligence.patterns WHERE pattern_id = $1`,
271
+ [patternId]
272
+ );
273
+ if (lookup.rows.length === 0) return null;
274
+ res = await client.query(
275
+ `SELECT * FROM tribal_intelligence.preview_pattern_decay($1::uuid, $2)`,
276
+ [lookup.rows[0].id, baseRate]
277
+ );
278
+ }
279
+ if (!res || res.rows.length === 0) return null;
280
+ const r = res.rows[0];
281
+ return {
282
+ patternName: r.pattern_name,
283
+ currentConfidence: parseFloat(r.current_confidence) || 0,
284
+ decayedConfidence: parseFloat(r.decayed_confidence) || 0,
285
+ daysIdle: parseFloat(r.days_idle) || 0,
286
+ decayAmount: parseFloat(r.decay_amount) || 0,
287
+ effectiveRate: parseFloat(r.effective_rate) || 0,
288
+ currentTier: r.current_tier,
289
+ projectedTier: r.projected_tier,
290
+ };
291
+ } finally {
292
+ client.release();
293
+ }
294
+ }
295
+
296
+ module.exports = {
297
+ updateTiers,
298
+ getConfidenceTierDistribution,
299
+ promotePatternConfidence,
300
+ demoteWithRollback,
301
+ scanRollbackCandidates,
302
+ decayPatterns,
303
+ getDriftReport,
304
+ previewDecay,
305
+ };