@sdods/db 0.2.0

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 (87) hide show
  1. package/LICENSE +17 -0
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/col.d.ts +32 -0
  4. package/dist/col.js +58 -0
  5. package/dist/create-db.d.ts +14 -0
  6. package/dist/create-db.js +58 -0
  7. package/dist/driver.d.ts +10 -0
  8. package/dist/driver.js +16 -0
  9. package/dist/ids.d.ts +2 -0
  10. package/dist/ids.js +2 -0
  11. package/dist/index.d.ts +17 -0
  12. package/dist/index.js +22 -0
  13. package/dist/ingest/attachments.d.ts +40 -0
  14. package/dist/ingest/attachments.js +154 -0
  15. package/dist/ingest/cucumber-ingest.d.ts +40 -0
  16. package/dist/ingest/cucumber-ingest.js +543 -0
  17. package/dist/ingest/finalize.d.ts +28 -0
  18. package/dist/ingest/finalize.js +132 -0
  19. package/dist/ingest/index.d.ts +26 -0
  20. package/dist/ingest/index.js +193 -0
  21. package/dist/ingest/message-index.d.ts +64 -0
  22. package/dist/ingest/message-index.js +146 -0
  23. package/dist/ingest/parse-ndjson.d.ts +10 -0
  24. package/dist/ingest/parse-ndjson.js +35 -0
  25. package/dist/ingest/pw-json-ingest.d.ts +14 -0
  26. package/dist/ingest/pw-json-ingest.js +259 -0
  27. package/dist/ingest/runner-json-ingest.d.ts +14 -0
  28. package/dist/ingest/runner-json-ingest.js +259 -0
  29. package/dist/ingest/types.d.ts +62 -0
  30. package/dist/ingest/types.js +89 -0
  31. package/dist/insights.d.ts +62 -0
  32. package/dist/insights.js +184 -0
  33. package/dist/migrate.d.ts +30 -0
  34. package/dist/migrate.js +114 -0
  35. package/dist/migrations/0001_platform_init.d.ts +5 -0
  36. package/dist/migrations/0001_platform_init.js +48 -0
  37. package/dist/migrations/0002_results.d.ts +5 -0
  38. package/dist/migrations/0002_results.js +124 -0
  39. package/dist/migrations/0003_improvement.d.ts +5 -0
  40. package/dist/migrations/0003_improvement.js +56 -0
  41. package/dist/migrations/0004_test_data.d.ts +5 -0
  42. package/dist/migrations/0004_test_data.js +48 -0
  43. package/dist/migrations/0005_integrations_agents_audit.d.ts +5 -0
  44. package/dist/migrations/0005_integrations_agents_audit.js +100 -0
  45. package/dist/migrations/0006_schedules.d.ts +5 -0
  46. package/dist/migrations/0006_schedules.js +34 -0
  47. package/dist/migrations/0007_hierarchy.d.ts +5 -0
  48. package/dist/migrations/0007_hierarchy.js +76 -0
  49. package/dist/migrations/0008_runner_naming.d.ts +14 -0
  50. package/dist/migrations/0008_runner_naming.js +35 -0
  51. package/dist/migrations/helpers.d.ts +15 -0
  52. package/dist/migrations/helpers.js +30 -0
  53. package/dist/migrations/index.d.ts +6 -0
  54. package/dist/migrations/index.js +22 -0
  55. package/dist/repos/auth.d.ts +109 -0
  56. package/dist/repos/auth.js +258 -0
  57. package/dist/repos/hierarchy.d.ts +151 -0
  58. package/dist/repos/hierarchy.js +465 -0
  59. package/dist/repos/improvement.d.ts +84 -0
  60. package/dist/repos/improvement.js +242 -0
  61. package/dist/repos/index.d.ts +8 -0
  62. package/dist/repos/index.js +8 -0
  63. package/dist/repos/platform.d.ts +250 -0
  64. package/dist/repos/platform.js +429 -0
  65. package/dist/repos/projects.d.ts +87 -0
  66. package/dist/repos/projects.js +118 -0
  67. package/dist/repos/runs.d.ts +306 -0
  68. package/dist/repos/runs.js +321 -0
  69. package/dist/repos/test-data.d.ts +84 -0
  70. package/dist/repos/test-data.js +291 -0
  71. package/dist/schema.d.ts +514 -0
  72. package/dist/schema.js +37 -0
  73. package/dist/scopes.d.ts +2 -0
  74. package/dist/scopes.js +2 -0
  75. package/dist/switch/export.d.ts +15 -0
  76. package/dist/switch/export.js +67 -0
  77. package/dist/switch/import.d.ts +16 -0
  78. package/dist/switch/import.js +106 -0
  79. package/dist/switch/index.d.ts +5 -0
  80. package/dist/switch/index.js +5 -0
  81. package/dist/switch/prune.d.ts +19 -0
  82. package/dist/switch/prune.js +50 -0
  83. package/dist/switch/switch.d.ts +34 -0
  84. package/dist/switch/switch.js +133 -0
  85. package/dist/test-data.d.ts +22 -0
  86. package/dist/test-data.js +79 -0
  87. package/package.json +50 -0
@@ -0,0 +1,465 @@
1
+ import { effectiveWorkspaceRole, } from '@sdods/contracts/scopes';
2
+ import { enc, nowIso, readBool, readJson, readTs } from '../col.js';
3
+ import { newId } from '../ids.js';
4
+ import { upsertProject } from './projects.js';
5
+ // ── organizations / workspaces ────────────────────────────────────────────
6
+ export async function upsertOrganization(db, input) {
7
+ const existing = await db
8
+ .selectFrom('organizations')
9
+ .select(['id'])
10
+ .where('slug', '=', input.slug)
11
+ .executeTakeFirst();
12
+ const values = {
13
+ name: input.name,
14
+ description: input.description ?? null,
15
+ url: input.url ?? null,
16
+ updated_at: nowIso(),
17
+ };
18
+ if (existing) {
19
+ await db.updateTable('organizations').set(values).where('id', '=', existing.id).execute();
20
+ return existing.id;
21
+ }
22
+ const id = newId();
23
+ await db
24
+ .insertInto('organizations')
25
+ .values({ id, slug: input.slug, created_at: nowIso(), ...values })
26
+ .execute();
27
+ return id;
28
+ }
29
+ export async function upsertWorkspace(db, input) {
30
+ const existing = await db
31
+ .selectFrom('workspaces')
32
+ .select(['id'])
33
+ .where('organization_id', '=', input.organizationId)
34
+ .where('slug', '=', input.slug)
35
+ .executeTakeFirst();
36
+ const values = { name: input.name, description: input.description ?? null, updated_at: nowIso() };
37
+ if (existing) {
38
+ await db.updateTable('workspaces').set(values).where('id', '=', existing.id).execute();
39
+ return existing.id;
40
+ }
41
+ const id = newId();
42
+ await db
43
+ .insertInto('workspaces')
44
+ .values({
45
+ id,
46
+ organization_id: input.organizationId,
47
+ slug: input.slug,
48
+ created_at: nowIso(),
49
+ ...values,
50
+ })
51
+ .execute();
52
+ return id;
53
+ }
54
+ export async function listOrganizations(db) {
55
+ const rows = await db.selectFrom('organizations').selectAll().orderBy('slug').execute();
56
+ return rows.map((o) => ({
57
+ id: o.id,
58
+ slug: o.slug,
59
+ name: o.name,
60
+ description: o.description,
61
+ url: o.url,
62
+ createdAt: readTs(o.created_at),
63
+ }));
64
+ }
65
+ export async function listWorkspaces(db, organizationId) {
66
+ let q = db
67
+ .selectFrom('workspaces')
68
+ .innerJoin('organizations', 'organizations.id', 'workspaces.organization_id')
69
+ .selectAll('workspaces')
70
+ .select('organizations.slug as org_slug')
71
+ .orderBy('workspaces.slug');
72
+ if (organizationId)
73
+ q = q.where('workspaces.organization_id', '=', organizationId);
74
+ const rows = await q.execute();
75
+ return rows.map(mapWorkspace);
76
+ }
77
+ export async function getWorkspaceBySlug(db, orgSlug, slug) {
78
+ const row = await db
79
+ .selectFrom('workspaces')
80
+ .innerJoin('organizations', 'organizations.id', 'workspaces.organization_id')
81
+ .selectAll('workspaces')
82
+ .select('organizations.slug as org_slug')
83
+ .where('organizations.slug', '=', orgSlug)
84
+ .where('workspaces.slug', '=', slug)
85
+ .executeTakeFirst();
86
+ return row ? mapWorkspace(row) : null;
87
+ }
88
+ function mapWorkspace(w) {
89
+ return {
90
+ id: w.id,
91
+ organizationId: w.organization_id,
92
+ organizationSlug: w.org_slug,
93
+ slug: w.slug,
94
+ name: w.name,
95
+ description: w.description,
96
+ createdAt: readTs(w.created_at),
97
+ };
98
+ }
99
+ // ── memberships ───────────────────────────────────────────────────────────
100
+ export async function addOrgMember(db, organizationId, userId, role) {
101
+ const existing = await db
102
+ .selectFrom('org_members')
103
+ .select(['id'])
104
+ .where('organization_id', '=', organizationId)
105
+ .where('user_id', '=', userId)
106
+ .executeTakeFirst();
107
+ if (existing) {
108
+ await db.updateTable('org_members').set({ role }).where('id', '=', existing.id).execute();
109
+ return;
110
+ }
111
+ await db
112
+ .insertInto('org_members')
113
+ .values({
114
+ id: newId(),
115
+ organization_id: organizationId,
116
+ user_id: userId,
117
+ role,
118
+ created_at: nowIso(),
119
+ })
120
+ .execute();
121
+ }
122
+ export async function removeOrgMember(db, organizationId, userId) {
123
+ await db
124
+ .deleteFrom('org_members')
125
+ .where('organization_id', '=', organizationId)
126
+ .where('user_id', '=', userId)
127
+ .execute();
128
+ }
129
+ export async function addWorkspaceMember(db, workspaceId, userId, role) {
130
+ const existing = await db
131
+ .selectFrom('workspace_members')
132
+ .select(['id'])
133
+ .where('workspace_id', '=', workspaceId)
134
+ .where('user_id', '=', userId)
135
+ .executeTakeFirst();
136
+ if (existing) {
137
+ await db.updateTable('workspace_members').set({ role }).where('id', '=', existing.id).execute();
138
+ return;
139
+ }
140
+ await db
141
+ .insertInto('workspace_members')
142
+ .values({ id: newId(), workspace_id: workspaceId, user_id: userId, role, created_at: nowIso() })
143
+ .execute();
144
+ }
145
+ export async function removeWorkspaceMember(db, workspaceId, userId) {
146
+ await db
147
+ .deleteFrom('workspace_members')
148
+ .where('workspace_id', '=', workspaceId)
149
+ .where('user_id', '=', userId)
150
+ .execute();
151
+ }
152
+ /** `setRole` for either level. */
153
+ export async function setRole(db, target) {
154
+ if ('organizationId' in target)
155
+ return addOrgMember(db, target.organizationId, target.userId, target.role);
156
+ return addWorkspaceMember(db, target.workspaceId, target.userId, target.role);
157
+ }
158
+ export async function listOrgMembers(db, organizationId) {
159
+ const rows = await db
160
+ .selectFrom('org_members')
161
+ .innerJoin('users', 'users.id', 'org_members.user_id')
162
+ .select([
163
+ 'org_members.id as id',
164
+ 'org_members.user_id as user_id',
165
+ 'users.username as username',
166
+ 'org_members.role as role',
167
+ 'org_members.created_at as created_at',
168
+ ])
169
+ .where('org_members.organization_id', '=', organizationId)
170
+ .orderBy('users.username')
171
+ .execute();
172
+ return rows.map((r) => ({
173
+ id: r.id,
174
+ userId: r.user_id,
175
+ username: r.username,
176
+ role: r.role,
177
+ createdAt: readTs(r.created_at),
178
+ }));
179
+ }
180
+ export async function listWorkspaceMembers(db, workspaceId) {
181
+ const rows = await db
182
+ .selectFrom('workspace_members')
183
+ .innerJoin('users', 'users.id', 'workspace_members.user_id')
184
+ .select([
185
+ 'workspace_members.id as id',
186
+ 'workspace_members.user_id as user_id',
187
+ 'users.username as username',
188
+ 'workspace_members.role as role',
189
+ 'workspace_members.created_at as created_at',
190
+ ])
191
+ .where('workspace_members.workspace_id', '=', workspaceId)
192
+ .orderBy('users.username')
193
+ .execute();
194
+ return rows.map((r) => ({
195
+ id: r.id,
196
+ userId: r.user_id,
197
+ username: r.username,
198
+ role: r.role,
199
+ createdAt: readTs(r.created_at),
200
+ }));
201
+ }
202
+ export async function listUserOrgs(db, userId) {
203
+ const rows = await db
204
+ .selectFrom('org_members')
205
+ .innerJoin('organizations', 'organizations.id', 'org_members.organization_id')
206
+ .select([
207
+ 'organizations.id as id',
208
+ 'organizations.slug as slug',
209
+ 'organizations.name as name',
210
+ 'org_members.role as role',
211
+ ])
212
+ .where('org_members.user_id', '=', userId)
213
+ .orderBy('organizations.slug')
214
+ .execute();
215
+ return rows.map((r) => ({ id: r.id, slug: r.slug, name: r.name, role: r.role }));
216
+ }
217
+ /**
218
+ * Every workspace the user can see with the effective role (max of explicit workspace role and the
219
+ * role implied by org membership; platform admins see everything as admin).
220
+ */
221
+ export async function listUserWorkspaces(db, userId, platformRole) {
222
+ const orgRoles = new Map();
223
+ for (const o of await listUserOrgs(db, userId))
224
+ orgRoles.set(o.id, o.role);
225
+ const wsRoles = new Map();
226
+ const memberships = await db
227
+ .selectFrom('workspace_members')
228
+ .select(['workspace_id', 'role'])
229
+ .where('user_id', '=', userId)
230
+ .execute();
231
+ for (const m of memberships)
232
+ wsRoles.set(m.workspace_id, m.role);
233
+ const all = await listWorkspaces(db);
234
+ const out = [];
235
+ for (const w of all) {
236
+ const role = effectiveWorkspaceRole({
237
+ platformRole,
238
+ orgRole: orgRoles.get(w.organizationId),
239
+ workspaceRole: wsRoles.get(w.id),
240
+ });
241
+ if (role)
242
+ out.push({ ...w, role });
243
+ }
244
+ return out;
245
+ }
246
+ export async function effectiveRoleForWorkspace(db, userId, workspaceId, platformRole) {
247
+ const ws = await db
248
+ .selectFrom('workspaces')
249
+ .select(['organization_id'])
250
+ .where('id', '=', workspaceId)
251
+ .executeTakeFirst();
252
+ if (!ws)
253
+ return null;
254
+ const org = await db
255
+ .selectFrom('org_members')
256
+ .select(['role'])
257
+ .where('organization_id', '=', ws.organization_id)
258
+ .where('user_id', '=', userId)
259
+ .executeTakeFirst();
260
+ const m = await db
261
+ .selectFrom('workspace_members')
262
+ .select(['role'])
263
+ .where('workspace_id', '=', workspaceId)
264
+ .where('user_id', '=', userId)
265
+ .executeTakeFirst();
266
+ return effectiveWorkspaceRole({
267
+ platformRole,
268
+ orgRole: org?.role,
269
+ workspaceRole: m?.role,
270
+ });
271
+ }
272
+ /** Grant `owner` on every organization that has no owner yet (admin bootstrap). */
273
+ export async function bootstrapOwner(db, userId) {
274
+ const orgs = await db.selectFrom('organizations').select(['id', 'slug']).execute();
275
+ const granted = [];
276
+ for (const o of orgs) {
277
+ const owner = await db
278
+ .selectFrom('org_members')
279
+ .select(['id'])
280
+ .where('organization_id', '=', o.id)
281
+ .where('role', '=', 'owner')
282
+ .executeTakeFirst();
283
+ if (owner)
284
+ continue;
285
+ await addOrgMember(db, o.id, userId, 'owner');
286
+ granted.push(o.slug);
287
+ }
288
+ return granted;
289
+ }
290
+ // ── modules / processes ───────────────────────────────────────────────────
291
+ export async function upsertModule(db, projectId, m) {
292
+ const existing = await db
293
+ .selectFrom('modules')
294
+ .select(['id'])
295
+ .where('project_id', '=', projectId)
296
+ .where('name', '=', m.name)
297
+ .executeTakeFirst();
298
+ const values = {
299
+ title: m.title ?? null,
300
+ description: m.description ?? null,
301
+ path: m.path ?? m.name,
302
+ layers_json: enc.json(m.layers ?? []),
303
+ testing_types_json: enc.json(m.testingTypes ?? []),
304
+ tags_json: enc.json(m.tags ?? []),
305
+ owner: m.owner ?? null,
306
+ jira_component: m.jiraComponent ?? null,
307
+ routes_json: enc.json(m.routes ?? []),
308
+ endpoints_json: enc.json(m.endpoints ?? []),
309
+ updated_at: nowIso(),
310
+ };
311
+ if (existing) {
312
+ await db.updateTable('modules').set(values).where('id', '=', existing.id).execute();
313
+ return existing.id;
314
+ }
315
+ const id = newId();
316
+ await db
317
+ .insertInto('modules')
318
+ .values({ id, project_id: projectId, name: m.name, created_at: nowIso(), ...values })
319
+ .execute();
320
+ return id;
321
+ }
322
+ export async function listModules(db, projectId) {
323
+ const rows = await db
324
+ .selectFrom('modules')
325
+ .selectAll()
326
+ .where('project_id', '=', projectId)
327
+ .orderBy('name')
328
+ .execute();
329
+ return rows.map((r) => ({
330
+ id: r.id,
331
+ name: r.name,
332
+ title: r.title,
333
+ description: r.description,
334
+ path: r.path,
335
+ layers: readJson(r.layers_json) ?? [],
336
+ testingTypes: readJson(r.testing_types_json) ?? [],
337
+ tags: readJson(r.tags_json) ?? [],
338
+ owner: r.owner,
339
+ jiraComponent: r.jira_component,
340
+ routes: readJson(r.routes_json) ?? [],
341
+ endpoints: readJson(r.endpoints_json) ?? [],
342
+ }));
343
+ }
344
+ export async function upsertProcess(db, driver, scope, p) {
345
+ let q = db.selectFrom('processes').select(['id']).where('name', '=', p.name);
346
+ q = scope.projectId
347
+ ? q.where('project_id', '=', scope.projectId)
348
+ : q.where('project_id', 'is', null).where('workspace_id', '=', scope.workspaceId ?? null);
349
+ const existing = await q.executeTakeFirst();
350
+ const { name: _n, title, description, trigger, ...config } = p;
351
+ const values = {
352
+ title: title ?? null,
353
+ description: description ?? null,
354
+ trigger: trigger ?? 'manual',
355
+ config_json: enc.json(config),
356
+ enabled: enc.bool(driver, true),
357
+ updated_at: nowIso(),
358
+ };
359
+ if (existing) {
360
+ await db.updateTable('processes').set(values).where('id', '=', existing.id).execute();
361
+ return existing.id;
362
+ }
363
+ const id = newId();
364
+ await db
365
+ .insertInto('processes')
366
+ .values({
367
+ id,
368
+ project_id: scope.projectId ?? null,
369
+ workspace_id: scope.workspaceId ?? null,
370
+ name: p.name,
371
+ last_run_id: null,
372
+ last_status: null,
373
+ created_at: nowIso(),
374
+ ...values,
375
+ })
376
+ .execute();
377
+ return id;
378
+ }
379
+ export async function listProcesses(db, scope = {}) {
380
+ let q = db.selectFrom('processes').selectAll().orderBy('name');
381
+ if (scope.projectId)
382
+ q = q.where('project_id', '=', scope.projectId);
383
+ if (scope.workspaceId)
384
+ q = q.where('workspace_id', '=', scope.workspaceId);
385
+ const rows = await q.execute();
386
+ return rows.map((r) => ({
387
+ id: r.id,
388
+ projectId: r.project_id,
389
+ workspaceId: r.workspace_id,
390
+ name: r.name,
391
+ title: r.title,
392
+ description: r.description,
393
+ trigger: r.trigger,
394
+ config: readJson(r.config_json) ?? {},
395
+ enabled: readBool(r.enabled),
396
+ lastRunId: r.last_run_id,
397
+ lastStatus: r.last_status,
398
+ }));
399
+ }
400
+ export async function updateProcessState(db, id, patch) {
401
+ await db
402
+ .updateTable('processes')
403
+ .set({
404
+ last_run_id: patch.lastRunId ?? null,
405
+ last_status: patch.lastStatus ?? null,
406
+ updated_at: nowIso(),
407
+ })
408
+ .where('id', '=', id)
409
+ .execute();
410
+ }
411
+ /** Upsert organization → workspaces → projects → modules/processes from the yaml files. */
412
+ export async function syncHierarchy(db, driver, input) {
413
+ const result = {
414
+ organizationId: null,
415
+ workspaces: {},
416
+ projects: {},
417
+ modules: 0,
418
+ processes: 0,
419
+ };
420
+ const wf = input.workspaceFile ?? null;
421
+ let defaultWorkspaceId = null;
422
+ if (wf) {
423
+ result.organizationId = await upsertOrganization(db, wf.organization);
424
+ for (const ws of wf.workspaces) {
425
+ const id = await upsertWorkspace(db, {
426
+ organizationId: result.organizationId,
427
+ slug: ws.slug,
428
+ name: ws.name,
429
+ description: ws.description ?? null,
430
+ });
431
+ result.workspaces[ws.slug] = id;
432
+ for (const p of wf.defaults.processes) {
433
+ await upsertProcess(db, driver, { workspaceId: id }, p);
434
+ result.processes++;
435
+ }
436
+ }
437
+ defaultWorkspaceId = result.workspaces[wf.defaultWorkspace] ?? null;
438
+ }
439
+ for (const { config, root } of input.projects) {
440
+ const workspaceId = (config.workspace && result.workspaces[config.workspace]) || defaultWorkspaceId;
441
+ const projectId = await upsertProject(db, driver, {
442
+ slug: config.slug,
443
+ name: config.name,
444
+ workspaceId,
445
+ description: config.description ?? null,
446
+ rootPath: root ?? null,
447
+ config: config,
448
+ layers: config.layers,
449
+ browsers: config.browsers,
450
+ tagsPolicy: config.tags,
451
+ screenshotPolicy: config.screenshots,
452
+ });
453
+ result.projects[config.slug] = projectId;
454
+ for (const m of config.modules ?? []) {
455
+ await upsertModule(db, projectId, m);
456
+ result.modules++;
457
+ }
458
+ for (const p of config.processes ?? []) {
459
+ await upsertProcess(db, driver, { projectId }, p);
460
+ result.processes++;
461
+ }
462
+ }
463
+ return result;
464
+ }
465
+ //# sourceMappingURL=hierarchy.js.map
@@ -0,0 +1,84 @@
1
+ import type { Kysely } from 'kysely';
2
+ import type { HealEvent } from '@sdods/contracts/types';
3
+ import type { Driver } from '../driver.js';
4
+ import type { Database } from '../schema.js';
5
+ export declare function insertHealEvent(db: Kysely<Database>, driver: Driver, input: {
6
+ projectId: string;
7
+ runId: string;
8
+ scenarioId?: string | null;
9
+ attemptId?: string | null;
10
+ stepId?: string | null;
11
+ event: HealEvent;
12
+ source?: 'runtime' | 'agent';
13
+ }): Promise<string>;
14
+ export interface LocatorDelta {
15
+ selector: string;
16
+ pageHint?: string | null;
17
+ fails?: number;
18
+ heals?: number;
19
+ uses?: number;
20
+ lastStrategy?: string | null;
21
+ suggestedSelector?: string | null;
22
+ at?: string;
23
+ }
24
+ /** Increment counters for one selector (upsert on project_id + selector). */
25
+ export declare function bumpLocatorStats(db: Kysely<Database>, projectId: string, d: LocatorDelta): Promise<void>;
26
+ export declare function listLocatorStats(db: Kysely<Database>, projectId: string, limit?: number): Promise<{
27
+ id: string;
28
+ selector: string;
29
+ pageHint: string | null;
30
+ failCount: number;
31
+ healCount: number;
32
+ useCount: number;
33
+ lastFailedAt: string | null;
34
+ lastHealedAt: string | null;
35
+ lastStrategy: string | null;
36
+ suggestedSelector: string | null;
37
+ }[]>;
38
+ export declare function listHealEvents(db: Kysely<Database>, opts: {
39
+ projectId?: string;
40
+ runId?: string;
41
+ limit?: number;
42
+ }): Promise<{
43
+ id: string;
44
+ runId: string;
45
+ scenarioId: string | null;
46
+ pageUrl: string | null;
47
+ originalSelector: string;
48
+ context: Record<string, unknown>;
49
+ strategyUsed: string | null;
50
+ healedSelector: string | null;
51
+ candidates: unknown[];
52
+ succeeded: boolean;
53
+ durationMs: number | null;
54
+ createdAt: string | null;
55
+ }[]>;
56
+ /**
57
+ * Recompute flaky stats for one (project, fingerprint, runner_project) over the last `window` runs.
58
+ */
59
+ export declare function recomputeFlakyStats(db: Kysely<Database>, driver: Driver, input: {
60
+ projectId: string;
61
+ fingerprint: string;
62
+ runnerProject: string;
63
+ window?: number;
64
+ }): Promise<void>;
65
+ export declare function listFlakyStats(db: Kysely<Database>, projectId: string, limit?: number): Promise<{
66
+ id: string;
67
+ fingerprint: string;
68
+ runnerProject: string;
69
+ featureUri: string | null;
70
+ scenarioName: string | null;
71
+ windowSize: number;
72
+ runsCount: number;
73
+ passCount: number;
74
+ failCount: number;
75
+ flakyCount: number;
76
+ flakyRate: number;
77
+ failStreak: number;
78
+ lastStatus: string | null;
79
+ lastRunId: string | null;
80
+ lastFailedAt: string | null;
81
+ quarantined: boolean;
82
+ }[]>;
83
+ export declare function setQuarantine(db: Kysely<Database>, driver: Driver, id: string, quarantined: boolean): Promise<void>;
84
+ //# sourceMappingURL=improvement.d.ts.map