@aperdomoll90/ledger-ai 1.4.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/dist/cli.js +177 -221
  2. package/dist/commands/add.js +51 -100
  3. package/dist/commands/backfill.js +55 -0
  4. package/dist/commands/backup.js +10 -10
  5. package/dist/commands/check.js +21 -29
  6. package/dist/commands/config.js +13 -12
  7. package/dist/commands/delete.js +22 -17
  8. package/dist/commands/eval-judge.js +11 -0
  9. package/dist/commands/eval.js +321 -0
  10. package/dist/commands/export.js +8 -10
  11. package/dist/commands/get.js +9 -0
  12. package/dist/commands/hunt.js +206 -0
  13. package/dist/commands/ingest.js +15 -14
  14. package/dist/commands/init.js +18 -20
  15. package/dist/commands/list.js +21 -7
  16. package/dist/commands/migrate.js +11 -11
  17. package/dist/commands/onboard.js +2 -2
  18. package/dist/commands/pull.js +3 -2
  19. package/dist/commands/push.js +8 -8
  20. package/dist/commands/restore.js +38 -38
  21. package/dist/commands/show.js +13 -16
  22. package/dist/commands/sync.js +58 -19
  23. package/dist/commands/tag.js +20 -14
  24. package/dist/commands/update.js +50 -18
  25. package/dist/commands/wizard.js +3 -3
  26. package/dist/lib/ai-search.js +163 -0
  27. package/dist/lib/audit.js +19 -0
  28. package/dist/lib/backfill.js +60 -0
  29. package/dist/lib/config.js +19 -2
  30. package/dist/lib/document-classification.js +5 -0
  31. package/dist/lib/document-fetching.js +77 -0
  32. package/dist/lib/document-operations.js +150 -0
  33. package/dist/lib/documents/classification.js +5 -0
  34. package/dist/lib/documents/fetching.js +89 -0
  35. package/dist/lib/documents/operations.js +304 -0
  36. package/dist/lib/domains.js +116 -0
  37. package/dist/lib/embeddings.js +190 -0
  38. package/dist/lib/errors.js +3 -1
  39. package/dist/lib/eval/eval-advanced.js +289 -0
  40. package/dist/lib/eval/eval-judge-session.js +233 -0
  41. package/dist/lib/eval/eval-store.js +105 -0
  42. package/dist/lib/eval/eval.js +303 -0
  43. package/dist/lib/file-writer.js +23 -0
  44. package/dist/lib/generators.js +44 -45
  45. package/dist/lib/hunter-db.js +235 -0
  46. package/dist/lib/hunter-rss.js +30 -0
  47. package/dist/lib/hunter-scoring.js +55 -0
  48. package/dist/lib/hunter-types.js +36 -0
  49. package/dist/lib/lint-configs.js +20 -0
  50. package/dist/lib/migrate.js +2 -2
  51. package/dist/lib/notes.js +173 -59
  52. package/dist/lib/observability.js +296 -0
  53. package/dist/lib/op-add-note-types.test.js +7 -6
  54. package/dist/lib/prompt.js +8 -8
  55. package/dist/lib/rate-limiter.js +103 -0
  56. package/dist/lib/search/ai-search.js +396 -0
  57. package/dist/lib/search/chunk-context-enrichment.js +155 -0
  58. package/dist/lib/search/embeddings.js +293 -0
  59. package/dist/lib/search/reranker.js +120 -0
  60. package/dist/lib/search/semantic-cache.js +53 -0
  61. package/dist/lib/type-registry.test.js +6 -6
  62. package/dist/mcp-server.js +553 -66
  63. package/dist/migrations/migrations/005-audit-log.sql +22 -0
  64. package/dist/migrations/migrations/005_opportunities.sql +48 -0
  65. package/dist/migrations/migrations/006-audited-operations.sql +235 -0
  66. package/dist/migrations/migrations/006_hunt_analytics.sql +38 -0
  67. package/dist/migrations/migrations/007-eval-golden-judgments.sql +119 -0
  68. package/dist/migrations/migrations/008-drop-expected-doc-ids.sql +9 -0
  69. package/dist/migrations/migrations/008-judge-helpers.sql +21 -0
  70. package/dist/migrations/migrations/009-semantic-cache.sql +216 -0
  71. package/dist/scripts/batch-grade.js +344 -0
  72. package/dist/scripts/benchmark-ingestion.js +376 -0
  73. package/dist/scripts/convert-judgments-to-graded.js +88 -0
  74. package/dist/scripts/diagnose-first-result.js +333 -0
  75. package/dist/scripts/drop-golden-query.js +53 -0
  76. package/dist/scripts/eval-search.js +115 -0
  77. package/dist/scripts/grade-unjudged-top1.js +138 -0
  78. package/dist/scripts/hunter-analytics.js +38 -0
  79. package/dist/scripts/hunter-cron.js +63 -0
  80. package/dist/scripts/hunter-purge.js +25 -0
  81. package/dist/scripts/migrate-v2.js +140 -0
  82. package/dist/scripts/reindex.js +74 -0
  83. package/dist/scripts/sync-local-docs.js +153 -0
  84. package/package.json +7 -1
@@ -0,0 +1,22 @@
1
+ -- Migration 005: Audit Log Table
2
+ -- Phase 1 of v2 roadmap
3
+ -- Append-only log of every write operation for rollback, sync, rate limiting, and observability
4
+ --
5
+ -- Design decisions:
6
+ -- - No FK on note_id (audit entries must survive note deletion)
7
+ -- - JSONB diff column stores old values for rollback
8
+ -- - Indexes on note_id (lookup by note), created_at (time-range), domain (filter by domain)
9
+
10
+ CREATE TABLE IF NOT EXISTS audit_log (
11
+ id bigserial PRIMARY KEY,
12
+ note_id bigint,
13
+ domain text,
14
+ operation text NOT NULL,
15
+ agent text NOT NULL,
16
+ diff jsonb,
17
+ created_at timestamptz DEFAULT now()
18
+ );
19
+
20
+ CREATE INDEX IF NOT EXISTS idx_audit_note_id ON audit_log (note_id);
21
+ CREATE INDEX IF NOT EXISTS idx_audit_created ON audit_log (created_at);
22
+ CREATE INDEX IF NOT EXISTS idx_audit_domain ON audit_log (domain);
@@ -0,0 +1,48 @@
1
+ -- 005_opportunities.sql
2
+ -- Hunter agent: opportunity tracking for freelance and employment leads
3
+
4
+ CREATE TABLE opportunities (
5
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
6
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
7
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
8
+
9
+ -- Source
10
+ url TEXT UNIQUE NOT NULL,
11
+ title TEXT NOT NULL,
12
+ source TEXT NOT NULL,
13
+ raw_description TEXT,
14
+
15
+ -- Classification
16
+ track TEXT NOT NULL CHECK (track IN ('freelance', 'employment')),
17
+
18
+ -- Scoring (nullable — raw listings stored before scoring)
19
+ score INTEGER,
20
+ score_breakdown JSONB,
21
+ recommendation TEXT CHECK (recommendation IN ('apply', 'ask', 'skip')),
22
+ reject_reasons TEXT[],
23
+
24
+ -- AI Output (nullable — populated after scoring)
25
+ summary TEXT,
26
+ key_requirements TEXT[],
27
+ compensation_range TEXT,
28
+ scored_at TIMESTAMPTZ,
29
+
30
+ -- Pipeline
31
+ status TEXT NOT NULL DEFAULT 'new' CHECK (status IN (
32
+ 'new', 'reviewed', 'applied', 'interviewing', 'won', 'lost', 'rejected'
33
+ )),
34
+ status_changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
35
+ notes TEXT
36
+ );
37
+
38
+ -- Indexes
39
+ CREATE INDEX idx_opportunities_track ON opportunities(track);
40
+ CREATE INDEX idx_opportunities_status ON opportunities(status);
41
+ CREATE INDEX idx_opportunities_score ON opportunities(score DESC);
42
+ CREATE INDEX idx_opportunities_created ON opportunities(created_at DESC);
43
+ CREATE INDEX idx_opportunities_unscored ON opportunities(created_at) WHERE score IS NULL;
44
+
45
+ -- Auto-update updated_at trigger (reuse pattern from notes table)
46
+ CREATE TRIGGER update_opportunities_updated_at
47
+ BEFORE UPDATE ON opportunities
48
+ FOR EACH ROW EXECUTE FUNCTION update_updated_at();
@@ -0,0 +1,235 @@
1
+ -- Migration 006: Transactional Note Operations with Audit
2
+ -- Every note write (create, update, delete) is wrapped in a transaction with its audit entry.
3
+ -- If either the note write or the audit entry fails, both roll back.
4
+ -- This prevents: orphaned notes without audit trails, audit entries for failed writes,
5
+ -- and partial chunk writes.
6
+
7
+ -- =============================================================================
8
+ -- 1. CREATE: Insert one or more note rows + audit entry in one transaction
9
+ -- =============================================================================
10
+ -- Accepts arrays for multi-chunk support. Single-chunk = arrays of length 1.
11
+ -- Returns the IDs of all inserted rows.
12
+
13
+ CREATE OR REPLACE FUNCTION note_create(
14
+ p_contents text[],
15
+ p_metadatas jsonb[],
16
+ p_embeddings vector(1536)[],
17
+ p_ids bigint[] DEFAULT NULL, -- optional: preserve specific IDs (for upsert)
18
+ p_created_at timestamptz DEFAULT NULL -- optional: preserve original timestamp
19
+ ) RETURNS bigint[] AS $$
20
+ DECLARE
21
+ v_ids bigint[] := '{}';
22
+ v_id bigint;
23
+ v_ts timestamptz := COALESCE(p_created_at, now());
24
+ i int;
25
+ BEGIN
26
+ FOR i IN 1..array_length(p_contents, 1) LOOP
27
+ IF p_ids IS NOT NULL AND p_ids[i] IS NOT NULL THEN
28
+ INSERT INTO notes (id, content, metadata, embedding, created_at)
29
+ VALUES (p_ids[i], p_contents[i], p_metadatas[i], p_embeddings[i], v_ts)
30
+ RETURNING id INTO v_id;
31
+ ELSE
32
+ INSERT INTO notes (content, metadata, embedding, created_at)
33
+ VALUES (p_contents[i], p_metadatas[i], p_embeddings[i], v_ts)
34
+ RETURNING id INTO v_id;
35
+ END IF;
36
+ v_ids := v_ids || v_id;
37
+ END LOOP;
38
+
39
+ -- Audit: log creation using first chunk's metadata for domain/agent
40
+ INSERT INTO audit_log (note_id, domain, operation, agent, diff)
41
+ VALUES (
42
+ v_ids[1],
43
+ p_metadatas[1]->>'domain',
44
+ 'create',
45
+ COALESCE(p_metadatas[1]->>'agent', 'unknown'),
46
+ NULL
47
+ );
48
+
49
+ RETURN v_ids;
50
+ END;
51
+ $$ LANGUAGE plpgsql;
52
+
53
+ -- =============================================================================
54
+ -- 2. UPDATE: Update a single note in-place + audit entry
55
+ -- =============================================================================
56
+ -- Reads old content/metadata before overwriting for the audit diff.
57
+
58
+ CREATE OR REPLACE FUNCTION note_update(
59
+ p_id bigint,
60
+ p_content text,
61
+ p_metadata jsonb,
62
+ p_embedding vector(1536)
63
+ ) RETURNS void AS $$
64
+ DECLARE
65
+ v_old_content text;
66
+ v_old_metadata jsonb;
67
+ BEGIN
68
+ -- Read old values for audit diff
69
+ SELECT content, metadata INTO v_old_content, v_old_metadata
70
+ FROM notes WHERE id = p_id;
71
+
72
+ IF NOT FOUND THEN
73
+ RAISE EXCEPTION 'Note % not found', p_id;
74
+ END IF;
75
+
76
+ -- Update the note
77
+ UPDATE notes
78
+ SET content = p_content, metadata = p_metadata, embedding = p_embedding, updated_at = now()
79
+ WHERE id = p_id;
80
+
81
+ -- Audit: log update with old values for rollback
82
+ INSERT INTO audit_log (note_id, domain, operation, agent, diff)
83
+ VALUES (
84
+ p_id,
85
+ p_metadata->>'domain',
86
+ 'update',
87
+ COALESCE(p_metadata->>'agent', 'unknown'),
88
+ jsonb_build_object('content', v_old_content, 'metadata', v_old_metadata)
89
+ );
90
+ END;
91
+ $$ LANGUAGE plpgsql;
92
+
93
+ -- =============================================================================
94
+ -- 3. REPLACE: Delete old note/chunks + insert new note/chunks + audit
95
+ -- =============================================================================
96
+ -- For chunk-count changes: atomically removes old content and inserts new.
97
+ -- Preserves the original note ID for the first new chunk.
98
+
99
+ CREATE OR REPLACE FUNCTION note_replace(
100
+ p_old_id bigint, -- original note ID to preserve
101
+ p_old_chunk_group text, -- NULL if old was single-chunk
102
+ p_contents text[],
103
+ p_metadatas jsonb[],
104
+ p_embeddings vector(1536)[],
105
+ p_created_at timestamptz
106
+ ) RETURNS bigint[] AS $$
107
+ DECLARE
108
+ v_old_content text;
109
+ v_old_metadata jsonb;
110
+ v_ids bigint[] := '{}';
111
+ v_id bigint;
112
+ i int;
113
+ BEGIN
114
+ -- Read old values for audit diff (from the original note)
115
+ SELECT content, metadata INTO v_old_content, v_old_metadata
116
+ FROM notes WHERE id = p_old_id;
117
+
118
+ -- Delete old note/chunks
119
+ IF p_old_chunk_group IS NOT NULL THEN
120
+ DELETE FROM notes WHERE metadata->>'chunk_group' = p_old_chunk_group;
121
+ ELSE
122
+ DELETE FROM notes WHERE id = p_old_id;
123
+ END IF;
124
+
125
+ -- Insert new note/chunks
126
+ FOR i IN 1..array_length(p_contents, 1) LOOP
127
+ IF i = 1 THEN
128
+ -- First chunk preserves the original ID
129
+ INSERT INTO notes (id, content, metadata, embedding, created_at)
130
+ VALUES (p_old_id, p_contents[i], p_metadatas[i], p_embeddings[i], p_created_at)
131
+ RETURNING id INTO v_id;
132
+ ELSE
133
+ INSERT INTO notes (content, metadata, embedding, created_at)
134
+ VALUES (p_contents[i], p_metadatas[i], p_embeddings[i], p_created_at)
135
+ RETURNING id INTO v_id;
136
+ END IF;
137
+ v_ids := v_ids || v_id;
138
+ END LOOP;
139
+
140
+ -- Audit: log as update with old values for rollback
141
+ INSERT INTO audit_log (note_id, domain, operation, agent, diff)
142
+ VALUES (
143
+ p_old_id,
144
+ p_metadatas[1]->>'domain',
145
+ 'update',
146
+ COALESCE(p_metadatas[1]->>'agent', 'unknown'),
147
+ jsonb_build_object('content', v_old_content, 'metadata', v_old_metadata)
148
+ );
149
+
150
+ RETURN v_ids;
151
+ END;
152
+ $$ LANGUAGE plpgsql;
153
+
154
+ -- =============================================================================
155
+ -- 4. DELETE: Delete note/chunks + audit entry with full content for rollback
156
+ -- =============================================================================
157
+
158
+ CREATE OR REPLACE FUNCTION note_delete(
159
+ p_id bigint,
160
+ p_chunk_group text, -- NULL if single-chunk note
161
+ p_agent text
162
+ ) RETURNS void AS $$
163
+ DECLARE
164
+ v_content text;
165
+ v_metadata jsonb;
166
+ BEGIN
167
+ -- Read full content for rollback
168
+ SELECT content, metadata INTO v_content, v_metadata
169
+ FROM notes WHERE id = p_id;
170
+
171
+ IF NOT FOUND THEN
172
+ RAISE EXCEPTION 'Note % not found', p_id;
173
+ END IF;
174
+
175
+ -- Audit FIRST: store full content before deletion
176
+ INSERT INTO audit_log (note_id, domain, operation, agent, diff)
177
+ VALUES (
178
+ p_id,
179
+ v_metadata->>'domain',
180
+ 'delete',
181
+ p_agent,
182
+ jsonb_build_object('content', v_content, 'metadata', v_metadata)
183
+ );
184
+
185
+ -- Then delete
186
+ IF p_chunk_group IS NOT NULL THEN
187
+ DELETE FROM notes WHERE metadata->>'chunk_group' = p_chunk_group;
188
+ ELSE
189
+ DELETE FROM notes WHERE id = p_id;
190
+ END IF;
191
+ END;
192
+ $$ LANGUAGE plpgsql;
193
+
194
+ -- =============================================================================
195
+ -- 5. UPDATE METADATA: Update metadata fields + audit with changed fields
196
+ -- =============================================================================
197
+
198
+ CREATE OR REPLACE FUNCTION note_update_metadata(
199
+ p_id bigint,
200
+ p_metadata jsonb -- fields to merge (not full replacement)
201
+ ) RETURNS void AS $$
202
+ DECLARE
203
+ v_old_metadata jsonb;
204
+ v_merged jsonb;
205
+ v_changed jsonb;
206
+ BEGIN
207
+ SELECT metadata INTO v_old_metadata FROM notes WHERE id = p_id;
208
+
209
+ IF NOT FOUND THEN
210
+ RAISE EXCEPTION 'Note % not found', p_id;
211
+ END IF;
212
+
213
+ -- Merge: new fields override old, old fields preserved
214
+ v_merged := v_old_metadata || p_metadata;
215
+
216
+ -- Calculate which fields actually changed (old values of changed keys)
217
+ SELECT COALESCE(jsonb_object_agg(key, v_old_metadata->key), '{}'::jsonb)
218
+ INTO v_changed
219
+ FROM jsonb_each(p_metadata)
220
+ WHERE v_old_metadata->key IS DISTINCT FROM p_metadata->key;
221
+
222
+ -- Update
223
+ UPDATE notes SET metadata = v_merged, updated_at = now() WHERE id = p_id;
224
+
225
+ -- Audit
226
+ INSERT INTO audit_log (note_id, domain, operation, agent, diff)
227
+ VALUES (
228
+ p_id,
229
+ v_merged->>'domain',
230
+ 'update_metadata',
231
+ COALESCE(v_merged->>'agent', 'unknown'),
232
+ jsonb_build_object('metadata', v_changed)
233
+ );
234
+ END;
235
+ $$ LANGUAGE plpgsql;
@@ -0,0 +1,38 @@
1
+ -- 006_hunt_analytics.sql
2
+ -- Hunter agent: periodic analytics summaries
3
+
4
+ CREATE TABLE hunt_analytics (
5
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
6
+ period_start DATE NOT NULL,
7
+ period_end DATE NOT NULL,
8
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
9
+
10
+ -- Volume
11
+ total_found INTEGER NOT NULL,
12
+ freelance_count INTEGER NOT NULL,
13
+ employment_count INTEGER NOT NULL,
14
+
15
+ -- Score Distribution
16
+ score_80_plus INTEGER NOT NULL,
17
+ score_60_79 INTEGER NOT NULL,
18
+ score_40_59 INTEGER NOT NULL,
19
+ score_below_40 INTEGER NOT NULL,
20
+
21
+ -- Pipeline
22
+ applied_count INTEGER NOT NULL,
23
+ won_count INTEGER NOT NULL,
24
+ lost_count INTEGER NOT NULL,
25
+ apply_rate NUMERIC(5,2),
26
+ win_rate NUMERIC(5,2),
27
+
28
+ -- Patterns (JSONB)
29
+ top_reject_reasons JSONB,
30
+ top_skills_demanded JSONB,
31
+ avg_score_by_track JSONB,
32
+ compensation_ranges JSONB,
33
+
34
+ -- Insights
35
+ summary TEXT
36
+ );
37
+
38
+ CREATE UNIQUE INDEX idx_hunt_analytics_period ON hunt_analytics(period_start, period_end);
@@ -0,0 +1,119 @@
1
+ -- ============================================================
2
+ -- Migration 007 — Phase 4.6.2: Graded Relevance
3
+ --
4
+ -- Adds eval_golden_judgments table + 3 RPC functions for atomic writes.
5
+ -- Does NOT drop eval_golden_dataset.expected_doc_ids (deferred to migration 008).
6
+ -- ============================================================
7
+
8
+ -- ============================================================
9
+ -- Table
10
+ -- ============================================================
11
+ CREATE TABLE IF NOT EXISTS eval_golden_judgments (
12
+ id bigserial PRIMARY KEY,
13
+ golden_id bigint NOT NULL REFERENCES eval_golden_dataset(id) ON DELETE CASCADE,
14
+ document_id bigint NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
15
+ grade smallint NOT NULL CHECK (grade BETWEEN 0 AND 3),
16
+ judged_at timestamptz NOT NULL DEFAULT now(),
17
+ judged_by text NOT NULL DEFAULT 'adrian',
18
+ notes text,
19
+ UNIQUE (golden_id, document_id)
20
+ );
21
+
22
+ -- ============================================================
23
+ -- Indexes
24
+ -- ============================================================
25
+ CREATE INDEX IF NOT EXISTS idx_golden_judgments_golden_id ON eval_golden_judgments(golden_id);
26
+ CREATE INDEX IF NOT EXISTS idx_golden_judgments_document_id ON eval_golden_judgments(document_id);
27
+ CREATE INDEX IF NOT EXISTS idx_golden_judgments_grade ON eval_golden_judgments(grade);
28
+
29
+ -- ============================================================
30
+ -- Row-level security
31
+ -- ============================================================
32
+ ALTER TABLE eval_golden_judgments ENABLE ROW LEVEL SECURITY;
33
+
34
+ DROP POLICY IF EXISTS eval_golden_judgments_service_all ON eval_golden_judgments;
35
+ CREATE POLICY eval_golden_judgments_service_all
36
+ ON eval_golden_judgments
37
+ FOR ALL
38
+ TO service_role
39
+ USING (true)
40
+ WITH CHECK (true);
41
+
42
+ -- ============================================================
43
+ -- RPC: judgment_create
44
+ -- Inserts one judgment. Errors on duplicate (golden_id, document_id).
45
+ -- Returns the new row id.
46
+ -- ============================================================
47
+ CREATE OR REPLACE FUNCTION judgment_create(
48
+ p_golden_id bigint,
49
+ p_document_id bigint,
50
+ p_grade smallint,
51
+ p_judged_by text DEFAULT 'adrian',
52
+ p_notes text DEFAULT NULL
53
+ ) RETURNS bigint
54
+ LANGUAGE plpgsql
55
+ AS $$
56
+ DECLARE
57
+ v_id bigint;
58
+ BEGIN
59
+ INSERT INTO eval_golden_judgments (golden_id, document_id, grade, judged_by, notes)
60
+ VALUES (p_golden_id, p_document_id, p_grade, p_judged_by, p_notes)
61
+ RETURNING id INTO v_id;
62
+ RETURN v_id;
63
+ END;
64
+ $$;
65
+
66
+ -- ============================================================
67
+ -- RPC: judgment_update
68
+ -- Updates grade (and optionally notes) for an existing judgment.
69
+ -- Bumps judged_at. Errors if no row matches.
70
+ -- ============================================================
71
+ CREATE OR REPLACE FUNCTION judgment_update(
72
+ p_golden_id bigint,
73
+ p_document_id bigint,
74
+ p_grade smallint,
75
+ p_notes text DEFAULT NULL
76
+ ) RETURNS void
77
+ LANGUAGE plpgsql
78
+ AS $$
79
+ DECLARE
80
+ v_updated int;
81
+ BEGIN
82
+ UPDATE eval_golden_judgments
83
+ SET grade = p_grade,
84
+ notes = COALESCE(p_notes, notes),
85
+ judged_at = now()
86
+ WHERE golden_id = p_golden_id
87
+ AND document_id = p_document_id;
88
+
89
+ GET DIAGNOSTICS v_updated = ROW_COUNT;
90
+ IF v_updated = 0 THEN
91
+ RAISE EXCEPTION 'judgment_update: no judgment exists for (golden_id=%, document_id=%)',
92
+ p_golden_id, p_document_id;
93
+ END IF;
94
+ END;
95
+ $$;
96
+
97
+ -- ============================================================
98
+ -- RPC: judgment_delete
99
+ -- Removes a judgment. Idempotent on missing row (no error).
100
+ -- ============================================================
101
+ CREATE OR REPLACE FUNCTION judgment_delete(
102
+ p_golden_id bigint,
103
+ p_document_id bigint
104
+ ) RETURNS void
105
+ LANGUAGE plpgsql
106
+ AS $$
107
+ BEGIN
108
+ DELETE FROM eval_golden_judgments
109
+ WHERE golden_id = p_golden_id
110
+ AND document_id = p_document_id;
111
+ END;
112
+ $$;
113
+
114
+ -- ============================================================
115
+ -- Grants
116
+ -- ============================================================
117
+ GRANT EXECUTE ON FUNCTION judgment_create(bigint, bigint, smallint, text, text) TO service_role;
118
+ GRANT EXECUTE ON FUNCTION judgment_update(bigint, bigint, smallint, text) TO service_role;
119
+ GRANT EXECUTE ON FUNCTION judgment_delete(bigint, bigint) TO service_role;
@@ -0,0 +1,9 @@
1
+ -- ============================================================
2
+ -- Migration 008b — Phase 4.6.2 cleanup: drop legacy column
3
+ --
4
+ -- Runs AFTER the graded dataset is fully populated and run 14
5
+ -- has landed. The expected_doc_ids data was converted to grade-3
6
+ -- rows in eval_golden_judgments by convert-judgments-to-graded.ts.
7
+ -- ============================================================
8
+
9
+ ALTER TABLE eval_golden_dataset DROP COLUMN IF EXISTS expected_doc_ids;
@@ -0,0 +1,21 @@
1
+ -- ============================================================
2
+ -- Migration 008 — Phase 4.6.2: judge helpers
3
+ --
4
+ -- Adds the count_golden_with_min_judgments() function used by the
5
+ -- ledger eval:judge CLI progress display.
6
+ -- ============================================================
7
+
8
+ CREATE OR REPLACE FUNCTION count_golden_with_min_judgments(p_min int)
9
+ RETURNS bigint
10
+ LANGUAGE sql
11
+ STABLE
12
+ AS $$
13
+ SELECT count(*)::bigint FROM (
14
+ SELECT golden_id
15
+ FROM eval_golden_judgments
16
+ GROUP BY golden_id
17
+ HAVING count(*) >= p_min
18
+ ) AS qualifying;
19
+ $$;
20
+
21
+ GRANT EXECUTE ON FUNCTION count_golden_with_min_judgments(int) TO service_role;