@ultimat3/testing 29.3.2 → 30.0.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 (93) hide show
  1. package/CLAUDE.md +56 -63
  2. package/README.md +281 -6
  3. package/package.json +27 -16
  4. package/src/ax-node.ts +96 -0
  5. package/src/cassette-errors.ts +91 -0
  6. package/src/cassette-format.ts +175 -0
  7. package/src/cassette-record.ts +120 -0
  8. package/src/cassette-secret-scan.ts +77 -0
  9. package/src/cassette.ts +166 -0
  10. package/src/cdp-browser.ts +5 -3
  11. package/src/cdp-e2e-page.ts +91 -3
  12. package/src/cdp-e2e-session.ts +18 -1
  13. package/src/define-exercise.ts +82 -0
  14. package/src/drill-budgets.ts +54 -0
  15. package/src/drill-errors.ts +45 -0
  16. package/src/drill-fleet.ts +251 -0
  17. package/src/drill-jobs.ts +79 -0
  18. package/src/drill-load.ts +166 -0
  19. package/src/drill-probe.ts +68 -0
  20. package/src/drill-run.ts +144 -0
  21. package/src/drill-services.ts +70 -0
  22. package/src/drill-sync.ts +108 -0
  23. package/src/drill-verdict.ts +256 -0
  24. package/src/drill-writes.ts +123 -0
  25. package/src/drills.ts +109 -0
  26. package/src/e2e-a11y-run.ts +189 -0
  27. package/src/e2e-a11y-tabs.ts +113 -0
  28. package/src/e2e-a11y.ts +153 -0
  29. package/src/e2e-driver.ts +46 -10
  30. package/src/e2e-page.ts +28 -3
  31. package/src/e2e-run.ts +1 -0
  32. package/src/e2e-spawn.ts +3 -2
  33. package/src/errors.ts +43 -0
  34. package/src/exercise-actor.ts +124 -0
  35. package/src/exercise-crash.ts +46 -0
  36. package/src/exercise-input.ts +182 -0
  37. package/src/exercise-override-input.ts +34 -0
  38. package/src/exercise-report.ts +282 -0
  39. package/src/exercise-request.ts +38 -0
  40. package/src/exercise-sweeps.ts +100 -0
  41. package/src/exercise.ts +448 -0
  42. package/src/factories.ts +10 -7
  43. package/src/factory-registry.ts +49 -15
  44. package/src/fake-identity-provider.ts +191 -0
  45. package/src/fault-proxy.ts +342 -0
  46. package/src/file-boundary.ts +4 -1
  47. package/src/fixture-jobs.ts +3 -1
  48. package/src/fixture-statements.ts +4 -1
  49. package/src/free-port.ts +59 -0
  50. package/src/index.ts +148 -3
  51. package/src/island-dom.ts +5 -0
  52. package/src/isolated-plugins.ts +1 -1
  53. package/src/job-crash-proof.ts +249 -0
  54. package/src/job-sim-driver.ts +164 -0
  55. package/src/job-sim-effects.ts +142 -0
  56. package/src/job-sim-errors.ts +72 -0
  57. package/src/job-sim-schedule.ts +154 -0
  58. package/src/job-sim-world.ts +198 -0
  59. package/src/job-simulate.ts +179 -0
  60. package/src/mock-route.ts +81 -0
  61. package/src/module-mock-boundary.ts +85 -0
  62. package/src/module-mock-error.ts +28 -0
  63. package/src/plan-oracle.ts +164 -0
  64. package/src/plan-sweep-fix.ts +123 -0
  65. package/src/plan-sweep-report.ts +84 -0
  66. package/src/plan-sweep.ts +148 -0
  67. package/src/preload.ts +24 -4
  68. package/src/registry-snapshot.ts +1 -1
  69. package/src/repro-stand-in.ts +67 -0
  70. package/src/repro.ts +158 -0
  71. package/src/request-log.ts +64 -0
  72. package/src/reusable-database.ts +1 -1
  73. package/src/scale-sweep-graph.ts +82 -0
  74. package/src/scale-sweep-run.ts +134 -0
  75. package/src/scale-sweep.ts +74 -0
  76. package/src/sealed-network.ts +63 -46
  77. package/src/seed-graph-error.ts +50 -0
  78. package/src/seed-graph-order.ts +98 -0
  79. package/src/seed-graph-pass.ts +68 -0
  80. package/src/seed-graph-plan.ts +81 -0
  81. package/src/seed-graph-row.ts +134 -0
  82. package/src/seed-graph-shape.ts +104 -0
  83. package/src/seed-graph-values.ts +135 -0
  84. package/src/seed-graph.ts +250 -0
  85. package/src/seed-writer.ts +180 -0
  86. package/src/subject-proof-error.ts +23 -0
  87. package/src/subject-proof.ts +180 -0
  88. package/src/template-db-roles.ts +51 -0
  89. package/src/template-db.ts +8 -2
  90. package/src/tenant-canary.ts +120 -0
  91. package/src/tenant-proof-report.ts +62 -0
  92. package/src/tenant-proof.ts +177 -0
  93. package/src/test-types.ts +12 -3
@@ -0,0 +1,282 @@
1
+ // What one primitive did in the exercise pass, and the room it did it in: the network sealed with
2
+ // every allow and mock cleared, the mail fixture, the clock frozen, statements and committed rows
3
+ // observed. Every seam is process-global, so the room hands back exactly what it found.
4
+
5
+ import { isSelfOrigin, peerAddressClass } from '@ultimat3/core';
6
+ import type { StatementObserver } from '@ultimat3/db';
7
+ import {
8
+ expectedSeqScanReason,
9
+ setStatementObserver,
10
+ statementFingerprint,
11
+ statementObserver,
12
+ } from '@ultimat3/db';
13
+ import type { RowObserver } from '@ultimat3/entity';
14
+ import { captureDeterminism, installDeterminism, restoreCapturedDeterminism } from './determinism';
15
+ import { testMail } from './fixture-mail';
16
+ import type { PlanProof, SentStatement } from './plan-sweep';
17
+ import type { ScaleProof, StatementTally } from './scale-sweep';
18
+ import {
19
+ captureNetwork,
20
+ isNetworkSealed,
21
+ requestLog,
22
+ resetNetwork,
23
+ restoreCapturedNetwork,
24
+ sealNetwork,
25
+ unsealNetwork,
26
+ } from './sealed-network';
27
+
28
+ export type ExerciseEffect =
29
+ | { readonly kind: 'row'; readonly entity: string; readonly op: string; readonly rows: number }
30
+ | { readonly kind: 'mail'; readonly mail: string; readonly count: number }
31
+ /** `refused`: sealed, never sent. A call to this machine is the only one that is not. */
32
+ | {
33
+ readonly kind: 'network';
34
+ readonly method: string;
35
+ readonly host: string;
36
+ readonly refused: boolean;
37
+ }
38
+ | { readonly kind: 'enqueue'; readonly job: string; readonly count: number };
39
+
40
+ export type ExerciseOutcome = 'reached' | 'refused' | 'unreached' | 'off';
41
+
42
+ /** B's call before A's positive control, and after it. */
43
+ export type TenantLeg = 'before' | 'after';
44
+
45
+ export interface TenantFinding {
46
+ readonly code: 'X_TENANT_LEAK' | 'X_TENANT_WRITE_CROSSED';
47
+ readonly leg: TenantLeg;
48
+ /** LEAK: where A's canary or id was found, e.g. 'output.items[0].url' or 'effect.mail[0].to' */
49
+ readonly path?: string;
50
+ /** WRITE_CROSSED: the table and the primary key of A's changed row */
51
+ readonly table?: string;
52
+ readonly id?: string;
53
+ }
54
+
55
+ /** The tenant proof's verdict on one primitive: B attacked A's rows twice, around A's own call. */
56
+ export type TenantProof =
57
+ | { readonly verdict: 'proven' }
58
+ /** The policy admits anonymous: read half skipped, write half kept and clean. */
59
+ | { readonly verdict: 'public'; readonly reason: string }
60
+ /** A's positive control was not reached, so B's clean answer proves nothing. */
61
+ | { readonly verdict: 'unproven'; readonly reason: string }
62
+ | { readonly verdict: 'crossed'; readonly findings: readonly TenantFinding[] };
63
+
64
+ /** One effect a run killed at `boundary` made a different number of times than a clean run. */
65
+ export interface CrashFinding {
66
+ /** `charge`, `charge (started)`, `unwinding`, `compensate:charge`, `ack`. */
67
+ readonly boundary: string;
68
+ /** `forced-fail`: a `compensates: true` job made to fail for good, then unwound. */
69
+ readonly mode: 'clean' | 'forced-fail';
70
+ /** `row:posts:insert`, `mail:receipt→a@x.example`, `network:POST api.example/v1`, `enqueue:x`. */
71
+ readonly effect: string;
72
+ readonly clean: number;
73
+ readonly killed: number;
74
+ /** The last step the clean run had written before the effect; absent, before the first. */
75
+ readonly after?: string;
76
+ }
77
+
78
+ /** The crash proof's verdict on a reached job: killed at every step boundary (`crashProof`). */
79
+ export type CrashProof =
80
+ | { readonly verdict: 'held'; readonly boundaries: number; readonly reruns: number }
81
+ | {
82
+ readonly verdict: 'diverged';
83
+ readonly boundaries: number;
84
+ /** Lost answers that reran a step: at-least-once, counted, never red. */
85
+ readonly reruns: number;
86
+ readonly findings: readonly CrashFinding[];
87
+ }
88
+ /** The proof itself could not finish: reported, never red. */
89
+ | { readonly verdict: 'unproven'; readonly reason: string };
90
+
91
+ export interface ExerciseRecord {
92
+ readonly kind: 'action' | 'query' | 'job';
93
+ readonly name: string;
94
+ readonly outcome: ExerciseOutcome;
95
+ readonly code?: string;
96
+ readonly reason?: string;
97
+ readonly statements: number;
98
+ readonly effects: readonly ExerciseEffect[];
99
+ readonly ms: number;
100
+ /** For an `unreached` one: the line to paste into `defineExercise({ … })`. */
101
+ readonly override?: string;
102
+ /** On every record that ran — absent only on `off` and `refused`. The fields above are A's. */
103
+ readonly tenant?: TenantProof;
104
+ /** On a reached job only: its control's input, killed at every step boundary. */
105
+ readonly crash?: CrashProof;
106
+ /** Every statement it sent, planned with no index but the declared ones (`plan-sweep.ts`). */
107
+ readonly plans?: PlanProof;
108
+ /** On a reached primitive: its statement shapes at 2 and at 8 rows a table (`scale-sweep.ts`). */
109
+ readonly scale?: ScaleProof;
110
+ }
111
+
112
+ /** What an effect carried, for the tenant proof's read half: searched, never reported as is. */
113
+ export interface EffectPayload {
114
+ /** `effect.mail[0]`, `effect.network[1]`, `effect.enqueue[0]`. */
115
+ readonly path: string;
116
+ readonly value: unknown;
117
+ }
118
+
119
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
120
+
121
+ /** `publishPost: { actor, input: (drawn) => ({ ...drawn, postId: '<…>' }) },` — valid TS to paste. */
122
+ export function overrideLine(name: string, idFields: readonly string[]): string {
123
+ const key = IDENTIFIER.test(name) ? name : JSON.stringify(name);
124
+ const bound = idFields
125
+ .map((field) => `, ${IDENTIFIER.test(field) ? field : JSON.stringify(field)}: '<a real id>'`)
126
+ .join('');
127
+ return `${key}: { actor: <an actor its policy permits>, input: (drawn) => ({ ...drawn${bound} }) },`;
128
+ }
129
+
130
+ const hostOf = (url: string): string => (URL.canParse(url) ? new URL(url).host : url);
131
+
132
+ /** `sealed-network.ts`'s own exemption: this machine and a server this process booted. */
133
+ const local = (url: string): boolean => {
134
+ if (isSelfOrigin(url)) return true;
135
+ if (!URL.canParse(url)) return false;
136
+ const name = new URL(url).hostname.replace(/^\[|\]$/g, '');
137
+ if (name.toLowerCase() === 'localhost') return true;
138
+ const kind = peerAddressClass(name);
139
+ return kind === 'loopback' || kind === 'unspecified';
140
+ };
141
+
142
+ const tally = (keys: readonly string[]): ReadonlyMap<string, number> => {
143
+ const counts = new Map<string, number>();
144
+ for (const key of keys) counts.set(key, (counts.get(key) ?? 0) + 1);
145
+ return counts;
146
+ };
147
+
148
+ /** What happened between `begin()` and `end()`. */
149
+ export interface Observed {
150
+ readonly statements: number;
151
+ readonly effects: readonly ExerciseEffect[];
152
+ /** Calls out the seal refused: each makes the primitive `unreached`, whatever it answered. */
153
+ readonly refusedHosts: readonly string[];
154
+ /** Each mail's addresses and text, each call out's url and body — a sealed one included. */
155
+ readonly payloads: readonly EffectPayload[];
156
+ /** Each statement shape and how often, the `expectedQueryLoop` ones told apart. */
157
+ readonly shapes: readonly StatementTally[];
158
+ }
159
+
160
+ export interface ExerciseRoom extends AsyncDisposable {
161
+ /** `subject`: the primitive the statements until `end()` are sent for — what the plan sweep reads. */
162
+ begin(subject?: string): void;
163
+ end(): Observed;
164
+ /** Every distinct statement text sent between a `begin(subject)` and its `end()`, so far. */
165
+ sent(): ReadonlyMap<string, SentStatement>;
166
+ }
167
+
168
+ export async function openExerciseRoom(): Promise<ExerciseRoom> {
169
+ const { setRowObserver } = await import('@ultimat3/entity');
170
+ const clock = captureDeterminism();
171
+ if (!clock.installed) installDeterminism();
172
+ const network = captureNetwork();
173
+ const sealedBefore = isNetworkSealed();
174
+ // Every allow and mock cleared: a test's `mockFetch` is not the app's, and a call out here is
175
+ // a refusal by definition.
176
+ resetNetwork();
177
+ sealNetwork();
178
+ const mail = await testMail();
179
+ let statements = 0;
180
+ let logFrom = 0;
181
+ let subject: string | undefined;
182
+ const rows: string[] = [];
183
+ const shapes = new Map<string, { count: number; unexpected: number; expected?: string }>();
184
+ const sent = new Map<string, { primitives: Set<string>; scan?: string }>();
185
+ const previousStatements = statementObserver();
186
+ const counting: StatementObserver = {
187
+ onStatement: (event) => {
188
+ statements += 1;
189
+ const fingerprint = statementFingerprint(event);
190
+ const shape = shapes.get(fingerprint) ?? { count: 0, unexpected: 0 };
191
+ shape.count += 1;
192
+ if (event.expected === undefined) shape.unexpected += 1;
193
+ else shape.expected = event.expected;
194
+ shapes.set(fingerprint, shape);
195
+ if (subject === undefined) return;
196
+ // Whitespace collapsed, so one builder's two indentations are one statement to plan.
197
+ const text = event.text.replace(/\s+/g, ' ').trim();
198
+ const one = sent.get(text) ?? { primitives: new Set<string>() };
199
+ one.primitives.add(subject);
200
+ // Read here, in the sender's own async context: the scope is the statement's, not the room's.
201
+ const scan = expectedSeqScanReason();
202
+ if (scan !== undefined) one.scan = scan;
203
+ sent.set(text, one);
204
+ },
205
+ };
206
+ setStatementObserver(counting);
207
+ const rowObserver: RowObserver = {
208
+ onChange: (change) => rows.push(`${change.entity}\u0000${change.op}`),
209
+ onBulk: (change) => {
210
+ for (let i = 0; i < change.rows; i += 1) rows.push(`${change.entity}\u0000${change.op}`);
211
+ },
212
+ };
213
+ const previousRows = setRowObserver(rowObserver);
214
+
215
+ return {
216
+ begin: (name) => {
217
+ subject = name;
218
+ statements = 0;
219
+ shapes.clear();
220
+ rows.length = 0;
221
+ mail.clear();
222
+ logFrom = requestLog().length;
223
+ },
224
+ end: (): Observed => {
225
+ const calls = requestLog().slice(logFrom);
226
+ const effects: ExerciseEffect[] = [
227
+ ...[...tally(rows)].map(([key, count]): ExerciseEffect => {
228
+ const [entity = '', op = ''] = key.split('\u0000');
229
+ return { kind: 'row', entity, op, rows: count };
230
+ }),
231
+ ...[...tally(mail.outbox().map((sent) => sent.message.mailId))].map(
232
+ ([id, count]): ExerciseEffect => ({ kind: 'mail', mail: id, count }),
233
+ ),
234
+ ...calls.map(
235
+ (call): ExerciseEffect => ({
236
+ kind: 'network',
237
+ method: call.method,
238
+ host: hostOf(call.url),
239
+ refused: !local(call.url),
240
+ }),
241
+ ),
242
+ ];
243
+ const refusedHosts = calls.filter((call) => !local(call.url)).map((c) => hostOf(c.url));
244
+ const payloads: EffectPayload[] = [
245
+ ...mail.outbox().map(({ message }, i) => ({
246
+ path: `effect.mail[${i}]`,
247
+ value: {
248
+ to: message.to,
249
+ cc: message.cc,
250
+ bcc: message.bcc,
251
+ replyTo: message.replyTo,
252
+ subject: message.subject,
253
+ text: message.text,
254
+ html: message.html,
255
+ unsubscribeUrl: message.unsubscribeUrl,
256
+ },
257
+ })),
258
+ // Headers left out: a bearer token is not A's row, and a header is no row's text.
259
+ ...calls.map((call, i) => ({
260
+ path: `effect.network[${i}]`,
261
+ value: { url: call.url, body: call.body },
262
+ })),
263
+ ];
264
+ subject = undefined;
265
+ const tallies = [...shapes].map(([fingerprint, shape]) => ({ fingerprint, ...shape }));
266
+ return { statements, effects, refusedHosts, payloads, shapes: tallies };
267
+ },
268
+ sent: () => sent,
269
+ [Symbol.asyncDispose]: async (): Promise<void> => {
270
+ // Each restore runs whatever the one before it did: a throw must not strand a seam.
271
+ try {
272
+ mail[Symbol.dispose]();
273
+ } finally {
274
+ setRowObserver(previousRows);
275
+ setStatementObserver(previousStatements);
276
+ restoreCapturedNetwork(network);
277
+ if (!sealedBefore) unsealNetwork();
278
+ restoreCapturedDeterminism(clock);
279
+ }
280
+ },
281
+ };
282
+ }
@@ -0,0 +1,38 @@
1
+ // The context an action or a query runs in during the exercise pass: an HTTP request's, built the
2
+ // way the pipeline builds one, because `surface: 'http'` is the call the pass makes. A bare `Ctx`
3
+ // has no inbound headers, so a session action's `useRequestContext()` answered X_NO_REQUEST, a 500.
4
+
5
+ import type { Actor } from '@ultimat3/core';
6
+ import { anonymousActor, runWithContext, useContext, withChildContext } from '@ultimat3/core';
7
+
8
+ /** Where the pass's requests say they came from: no host the app serves, and never sent. */
9
+ const EXERCISE_ORIGIN = 'https://exercise.test';
10
+
11
+ /**
12
+ * `fn` inside a fresh request as `actor`: no cookie, no header, a POST to the primitive's own name,
13
+ * nothing spending a rate bucket. The actor is ON the request, so the caller passes `invoke` no
14
+ * `actor` — its impersonation path rebuilds a plain child context and drops the request's fields.
15
+ * The child is derived first for the services an actor binds, then the request's fields go over
16
+ * it, as `withAnonymousRequest` (`@ultimat3/http`) does.
17
+ */
18
+ export async function withExerciseRequest<T>(
19
+ name: string,
20
+ actor: Actor | null,
21
+ fn: () => Promise<T>,
22
+ ): Promise<T> {
23
+ const http = await import('@ultimat3/http');
24
+ const request = http.requestContext({
25
+ url: new URL(`/${encodeURIComponent(name)}`, EXERCISE_ORIGIN),
26
+ method: 'POST',
27
+ role: 'web',
28
+ config: http.defineHttpConfig({ rateLimit: { scope: 'process' } }),
29
+ requestHeaders: new Headers(),
30
+ });
31
+ return runWithContext(http.asCtx(request), () =>
32
+ withChildContext({ actor: actor ?? anonymousActor() }, () => {
33
+ const child = useContext();
34
+ const scope = { ...child, ...request, actor: child.actor };
35
+ return runWithContext(http.asCtx(scope), fn);
36
+ }),
37
+ );
38
+ }
@@ -0,0 +1,100 @@
1
+ // What runs after every primitive's own call: the scale sweep (each reached one again, at 2 and at 8
2
+ // rows a table) and the plan sweep (every statement the pass sent, planned on the migrated
3
+ // database). Both verdicts land on the record of the primitive that sent the statements.
4
+
5
+ import type { Actor } from '@ultimat3/core';
6
+ import type { Policy } from '@ultimat3/policy';
7
+ import type { StandardSchemaV1 } from '@ultimat3/schema';
8
+ import type { ExerciseOverrides } from './define-exercise';
9
+ import type { ActorRows, RoleGrants } from './exercise-actor';
10
+ import { asSeededRow, chooseActor } from './exercise-actor';
11
+ import type { Sampler } from './exercise-input';
12
+ import { bindIds, drawInput, seededIds } from './exercise-input';
13
+ import { overrideInput } from './exercise-override-input';
14
+ import type { ExerciseRecord } from './exercise-report';
15
+ import { sweepPlans } from './plan-sweep';
16
+ import type { ScaleSweepOptions } from './scale-sweep-run';
17
+ import { scaleSweep } from './scale-sweep-run';
18
+ import type { SeedGraph } from './seed-graph-plan';
19
+ import type { SeedEntity } from './seed-graph-shape';
20
+
21
+ /** The pass's options, as far as choosing who calls and with what goes. */
22
+ interface CallOptions {
23
+ readonly entities: readonly SeedEntity[];
24
+ readonly overrides?: ExerciseOverrides;
25
+ readonly roles: RoleGrants;
26
+ }
27
+
28
+ /** The seeded rows of the entity `defineExercise(…, { actor })` named, or none. */
29
+ export const actorRowsOf = (options: CallOptions, graph: SeedGraph): ActorRows | undefined => {
30
+ const name = options.overrides?.actorEntity;
31
+ const entity = options.entities.find((one) => one.$name === name);
32
+ if (entity === undefined) return undefined;
33
+ return { entity, rows: graph.inserts.filter((row) => row.entity === entity.$name) };
34
+ };
35
+
36
+ /** The actor of `chosen`, seated as a row of the app's actor entity in `tenant` when it names one. */
37
+ export const seated = (actor: Actor | null, tenant: string, rows: ActorRows | undefined) =>
38
+ rows === undefined ? actor : asSeededRow(actor, tenant, rows);
39
+
40
+ /** A primitive as a sized call needs it: its schema and its policy. */
41
+ interface Callable {
42
+ readonly name: string;
43
+ readonly input: StandardSchemaV1;
44
+ readonly policy: Policy | undefined;
45
+ readonly permissions: readonly string[];
46
+ }
47
+
48
+ /** One call of `subject` in `tenant` of a sized graph: the control's actor and first input. */
49
+ export async function sizedCall(
50
+ subject: Callable,
51
+ options: CallOptions,
52
+ sampler: Sampler,
53
+ at: { readonly graph: SeedGraph; readonly tenant: string; readonly self: boolean },
54
+ ): Promise<{ readonly actor: Actor | null; readonly input: unknown } | undefined> {
55
+ const { graph, tenant, self } = at;
56
+ const override = options.overrides?.entries.get(subject.name);
57
+ const chosen = chooseActor(subject.policy, subject.permissions, options.roles, tenant, override);
58
+ if (chosen.kind === 'refused') return undefined;
59
+ const rows = actorRowsOf(options, graph);
60
+ const actor = seated(chosen.actor, tenant, rows);
61
+ const scope = {
62
+ tenant,
63
+ actorId: actor?.id,
64
+ subject: subject.name,
65
+ actorEntity: rows?.entity.$name,
66
+ };
67
+ const lookup = seededIds(graph, options.entities, { ...scope, self });
68
+ const drawn = bindIds(drawInput(sampler, subject.input, 0, 0), lookup);
69
+ const bindInput = overrideInput(subject.name, override);
70
+ return { actor, input: bindInput === undefined ? drawn : await bindInput(drawn, actor) };
71
+ }
72
+
73
+ /**
74
+ * The scale sweep inside the room, the room closed, then the plan sweep — on the installed client
75
+ * only: with none, the pass ran on a driver that sends no SQL and there is no plan to read.
76
+ */
77
+ export async function attachSweeps(
78
+ records: readonly ExerciseRecord[],
79
+ options: ScaleSweepOptions,
80
+ ): Promise<readonly ExerciseRecord[]> {
81
+ let scale: Awaited<ReturnType<typeof scaleSweep>>;
82
+ try {
83
+ scale = await scaleSweep(options);
84
+ } finally {
85
+ await options.room[Symbol.asyncDispose]();
86
+ }
87
+ const db = await import('@ultimat3/db');
88
+ const plans = db.dbClientInstalled()
89
+ ? await sweepPlans(db.db(), options.room.sent(), options.entities)
90
+ : new Map();
91
+ return records.map((record) => {
92
+ const proof = plans.get(record.name);
93
+ const sized = scale.get(record.name);
94
+ return {
95
+ ...record,
96
+ ...(proof === undefined ? {} : { plans: proof }),
97
+ ...(sized === undefined ? {} : { scale: sized }),
98
+ };
99
+ });
100
+ }