@jhizzard/termdeck 0.9.0 → 0.10.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.
@@ -0,0 +1,126 @@
1
+ -- 009_memory_relationship_metadata.sql
2
+ --
3
+ -- Sprint 38 (T1) — Knowledge graph substrate.
4
+ -- Adds graph-edge metadata columns to memory_relationships, expands the
5
+ -- relationship_type vocabulary from 5 to 8 values, and ships a recursive-CTE
6
+ -- traversal function (expand_memory_neighborhood) for N-hop neighborhood
7
+ -- queries.
8
+ --
9
+ -- Idempotent: safe to re-run.
10
+ --
11
+ -- Pre-existing state (verified against petvetbid 2026-04-27 17:25 ET):
12
+ -- memory_relationships has 749 live edges. The migration adds nullable
13
+ -- columns and a wider CHECK; no existing row violates the new constraint.
14
+ --
15
+ -- The original CHECK on relationship_type is anonymous (defined inline in
16
+ -- migration 001), so its name is auto-generated (e.g., memory_relationships_check1).
17
+ -- We can't rely on a known name for DROP CONSTRAINT; the DO block below
18
+ -- introspects pg_constraint and drops any CHECK on this table whose
19
+ -- definition references "relationship_type IN" — preserving the separate
20
+ -- (source_id <> target_id) CHECK.
21
+
22
+ -- ── 1. Metadata columns ──────────────────────────────────────────────────
23
+
24
+ alter table memory_relationships
25
+ add column if not exists weight float,
26
+ add column if not exists inferred_at timestamptz,
27
+ add column if not exists inferred_by text;
28
+
29
+ -- ── 2. Expand relationship_type CHECK vocabulary ─────────────────────────
30
+
31
+ do $$
32
+ declare
33
+ c record;
34
+ begin
35
+ for c in
36
+ select con.conname
37
+ from pg_constraint con
38
+ join pg_class cls on cls.oid = con.conrelid
39
+ where cls.relname = 'memory_relationships'
40
+ and con.contype = 'c'
41
+ and pg_get_constraintdef(con.oid) ilike '%relationship_type%'
42
+ loop
43
+ execute format('alter table memory_relationships drop constraint %I', c.conname);
44
+ end loop;
45
+ end
46
+ $$;
47
+
48
+ alter table memory_relationships
49
+ add constraint memory_relationships_relationship_type_check
50
+ check (relationship_type in (
51
+ 'supersedes',
52
+ 'relates_to',
53
+ 'contradicts',
54
+ 'elaborates',
55
+ 'caused_by',
56
+ 'blocks',
57
+ 'inspired_by',
58
+ 'cross_project_link'
59
+ ));
60
+
61
+ -- ── 3. Indexes for graph traversal ───────────────────────────────────────
62
+
63
+ create index if not exists memory_relationships_weight_idx
64
+ on memory_relationships(weight)
65
+ where weight is not null;
66
+
67
+ create index if not exists memory_relationships_inferred_at_idx
68
+ on memory_relationships(inferred_at)
69
+ where inferred_at is not null;
70
+
71
+ -- ── 4. expand_memory_neighborhood — recursive-CTE N-hop traversal ────────
72
+ --
73
+ -- Returns one row per (memory_id, depth) reachable from start_id within
74
+ -- max_depth hops. Edges are undirected for traversal purposes (source_id
75
+ -- and target_id treated symmetrically) — agents linking memories don't
76
+ -- always think directionally, and graph-aware recall benefits from
77
+ -- bidirectional reachability.
78
+ --
79
+ -- Cycle-safe: the path[] array tracks visited nodes; we only follow an
80
+ -- edge if its other endpoint is not already in path. Without this check,
81
+ -- the CTE would loop indefinitely on any cycle in the graph.
82
+ --
83
+ -- edge_kinds[] mirrors path: position i in edge_kinds is the relationship_type
84
+ -- of the edge that brought us to position i+1 in path. Caller can filter
85
+ -- downstream by inspecting edge_kinds (e.g., drop rows whose path traversed
86
+ -- a 'contradicts' edge).
87
+ --
88
+ -- Performance note: with ~5K memory_items and ~750 edges (current scale),
89
+ -- recursive expansion to depth 2 returns in <50ms unsharded. At >50K edges
90
+ -- consider materializing precomputed neighborhoods.
91
+
92
+ create or replace function expand_memory_neighborhood(
93
+ start_id uuid,
94
+ max_depth int default 2
95
+ )
96
+ returns table (
97
+ memory_id uuid,
98
+ depth int,
99
+ path uuid[],
100
+ edge_kinds text[]
101
+ )
102
+ language sql stable
103
+ as $$
104
+ with recursive neighborhood as (
105
+ select
106
+ start_id as memory_id,
107
+ 0 as depth,
108
+ array[start_id] as path,
109
+ array[]::text[] as edge_kinds
110
+ union all
111
+ select
112
+ case when r.source_id = n.memory_id then r.target_id else r.source_id end,
113
+ n.depth + 1,
114
+ n.path || (case when r.source_id = n.memory_id then r.target_id else r.source_id end),
115
+ n.edge_kinds || r.relationship_type
116
+ from neighborhood n
117
+ join memory_relationships r
118
+ on (r.source_id = n.memory_id or r.target_id = n.memory_id)
119
+ where n.depth < max_depth
120
+ and not (
121
+ case when r.source_id = n.memory_id then r.target_id else r.source_id end
122
+ = any (n.path)
123
+ )
124
+ )
125
+ select memory_id, depth, path, edge_kinds from neighborhood;
126
+ $$;
@@ -0,0 +1,147 @@
1
+ -- Mnestra v0.3 — memory_recall_graph (Sprint 38 / T3)
2
+ --
3
+ -- Graph-aware recall. Two-stage retrieval:
4
+ --
5
+ -- Stage 1: vector recall via match_memories() — top-K nearest neighbors
6
+ -- in embedding space (existing RPC; tombstone filters inherited).
7
+ -- Stage 2: graph expansion via expand_memory_neighborhood() — for each
8
+ -- stage-1 seed, walk the relationship graph to max_depth.
9
+ --
10
+ -- Re-rank the union of (stage 1 ∪ stage 2) by a combined signal:
11
+ --
12
+ -- final_score = vector_score × edge_weight × recency_score
13
+ --
14
+ -- where:
15
+ -- vector_score = cosine similarity of seed memory to query (0..1)
16
+ -- edge_weight = mean of memory_relationships.weight along the path,
17
+ -- defaulting to 0.5 for edges T2 hasn't classified yet
18
+ -- (the 749 pre-T2 edges all start with weight = NULL).
19
+ -- recency_score = exp(-age_seconds / (30 × 86400)) (30-day half-life)
20
+ --
21
+ -- Initial (depth=0) results are seeded with edge_weight = 1.0 (no path).
22
+ --
23
+ -- Hard dependencies:
24
+ -- • migration 009 must have run (introduces memory_relationships.weight
25
+ -- and the expand_memory_neighborhood function).
26
+ -- • migration 001 (match_memories, memory_items, memory_relationships).
27
+ --
28
+ -- Rerun-safe: CREATE OR REPLACE.
29
+
30
+ create or replace function memory_recall_graph (
31
+ query_embedding vector(1536),
32
+ project_filter text default null,
33
+ max_depth int default 2,
34
+ k int default 10
35
+ )
36
+ returns table (
37
+ memory_id uuid,
38
+ content text,
39
+ project text,
40
+ depth int,
41
+ vector_score float,
42
+ edge_weight float,
43
+ recency_score float,
44
+ final_score float,
45
+ path uuid[]
46
+ )
47
+ language sql stable
48
+ as $$
49
+ with initial as (
50
+ -- Stage 1: top-K vector recall. match_memories already filters
51
+ -- is_active / archived / superseded_by / project, so depth-0 hits
52
+ -- are tombstone-clean. match_threshold=0.0 returns the full top-K
53
+ -- (we rank by combined signal, not by raw similarity threshold).
54
+ select
55
+ mm.id as memory_id,
56
+ mm.content,
57
+ mm.project,
58
+ 0 as depth,
59
+ mm.similarity as vector_score,
60
+ mi.created_at,
61
+ array[mm.id] as path
62
+ from match_memories(query_embedding, 0.0, k, project_filter) mm
63
+ join memory_items mi on mi.id = mm.id
64
+ ),
65
+ expanded as (
66
+ -- Stage 2: graph expansion. For each stage-1 seed, walk the graph to
67
+ -- max_depth via T1's expand_memory_neighborhood RPC. Carry the seed's
68
+ -- vector_score forward (we do NOT re-embed neighbors — the assumption
69
+ -- is "if A is relevant to the query and B is connected to A, then B
70
+ -- inherits some of A's relevance, attenuated by the path weight").
71
+ select
72
+ n.memory_id,
73
+ mi.content,
74
+ mi.project,
75
+ n.depth,
76
+ i.vector_score,
77
+ mi.created_at,
78
+ n.path,
79
+ coalesce(
80
+ (
81
+ -- Average of edge weights along the path (cycle-safe: T1's CTE
82
+ -- guarantees no repeats). Pairwise lookup over consecutive path
83
+ -- elements, treating the relationship as undirected so A→B and
84
+ -- B→A both count.
85
+ select avg(coalesce(r.weight, 0.5))
86
+ from generate_series(1, array_length(n.path, 1) - 1) as g
87
+ join memory_relationships r
88
+ on (r.source_id = n.path[g] and r.target_id = n.path[g + 1])
89
+ or (r.source_id = n.path[g + 1] and r.target_id = n.path[g])
90
+ ),
91
+ 0.5
92
+ ) as edge_weight
93
+ from initial i
94
+ cross join lateral expand_memory_neighborhood(i.memory_id, max_depth) n
95
+ join memory_items mi
96
+ on mi.id = n.memory_id
97
+ and mi.is_active = true
98
+ and mi.archived = false
99
+ and mi.superseded_by is null
100
+ where n.depth > 0
101
+ and (project_filter is null or mi.project = project_filter)
102
+ ),
103
+ unioned as (
104
+ -- depth-0 seeds get edge_weight = 1.0 (no path traversed).
105
+ select memory_id, content, project, depth, vector_score,
106
+ 1.0::float as edge_weight, created_at, path
107
+ from initial
108
+ union all
109
+ select memory_id, content, project, depth, vector_score,
110
+ edge_weight, created_at, path
111
+ from expanded
112
+ ),
113
+ scored as (
114
+ -- Same memory may be reached via multiple paths (vector seed AND graph
115
+ -- expansion, or two different seeds). Keep the strongest path: highest
116
+ -- final_score wins, with depth-0 (vector hit) preferred on ties.
117
+ select distinct on (memory_id)
118
+ memory_id,
119
+ content,
120
+ project,
121
+ depth,
122
+ vector_score,
123
+ edge_weight,
124
+ exp(-extract(epoch from (now() - created_at))::float / (30.0 * 86400.0))
125
+ as recency_score,
126
+ vector_score * edge_weight
127
+ * exp(-extract(epoch from (now() - created_at))::float / (30.0 * 86400.0))
128
+ as final_score,
129
+ path
130
+ from unioned
131
+ order by memory_id, final_score desc, depth asc
132
+ )
133
+ select
134
+ memory_id, content, project, depth, vector_score,
135
+ edge_weight, recency_score, final_score, path
136
+ from scored
137
+ order by final_score desc
138
+ limit 50;
139
+ $$;
140
+
141
+ -- Lightweight grant convention follows existing match_memories. Service-role
142
+ -- and authenticated callers should already have execute by inheritance, but
143
+ -- be explicit for the new function so PostgREST surfaces it without a manual
144
+ -- dashboard step.
145
+
146
+ grant execute on function memory_recall_graph(vector, text, int, int)
147
+ to authenticated, service_role, anon;
@@ -0,0 +1,237 @@
1
+ -- Sprint 39 T3 — chopin-nashville project-tag backfill.
2
+ --
3
+ -- Why this exists:
4
+ -- memory_items rows tagged project='chopin-nashville' are ~96% polluted
5
+ -- with content from other projects (termdeck, mnestra, rumen, podium, pvb,
6
+ -- dor). Root cause is the harness session-end hook
7
+ -- (~/.claude/hooks/memory-session-end.js, OUT OF THIS REPO): its
8
+ -- PROJECT_MAP iteration tests /ChopinNashville/i first and there are no
9
+ -- entries for termdeck/mnestra/rumen/podium/dor — so any session whose
10
+ -- cwd lives under ~/Documents/Graciella/ChopinNashville/... falls into
11
+ -- chopin-nashville, including the entire TermDeck checkout (which lives at
12
+ -- ChopinNashville/SideHustles/TermDeck/termdeck) and Podium (which lives at
13
+ -- ChopinNashville/2026/ChopinInBohemia/podium).
14
+ --
15
+ -- This migration heals the historical rows. The forward-fix to the harness
16
+ -- hook is Joshua's responsibility (out-of-repo file) and is NOT covered
17
+ -- here — without it, new mis-tagged rows will continue to be written until
18
+ -- he extends PROJECT_MAP with the missing project entries.
19
+ --
20
+ -- What this migration does NOT do:
21
+ -- - Does NOT touch mnestra_session_memory / mnestra_project_memory / etc.
22
+ -- (legacy rag-events tables; different write path; separate cleanup).
23
+ -- - Does NOT consolidate duplicate project tags like 'gorgias' vs
24
+ -- 'gorgias-ticket-monitor', 'pvb' vs 'PVB', or 'mnestra' vs 'engram'.
25
+ -- Those are visible in `SELECT project, count(*) FROM memory_items GROUP
26
+ -- BY project` but they're a separate cleanup pass.
27
+ -- - Does NOT touch the ~898 "other/uncertain" chopin-nashville rows that
28
+ -- don't carry an unambiguous project keyword. A future sprint can run an
29
+ -- LLM-classification pass; for this migration, conservative wins.
30
+ --
31
+ -- Heuristic — content keyword bucketing:
32
+ -- The migration runs UPDATEs sequentially. Earlier buckets claim ambiguous
33
+ -- multi-project rows first; later buckets only see rows that no earlier
34
+ -- bucket has already re-tagged. Order is by bucket size (largest first):
35
+ --
36
+ -- 1. termdeck / mnestra — keywords: termdeck, mnestra, "4+1 sprint"
37
+ -- 2. rumen — keyword: rumen
38
+ -- 3. podium — keyword: podium
39
+ -- 4. pvb — keywords: PVB, petvetbid, pet vet bid
40
+ -- 5. dor / openclaw — TIGHTENED:
41
+ -- word-boundary uppercase DOR (rules out
42
+ -- "dormant", "vendored", "indoor", etc.),
43
+ -- plus path/identifier markers and
44
+ -- openclaw substring.
45
+ --
46
+ -- Spot-check baseline (T3 audit, 2026-04-27):
47
+ -- termdeck/mnestra: 130 rows, all 6 sampled were true positives (TermDeck
48
+ -- server code, Mnestra wizard, sprint orchestration).
49
+ -- rumen: 92 rows, all 6 sampled were true positives.
50
+ -- podium: 58 rows, all 6 sampled were true positives.
51
+ -- pvb: 7 rows, 1 of those overlaps with mnestra ("Mnestra
52
+ -- repo … petvetbid project") and gets claimed by bucket 1.
53
+ -- dor (tightened): 3 rows after tightening from 6 — the original
54
+ -- `%dor%` ILIKE pattern caught false positives like
55
+ -- "dormant", "vendored". Final 3 rows are all true
56
+ -- DOR/OpenClaw mentions.
57
+ -- chopin-nashville total: 1,169 rows. Legitimate-signal baseline (rows
58
+ -- matching Acceptd / NICPC / Bohemia / laureate /
59
+ -- applicant / competition / repertoire keywords): 71.
60
+ --
61
+ -- Idempotence:
62
+ -- Every UPDATE is gated by `WHERE project = 'chopin-nashville'`. After the
63
+ -- first run, those rows have a different project tag, so re-running this
64
+ -- migration is a no-op (zero rows updated per bucket). RAISE NOTICE on a
65
+ -- re-run will print zeros, which is the expected idempotent signal.
66
+ --
67
+ -- Application:
68
+ -- THIS MIGRATION IS NOT EXECUTED BY THE LANE THAT WROTE IT. Orchestrator
69
+ -- reviews the RAISE NOTICE counts after applying. Apply via the bundled
70
+ -- migration runner at packages/server/src/setup/migration-runner.js (which
71
+ -- uses node-postgres client.query — psql metacommands like \gset are NOT
72
+ -- available, so the count probes use GET DIAGNOSTICS ROW_COUNT inside DO
73
+ -- blocks). Manual fallback: `psql "$DATABASE_URL" -f 011_project_tag_backfill.sql`.
74
+
75
+ BEGIN;
76
+
77
+ -- ============================================================
78
+ -- AUDIT BEFORE
79
+ -- ============================================================
80
+ DO $$
81
+ DECLARE
82
+ before_chopin int;
83
+ before_termdeck int;
84
+ before_rumen int;
85
+ before_podium int;
86
+ before_pvb int;
87
+ before_dor int;
88
+ BEGIN
89
+ SELECT count(*) INTO before_chopin FROM memory_items WHERE project = 'chopin-nashville';
90
+ SELECT count(*) INTO before_termdeck FROM memory_items WHERE project = 'termdeck';
91
+ SELECT count(*) INTO before_rumen FROM memory_items WHERE project = 'rumen';
92
+ SELECT count(*) INTO before_podium FROM memory_items WHERE project = 'podium';
93
+ SELECT count(*) INTO before_pvb FROM memory_items WHERE project = 'pvb';
94
+ SELECT count(*) INTO before_dor FROM memory_items WHERE project = 'dor';
95
+ RAISE NOTICE '[011-backfill] BEFORE chopin-nashville=% termdeck=% rumen=% podium=% pvb=% dor=%',
96
+ before_chopin, before_termdeck, before_rumen, before_podium, before_pvb, before_dor;
97
+ END $$;
98
+
99
+ -- ============================================================
100
+ -- BUCKET 1 — TermDeck / Mnestra (claims multi-project mentions first)
101
+ -- ============================================================
102
+ DO $$
103
+ DECLARE
104
+ rows_updated int;
105
+ BEGIN
106
+ UPDATE memory_items SET project = 'termdeck'
107
+ WHERE project = 'chopin-nashville'
108
+ AND (
109
+ content ILIKE '%termdeck%'
110
+ OR content ILIKE '%mnestra%'
111
+ OR content ILIKE '%4+1 sprint%'
112
+ );
113
+ GET DIAGNOSTICS rows_updated = ROW_COUNT;
114
+ RAISE NOTICE '[011-backfill] bucket 1 (termdeck/mnestra): % rows re-tagged', rows_updated;
115
+ END $$;
116
+
117
+ -- ============================================================
118
+ -- BUCKET 2 — Rumen
119
+ -- ============================================================
120
+ DO $$
121
+ DECLARE
122
+ rows_updated int;
123
+ BEGIN
124
+ UPDATE memory_items SET project = 'rumen'
125
+ WHERE project = 'chopin-nashville'
126
+ AND content ILIKE '%rumen%';
127
+ GET DIAGNOSTICS rows_updated = ROW_COUNT;
128
+ RAISE NOTICE '[011-backfill] bucket 2 (rumen): % rows re-tagged', rows_updated;
129
+ END $$;
130
+
131
+ -- ============================================================
132
+ -- BUCKET 3 — Podium
133
+ -- ============================================================
134
+ DO $$
135
+ DECLARE
136
+ rows_updated int;
137
+ BEGIN
138
+ UPDATE memory_items SET project = 'podium'
139
+ WHERE project = 'chopin-nashville'
140
+ AND content ILIKE '%podium%';
141
+ GET DIAGNOSTICS rows_updated = ROW_COUNT;
142
+ RAISE NOTICE '[011-backfill] bucket 3 (podium): % rows re-tagged', rows_updated;
143
+ END $$;
144
+
145
+ -- ============================================================
146
+ -- BUCKET 4 — PVB (case-insensitive PVB / petvetbid markers)
147
+ -- ============================================================
148
+ DO $$
149
+ DECLARE
150
+ rows_updated int;
151
+ BEGIN
152
+ UPDATE memory_items SET project = 'pvb'
153
+ WHERE project = 'chopin-nashville'
154
+ AND (
155
+ content ILIKE '%PVB%'
156
+ OR content ILIKE '%petvetbid%'
157
+ OR content ILIKE '%pet vet bid%'
158
+ );
159
+ GET DIAGNOSTICS rows_updated = ROW_COUNT;
160
+ RAISE NOTICE '[011-backfill] bucket 4 (pvb): % rows re-tagged', rows_updated;
161
+ END $$;
162
+
163
+ -- ============================================================
164
+ -- BUCKET 5 — DOR / OpenClaw (TIGHTENED — word boundary + identifiers)
165
+ --
166
+ -- Original briefing heuristic was `content ILIKE '%dor%'`, which produced a
167
+ -- ~33% false-positive rate (matched "dormant", "vendored", "indoor", etc.).
168
+ -- T3 audit tightened to:
169
+ -- • POSIX word boundary `\mDOR\M` — case-sensitive uppercase only, so
170
+ -- "dormant" / "DormHall" / "vendor" / "indoor" no longer match.
171
+ -- • path/identifier markers: /DOR/, ~/Documents/DOR, dor.config,
172
+ -- "Rust LLM gateway" (DOR's tagline).
173
+ -- • openclaw substring (OpenClaw is the slack-channel automation product
174
+ -- that lives next to DOR in Joshua's stack).
175
+ -- ============================================================
176
+ DO $$
177
+ DECLARE
178
+ rows_updated int;
179
+ BEGIN
180
+ UPDATE memory_items SET project = 'dor'
181
+ WHERE project = 'chopin-nashville'
182
+ AND (
183
+ content ~ '\mDOR\M'
184
+ OR content ILIKE '%/DOR/%'
185
+ OR content ILIKE '%~/Documents/DOR%'
186
+ OR content ILIKE '%dor.config%'
187
+ OR content ILIKE '%Rust LLM gateway%'
188
+ OR content ILIKE '%openclaw%'
189
+ );
190
+ GET DIAGNOSTICS rows_updated = ROW_COUNT;
191
+ RAISE NOTICE '[011-backfill] bucket 5 (dor): % rows re-tagged', rows_updated;
192
+ END $$;
193
+
194
+ -- ============================================================
195
+ -- AUDIT AFTER
196
+ -- ============================================================
197
+ DO $$
198
+ DECLARE
199
+ after_chopin int;
200
+ after_termdeck int;
201
+ after_rumen int;
202
+ after_podium int;
203
+ after_pvb int;
204
+ after_dor int;
205
+ BEGIN
206
+ SELECT count(*) INTO after_chopin FROM memory_items WHERE project = 'chopin-nashville';
207
+ SELECT count(*) INTO after_termdeck FROM memory_items WHERE project = 'termdeck';
208
+ SELECT count(*) INTO after_rumen FROM memory_items WHERE project = 'rumen';
209
+ SELECT count(*) INTO after_podium FROM memory_items WHERE project = 'podium';
210
+ SELECT count(*) INTO after_pvb FROM memory_items WHERE project = 'pvb';
211
+ SELECT count(*) INTO after_dor FROM memory_items WHERE project = 'dor';
212
+ RAISE NOTICE '[011-backfill] AFTER chopin-nashville=% termdeck=% rumen=% podium=% pvb=% dor=%',
213
+ after_chopin, after_termdeck, after_rumen, after_podium, after_pvb, after_dor;
214
+ RAISE NOTICE '[011-backfill] If apply succeeds and chopin-nashville count is around the legitimate baseline (~71 rows match competition/laureate/applicant/Acceptd/NICPC/Bohemia/repertoire keywords as of T3 audit), the migration succeeded. The ~898 rows that remain under chopin-nashville without a clear keyword signal are deliberate — a future LLM-classification pass can address them if needed.';
215
+ END $$;
216
+
217
+ COMMIT;
218
+
219
+ -- ============================================================
220
+ -- POST-APPLY: optional verification queries (NOT part of the migration).
221
+ -- Run separately to confirm Flashback against project='termdeck' now hits
222
+ -- the re-tagged rows.
223
+ -- ============================================================
224
+ --
225
+ -- 1. Tag distribution after migration:
226
+ -- SELECT project, count(*) FROM memory_items GROUP BY project ORDER BY count(*) DESC LIMIT 20;
227
+ --
228
+ -- 2. Confirm no chopin-nashville rows match obvious termdeck/rumen keywords:
229
+ -- SELECT count(*) FROM memory_items
230
+ -- WHERE project='chopin-nashville'
231
+ -- AND (content ILIKE '%termdeck%' OR content ILIKE '%rumen%' OR content ILIKE '%podium%');
232
+ -- -- Expected: 0
233
+ --
234
+ -- 3. Confirm Flashback project-bound test corpus (>= 5 termdeck-tagged rows
235
+ -- matching the canonical probe question):
236
+ -- SELECT count(*) FROM memory_items
237
+ -- WHERE project='termdeck' AND content ILIKE '%shell error%';
@@ -0,0 +1,49 @@
1
+ -- Sprint 38 T2 — Graph-inference cron schedule.
2
+ -- Schedules the graph-inference Supabase Edge Function to run daily at
3
+ -- 03:00 UTC (≈ 23:00 ET, after typical work hours). The Edge Function
4
+ -- scans memory_items for pairs above GRAPH_INFERENCE_THRESHOLD (default
5
+ -- 0.85 cosine similarity), inserts edges into memory_relationships with
6
+ -- inferred_by = 'cron-YYYY-MM-DD' for audit trail, and optionally
7
+ -- classifies edge types via Haiku 4.5 (gated by GRAPH_LLM_CLASSIFY=1).
8
+ --
9
+ -- Coexists with the existing rag-system MCP-side ingest classifier:
10
+ -- this cron handles backfill + cross-project + stale-edge refresh; the
11
+ -- ingest classifier handles fresh per-memory edges in the 0.75-0.92
12
+ -- window. See Sprint 38 T2 FINDING for the full coexistence design.
13
+ --
14
+ -- Before applying:
15
+ -- 1. The pg_cron and pg_net extensions must already be enabled
16
+ -- (rumen migration 002 enables them; 003 assumes that prereq).
17
+ -- 2. Add a Vault secret named 'graph_inference_service_role_key'
18
+ -- containing the project's service-role JWT. Separate from
19
+ -- 'rumen_service_role_key' so a key rotation on one cron doesn't
20
+ -- affect the other.
21
+ -- 3. Replace <project-ref> below with the project's Supabase ref
22
+ -- (stack-installer substitutes this at apply-time, same as 002).
23
+ --
24
+ -- Apply with:
25
+ -- psql "$DIRECT_URL" -f migrations/003_graph_inference_schedule.sql
26
+
27
+ -- Idempotent unschedule.
28
+ SELECT cron.unschedule('graph-inference-tick')
29
+ WHERE EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'graph-inference-tick');
30
+
31
+ -- Schedule daily at 03:00 UTC.
32
+ SELECT cron.schedule(
33
+ 'graph-inference-tick',
34
+ '0 3 * * *',
35
+ $$
36
+ SELECT net.http_post(
37
+ url := 'https://<project-ref>.supabase.co/functions/v1/graph-inference',
38
+ headers := jsonb_build_object(
39
+ 'Content-Type', 'application/json',
40
+ 'Authorization', 'Bearer ' || (SELECT decrypted_secret FROM vault.decrypted_secrets WHERE name = 'graph_inference_service_role_key')
41
+ ),
42
+ body := '{}'::jsonb
43
+ );
44
+ $$
45
+ );
46
+
47
+ -- Verify:
48
+ -- SELECT jobname, schedule, active FROM cron.job WHERE jobname = 'graph-inference-tick';
49
+ -- SELECT * FROM cron.job_run_details WHERE jobname = 'graph-inference-tick' ORDER BY start_time DESC LIMIT 5;