@rulemetric/local 0.12.3 → 0.12.5
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/dist/meta.json +2 -2
- package/dist/server.mjs +292 -255
- package/dist/supabase/migrations/00206_reconcile_attribution_producer.sql +156 -0
- package/dist/supabase/migrations/00207_repair_project_paths.sql +183 -0
- package/dist/supabase/migrations/00208_auto_accept_candidate_view.sql +131 -0
- package/dist/supabase/migrations/00209_suggestion_pipeline_conformance.sql +296 -0
- package/dist/supabase/migrations/00210_shared_eval_target_health_gates.sql +237 -0
- package/dist/supabase/migrations/00211_training_candidate_view.sql +119 -0
- package/dist/supabase/migrations/00212_reconcile_junk_session_paths.sql +393 -0
- package/package.json +2 -2
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
-- Sessions carrying a bare home-directory project_path are repairable, and the
|
|
2
|
+
-- reconciler must chase them.
|
|
3
|
+
--
|
|
4
|
+
-- THE DEFECT CLASS (found 2026-08-27, via "why does Thrakopontia show no
|
|
5
|
+
-- suggestions?"). Until 2026-08-26 the subdir-consolidation normalizer in
|
|
6
|
+
-- POST /api/sessions rewrote real project paths into `/Users/<name>` — home is
|
|
7
|
+
-- a prefix of every path the user works in, and the loop had no home-dir
|
|
8
|
+
-- guard. 219 sessions on the cloud DB carry a bare home path; for 34 of them
|
|
9
|
+
-- the transcript on disk still names the true checkout. Fill-only merge
|
|
10
|
+
-- semantics then froze the junk in place: the very next hook POST carried the
|
|
11
|
+
-- correct cwd and was discarded, and the SessionEnd reimport that would have
|
|
12
|
+
-- corrected it is an unreliable messenger (Claude Code cancels the hook after
|
|
13
|
+
-- its first network call). The path keys real behaviour — insights job
|
|
14
|
+
-- targeting, suggestion project scoping, the session feed's exact-path filter
|
|
15
|
+
-- — so these rows starve their real project of its own evidence.
|
|
16
|
+
--
|
|
17
|
+
-- 00206's producer explicitly EXCLUDED home paths as "orphaned correctly".
|
|
18
|
+
-- That was written before we knew the normalizer had been rewriting real
|
|
19
|
+
-- sessions INTO home: a bare home path is not a fact about where the session
|
|
20
|
+
-- ran, it is (almost always) a fact about what the normalizer did to the row.
|
|
21
|
+
-- The worker-side recovery already treats it that way (recoverAnchors ignores
|
|
22
|
+
-- a home stored path and reads the transcript), and repairAttribution now
|
|
23
|
+
-- upgrades a junk stored path on anchor-backed evidence — this migration makes
|
|
24
|
+
-- the producer and the conformance monitor agree.
|
|
25
|
+
--
|
|
26
|
+
-- Three pieces:
|
|
27
|
+
-- 1. enqueue_reconcile_attribution(): junk-path sessions become eligible.
|
|
28
|
+
-- 2. suggestion_pipeline_conformance(): new metric + CHECK 6, so the shape
|
|
29
|
+
-- is visible even for users whose worker never runs (their repair lane).
|
|
30
|
+
-- 3. Idempotent DELETE of bare-home project_checkouts rows — the laundering
|
|
31
|
+
-- vector by which resolveProjectIdForPath('/Users/<name>') attributed
|
|
32
|
+
-- home-path sessions to an unrelated project. The API guards new inserts
|
|
33
|
+
-- (upsertProjectCheckout refuses home paths) but rows minted by older
|
|
34
|
+
-- API builds still running elsewhere (local-mode installs) would
|
|
35
|
+
-- re-poison resolution.
|
|
36
|
+
-- ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
-- ── 1. Producer: chase junk-path sessions too ──────────────────────────────
|
|
39
|
+
|
|
40
|
+
create or replace function public.enqueue_reconcile_attribution()
|
|
41
|
+
returns integer
|
|
42
|
+
language plpgsql
|
|
43
|
+
security definer
|
|
44
|
+
set search_path = public
|
|
45
|
+
as $$
|
|
46
|
+
declare
|
|
47
|
+
queued integer;
|
|
48
|
+
begin
|
|
49
|
+
with eligible as (
|
|
50
|
+
select s.user_id
|
|
51
|
+
from public.sessions s
|
|
52
|
+
where (
|
|
53
|
+
s.project_id is null
|
|
54
|
+
or not exists (
|
|
55
|
+
select 1 from public.project_checkouts pc where pc.project_id = s.project_id
|
|
56
|
+
)
|
|
57
|
+
-- A bare home dir is never a project checkout, so an attributed
|
|
58
|
+
-- session carrying one is self-contradictory — the pre-2026-08-26
|
|
59
|
+
-- normalizer shape. The handler repairs it from the transcript on the
|
|
60
|
+
-- machine that ran it.
|
|
61
|
+
or s.project_path ~ '^/(Users|home)/[^/]+/?$'
|
|
62
|
+
)
|
|
63
|
+
-- Only chase orphans a worker could plausibly still repair. A session
|
|
64
|
+
-- whose checkout was deleted years ago is permanently unrepairable, and
|
|
65
|
+
-- re-walking it every hour forever is pure noise. 90 days is well past
|
|
66
|
+
-- Claude Code's transcript retention, which is the recovery source for
|
|
67
|
+
-- the pathless shape.
|
|
68
|
+
and s.started_at > now() - interval '90 days'
|
|
69
|
+
-- Sessions with no external id cannot be addressed through
|
|
70
|
+
-- POST /api/sessions at all (it matches on external_session_id), so the
|
|
71
|
+
-- handler would only count them as unrepairable. Do not enqueue for a
|
|
72
|
+
-- user who has nothing BUT those.
|
|
73
|
+
and s.external_session_id is not null
|
|
74
|
+
-- Temp fixtures are orphaned CORRECTLY. (Home dirs used to sit in this
|
|
75
|
+
-- exclusion; they are the repair target now — see the header.)
|
|
76
|
+
and (
|
|
77
|
+
s.project_path is null
|
|
78
|
+
or s.project_path !~ '^/(private/)?(tmp|var/folders)'
|
|
79
|
+
)
|
|
80
|
+
group by s.user_id
|
|
81
|
+
), fresh as (
|
|
82
|
+
select e.user_id
|
|
83
|
+
from eligible e
|
|
84
|
+
-- One in flight at a time, per the 00083/00086 pattern.
|
|
85
|
+
where not exists (
|
|
86
|
+
select 1 from public.agent_jobs j
|
|
87
|
+
where j.task_kind = 'cron_reconcile_attribution'
|
|
88
|
+
and j.status in ('pending', 'claimed', 'running')
|
|
89
|
+
and j.user_id = e.user_id
|
|
90
|
+
)
|
|
91
|
+
-- Do not re-run within the hour for a user whose last pass just completed.
|
|
92
|
+
-- The orphan set only changes when new sessions land or a checkout appears.
|
|
93
|
+
and not exists (
|
|
94
|
+
select 1 from public.agent_jobs j
|
|
95
|
+
where j.task_kind = 'cron_reconcile_attribution'
|
|
96
|
+
and j.user_id = e.user_id
|
|
97
|
+
and j.status = 'completed'
|
|
98
|
+
and j.completed_at > now() - interval '50 minutes'
|
|
99
|
+
)
|
|
100
|
+
), ins as (
|
|
101
|
+
insert into agent_jobs (user_id, task_kind, status, payload, dedupe_key)
|
|
102
|
+
select f.user_id,
|
|
103
|
+
'cron_reconcile_attribution',
|
|
104
|
+
'pending',
|
|
105
|
+
jsonb_build_object('limit', 200),
|
|
106
|
+
'reconcile-attribution:' || f.user_id
|
|
107
|
+
from fresh f
|
|
108
|
+
returning 1
|
|
109
|
+
)
|
|
110
|
+
select count(*)::int into queued from ins;
|
|
111
|
+
|
|
112
|
+
return queued;
|
|
113
|
+
end;
|
|
114
|
+
$$;
|
|
115
|
+
|
|
116
|
+
comment on function public.enqueue_reconcile_attribution() is
|
|
117
|
+
'Hourly producer for cron_reconcile_attribution (:20). Enqueues one run per user holding sessions that are invisible to insights (project_id NULL, checkout-less project) OR self-contradictory (attributed but carrying a bare home-dir project_path — the pre-2026-08-26 normalizer shape). Does NOT repair anything itself: the evidence (transcript cwd, git anchors) lives on the machine that ran the session; the handler reads it and POSTs to POST /api/sessions and /api/sessions/:id/reimport.';
|
|
118
|
+
|
|
119
|
+
-- ── 1b. Health view: agree with the producer ───────────────────────────────
|
|
120
|
+
-- Three definitions of "the reconciler's universe" exist (producer, endpoint,
|
|
121
|
+
-- this view); a shape one includes and another excludes is exactly how the
|
|
122
|
+
-- home-path sessions stayed invisible for months. Same widening as above.
|
|
123
|
+
|
|
124
|
+
create or replace view public.session_attribution_health as
|
|
125
|
+
select
|
|
126
|
+
s.user_id,
|
|
127
|
+
count(*)::int as total_unattributed,
|
|
128
|
+
count(*) filter (
|
|
129
|
+
where s.started_at > now() - interval '90 days'
|
|
130
|
+
and s.external_session_id is not null
|
|
131
|
+
and (
|
|
132
|
+
s.project_path is null
|
|
133
|
+
or s.project_path !~ '^/(private/)?(tmp|var/folders)'
|
|
134
|
+
)
|
|
135
|
+
)::int as repairable_recent,
|
|
136
|
+
sum(s.event_count)::bigint as orphaned_events,
|
|
137
|
+
max(s.started_at) as newest_orphan
|
|
138
|
+
from public.sessions s
|
|
139
|
+
where s.project_id is null
|
|
140
|
+
or not exists (
|
|
141
|
+
select 1 from public.project_checkouts pc where pc.project_id = s.project_id
|
|
142
|
+
)
|
|
143
|
+
or s.project_path ~ '^/(Users|home)/[^/]+/?$'
|
|
144
|
+
group by s.user_id;
|
|
145
|
+
|
|
146
|
+
comment on view public.session_attribution_health is
|
|
147
|
+
'Per-user count of sessions invisible to the insights pipeline (unattributed, checkout-less project, or a self-contradictory bare home-dir project_path). repairable_recent is the subset cron_reconcile_attribution will attempt; a total that keeps climbing while repairable_recent stays near zero means the orphans are structurally unrepairable, not that the reconciler is failing.';
|
|
148
|
+
|
|
149
|
+
-- ── 2. Conformance: metric + CHECK 6 ───────────────────────────────────────
|
|
150
|
+
-- Full replace of 00209''s function body with one metric and one check added.
|
|
151
|
+
-- (Producer-function churn is the known cost of logic-in-SQL; see the
|
|
152
|
+
-- 2026-08-27 migration-treadmill review.)
|
|
153
|
+
|
|
154
|
+
create or replace function public.suggestion_pipeline_conformance()
|
|
155
|
+
returns table (check_id text, user_id uuid, detail text)
|
|
156
|
+
language plpgsql
|
|
157
|
+
security definer
|
|
158
|
+
set search_path = public
|
|
159
|
+
as $$
|
|
160
|
+
begin
|
|
161
|
+
-- ── Snapshot metrics first, so the monotone checks below (and future ones)
|
|
162
|
+
-- have history even on days when every check is quiet. ────────────────────
|
|
163
|
+
insert into pipeline_conformance_metrics (user_id, metric, value)
|
|
164
|
+
select s.user_id, m.metric, m.value
|
|
165
|
+
from (
|
|
166
|
+
select distinct is2.user_id from instruction_suggestions is2
|
|
167
|
+
union
|
|
168
|
+
select distinct se.user_id from sessions se where se.started_at > now() - interval '90 days'
|
|
169
|
+
) s
|
|
170
|
+
cross join lateral (
|
|
171
|
+
select 'stale_open_rows' as metric,
|
|
172
|
+
(select count(*) from instruction_suggestions x
|
|
173
|
+
where x.user_id = s.user_id and x.kind = 'add'
|
|
174
|
+
and x.accepted_at is null and x.dismissed_at is null
|
|
175
|
+
and x.is_stale) as value
|
|
176
|
+
union all
|
|
177
|
+
select 'undecided_add_rows',
|
|
178
|
+
(select count(*) from instruction_suggestions x
|
|
179
|
+
where x.user_id = s.user_id and x.kind = 'add'
|
|
180
|
+
and x.accepted_at is null and x.dismissed_at is null)
|
|
181
|
+
union all
|
|
182
|
+
select 'auto_accept_candidates',
|
|
183
|
+
(select count(*) from auto_accept_candidate_suggestions v
|
|
184
|
+
where v.user_id = s.user_id)
|
|
185
|
+
union all
|
|
186
|
+
-- The 0.5–0.79 band: emitted by the scorer (recommend.ts floors at 0.5),
|
|
187
|
+
-- below the adoption default (0.8). Feed-only by construction — counted so
|
|
188
|
+
-- "open suggestions" stops being one number over two populations (census
|
|
189
|
+
-- cause 1, 2026-08-27: 393 of 477 open skills sat in this band).
|
|
190
|
+
select 'feed_only_band_rows',
|
|
191
|
+
(select count(*) from instruction_suggestions x
|
|
192
|
+
join instructions i on i.id = x.instruction_id
|
|
193
|
+
where x.user_id = s.user_id and x.kind = 'add'
|
|
194
|
+
and x.accepted_at is null and x.dismissed_at is null
|
|
195
|
+
and i.type in ('instruction', 'skill')
|
|
196
|
+
and x.score < 0.8
|
|
197
|
+
and coalesce(i.frontmatter->>'source', '') <> 'insights')
|
|
198
|
+
union all
|
|
199
|
+
-- Undecided rows quiet for >90 days that the weekly prune can NEVER
|
|
200
|
+
-- collect, by design: they carry instruction_suggestion_events, the FK is
|
|
201
|
+
-- ON DELETE CASCADE, and those events are the only record the funnel's
|
|
202
|
+
-- numerator was ever zero (00180's comment names naive pruning as the
|
|
203
|
+
-- failure mode). Counted here instead of deleted: the census's 2026-08-27
|
|
204
|
+
-- "cause 4" proposed pruning them and was wrong — visibility is the fix
|
|
205
|
+
-- that does not destroy evidence.
|
|
206
|
+
select 'dormant_open_rows',
|
|
207
|
+
(select count(*) from instruction_suggestions x
|
|
208
|
+
where x.user_id = s.user_id and x.kind = 'add'
|
|
209
|
+
and x.accepted_at is null and x.dismissed_at is null
|
|
210
|
+
and x.generated_at < now() - interval '90 days'
|
|
211
|
+
and coalesce(
|
|
212
|
+
(select max(ev.created_at) from instruction_suggestion_events ev
|
|
213
|
+
where ev.suggestion_id = x.id),
|
|
214
|
+
x.generated_at
|
|
215
|
+
) < now() - interval '90 days')
|
|
216
|
+
union all
|
|
217
|
+
-- Sessions attributed to a real project while carrying a bare home-dir
|
|
218
|
+
-- project_path — self-contradictory (a home dir is never a checkout), the
|
|
219
|
+
-- pre-2026-08-26 normalizer's signature. The reconciler repairs the ones
|
|
220
|
+
-- whose transcript survives; this count is the residue trending to zero.
|
|
221
|
+
select 'misattributed_session_paths',
|
|
222
|
+
(select count(*) from sessions x
|
|
223
|
+
where x.user_id = s.user_id
|
|
224
|
+
and x.project_id is not null
|
|
225
|
+
and x.project_path ~ '^/(Users|home)/[^/]+/?$'
|
|
226
|
+
and x.started_at > now() - interval '90 days')
|
|
227
|
+
) m;
|
|
228
|
+
|
|
229
|
+
return query
|
|
230
|
+
|
|
231
|
+
-- ── CHECK 1: minted_on_forbidden_path ──────────────────────────────────────
|
|
232
|
+
-- Incident: census 2026-08-27 — 258 of 685 open proposals sat on paths the
|
|
233
|
+
-- writer is forbidden to touch (home dir, TMPDIR, worktrees, the dead
|
|
234
|
+
-- apps/cli). Fixed the same day by gating both mint sites on
|
|
235
|
+
-- assessMintablePath(). This check is that fix's regression alarm: any NEW
|
|
236
|
+
-- row on a non-canonical path means a mint site was added or the gate broke.
|
|
237
|
+
select 'minted_on_forbidden_path'::text,
|
|
238
|
+
s.user_id,
|
|
239
|
+
count(*) || ' suggestion(s) minted in the last 25h on non-canonical path(s), e.g. ' || min(s.project_path)
|
|
240
|
+
from instruction_suggestions s
|
|
241
|
+
where s.generated_at > now() - interval '25 hours'
|
|
242
|
+
and not exists (
|
|
243
|
+
select 1 from project_canonical_checkouts c where c.path = s.project_path
|
|
244
|
+
)
|
|
245
|
+
group by s.user_id
|
|
246
|
+
|
|
247
|
+
union all
|
|
248
|
+
|
|
249
|
+
-- ── CHECK 2: producer_silent_with_candidates ───────────────────────────────
|
|
250
|
+
-- Incident: gate 4 (00184→00197) — the DB producer's type clause matched zero
|
|
251
|
+
-- rows, pg_cron fired and SUCCEEDED at 05:00 for three days, enqueued
|
|
252
|
+
-- nothing, and no log line existed anywhere. Candidates older than 25h with
|
|
253
|
+
-- no job even CREATED in 25h cannot be a quiet night: the producer is hourly.
|
|
254
|
+
select 'producer_silent_with_candidates'::text,
|
|
255
|
+
v.user_id,
|
|
256
|
+
count(*) || ' candidate(s) (oldest ' ||
|
|
257
|
+
extract(day from now() - min(v.generated_at)) || 'd) but no auto-accept job created in 25h — the producer is not seeing them'
|
|
258
|
+
from auto_accept_candidate_suggestions v
|
|
259
|
+
where v.generated_at < now() - interval '25 hours'
|
|
260
|
+
and not exists (
|
|
261
|
+
select 1 from agent_jobs j
|
|
262
|
+
where j.user_id = v.user_id
|
|
263
|
+
and j.task_kind = 'cron_auto_accept_suggestions'
|
|
264
|
+
and j.created_at > now() - interval '25 hours'
|
|
265
|
+
)
|
|
266
|
+
group by v.user_id
|
|
267
|
+
|
|
268
|
+
union all
|
|
269
|
+
|
|
270
|
+
-- ── CHECK 3: endpoint_refuses_everything ───────────────────────────────────
|
|
271
|
+
-- Incident: gates 1–4 (#372, 2026-08-16..19) — the producer queued jobs, the
|
|
272
|
+
-- worker ran them, the ENDPOINT refused every candidate on a clause the
|
|
273
|
+
-- producer didn't have, and four days of `nothing-eligible` looked like four
|
|
274
|
+
-- quiet nights. Signature: jobs completing against a standing candidate set
|
|
275
|
+
-- while NOTHING moves — no acceptance, no refusal event. A healthy run
|
|
276
|
+
-- always moves one of those; a placeholder-refusal loop that repeats forever
|
|
277
|
+
-- without logging also lands here, and deserves to.
|
|
278
|
+
select 'endpoint_refuses_everything'::text,
|
|
279
|
+
v.user_id,
|
|
280
|
+
count(distinct v.suggestion_id) ||
|
|
281
|
+
' candidate(s) standing >48h while auto-accept jobs completed — zero adoptions, zero refusal events. The producer and the endpoint disagree about eligibility.'
|
|
282
|
+
from auto_accept_candidate_suggestions v
|
|
283
|
+
where v.generated_at < now() - interval '48 hours'
|
|
284
|
+
and exists (
|
|
285
|
+
select 1 from agent_jobs j
|
|
286
|
+
where j.user_id = v.user_id
|
|
287
|
+
and j.task_kind = 'cron_auto_accept_suggestions'
|
|
288
|
+
and j.status = 'completed'
|
|
289
|
+
and j.updated_at > now() - interval '48 hours'
|
|
290
|
+
)
|
|
291
|
+
and not exists (
|
|
292
|
+
select 1 from instruction_suggestions a
|
|
293
|
+
where a.user_id = v.user_id
|
|
294
|
+
and a.accepted_at > now() - interval '48 hours'
|
|
295
|
+
)
|
|
296
|
+
and not exists (
|
|
297
|
+
select 1 from instruction_suggestion_events ev
|
|
298
|
+
join instruction_suggestions es on es.id = ev.suggestion_id
|
|
299
|
+
where es.user_id = v.user_id
|
|
300
|
+
and ev.event_type in ('redundant', 'partial_overlap', 'unresolvable_reference')
|
|
301
|
+
and ev.created_at > now() - interval '48 hours'
|
|
302
|
+
)
|
|
303
|
+
group by v.user_id
|
|
304
|
+
|
|
305
|
+
union all
|
|
306
|
+
|
|
307
|
+
-- ── CHECK 4: stale_flag_only_grows ─────────────────────────────────────────
|
|
308
|
+
-- Incident: gate 7 (2026-08-24) — is_stale was set by the mark-stale cron and
|
|
309
|
+
-- NOTHING ever cleared it; 51 adoption-grade proposals went invisible, and
|
|
310
|
+
-- one user's whole skill lane was structurally dead for weeks. A one-way flag
|
|
311
|
+
-- is invisible at any instant; over days it is a staircase. Fires when the
|
|
312
|
+
-- last 8 daily samples are all positive, never decrease, and end higher than
|
|
313
|
+
-- they started.
|
|
314
|
+
select 'stale_flag_only_grows'::text,
|
|
315
|
+
g.user_id,
|
|
316
|
+
'stale-flagged open rows grew ' || g.first_value || ' → ' || g.last_value ||
|
|
317
|
+
' over ' || g.samples || ' daily samples with no decrease — nothing is un-flagging them'
|
|
318
|
+
from (
|
|
319
|
+
select m.user_id,
|
|
320
|
+
count(*) as samples,
|
|
321
|
+
(array_agg(m.value order by m.run_at))[1] as first_value,
|
|
322
|
+
(array_agg(m.value order by m.run_at desc))[1] as last_value,
|
|
323
|
+
bool_and(m.value > 0) as all_positive,
|
|
324
|
+
-- Non-decreasing across the window: max of pairwise drops is 0.
|
|
325
|
+
coalesce(max(m.drop), 0) = 0 as never_decreased
|
|
326
|
+
from (
|
|
327
|
+
select mm.user_id, mm.run_at, mm.value,
|
|
328
|
+
greatest(lag(mm.value) over (partition by mm.user_id order by mm.run_at) - mm.value, 0) as drop
|
|
329
|
+
from pipeline_conformance_metrics mm
|
|
330
|
+
where mm.metric = 'stale_open_rows'
|
|
331
|
+
and mm.run_at > now() - interval '8 days'
|
|
332
|
+
) m
|
|
333
|
+
group by m.user_id
|
|
334
|
+
) g
|
|
335
|
+
where g.samples >= 8 and g.all_positive and g.never_decreased and g.last_value > g.first_value
|
|
336
|
+
|
|
337
|
+
union all
|
|
338
|
+
|
|
339
|
+
-- ── CHECK 5: surface_budget_spent_on_unadoptable ───────────────────────────
|
|
340
|
+
-- Incident: gates 5/6 and census cause 2 — ~840 feature/hook/mcp pitches per
|
|
341
|
+
-- 30 days entered the SessionStart feed, burned their five-ask surface budget
|
|
342
|
+
-- on an audience that cannot act on them, and converted into permanent
|
|
343
|
+
-- silence-declines. Fixed 2026-08-27 by excluding non-adoptable types from
|
|
344
|
+
-- the session-start render set (feed.ts). Any surfaced event on such a row
|
|
345
|
+
-- after that is the filter regressing.
|
|
346
|
+
select 'surface_budget_spent_on_unadoptable'::text,
|
|
347
|
+
s.user_id,
|
|
348
|
+
count(distinct ev.suggestion_id) ||
|
|
349
|
+
' non-adoptable proposal(s) (feature/hook/mcp/…) surfaced into sessions in the last 7d — they burn budget for an audience that cannot act'
|
|
350
|
+
from instruction_suggestion_events ev
|
|
351
|
+
join instruction_suggestions s on s.id = ev.suggestion_id
|
|
352
|
+
join instructions i on i.id = s.instruction_id
|
|
353
|
+
where ev.event_type in ('surfaced', 'surfaced_with_content')
|
|
354
|
+
and ev.created_at > now() - interval '7 days'
|
|
355
|
+
and s.kind = 'add'
|
|
356
|
+
and i.type not in ('instruction', 'skill')
|
|
357
|
+
group by s.user_id
|
|
358
|
+
|
|
359
|
+
union all
|
|
360
|
+
|
|
361
|
+
-- ── CHECK 6: session_capture_misattributed ─────────────────────────────────
|
|
362
|
+
-- Incident: 2026-08-27 — Thrakopontia's only session carried
|
|
363
|
+
-- project_path='/Users/nickyeager' (the pre-08-26 normalizer rewrote it) and
|
|
364
|
+
-- froze at 188 events while its transcript grew for three days, so the
|
|
365
|
+
-- project produced zero suggestions and the only symptom was an empty VS
|
|
366
|
+
-- Code panel. A RECENT session in this shape means either the normalizer
|
|
367
|
+
-- guard regressed (new junk being minted) or the reconciler is not running
|
|
368
|
+
-- for this user (worker off) — both actionable. Rows older than 7 days sit
|
|
369
|
+
-- in the misattributed_session_paths metric instead: their transcripts are
|
|
370
|
+
-- likely pruned, and a check that can never clear is a permanent red light
|
|
371
|
+
-- nobody reads.
|
|
372
|
+
select 'session_capture_misattributed'::text,
|
|
373
|
+
x.user_id,
|
|
374
|
+
count(*) || ' session(s) active in the last 7d attributed to a project while carrying a bare home-directory project_path — normalizer regression, or the reconciler is not repairing them (worker offline / transcript gone)'
|
|
375
|
+
from sessions x
|
|
376
|
+
where x.project_id is not null
|
|
377
|
+
and x.project_path ~ '^/(Users|home)/[^/]+/?$'
|
|
378
|
+
and x.started_at > now() - interval '7 days'
|
|
379
|
+
group by x.user_id;
|
|
380
|
+
end;
|
|
381
|
+
$$;
|
|
382
|
+
|
|
383
|
+
comment on function public.suggestion_pipeline_conformance() is
|
|
384
|
+
'Daily conformance sweep over the suggestion/promotion pipeline. Each check encodes the observable signature of a dated incident (gate 4 silent producer, #372 producer/endpoint drift, gate 7 one-way staleness, census causes 2 and 3, the 2026-08-27 misattributed-session-path capture defect). Detection only — never mutates pipeline state. Findings land as one consolidated notification per user (pattern: 00147).';
|
|
385
|
+
|
|
386
|
+
-- ── 3. Delete the laundering vector ────────────────────────────────────────
|
|
387
|
+
-- A bare-home project_checkouts row makes resolveProjectIdForPath attribute
|
|
388
|
+
-- every genuine home-dir session to whatever project the residue points at.
|
|
389
|
+
-- Zero rows exist on the cloud DB at authoring time (the API-side guard has
|
|
390
|
+
-- held since it shipped) — this is the backstop against rows minted by older
|
|
391
|
+
-- API builds (local-mode installs) that predate the guard.
|
|
392
|
+
delete from public.project_checkouts
|
|
393
|
+
where path ~ '^/(Users|home)/[^/]+/?$';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rulemetric/local",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.5",
|
|
4
4
|
"description": "RuleMetric local-mode runtime: bundled API server, web dashboard, and database migrations for fully local (zero cloud contact) deployments. Installed on demand by `rulemetric local up` — not meant to be used directly.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"postgres": "^3.4.0",
|
|
29
29
|
"resend": "^4.0.0",
|
|
30
30
|
"zod": "^3.24.0",
|
|
31
|
-
"@rulemetric/skills-registry": "0.12.
|
|
31
|
+
"@rulemetric/skills-registry": "0.12.5"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"esbuild": "^0.25.0",
|