gitmem-mcp 1.5.1 → 1.6.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/schema/setup.sql CHANGED
@@ -24,11 +24,34 @@ CREATE TABLE IF NOT EXISTS gitmem_learnings (
24
24
  embedding vector(1536),
25
25
  project TEXT DEFAULT 'default',
26
26
  source_date DATE DEFAULT CURRENT_DATE,
27
+ source_linear_issue TEXT,
28
+ persona_name TEXT,
29
+ why_this_matters TEXT,
30
+ action_protocol TEXT,
31
+ self_check_criteria TEXT,
32
+ is_active BOOLEAN DEFAULT true,
33
+ decay_multiplier FLOAT DEFAULT 1.0,
34
+ repeat_mistake BOOLEAN DEFAULT false,
35
+ related_scar_id UUID,
36
+ repeat_mistake_details JSONB,
27
37
  created_at TIMESTAMPTZ DEFAULT NOW(),
28
38
  updated_at TIMESTAMPTZ DEFAULT NOW()
29
39
  );
30
40
 
31
- -- Index for faster vector search
41
+ -- Migration: ensure all columns exist (idempotent for upgrades)
42
+ ALTER TABLE gitmem_learnings ADD COLUMN IF NOT EXISTS embedding vector(1536);
43
+ ALTER TABLE gitmem_learnings ADD COLUMN IF NOT EXISTS source_linear_issue TEXT;
44
+ ALTER TABLE gitmem_learnings ADD COLUMN IF NOT EXISTS persona_name TEXT;
45
+ ALTER TABLE gitmem_learnings ADD COLUMN IF NOT EXISTS why_this_matters TEXT;
46
+ ALTER TABLE gitmem_learnings ADD COLUMN IF NOT EXISTS action_protocol TEXT;
47
+ ALTER TABLE gitmem_learnings ADD COLUMN IF NOT EXISTS self_check_criteria TEXT;
48
+ ALTER TABLE gitmem_learnings ADD COLUMN IF NOT EXISTS is_active BOOLEAN DEFAULT true;
49
+ ALTER TABLE gitmem_learnings ADD COLUMN IF NOT EXISTS decay_multiplier FLOAT DEFAULT 1.0;
50
+ ALTER TABLE gitmem_learnings ADD COLUMN IF NOT EXISTS repeat_mistake BOOLEAN DEFAULT false;
51
+ ALTER TABLE gitmem_learnings ADD COLUMN IF NOT EXISTS related_scar_id UUID;
52
+ ALTER TABLE gitmem_learnings ADD COLUMN IF NOT EXISTS repeat_mistake_details JSONB;
53
+
54
+ -- Indexes (created after migration ensures columns exist)
32
55
  CREATE INDEX IF NOT EXISTS idx_gitmem_learnings_embedding
33
56
  ON gitmem_learnings USING ivfflat (embedding vector_cosine_ops)
34
57
  WITH (lists = 10);
@@ -48,14 +71,27 @@ CREATE TABLE IF NOT EXISTS gitmem_sessions (
48
71
  session_date DATE DEFAULT CURRENT_DATE,
49
72
  agent TEXT DEFAULT 'Unknown',
50
73
  project TEXT DEFAULT 'default',
74
+ linear_issue TEXT,
75
+ recording_path TEXT,
76
+ transcript_path TEXT,
51
77
  decisions TEXT[] DEFAULT '{}',
52
78
  open_threads TEXT[] DEFAULT '{}',
53
79
  closing_reflection JSONB,
80
+ close_compliance JSONB,
81
+ rapport_summary TEXT,
54
82
  embedding vector(1536),
55
83
  created_at TIMESTAMPTZ DEFAULT NOW(),
56
84
  updated_at TIMESTAMPTZ DEFAULT NOW()
57
85
  );
58
86
 
87
+ -- Migration: ensure all columns exist
88
+ ALTER TABLE gitmem_sessions ADD COLUMN IF NOT EXISTS embedding vector(1536);
89
+ ALTER TABLE gitmem_sessions ADD COLUMN IF NOT EXISTS linear_issue TEXT;
90
+ ALTER TABLE gitmem_sessions ADD COLUMN IF NOT EXISTS recording_path TEXT;
91
+ ALTER TABLE gitmem_sessions ADD COLUMN IF NOT EXISTS transcript_path TEXT;
92
+ ALTER TABLE gitmem_sessions ADD COLUMN IF NOT EXISTS close_compliance JSONB;
93
+ ALTER TABLE gitmem_sessions ADD COLUMN IF NOT EXISTS rapport_summary TEXT;
94
+
59
95
  CREATE INDEX IF NOT EXISTS idx_gitmem_sessions_agent
60
96
  ON gitmem_sessions (agent);
61
97
 
@@ -72,12 +108,21 @@ CREATE TABLE IF NOT EXISTS gitmem_decisions (
72
108
  decision TEXT NOT NULL,
73
109
  rationale TEXT NOT NULL,
74
110
  alternatives_considered TEXT[] DEFAULT '{}',
111
+ personas_involved TEXT[] DEFAULT '{}',
112
+ docs_affected TEXT[] DEFAULT '{}',
113
+ linear_issue TEXT,
75
114
  session_id UUID REFERENCES gitmem_sessions(id),
76
115
  project TEXT DEFAULT 'default',
77
116
  embedding vector(1536),
78
117
  created_at TIMESTAMPTZ DEFAULT NOW()
79
118
  );
80
119
 
120
+ -- Migration: ensure all columns exist
121
+ ALTER TABLE gitmem_decisions ADD COLUMN IF NOT EXISTS embedding vector(1536);
122
+ ALTER TABLE gitmem_decisions ADD COLUMN IF NOT EXISTS personas_involved TEXT[] DEFAULT '{}';
123
+ ALTER TABLE gitmem_decisions ADD COLUMN IF NOT EXISTS docs_affected TEXT[] DEFAULT '{}';
124
+ ALTER TABLE gitmem_decisions ADD COLUMN IF NOT EXISTS linear_issue TEXT;
125
+
81
126
  CREATE INDEX IF NOT EXISTS idx_gitmem_decisions_session
82
127
  ON gitmem_decisions (session_id);
83
128
 
@@ -89,10 +134,15 @@ CREATE TABLE IF NOT EXISTS gitmem_scar_usage (
89
134
  scar_id UUID REFERENCES gitmem_learnings(id),
90
135
  session_id UUID REFERENCES gitmem_sessions(id),
91
136
  agent TEXT DEFAULT 'Unknown',
137
+ issue_id TEXT,
138
+ issue_identifier TEXT,
92
139
  reference_type TEXT CHECK (reference_type IN ('explicit', 'implicit', 'acknowledged', 'refuted', 'none')),
93
140
  reference_context TEXT,
94
141
  surfaced_at TIMESTAMPTZ,
142
+ acknowledged_at TIMESTAMPTZ,
143
+ referenced BOOLEAN,
95
144
  execution_successful BOOLEAN,
145
+ variant_id UUID,
96
146
  created_at TIMESTAMPTZ DEFAULT NOW()
97
147
  );
98
148
 
@@ -102,6 +152,135 @@ CREATE INDEX IF NOT EXISTS idx_gitmem_scar_usage_scar
102
152
  CREATE INDEX IF NOT EXISTS idx_gitmem_scar_usage_session
103
153
  ON gitmem_scar_usage (session_id);
104
154
 
155
+ -- Migration: add columns for older installs
156
+ ALTER TABLE gitmem_scar_usage ADD COLUMN IF NOT EXISTS issue_id TEXT;
157
+ ALTER TABLE gitmem_scar_usage ADD COLUMN IF NOT EXISTS issue_identifier TEXT;
158
+ ALTER TABLE gitmem_scar_usage ADD COLUMN IF NOT EXISTS acknowledged_at TIMESTAMPTZ;
159
+ ALTER TABLE gitmem_scar_usage ADD COLUMN IF NOT EXISTS referenced BOOLEAN;
160
+ ALTER TABLE gitmem_scar_usage ADD COLUMN IF NOT EXISTS variant_id UUID;
161
+
162
+ -- ============================================================================
163
+ -- Threads table (cross-session work tracking)
164
+ -- ============================================================================
165
+ CREATE TABLE IF NOT EXISTS gitmem_threads (
166
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
167
+ thread_id TEXT NOT NULL UNIQUE,
168
+ text TEXT NOT NULL,
169
+ status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('emerging', 'active', 'cooling', 'dormant', 'archived', 'resolved')),
170
+ thread_class TEXT DEFAULT 'operational' CHECK (thread_class IN ('operational', 'backlog')),
171
+ vitality_score FLOAT DEFAULT 1.0,
172
+ last_touched_at TIMESTAMPTZ DEFAULT NOW(),
173
+ touch_count INT DEFAULT 1,
174
+ resolved_at TIMESTAMPTZ,
175
+ resolution_note TEXT,
176
+ source_session UUID REFERENCES gitmem_sessions(id),
177
+ resolved_by_session UUID REFERENCES gitmem_sessions(id),
178
+ related_issues TEXT[] DEFAULT '{}',
179
+ domain TEXT[] DEFAULT '{}',
180
+ project TEXT DEFAULT 'default',
181
+ metadata JSONB DEFAULT '{}',
182
+ embedding vector(1536),
183
+ created_at TIMESTAMPTZ DEFAULT NOW(),
184
+ updated_at TIMESTAMPTZ DEFAULT NOW()
185
+ );
186
+
187
+ CREATE INDEX IF NOT EXISTS idx_gitmem_threads_status
188
+ ON gitmem_threads (status);
189
+
190
+ CREATE INDEX IF NOT EXISTS idx_gitmem_threads_project
191
+ ON gitmem_threads (project);
192
+
193
+ -- ============================================================================
194
+ -- Knowledge triples (knowledge graph)
195
+ -- ============================================================================
196
+ CREATE TABLE IF NOT EXISTS knowledge_triples (
197
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
198
+ subject TEXT NOT NULL,
199
+ predicate TEXT NOT NULL CHECK (predicate IN ('created_in', 'influenced_by', 'supersedes', 'demonstrates', 'affects_doc', 'created_thread', 'resolves_thread', 'relates_to_thread')),
200
+ object TEXT NOT NULL,
201
+ event_time TIMESTAMPTZ DEFAULT NOW(),
202
+ decay_weight FLOAT DEFAULT 1.0,
203
+ half_life_days INT DEFAULT 9999,
204
+ decay_floor FLOAT DEFAULT 0.1,
205
+ source_type TEXT,
206
+ source_id UUID,
207
+ source_linear_issue TEXT,
208
+ domain TEXT[] DEFAULT '{}',
209
+ project TEXT DEFAULT 'default',
210
+ created_by TEXT,
211
+ created_at TIMESTAMPTZ DEFAULT NOW(),
212
+ updated_at TIMESTAMPTZ DEFAULT NOW()
213
+ );
214
+
215
+ CREATE INDEX IF NOT EXISTS idx_gitmem_triples_subject
216
+ ON knowledge_triples (subject);
217
+
218
+ CREATE INDEX IF NOT EXISTS idx_gitmem_triples_project
219
+ ON knowledge_triples (project);
220
+
221
+ -- ============================================================================
222
+ -- Query metrics (performance tracking)
223
+ -- ============================================================================
224
+ CREATE TABLE IF NOT EXISTS gitmem_query_metrics (
225
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
226
+ session_id UUID REFERENCES gitmem_sessions(id),
227
+ agent TEXT,
228
+ tool_name TEXT NOT NULL,
229
+ query_text TEXT,
230
+ tables_searched TEXT[],
231
+ latency_ms INT,
232
+ result_count INT,
233
+ similarity_scores FLOAT[],
234
+ context_bytes INT,
235
+ phase_tag TEXT,
236
+ linear_issue TEXT,
237
+ memories_surfaced UUID[],
238
+ metadata JSONB DEFAULT '{}',
239
+ created_at TIMESTAMPTZ DEFAULT NOW()
240
+ );
241
+
242
+ -- ============================================================================
243
+ -- Scar enforcement variants (A/B testing)
244
+ -- ============================================================================
245
+ CREATE TABLE IF NOT EXISTS scar_enforcement_variants (
246
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
247
+ scar_id UUID NOT NULL REFERENCES gitmem_learnings(id),
248
+ variant_type TEXT NOT NULL,
249
+ variant_config JSONB,
250
+ metadata JSONB DEFAULT '{}',
251
+ created_at TIMESTAMPTZ DEFAULT NOW(),
252
+ updated_at TIMESTAMPTZ DEFAULT NOW()
253
+ );
254
+
255
+ -- ============================================================================
256
+ -- Lite views (exclude embedding columns for reduced context)
257
+ -- ============================================================================
258
+ CREATE OR REPLACE VIEW gitmem_learnings_lite AS
259
+ SELECT id, learning_type, title, description, severity, scar_type,
260
+ counter_arguments, problem_context, solution_approach, applies_when,
261
+ keywords, domain, project, source_date, source_linear_issue,
262
+ persona_name, is_active, decay_multiplier, created_at, updated_at
263
+ FROM gitmem_learnings;
264
+
265
+ CREATE OR REPLACE VIEW gitmem_sessions_lite AS
266
+ SELECT id, session_title, session_date, agent, project, linear_issue,
267
+ decisions, open_threads, closing_reflection, close_compliance,
268
+ rapport_summary, created_at, updated_at
269
+ FROM gitmem_sessions;
270
+
271
+ CREATE OR REPLACE VIEW gitmem_decisions_lite AS
272
+ SELECT id, decision_date, title, decision, rationale,
273
+ alternatives_considered, personas_involved, docs_affected,
274
+ linear_issue, session_id, project, created_at
275
+ FROM gitmem_decisions;
276
+
277
+ CREATE OR REPLACE VIEW gitmem_threads_lite AS
278
+ SELECT id, thread_id, text, status, thread_class, vitality_score,
279
+ last_touched_at, touch_count, resolved_at, resolution_note,
280
+ source_session, resolved_by_session, related_issues, domain,
281
+ project, metadata, created_at, updated_at
282
+ FROM gitmem_threads;
283
+
105
284
  -- ============================================================================
106
285
  -- Semantic search RPC function
107
286
  -- ============================================================================
@@ -137,6 +316,106 @@ BEGIN
137
316
  END;
138
317
  $$;
139
318
 
319
+ -- ============================================================================
320
+ -- Scar search with temporal + behavioral decay weighting
321
+ -- ============================================================================
322
+ CREATE OR REPLACE FUNCTION gitmem_scar_search(
323
+ query_embedding vector(1536),
324
+ match_count INT DEFAULT 5,
325
+ similarity_threshold FLOAT DEFAULT 0.0
326
+ )
327
+ RETURNS TABLE (
328
+ id UUID,
329
+ title TEXT,
330
+ description TEXT,
331
+ severity TEXT,
332
+ scar_type TEXT,
333
+ counter_arguments TEXT[],
334
+ decay_multiplier FLOAT,
335
+ similarity FLOAT,
336
+ weighted_similarity FLOAT
337
+ )
338
+ LANGUAGE plpgsql
339
+ AS $$
340
+ DECLARE
341
+ decay_days INT;
342
+ temporal_weight FLOAT;
343
+ BEGIN
344
+ RETURN QUERY
345
+ SELECT
346
+ l.id,
347
+ l.title,
348
+ l.description,
349
+ l.severity,
350
+ l.scar_type,
351
+ l.counter_arguments,
352
+ COALESCE(l.decay_multiplier, 1.0) AS decay_multiplier,
353
+ (1 - (l.embedding <=> query_embedding))::FLOAT AS similarity,
354
+ -- Weighted similarity: raw * temporal_decay * behavioral_decay
355
+ (
356
+ (1 - (l.embedding <=> query_embedding)) *
357
+ -- Temporal decay based on scar_type
358
+ CASE
359
+ WHEN l.scar_type = 'process' THEN 1.0 -- permanent
360
+ WHEN l.scar_type = 'incident' THEN
361
+ GREATEST(0.1, 1.0 - 0.9 * (EXTRACT(EPOCH FROM (NOW() - l.created_at)) / (180.0 * 86400)))
362
+ WHEN l.scar_type = 'context' THEN
363
+ GREATEST(0.1, 1.0 - 0.9 * (EXTRACT(EPOCH FROM (NOW() - l.created_at)) / (30.0 * 86400)))
364
+ ELSE 1.0 -- default: no decay
365
+ END *
366
+ -- Behavioral decay multiplier
367
+ COALESCE(l.decay_multiplier, 1.0)
368
+ )::FLOAT AS weighted_similarity
369
+ FROM gitmem_learnings l
370
+ WHERE l.embedding IS NOT NULL
371
+ AND COALESCE(l.is_active, true) = true
372
+ AND (1 - (l.embedding <=> query_embedding)) > similarity_threshold
373
+ ORDER BY weighted_similarity DESC
374
+ LIMIT match_count;
375
+ END;
376
+ $$;
377
+
378
+ -- ============================================================================
379
+ -- Refresh behavioral decay scores from scar_usage patterns
380
+ -- Aggregates last 90 days of usage, computes dismiss rate,
381
+ -- updates decay_multiplier on learnings (minimum 3 surfacings required)
382
+ -- ============================================================================
383
+ CREATE OR REPLACE FUNCTION refresh_scar_behavioral_scores()
384
+ RETURNS TABLE (scars_updated INT, scars_scanned INT)
385
+ LANGUAGE plpgsql
386
+ AS $$
387
+ DECLARE
388
+ v_scanned INT := 0;
389
+ v_updated INT := 0;
390
+ BEGIN
391
+ -- Aggregate scar_usage from last 90 days, compute dismiss rate
392
+ WITH usage_stats AS (
393
+ SELECT
394
+ su.scar_id,
395
+ COUNT(*) AS times_surfaced,
396
+ COUNT(*) FILTER (WHERE su.reference_type IN ('none', 'refuted')) AS times_dismissed
397
+ FROM gitmem_scar_usage su
398
+ WHERE su.surfaced_at >= NOW() - INTERVAL '90 days'
399
+ GROUP BY su.scar_id
400
+ HAVING COUNT(*) >= 3 -- minimum surfacings for meaningful signal
401
+ )
402
+ UPDATE gitmem_learnings l
403
+ SET decay_multiplier = GREATEST(0.1, 1.0 - (us.times_dismissed::FLOAT / us.times_surfaced::FLOAT) * 0.8),
404
+ updated_at = NOW()
405
+ FROM usage_stats us
406
+ WHERE l.id = us.scar_id
407
+ AND COALESCE(l.is_active, true) = true;
408
+
409
+ GET DIAGNOSTICS v_updated = ROW_COUNT;
410
+
411
+ SELECT COUNT(DISTINCT scar_id) INTO v_scanned
412
+ FROM gitmem_scar_usage
413
+ WHERE surfaced_at >= NOW() - INTERVAL '90 days';
414
+
415
+ RETURN QUERY SELECT v_updated, v_scanned;
416
+ END;
417
+ $$;
418
+
140
419
  -- ============================================================================
141
420
  -- Row Level Security
142
421
  -- ============================================================================
@@ -146,32 +425,55 @@ ALTER TABLE gitmem_learnings ENABLE ROW LEVEL SECURITY;
146
425
  ALTER TABLE gitmem_sessions ENABLE ROW LEVEL SECURITY;
147
426
  ALTER TABLE gitmem_decisions ENABLE ROW LEVEL SECURITY;
148
427
  ALTER TABLE gitmem_scar_usage ENABLE ROW LEVEL SECURITY;
428
+ ALTER TABLE gitmem_threads ENABLE ROW LEVEL SECURITY;
429
+ ALTER TABLE knowledge_triples ENABLE ROW LEVEL SECURITY;
430
+ ALTER TABLE gitmem_query_metrics ENABLE ROW LEVEL SECURITY;
431
+ ALTER TABLE scar_enforcement_variants ENABLE ROW LEVEL SECURITY;
149
432
 
150
433
  -- Service role has full access (used by the MCP server)
151
- CREATE POLICY "Service role full access" ON gitmem_learnings
152
- FOR ALL USING (auth.role() = 'service_role');
153
-
154
- CREATE POLICY "Service role full access" ON gitmem_sessions
155
- FOR ALL USING (auth.role() = 'service_role');
156
-
157
- CREATE POLICY "Service role full access" ON gitmem_decisions
158
- FOR ALL USING (auth.role() = 'service_role');
159
-
160
- CREATE POLICY "Service role full access" ON gitmem_scar_usage
161
- FOR ALL USING (auth.role() = 'service_role');
162
-
163
- -- Block anonymous access
164
- CREATE POLICY "Block anonymous access" ON gitmem_learnings
165
- FOR ALL USING (auth.role() != 'anon');
166
-
167
- CREATE POLICY "Block anonymous access" ON gitmem_sessions
168
- FOR ALL USING (auth.role() != 'anon');
169
-
170
- CREATE POLICY "Block anonymous access" ON gitmem_decisions
171
- FOR ALL USING (auth.role() != 'anon');
172
-
173
- CREATE POLICY "Block anonymous access" ON gitmem_scar_usage
174
- FOR ALL USING (auth.role() != 'anon');
434
+ -- Drop-then-create for idempotency (CREATE POLICY has no IF NOT EXISTS)
435
+ DO $$ BEGIN
436
+ -- gitmem_learnings
437
+ DROP POLICY IF EXISTS "Service role full access" ON gitmem_learnings;
438
+ CREATE POLICY "Service role full access" ON gitmem_learnings FOR ALL USING (auth.role() = 'service_role');
439
+ DROP POLICY IF EXISTS "Block anonymous access" ON gitmem_learnings;
440
+ CREATE POLICY "Block anonymous access" ON gitmem_learnings FOR ALL USING (auth.role() != 'anon');
441
+ -- gitmem_sessions
442
+ DROP POLICY IF EXISTS "Service role full access" ON gitmem_sessions;
443
+ CREATE POLICY "Service role full access" ON gitmem_sessions FOR ALL USING (auth.role() = 'service_role');
444
+ DROP POLICY IF EXISTS "Block anonymous access" ON gitmem_sessions;
445
+ CREATE POLICY "Block anonymous access" ON gitmem_sessions FOR ALL USING (auth.role() != 'anon');
446
+ -- gitmem_decisions
447
+ DROP POLICY IF EXISTS "Service role full access" ON gitmem_decisions;
448
+ CREATE POLICY "Service role full access" ON gitmem_decisions FOR ALL USING (auth.role() = 'service_role');
449
+ DROP POLICY IF EXISTS "Block anonymous access" ON gitmem_decisions;
450
+ CREATE POLICY "Block anonymous access" ON gitmem_decisions FOR ALL USING (auth.role() != 'anon');
451
+ -- gitmem_scar_usage
452
+ DROP POLICY IF EXISTS "Service role full access" ON gitmem_scar_usage;
453
+ CREATE POLICY "Service role full access" ON gitmem_scar_usage FOR ALL USING (auth.role() = 'service_role');
454
+ DROP POLICY IF EXISTS "Block anonymous access" ON gitmem_scar_usage;
455
+ CREATE POLICY "Block anonymous access" ON gitmem_scar_usage FOR ALL USING (auth.role() != 'anon');
456
+ -- gitmem_threads
457
+ DROP POLICY IF EXISTS "Service role full access" ON gitmem_threads;
458
+ CREATE POLICY "Service role full access" ON gitmem_threads FOR ALL USING (auth.role() = 'service_role');
459
+ DROP POLICY IF EXISTS "Block anonymous access" ON gitmem_threads;
460
+ CREATE POLICY "Block anonymous access" ON gitmem_threads FOR ALL USING (auth.role() != 'anon');
461
+ -- knowledge_triples
462
+ DROP POLICY IF EXISTS "Service role full access" ON knowledge_triples;
463
+ CREATE POLICY "Service role full access" ON knowledge_triples FOR ALL USING (auth.role() = 'service_role');
464
+ DROP POLICY IF EXISTS "Block anonymous access" ON knowledge_triples;
465
+ CREATE POLICY "Block anonymous access" ON knowledge_triples FOR ALL USING (auth.role() != 'anon');
466
+ -- gitmem_query_metrics
467
+ DROP POLICY IF EXISTS "Service role full access" ON gitmem_query_metrics;
468
+ CREATE POLICY "Service role full access" ON gitmem_query_metrics FOR ALL USING (auth.role() = 'service_role');
469
+ DROP POLICY IF EXISTS "Block anonymous access" ON gitmem_query_metrics;
470
+ CREATE POLICY "Block anonymous access" ON gitmem_query_metrics FOR ALL USING (auth.role() != 'anon');
471
+ -- scar_enforcement_variants
472
+ DROP POLICY IF EXISTS "Service role full access" ON scar_enforcement_variants;
473
+ CREATE POLICY "Service role full access" ON scar_enforcement_variants FOR ALL USING (auth.role() = 'service_role');
474
+ DROP POLICY IF EXISTS "Block anonymous access" ON scar_enforcement_variants;
475
+ CREATE POLICY "Block anonymous access" ON scar_enforcement_variants FOR ALL USING (auth.role() != 'anon');
476
+ END $$;
175
477
 
176
478
  -- ============================================================================
177
479
  -- Auto-update timestamps
@@ -184,10 +486,172 @@ BEGIN
184
486
  END;
185
487
  $$ LANGUAGE plpgsql;
186
488
 
489
+ DROP TRIGGER IF EXISTS gitmem_learnings_updated ON gitmem_learnings;
187
490
  CREATE TRIGGER gitmem_learnings_updated
188
491
  BEFORE UPDATE ON gitmem_learnings
189
492
  FOR EACH ROW EXECUTE FUNCTION gitmem_update_timestamp();
190
493
 
494
+ DROP TRIGGER IF EXISTS gitmem_sessions_updated ON gitmem_sessions;
191
495
  CREATE TRIGGER gitmem_sessions_updated
192
496
  BEFORE UPDATE ON gitmem_sessions
193
497
  FOR EACH ROW EXECUTE FUNCTION gitmem_update_timestamp();
498
+
499
+ -- ============================================================================
500
+ -- License Management Tables (deployed on GitMem's infrastructure, not user's)
501
+ -- These tables are included here for reference and for our own deployment.
502
+ -- Users do NOT need to run these — they exist on gitmem-api.supabase.co
503
+ -- ============================================================================
504
+
505
+ CREATE TABLE IF NOT EXISTS gitmem_licenses (
506
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
507
+ api_key TEXT UNIQUE NOT NULL,
508
+ tier TEXT NOT NULL DEFAULT 'pro' CHECK (tier IN ('pro', 'dev')),
509
+ owner_email TEXT,
510
+ is_active BOOLEAN NOT NULL DEFAULT true,
511
+ max_activations INTEGER NOT NULL DEFAULT 3,
512
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
513
+ expires_at TIMESTAMPTZ,
514
+ last_validated_at TIMESTAMPTZ,
515
+ metadata JSONB DEFAULT '{}'
516
+ );
517
+
518
+ CREATE TABLE IF NOT EXISTS gitmem_license_activations (
519
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
520
+ license_id UUID NOT NULL REFERENCES gitmem_licenses(id) ON DELETE CASCADE,
521
+ install_id TEXT NOT NULL,
522
+ activated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
523
+ last_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
524
+ device_info JSONB DEFAULT '{}',
525
+ UNIQUE(license_id, install_id)
526
+ );
527
+
528
+ -- RLS: service role only (these are admin tables on our infrastructure)
529
+ ALTER TABLE gitmem_licenses ENABLE ROW LEVEL SECURITY;
530
+ ALTER TABLE gitmem_license_activations ENABLE ROW LEVEL SECURITY;
531
+
532
+ CREATE POLICY "Service role full access" ON gitmem_licenses
533
+ FOR ALL USING (auth.role() = 'service_role');
534
+
535
+ CREATE POLICY "Service role full access" ON gitmem_license_activations
536
+ FOR ALL USING (auth.role() = 'service_role');
537
+
538
+ -- Index for fast key lookup
539
+ CREATE INDEX IF NOT EXISTS idx_gitmem_licenses_api_key
540
+ ON gitmem_licenses (api_key);
541
+
542
+ CREATE INDEX IF NOT EXISTS idx_gitmem_license_activations_license
543
+ ON gitmem_license_activations (license_id);
544
+
545
+ -- ============================================================================
546
+ -- License Validation RPC
547
+ -- Checks key validity, device limit, registers/updates activation
548
+ -- ============================================================================
549
+ CREATE OR REPLACE FUNCTION gitmem_validate_license(p_api_key TEXT, p_install_id TEXT)
550
+ RETURNS TABLE(tier TEXT, valid BOOLEAN, message TEXT)
551
+ LANGUAGE plpgsql
552
+ SECURITY DEFINER
553
+ AS $$
554
+ DECLARE
555
+ v_license_id UUID;
556
+ v_tier TEXT;
557
+ v_is_active BOOLEAN;
558
+ v_max_activations INTEGER;
559
+ v_expires_at TIMESTAMPTZ;
560
+ v_current_activations INTEGER;
561
+ BEGIN
562
+ -- Look up the license
563
+ SELECT l.id, l.tier, l.is_active, l.max_activations, l.expires_at
564
+ INTO v_license_id, v_tier, v_is_active, v_max_activations, v_expires_at
565
+ FROM gitmem_licenses l
566
+ WHERE l.api_key = p_api_key;
567
+
568
+ -- Key not found
569
+ IF v_license_id IS NULL THEN
570
+ RETURN QUERY SELECT NULL::TEXT, false, 'Invalid license key'::TEXT;
571
+ RETURN;
572
+ END IF;
573
+
574
+ -- Key deactivated
575
+ IF NOT v_is_active THEN
576
+ RETURN QUERY SELECT NULL::TEXT, false, 'License has been deactivated'::TEXT;
577
+ RETURN;
578
+ END IF;
579
+
580
+ -- Key expired
581
+ IF v_expires_at IS NOT NULL AND v_expires_at < NOW() THEN
582
+ RETURN QUERY SELECT NULL::TEXT, false, 'License has expired'::TEXT;
583
+ RETURN;
584
+ END IF;
585
+
586
+ -- Count current activations (excluding this install_id if already registered)
587
+ SELECT COUNT(*)
588
+ INTO v_current_activations
589
+ FROM gitmem_license_activations a
590
+ WHERE a.license_id = v_license_id
591
+ AND a.install_id != p_install_id;
592
+
593
+ -- Check device limit (if this is a new device)
594
+ IF v_current_activations >= v_max_activations THEN
595
+ -- Check if this install_id already exists (re-validation)
596
+ IF NOT EXISTS (
597
+ SELECT 1 FROM gitmem_license_activations a
598
+ WHERE a.license_id = v_license_id AND a.install_id = p_install_id
599
+ ) THEN
600
+ RETURN QUERY SELECT NULL::TEXT, false,
601
+ format('Device limit reached (%s/%s). Deactivate another device or contact support.', v_current_activations, v_max_activations)::TEXT;
602
+ RETURN;
603
+ END IF;
604
+ END IF;
605
+
606
+ -- Register or update activation
607
+ INSERT INTO gitmem_license_activations (license_id, install_id, last_seen_at)
608
+ VALUES (v_license_id, p_install_id, NOW())
609
+ ON CONFLICT (license_id, install_id)
610
+ DO UPDATE SET last_seen_at = NOW();
611
+
612
+ -- Update last_validated_at on the license
613
+ UPDATE gitmem_licenses SET last_validated_at = NOW() WHERE id = v_license_id;
614
+
615
+ -- Success
616
+ RETURN QUERY SELECT v_tier, true, 'Valid'::TEXT;
617
+ END;
618
+ $$;
619
+
620
+ -- ============================================================================
621
+ -- Device Deactivation RPC
622
+ -- Removes a device activation for a license key + install_id pair.
623
+ -- Called by `gitmem-mcp deactivate` to free up a device slot.
624
+ -- ============================================================================
625
+ CREATE OR REPLACE FUNCTION gitmem_deactivate_device(p_api_key TEXT, p_install_id TEXT)
626
+ RETURNS TABLE(success BOOLEAN, message TEXT)
627
+ LANGUAGE plpgsql
628
+ SECURITY DEFINER
629
+ AS $$
630
+ DECLARE
631
+ v_license_id UUID;
632
+ v_deleted INTEGER;
633
+ BEGIN
634
+ -- Look up the license
635
+ SELECT l.id INTO v_license_id
636
+ FROM gitmem_licenses l
637
+ WHERE l.api_key = p_api_key;
638
+
639
+ IF v_license_id IS NULL THEN
640
+ RETURN QUERY SELECT false, 'Invalid license key'::TEXT;
641
+ RETURN;
642
+ END IF;
643
+
644
+ -- Delete the activation for this install_id
645
+ DELETE FROM gitmem_license_activations a
646
+ WHERE a.license_id = v_license_id
647
+ AND a.install_id = p_install_id;
648
+
649
+ GET DIAGNOSTICS v_deleted = ROW_COUNT;
650
+
651
+ IF v_deleted > 0 THEN
652
+ RETURN QUERY SELECT true, 'Device deactivated'::TEXT;
653
+ ELSE
654
+ RETURN QUERY SELECT true, 'Device was not registered (already deactivated)'::TEXT;
655
+ END IF;
656
+ END;
657
+ $$;