@ripplo/testing 0.7.24 → 0.7.26

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 (2) hide show
  1. package/dist/index.js +1686 -1688
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -1054,1890 +1054,1888 @@ function collectValueSpaces(stateName, builders) {
1054
1054
  }));
1055
1055
  }
1056
1056
 
1057
- // src/locators.ts
1058
- function role(roleName, name, ...bindings) {
1059
- return { by: "role", name: name == null ? void 0 : nameValue(name, bindings), role: roleName };
1060
- }
1061
- function inside(scope, target) {
1062
- return { by: "inside", scope, target };
1063
- }
1064
- function testId(value2, ...bindings) {
1065
- if (typeof value2 === "string") {
1066
- return { by: "testId", value: value2 };
1067
- }
1068
- return { by: "testId", value: stringValueFromTemplate(value2, bindings) };
1069
- }
1070
- var alertdialog = named("alertdialog");
1071
- var button = named("button");
1072
- var cell = named("cell");
1073
- var checkbox = named("checkbox");
1074
- var columnheader = named("columnheader");
1075
- var combobox = named("combobox");
1076
- var dialog = named("dialog");
1077
- var heading = named("heading");
1078
- var img = named("img");
1079
- var link = named("link");
1080
- var listitem = named("listitem");
1081
- var menuitem = named("menuitem");
1082
- var option = named("option");
1083
- var radio = named("radio");
1084
- var row = named("row");
1085
- var searchbox = named("searchbox");
1086
- var slider = named("slider");
1087
- var spinbutton = named("spinbutton");
1088
- var switchControl = named("switch");
1089
- var tab = named("tab");
1090
- var textbox = named("textbox");
1091
- var treeitem = named("treeitem");
1092
- var alert = container("alert");
1093
- var banner = container("banner");
1094
- var complementary = container("complementary");
1095
- var contentinfo = container("contentinfo");
1096
- var form = container("form");
1097
- var grid = container("grid");
1098
- var group = container("group");
1099
- var list = container("list");
1100
- var main = container("main");
1101
- var menu = container("menu");
1102
- var navigation = container("navigation");
1103
- var progressbar = container("progressbar");
1104
- var radiogroup = container("radiogroup");
1105
- var region = container("region");
1106
- var status = container("status");
1107
- var table = container("table");
1108
- var tablist = container("tablist");
1109
- var tabpanel = container("tabpanel");
1110
- var toolbar = container("toolbar");
1111
- function named(roleName) {
1112
- return (name, ...bindings) => role(roleName, name, ...bindings);
1113
- }
1114
- function container(roleName) {
1115
- return (name, ...bindings) => role(roleName, name, ...bindings);
1116
- }
1117
- function nameValue(name, bindings) {
1118
- if (typeof name === "string") {
1119
- return name;
1120
- }
1121
- if (isBinding(name)) {
1122
- return name;
1123
- }
1124
- return stringValueFromTemplate(name, bindings);
1125
- }
1126
-
1127
- // src/singleton.ts
1128
- function singleton(name, config) {
1129
- const valueSpaceName = `singleton.${name}`;
1130
- const { constraints, generator, primitive } = config.value;
1131
- const schema = {
1132
- consistency: config.consistency ?? "strict",
1133
- default: config.default,
1134
- description: config.description,
1135
- name,
1136
- source: config.source,
1137
- type: primitive,
1138
- valueSpace: valueSpaceName
1139
- };
1140
- return {
1141
- is: isPredicate,
1142
- name,
1143
- schema,
1144
- source: config.source,
1145
- value: { __t: void 0, entity: name, field: "value", primitive, valueSpaceName },
1146
- valueSpaces: [{ constraints, generator, name: valueSpaceName, type: primitive }],
1147
- of: (value2) => ({
1148
- __entity: { kind: "singletonState", singleton: name, value: toSetValue(value2) },
1149
- is: isPredicate
1150
- })
1151
- };
1152
- function isPredicate(value2) {
1153
- return condLeaf({
1154
- assertion: { kind: "is", value: value2 },
1155
- kind: "singleton",
1156
- singleton: name,
1157
- wait: void 0
1158
- });
1159
- }
1160
- }
1161
-
1162
- // src/builtins.ts
1163
- function browserSingleton(name) {
1164
- return {
1165
- name,
1166
- is: (strings, ...values) => leaf({
1167
- kind: "browser",
1168
- name,
1169
- value: stringValueFromTemplate(strings, values),
1170
- wait: void 0
1171
- })
1172
- };
1173
- }
1174
- var url = browserSingleton("url");
1175
- var title = browserSingleton("title");
1176
- var viewport = browserSingleton("viewport");
1177
-
1178
- // src/actions.ts
1179
- function goto(strings, ...values) {
1180
- return stepBuilder({ kind: "goto", url: stringValueFromTemplate(strings, values) }, [], []);
1181
- }
1182
- function click(locator) {
1183
- return stepBuilder({ kind: "click", locator }, [], []);
1184
- }
1185
- function dblclick(locator) {
1186
- return stepBuilder({ kind: "dblclick", locator }, [], []);
1187
- }
1188
- function fill(locator, binding) {
1189
- return stepBuilder({ kind: "fill", locator, value: binding }, [], []);
1190
- }
1191
- function clear(locator) {
1192
- return stepBuilder({ kind: "clear", locator }, [], []);
1193
- }
1194
- function select(locator, binding) {
1195
- return stepBuilder({ kind: "select", locator, value: binding }, [], []);
1196
- }
1197
- function check(locator) {
1198
- return stepBuilder({ kind: "check", locator }, [], []);
1199
- }
1200
- function uncheck(locator) {
1201
- return stepBuilder({ kind: "uncheck", locator }, [], []);
1202
- }
1203
- function hover(locator) {
1204
- return stepBuilder({ kind: "hover", locator }, [], []);
1205
- }
1206
- function upload(locator, files) {
1207
- return stepBuilder({ files: [...files], kind: "upload", locator }, [], []);
1208
- }
1209
- function press(pressKey, locator) {
1210
- return stepBuilder({ key: pressKey, kind: "press", locator }, [], []);
1211
- }
1212
- function stepBuilder(action, expected, captures) {
1213
- return {
1214
- captures,
1215
- step: { action, expect: [...expected] },
1216
- expect: (...predicates) => stepBuilder(
1217
- action,
1218
- [...expected, ...predicates.map((p) => toPredicate(p))],
1219
- [...captures, ...predicates.flatMap((p) => isCapturePredicate(p) ? [captureOf(p)] : [])]
1220
- )
1221
- };
1222
- }
1223
-
1224
- // src/coherence.ts
1225
- function assertEntityCoherence(entities) {
1226
- const counts = entities.reduce(
1227
- (acc, d) => new Map([...acc, [d.entity, (acc.get(d.entity) ?? 0) + 1]]),
1228
- /* @__PURE__ */ new Map()
1229
- );
1230
- const over = entities.find((d) => d.kind === "only" && (counts.get(d.entity) ?? 0) > 1);
1231
- if (over != null) {
1232
- throw new Error(
1233
- `entity "${over.entity}" is declared only() but seeded ${String(counts.get(over.entity) ?? 0)} times \u2014 only() means a single exclusive instance; use of() for multiple instances`
1234
- );
1235
- }
1236
- }
1057
+ // ../spec/src/leaves.ts
1058
+ import { z } from "zod";
1059
+ var budgetSchema = z.enum(["fast", "slow", "async"]);
1060
+ var valueRefSchema = z.object({ ref: z.string().min(1) });
1061
+ var primitiveSchema = z.union([z.string(), z.number(), z.boolean()]);
1062
+ var templateSchema = z.object({
1063
+ template: z.array(z.union([z.string(), valueRefSchema])).min(1)
1064
+ });
1065
+ var setValueSchema = z.union([valueRefSchema, primitiveSchema, templateSchema, z.null()]);
1066
+ var changedSchema = z.object({ kind: z.literal("changed") });
1067
+ var updateValueSchema = z.union([setValueSchema, changedSchema]);
1068
+ var stringValueSchema = z.union([z.string(), valueRefSchema, templateSchema]);
1069
+ var roleLocatorSchema = z.object({
1070
+ by: z.literal("role"),
1071
+ name: z.union([stringValueSchema, z.undefined()]).optional().transform((value2) => value2),
1072
+ role: z.string().min(1)
1073
+ });
1074
+ var testIdLocatorSchema = z.object({ by: z.literal("testId"), value: stringValueSchema });
1075
+ var insideLocatorSchema = z.object({
1076
+ by: z.literal("inside"),
1077
+ scope: z.lazy(() => locatorSchema),
1078
+ target: z.lazy(() => locatorSchema)
1079
+ });
1080
+ var locatorSchema = z.discriminatedUnion("by", [
1081
+ roleLocatorSchema,
1082
+ testIdLocatorSchema,
1083
+ insideLocatorSchema
1084
+ ]);
1085
+ var primitiveTypeSchema = z.enum(["string", "number", "boolean"]);
1086
+ var consistencyClassSchema = z.enum(["strict", "eventual"]);
1087
+ var stringConstraintsSchema = z.object({
1088
+ kind: z.literal("string"),
1089
+ maxLength: z.number().int().positive().optional(),
1090
+ minLength: z.number().int().nonnegative().optional(),
1091
+ pattern: z.string().optional()
1092
+ });
1093
+ var numberConstraintsSchema = z.object({
1094
+ kind: z.literal("number"),
1095
+ max: z.number().int().optional(),
1096
+ min: z.number().int().optional()
1097
+ });
1098
+ var datetimeConstraintsSchema = z.object({
1099
+ kind: z.literal("datetime"),
1100
+ maxOffsetDays: z.number().int(),
1101
+ minOffsetDays: z.number().int()
1102
+ });
1103
+ var constraintsSchema = z.discriminatedUnion("kind", [
1104
+ stringConstraintsSchema,
1105
+ numberConstraintsSchema,
1106
+ datetimeConstraintsSchema
1107
+ ]);
1108
+ var generatorSchema = z.enum([
1109
+ "company.name",
1110
+ "date.iso",
1111
+ "internet.email",
1112
+ "internet.url",
1113
+ "person.fullName",
1114
+ "lorem.slug",
1115
+ "lorem.word"
1116
+ ]);
1117
+ var valueSpaceSchema = z.object({
1118
+ constraints: constraintsSchema.optional(),
1119
+ generator: generatorSchema,
1120
+ name: z.string().min(1),
1121
+ type: primitiveTypeSchema,
1122
+ values: z.array(primitiveSchema).min(1).optional()
1123
+ });
1124
+ var propSpecSchema = z.object({
1125
+ consistency: consistencyClassSchema.default("strict"),
1126
+ optional: z.boolean(),
1127
+ type: primitiveTypeSchema,
1128
+ valueSpace: z.string().min(1).optional()
1129
+ });
1130
+ var sourceSchema = z.enum(["backend", "client"]);
1131
+ var entitySchemaSchema = z.object({
1132
+ description: z.string().optional(),
1133
+ identity: z.array(z.string().min(1)).min(1),
1134
+ identityKind: z.enum(["surrogate", "natural"]),
1135
+ name: z.string().min(1),
1136
+ props: z.record(z.string().min(1), propSpecSchema),
1137
+ source: sourceSchema.default("backend")
1138
+ });
1139
+ var singletonSchemaSchema = z.object({
1140
+ consistency: consistencyClassSchema.default("strict"),
1141
+ default: primitiveSchema,
1142
+ description: z.string().optional(),
1143
+ name: z.string().min(1),
1144
+ source: sourceSchema.default("backend"),
1145
+ type: primitiveTypeSchema,
1146
+ valueSpace: z.string().min(1).optional()
1147
+ });
1148
+ var browserSingletonSchema = z.enum(["url", "title", "viewport"]);
1237
1149
 
1238
- // src/finalize.ts
1239
- function finalize(body) {
1240
- const descriptors = [...new Set(body.given.map((item) => item.__entity))];
1241
- const absences = descriptors.filter((d) => d.kind === "none");
1242
- const singletonStates = descriptors.filter(
1243
- (d) => d.kind === "singletonState"
1244
- );
1245
- const entities = topoSort(
1246
- descriptors.filter(
1247
- (d) => d.kind === "of" || d.kind === "only" || d.kind === "maybe"
1248
- )
1249
- );
1250
- assertNoMaybeRefs({ absences, entities, singletonStates });
1251
- assertEntityCoherence(entities);
1252
- const captures = body.steps.flatMap((builder) => builder.captures);
1253
- assertScopedConditions(body.steps, new Set(singletonStates.map((s) => s.singleton)));
1254
- const aliases = assignAliases([...entities, ...captures.map((c) => c.descriptor)]);
1255
- const { names, params } = assignParams(allBindings(body, entities, absences, singletonStates));
1256
- const ctx = {
1257
- aliases,
1258
- captures: new Map(
1259
- captures.map((c) => [c.assertion, c.descriptor])
1260
- ),
1261
- params: names
1262
- };
1263
- return {
1264
- absent: absences.map((a) => ({ entity: a.entity, where: setMap(a.where, ctx) })),
1265
- exclusive: [...new Set(entities.filter((d) => d.kind === "only").map((d) => d.entity))],
1266
- maybe: setupsOf(entities, "maybe", ctx),
1267
- params,
1268
- singletons: resolveSingletons(singletonStates, ctx),
1269
- steps: body.steps.map((builder) => resolveStep(builder.step, ctx)),
1270
- world: entities.filter((d) => d.kind === "of" || d.kind === "only").map((d) => ({ as: aliasFor(ctx, d), entity: d.entity, set: setMap(d.props, ctx) }))
1271
- };
1272
- }
1273
- function resolveSingletons(states, ctx) {
1274
- return states.reduce((acc, state) => {
1275
- const value2 = resolveValue(state.value, ctx);
1276
- const existing = acc[state.singleton];
1277
- if (existing !== void 0 && !sameSetValue(existing, value2)) {
1278
- throw new Error(
1279
- `singleton "${state.singleton}" is given two conflicting values \u2014 a test may set each singleton to one value`
1280
- );
1281
- }
1282
- return { ...acc, [state.singleton]: value2 };
1283
- }, {});
1284
- }
1285
- function assertNoMaybeRefs({ absences, entities, singletonStates }) {
1286
- const offenders = [
1287
- ...entities.filter((d) => d.kind === "of" || d.kind === "only").flatMap((d) => maybeBindings(Object.values(d.props), `${d.kind}(${d.entity})`)),
1288
- ...absences.flatMap((a) => maybeBindings(Object.values(a.where), `none(${a.entity})`)),
1289
- ...singletonStates.flatMap((s) => maybeBindings([s.value], `singleton "${s.singleton}"`))
1290
- ];
1291
- const first = offenders[0];
1292
- if (first == null || first.binding.__bind.kind !== "field") {
1293
- return;
1294
- }
1295
- const { descriptor, field: field2 } = first.binding.__bind;
1296
- throw new Error(
1297
- `${first.site} references ${descriptor.entity}.${field2} from a maybe(${descriptor.entity}) \u2014 maybe entities are not materialized at setup; reference them only in steps`
1298
- );
1299
- }
1300
- function maybeBindings(values, site) {
1301
- return values.flatMap((value2) => valueBindings(value2)).filter((b) => b.__bind.kind === "field" && b.__bind.descriptor.kind === "maybe").map((binding) => ({ binding, site }));
1302
- }
1303
- function valueBindings(value2) {
1304
- if (isBinding(value2)) {
1305
- return [value2];
1306
- }
1307
- if (isTemplate(value2)) {
1308
- return value2.template.flatMap((segment) => isBinding(segment) ? [segment] : []);
1309
- }
1310
- return [];
1311
- }
1312
- function isTemplate(value2) {
1313
- return typeof value2 === "object" && value2 !== null && "template" in value2;
1314
- }
1315
- function topoSort(entities) {
1316
- return emitReady([], entities);
1317
- }
1318
- function emitReady(done, remaining) {
1319
- if (remaining.length === 0) {
1320
- return [...done];
1321
- }
1322
- const ready = remaining.filter(
1323
- (d) => depsOf(d, remaining).every((dep) => done.includes(dep) || dep === d)
1324
- );
1325
- if (ready.length === 0) {
1326
- throw new Error("cyclic dependency between given entities");
1327
- }
1328
- return emitReady(
1329
- [...done, ...ready],
1330
- remaining.filter((d) => !ready.includes(d))
1331
- );
1332
- }
1333
- function depsOf(descriptor, among) {
1334
- return Object.values(descriptor.props).flatMap((v2) => valueBindings(v2)).flatMap((b) => b.__bind.kind === "field" ? [b.__bind.descriptor] : []).filter((target) => target !== descriptor && among.includes(target));
1335
- }
1336
- function assertScopedConditions(steps, givenSingletons) {
1337
- steps.flatMap((builder) => builder.step.expect).forEach((predicate) => {
1338
- assertPredicateScoped(predicate, givenSingletons);
1339
- });
1340
- }
1341
- function assertPredicateScoped(predicate, given) {
1342
- if (predicate.kind === "not") {
1343
- assertPredicateScoped(predicate.predicate, given);
1344
- return;
1345
- }
1346
- if (predicate.kind !== "when") {
1347
- return;
1348
- }
1349
- predicate.branches.forEach((row2) => {
1350
- const names = row2.condition == null ? [] : conditionSingletons(row2.condition);
1351
- names.forEach((name) => {
1352
- if (!given.has(name)) {
1353
- throw new Error(
1354
- `when() conditions on singleton "${name}", which is not in the workflow's given \u2014 add ${name}.of(...) to given`
1355
- );
1356
- }
1357
- });
1358
- row2.consequence.forEach((consequence) => {
1359
- assertPredicateScoped(consequence, given);
1360
- });
1361
- });
1362
- }
1363
- function conditionSingletons(predicate) {
1364
- if (predicate.kind === "singleton") {
1365
- return [predicate.singleton];
1366
- }
1367
- if (predicate.kind === "not") {
1368
- return conditionSingletons(predicate.predicate);
1369
- }
1370
- if (predicate.kind === "and") {
1371
- return predicate.predicates.flatMap((p) => conditionSingletons(p));
1372
- }
1373
- return [];
1374
- }
1375
- function assignAliases(entities) {
1376
- return entities.reduce(
1377
- (acc, d) => {
1378
- const ordinal = acc.counts[d.entity] ?? 0;
1379
- return {
1380
- aliases: new Map([...acc.aliases, [d, `${d.entity}_${String(ordinal)}`]]),
1381
- counts: { ...acc.counts, [d.entity]: ordinal + 1 }
1382
- };
1383
- },
1384
- { aliases: /* @__PURE__ */ new Map(), counts: {} }
1385
- ).aliases;
1386
- }
1387
- function assignParams(bindings) {
1388
- const unique = [...new Set(bindings.filter((b) => b.__bind.kind === "param"))];
1389
- const assigned = unique.reduce(
1390
- (acc, b) => {
1391
- const token = tokenOf(b);
1392
- const ordinal = acc.counts[token.base] ?? 0;
1393
- const name = ordinal === 0 ? token.base : `${token.base}_${String(ordinal - 1)}`;
1394
- if (name in acc.params) {
1395
- throw new Error(
1396
- `param name "${name}" collides with an existing param \u2014 rename the field so deduplicated param names stay unique`
1397
- );
1398
- }
1399
- return {
1400
- counts: { ...acc.counts, [token.base]: ordinal + 1 },
1401
- names: new Map([...acc.names, [b, name]]),
1402
- params: { ...acc.params, [name]: { valueSpace: token.valueSpace } }
1403
- };
1404
- },
1405
- { counts: {}, names: /* @__PURE__ */ new Map(), params: {} }
1406
- );
1407
- return { names: assigned.names, params: assigned.params };
1408
- }
1409
- function tokenOf(binding) {
1410
- if (binding.__bind.kind !== "param") {
1411
- throw new Error("internal: expected a param binding");
1412
- }
1413
- return binding.__bind.token;
1414
- }
1415
- function allBindings(body, entities, absences, singletonStates) {
1416
- return [
1417
- ...entities.flatMap((d) => Object.values(d.props).flatMap((v2) => valueBindings(v2))),
1418
- ...absences.flatMap((a) => Object.values(a.where).flatMap((v2) => valueBindings(v2))),
1419
- ...singletonStates.flatMap((s) => valueBindings(s.value)),
1420
- ...body.steps.flatMap((builder) => stepBindings(builder.step))
1421
- ];
1422
- }
1423
- function stepBindings(step) {
1424
- return [...actionBindings(step.action), ...step.expect.flatMap((p) => predicateBindings(p))];
1425
- }
1426
- function actionBindings(action) {
1427
- if (action.kind === "goto") {
1428
- return valueBindings(action.url);
1429
- }
1430
- if (action.kind === "fill" || action.kind === "select") {
1431
- return [...locatorBindings(action.locator), ...valueBindings(action.value)];
1432
- }
1433
- if (action.kind === "press") {
1434
- return action.locator == null ? [] : locatorBindings(action.locator);
1150
+ // ../spec/src/predicate.ts
1151
+ import { z as z2 } from "zod";
1152
+ var stateAssertionSchema = z2.discriminatedUnion("kind", [
1153
+ z2.object({
1154
+ as: z2.string().min(1),
1155
+ kind: z2.literal("created"),
1156
+ props: z2.record(z2.string().min(1), setValueSchema)
1157
+ }),
1158
+ z2.object({
1159
+ as: z2.string().min(1),
1160
+ kind: z2.literal("updated"),
1161
+ props: z2.record(z2.string().min(1), updateValueSchema)
1162
+ }),
1163
+ z2.object({ kind: z2.literal("deleted") })
1164
+ ]);
1165
+ var singletonAssertionSchema = z2.object({ kind: z2.literal("is"), value: setValueSchema });
1166
+ var whereValueSchema = z2.lazy(
1167
+ () => z2.union([setValueSchema, withinSchema])
1168
+ );
1169
+ var selectionSchema = z2.object({
1170
+ entity: z2.string().min(1),
1171
+ where: z2.record(z2.string().min(1), whereValueSchema)
1172
+ });
1173
+ var withinSchema = z2.object({
1174
+ field: z2.string().min(1),
1175
+ kind: z2.literal("within"),
1176
+ selection: selectionSchema
1177
+ });
1178
+ var wait = z2.union([budgetSchema, z2.undefined()]).optional().transform((value2) => value2);
1179
+ var singletonPredicateSchema = z2.object({
1180
+ assertion: singletonAssertionSchema,
1181
+ kind: z2.literal("singleton"),
1182
+ singleton: z2.string().min(1),
1183
+ wait
1184
+ });
1185
+ var countPredicateSchema = z2.object({
1186
+ entity: z2.string().min(1),
1187
+ kind: z2.literal("count"),
1188
+ value: z2.number().int().nonnegative()
1189
+ });
1190
+ var conditionSchema = z2.lazy(
1191
+ () => z2.discriminatedUnion("kind", [
1192
+ singletonPredicateSchema,
1193
+ countPredicateSchema,
1194
+ z2.object({ kind: z2.literal("not"), predicate: conditionSchema }),
1195
+ z2.object({ kind: z2.literal("and"), predicates: z2.array(conditionSchema) })
1196
+ ])
1197
+ );
1198
+ var whenBranchSchema = z2.lazy(
1199
+ () => z2.object({
1200
+ condition: z2.union([conditionSchema, z2.undefined()]).optional().transform((value2) => value2),
1201
+ consequence: z2.array(predicateSchema),
1202
+ name: z2.string().min(1)
1203
+ })
1204
+ );
1205
+ var predicateSchema = z2.lazy(
1206
+ () => z2.discriminatedUnion("kind", [
1207
+ z2.object({ kind: z2.literal("visible"), locator: locatorSchema, wait }),
1208
+ z2.object({ kind: z2.literal("disabled"), locator: locatorSchema, wait }),
1209
+ z2.object({ kind: z2.literal("enabled"), locator: locatorSchema, wait }),
1210
+ z2.object({ kind: z2.literal("focused"), locator: locatorSchema, wait }),
1211
+ z2.object({ kind: z2.literal("checked"), locator: locatorSchema, wait }),
1212
+ z2.object({ kind: z2.literal("value"), locator: locatorSchema, value: stringValueSchema, wait }),
1213
+ z2.object({ kind: z2.literal("text"), locator: locatorSchema, value: stringValueSchema, wait }),
1214
+ singletonPredicateSchema,
1215
+ z2.object({
1216
+ kind: z2.literal("browser"),
1217
+ name: browserSingletonSchema,
1218
+ value: stringValueSchema,
1219
+ wait
1220
+ }),
1221
+ z2.object({
1222
+ assertion: stateAssertionSchema,
1223
+ entity: z2.string().min(1),
1224
+ key: z2.record(z2.string().min(1), whereValueSchema),
1225
+ kind: z2.literal("state"),
1226
+ wait
1227
+ }),
1228
+ z2.object({ kind: z2.literal("not"), predicate: predicateSchema }),
1229
+ z2.object({ kind: z2.literal("and"), predicates: z2.array(predicateSchema) }),
1230
+ countPredicateSchema,
1231
+ z2.object({ branches: z2.array(whenBranchSchema), kind: z2.literal("when") })
1232
+ ])
1233
+ );
1234
+
1235
+ // ../spec/src/codec.ts
1236
+ import { z as z3 } from "zod";
1237
+ var envelopeSchema = z3.object({
1238
+ __codec: z3.string().min(1),
1239
+ data: z3.unknown(),
1240
+ version: z3.number().int().positive()
1241
+ });
1242
+ var CodecVersionError = class extends Error {
1243
+ codec;
1244
+ currentVersion;
1245
+ gotVersion;
1246
+ constructor(params) {
1247
+ super(
1248
+ `Unsupported ${params.codec} version ${String(params.gotVersion)} (current ${String(params.currentVersion)}). Upgrade Ripplo or rebuild with a compatible CLI.`
1249
+ );
1250
+ this.name = "CodecVersionError";
1251
+ this.codec = params.codec;
1252
+ this.currentVersion = params.currentVersion;
1253
+ this.gotVersion = params.gotVersion;
1435
1254
  }
1436
- if (action.kind === "upload") {
1437
- return locatorBindings(action.locator);
1255
+ };
1256
+ var CodecMismatchError = class extends Error {
1257
+ constructor(params) {
1258
+ super(`Codec mismatch: expected "${params.expected}", got "${params.got}"`);
1259
+ this.name = "CodecMismatchError";
1438
1260
  }
1439
- return locatorBindings(action.locator);
1261
+ };
1262
+ function defineCodec({
1263
+ name,
1264
+ schema
1265
+ }) {
1266
+ return {
1267
+ currentVersion: 1,
1268
+ name,
1269
+ decode: (raw) => decode({ name, raw, schema }),
1270
+ encode: (value2) => ({
1271
+ __codec: name,
1272
+ data: value2,
1273
+ version: 1
1274
+ })
1275
+ };
1440
1276
  }
1441
- function locatorBindings(locator) {
1442
- if (locator.by === "inside") {
1443
- return [...locatorBindings(locator.scope), ...locatorBindings(locator.target)];
1444
- }
1445
- if (locator.by === "role") {
1446
- return locator.name == null ? [] : valueBindings(locator.name);
1277
+ function decode({
1278
+ name,
1279
+ raw,
1280
+ schema
1281
+ }) {
1282
+ const envelope = envelopeSchema.parse(raw);
1283
+ if (envelope.__codec !== name) {
1284
+ throw new CodecMismatchError({ expected: name, got: envelope.__codec });
1447
1285
  }
1448
- return valueBindings(locator.value);
1449
- }
1450
- function predicateBindings(predicate) {
1451
- switch (predicate.kind) {
1452
- case "visible":
1453
- case "disabled":
1454
- case "enabled":
1455
- case "focused":
1456
- case "checked": {
1457
- return locatorBindings(predicate.locator);
1458
- }
1459
- case "value":
1460
- case "text": {
1461
- return [...locatorBindings(predicate.locator), ...valueBindings(predicate.value)];
1462
- }
1463
- case "singleton": {
1464
- return valueBindings(predicate.assertion.value);
1465
- }
1466
- case "browser": {
1467
- return valueBindings(predicate.value);
1468
- }
1469
- case "state": {
1470
- return [
1471
- ...predicate.assertion.kind === "deleted" ? [] : Object.values(predicate.assertion.props).flatMap(
1472
- (v2) => isChanged(v2) ? [] : valueBindings(v2)
1473
- ),
1474
- ...Object.values(predicate.key).flatMap((v2) => whereBindings(v2))
1475
- ];
1476
- }
1477
- case "not": {
1478
- return predicateBindings(predicate.predicate);
1479
- }
1480
- case "when": {
1481
- return predicate.branches.flatMap((row2) => [
1482
- ...row2.condition == null ? [] : predicateBindings(row2.condition),
1483
- ...row2.consequence.flatMap((consequence) => predicateBindings(consequence))
1484
- ]);
1485
- }
1486
- case "and": {
1487
- return predicate.predicates.flatMap((p) => predicateBindings(p));
1488
- }
1489
- case "count": {
1490
- return [];
1491
- }
1286
+ if (envelope.version !== 1) {
1287
+ throw new CodecVersionError({ codec: name, currentVersion: 1, gotVersion: envelope.version });
1492
1288
  }
1289
+ return schema.parse(envelope.data);
1493
1290
  }
1494
- function whereBindings(value2) {
1495
- return isWithin2(value2) ? Object.values(value2.selection.where).flatMap((v2) => whereBindings(v2)) : valueBindings(value2);
1496
- }
1497
- function isWithin2(value2) {
1498
- return typeof value2 === "object" && value2 !== null && "kind" in value2;
1291
+
1292
+ // ../spec/src/client-channel.ts
1293
+ var CLIENT_MOUNT_KEY = "__ripplo__";
1294
+ var CLIENT_SEED_KEY = "__ripplo_seed__";
1295
+
1296
+ // ../spec/src/lockfile.ts
1297
+ import { z as z4 } from "zod";
1298
+ var actionSchema = z4.discriminatedUnion("kind", [
1299
+ z4.object({ kind: z4.literal("goto"), url: stringValueSchema }),
1300
+ z4.object({ kind: z4.literal("fill"), locator: locatorSchema, value: setValueSchema }),
1301
+ z4.object({ kind: z4.literal("clear"), locator: locatorSchema }),
1302
+ z4.object({ kind: z4.literal("click"), locator: locatorSchema }),
1303
+ z4.object({ kind: z4.literal("dblclick"), locator: locatorSchema }),
1304
+ z4.object({ kind: z4.literal("select"), locator: locatorSchema, value: setValueSchema }),
1305
+ z4.object({ kind: z4.literal("check"), locator: locatorSchema }),
1306
+ z4.object({ kind: z4.literal("uncheck"), locator: locatorSchema }),
1307
+ z4.object({ kind: z4.literal("hover"), locator: locatorSchema }),
1308
+ z4.object({
1309
+ files: z4.array(z4.string().min(1)).min(1),
1310
+ kind: z4.literal("upload"),
1311
+ locator: locatorSchema
1312
+ }),
1313
+ z4.object({ key: z4.string().min(1), kind: z4.literal("press"), locator: locatorSchema.optional() })
1314
+ ]);
1315
+ var stepSchema = z4.object({
1316
+ action: actionSchema,
1317
+ expect: z4.array(predicateSchema).default([])
1318
+ });
1319
+ var paramSchema = z4.object({
1320
+ valueSpace: z4.string().min(1)
1321
+ });
1322
+ var setupSchema = z4.object({
1323
+ as: z4.string().min(1),
1324
+ entity: z4.string().min(1),
1325
+ set: z4.record(z4.string().min(1), setValueSchema)
1326
+ });
1327
+ var absenceSchema = z4.object({
1328
+ entity: z4.string().min(1),
1329
+ where: z4.record(z4.string().min(1), setValueSchema)
1330
+ });
1331
+ var resolvedTestSchema = z4.object({
1332
+ absent: z4.array(absenceSchema).default([]),
1333
+ exclusive: z4.array(z4.string().min(1)).default([]),
1334
+ intent: z4.string().min(1),
1335
+ name: z4.string().min(1),
1336
+ params: z4.record(z4.string().min(1), paramSchema),
1337
+ singletons: z4.record(z4.string().min(1), setValueSchema).default({}),
1338
+ slug: z4.string().min(1),
1339
+ steps: z4.array(stepSchema).default([]),
1340
+ workflow: z4.string().min(1),
1341
+ world: z4.array(setupSchema).default([])
1342
+ });
1343
+ var workflowSchema = z4.object({
1344
+ absent: z4.array(absenceSchema).default([]),
1345
+ exclusive: z4.array(z4.string().min(1)).default([]),
1346
+ intent: z4.string().min(1),
1347
+ maybe: z4.array(setupSchema).default([]),
1348
+ name: z4.string().min(1),
1349
+ params: z4.record(z4.string().min(1), paramSchema),
1350
+ singletons: z4.record(z4.string().min(1), setValueSchema).default({}),
1351
+ sourcePath: z4.string().min(1).optional(),
1352
+ steps: z4.array(stepSchema).default([]),
1353
+ stub: z4.boolean().default(false),
1354
+ tests: z4.array(resolvedTestSchema).default([]),
1355
+ world: z4.array(setupSchema).default([])
1356
+ });
1357
+ var fixtureEntrySchema = z4.object({
1358
+ sha256: z4.string().regex(/^[0-9a-f]{64}$/u),
1359
+ size: z4.number().int().nonnegative()
1360
+ });
1361
+ var lockfileSchema = z4.object({
1362
+ entities: z4.array(entitySchemaSchema),
1363
+ fixtures: z4.record(z4.string().min(1), fixtureEntrySchema).default({}),
1364
+ singletons: z4.array(singletonSchemaSchema).default([]),
1365
+ valueSpaces: z4.array(valueSpaceSchema),
1366
+ workflows: z4.array(workflowSchema)
1367
+ });
1368
+ var lockfileCodec = defineCodec({ name: "ripplo-lockfile", schema: lockfileSchema });
1369
+
1370
+ // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/esm/wrapper.js
1371
+ var import__ = __toESM(require_safe_stable_stringify(), 1);
1372
+ var configure = import__.default.configure;
1373
+
1374
+ // ../spec/src/sync-payload.ts
1375
+ import { z as z5 } from "zod";
1376
+ var stepDescriptorSchema = z5.object({
1377
+ index: z5.number().int().nonnegative(),
1378
+ kind: z5.string(),
1379
+ target: z5.string(),
1380
+ value: z5.string()
1381
+ });
1382
+ function slugify(name) {
1383
+ return name.toLowerCase().replaceAll(/[^a-z0-9]/g, "-").split("-").filter((part) => part.length > 0).join("-");
1499
1384
  }
1500
- function setMap(map, ctx) {
1501
- return Object.fromEntries(
1502
- Object.entries(map).map(([key2, value2]) => [key2, resolveValue(value2, ctx)])
1503
- );
1385
+
1386
+ // ../spec/src/session.ts
1387
+ import { z as z6 } from "zod";
1388
+ var sameSiteSchema = z6.enum(["Strict", "Lax", "None"]);
1389
+ var cookieSchema = z6.object({
1390
+ domain: z6.string().min(1),
1391
+ expires: z6.number(),
1392
+ httpOnly: z6.boolean(),
1393
+ name: z6.string().min(1),
1394
+ path: z6.string().min(1),
1395
+ sameSite: sameSiteSchema,
1396
+ secure: z6.boolean(),
1397
+ value: z6.string()
1398
+ });
1399
+ var originSchema = z6.object({
1400
+ localStorage: z6.array(z6.object({ name: z6.string().min(1), value: z6.string() })),
1401
+ origin: z6.string().min(1)
1402
+ });
1403
+ var sessionSchema = z6.object({
1404
+ cookies: z6.array(cookieSchema),
1405
+ headers: z6.record(z6.string().min(1), z6.string()).optional(),
1406
+ origins: z6.array(originSchema)
1407
+ });
1408
+
1409
+ // ../spec/src/engine.ts
1410
+ import { z as z7 } from "zod";
1411
+ var cellSchema = z7.union([primitiveSchema, z7.null()]);
1412
+ var rowSchema = z7.record(z7.string().min(1), cellSchema);
1413
+ var setupSpecSchema = z7.object({
1414
+ as: z7.string().min(1),
1415
+ entity: z7.string().min(1),
1416
+ fields: z7.record(z7.string().min(1), setValueSchema)
1417
+ });
1418
+ var setupRequestSchema = z7.object({
1419
+ entities: z7.array(setupSpecSchema),
1420
+ runId: z7.string().min(1),
1421
+ singletons: z7.record(z7.string().min(1), cellSchema).default({})
1422
+ });
1423
+ var setupRowSchema = z7.object({
1424
+ as: z7.string().min(1),
1425
+ row: rowSchema,
1426
+ session: sessionSchema.optional()
1427
+ });
1428
+ var setupResponseSchema = z7.object({
1429
+ rows: z7.array(setupRowSchema)
1430
+ });
1431
+ var stateRequestSchema = z7.object({
1432
+ entities: z7.array(z7.string().min(1)),
1433
+ runId: z7.string().min(1),
1434
+ singletons: z7.array(z7.string().min(1)).default([])
1435
+ });
1436
+ var stateResponseSchema = z7.object({
1437
+ entities: z7.record(z7.string().min(1), z7.array(rowSchema)),
1438
+ singletons: z7.record(z7.string().min(1), cellSchema).default({})
1439
+ });
1440
+ var teardownRequestSchema = z7.object({
1441
+ runId: z7.string().min(1)
1442
+ });
1443
+ var teardownResponseSchema = z7.object({
1444
+ ok: z7.literal(true)
1445
+ });
1446
+
1447
+ // src/expand-refs.ts
1448
+ function usedRefHeads({
1449
+ singletons,
1450
+ steps,
1451
+ workflow: workflow2,
1452
+ world
1453
+ }) {
1454
+ const values = [
1455
+ ...world.flatMap((setup) => Object.values(setup.set)),
1456
+ ...workflow2.absent.flatMap((absence) => Object.values(absence.where)),
1457
+ ...Object.values(singletons),
1458
+ ...steps.flatMap((step) => stepRefValues(step))
1459
+ ];
1460
+ return new Set(values.flatMap((value2) => refStrings(value2)).map((ref) => headOf(ref)));
1504
1461
  }
1505
- function resolveValue(value2, ctx) {
1506
- if (isBinding(value2)) {
1507
- return resolveBinding(value2, ctx);
1508
- }
1509
- if (isTemplate(value2)) {
1510
- return resolveTemplate(value2, ctx);
1462
+ function collectRefObjects(node) {
1463
+ if (Array.isArray(node)) {
1464
+ return node.flatMap((item) => collectRefObjects(item));
1511
1465
  }
1512
- return value2;
1513
- }
1514
- function resolveBinding(binding, ctx) {
1515
- const bind = binding.__bind;
1516
- if (bind.kind === "param") {
1517
- const name = ctx.params.get(binding);
1518
- if (name == null) {
1519
- throw new Error("internal: param binding was not collected");
1520
- }
1521
- return { ref: name };
1466
+ if (node == null || typeof node !== "object") {
1467
+ return [];
1522
1468
  }
1523
- const alias = ctx.aliases.get(bind.descriptor);
1524
- if (alias == null) {
1525
- throw new Error(
1526
- `references a "${bind.descriptor.entity}" entity that is not included in the test's given`
1527
- );
1469
+ if ("ref" in node && typeof node.ref === "string" && Object.keys(node).length === 1) {
1470
+ return [node.ref];
1528
1471
  }
1529
- return { ref: `${alias}.${bind.field}` };
1530
- }
1531
- function resolveTemplate(template, ctx) {
1532
- return {
1533
- template: template.template.map(
1534
- (segment) => isBinding(segment) ? resolveBinding(segment, ctx) : segment
1535
- )
1536
- };
1472
+ return Object.values(node).flatMap((value2) => collectRefObjects(value2));
1537
1473
  }
1538
- function setupsOf(entities, kind, ctx) {
1539
- return entities.filter((d) => d.kind === kind).map((d) => ({ as: aliasFor(ctx, d), entity: d.entity, set: setMap(d.props, ctx) }));
1474
+ function isRefValue(value2) {
1475
+ return value2 != null && typeof value2 === "object" && "ref" in value2 ? value2.ref : void 0;
1540
1476
  }
1541
- function aliasFor(ctx, descriptor) {
1542
- const alias = ctx.aliases.get(descriptor);
1543
- if (alias == null) {
1544
- throw new Error(`internal: no alias for ${descriptor.entity}`);
1545
- }
1546
- return alias;
1477
+ function headOf(ref) {
1478
+ const dot = ref.indexOf(".");
1479
+ return dot === -1 ? ref : ref.slice(0, dot);
1547
1480
  }
1548
- function resolveStep(step, ctx) {
1549
- return {
1550
- action: resolveAction(step.action, ctx),
1551
- expect: step.expect.map((predicate) => resolvePredicate(predicate, ctx))
1552
- };
1481
+ function stepRefValues(step) {
1482
+ return collectRefObjects(step).map((ref) => ({ ref }));
1553
1483
  }
1554
- function resolveAction(action, ctx) {
1555
- if (action.kind === "goto") {
1556
- return { ...action, url: resolveString(action.url, ctx) };
1557
- }
1558
- if (action.kind === "fill" || action.kind === "select") {
1559
- return {
1560
- ...action,
1561
- locator: resolveLocator(action.locator, ctx),
1562
- value: resolveValue(action.value, ctx)
1563
- };
1564
- }
1565
- if (action.kind === "press") {
1566
- return {
1567
- ...action,
1568
- locator: action.locator == null ? void 0 : resolveLocator(action.locator, ctx)
1569
- };
1484
+ function refStrings(value2) {
1485
+ const ref = isRefValue(value2);
1486
+ if (ref != null) {
1487
+ return [ref];
1570
1488
  }
1571
- if (action.kind === "upload") {
1572
- return { ...action, locator: resolveLocator(action.locator, ctx) };
1489
+ if (value2 != null && typeof value2 === "object" && "template" in value2) {
1490
+ return value2.template.flatMap((segment) => typeof segment === "string" ? [] : [segment.ref]);
1573
1491
  }
1574
- return { ...action, locator: resolveLocator(action.locator, ctx) };
1492
+ return [];
1575
1493
  }
1576
- function resolveString(value2, ctx) {
1577
- if (isBinding(value2)) {
1578
- return resolveBinding(value2, ctx);
1579
- }
1580
- if (isTemplate(value2)) {
1581
- return resolveTemplate(value2, ctx);
1582
- }
1583
- return value2;
1494
+
1495
+ // src/expand.ts
1496
+ var MAX_CANDIDATES = 4096;
1497
+ function singletonValuesOf(singletons) {
1498
+ return new Map(
1499
+ singletons.flatMap((singleton2) => {
1500
+ const space = singleton2.valueSpaces.find((s) => s.name === singleton2.schema.valueSpace);
1501
+ return space?.values == null ? [] : [[singleton2.schema.name, space.values]];
1502
+ })
1503
+ );
1584
1504
  }
1585
- function resolveLocator(locator, ctx) {
1586
- if (locator.by === "inside") {
1587
- return {
1588
- by: "inside",
1589
- scope: resolveLocator(locator.scope, ctx),
1590
- target: resolveLocator(locator.target, ctx)
1591
- };
1505
+ function expandWorkflow(workflow2, options) {
1506
+ if (workflow2.stub) {
1507
+ return { ...workflow2, tests: [] };
1592
1508
  }
1593
- if (locator.by === "role") {
1594
- return locator.name == null ? locator : { ...locator, name: resolveString(locator.name, ctx) };
1509
+ assertUniqueBranchNames(workflow2);
1510
+ const targets = collectTargets(workflow2.steps);
1511
+ const whenIds = new Map(targets.map((target, index) => [target.when, index]));
1512
+ const sims = proposeCandidates(workflow2, options).map((candidate) => simulate(workflow2, candidate)).filter((sim) => sim != null);
1513
+ if (targets.length === 0) {
1514
+ return { ...workflow2, tests: [requireMainTest(workflow2, sims)] };
1595
1515
  }
1596
- return { ...locator, value: resolveString(locator.value, ctx) };
1516
+ const { tests } = targets.reduce(
1517
+ (acc, target) => coverTarget({ acc, sims, target, whenIds, workflow: workflow2 }),
1518
+ { covered: /* @__PURE__ */ new Set(), tests: [] }
1519
+ );
1520
+ return { ...workflow2, tests };
1597
1521
  }
1598
- function resolvePredicate(predicate, ctx) {
1599
- switch (predicate.kind) {
1600
- case "visible":
1601
- case "disabled":
1602
- case "enabled":
1603
- case "focused":
1604
- case "checked": {
1605
- return { ...predicate, locator: resolveLocator(predicate.locator, ctx) };
1606
- }
1607
- case "value":
1608
- case "text": {
1609
- return {
1610
- ...predicate,
1611
- locator: resolveLocator(predicate.locator, ctx),
1612
- value: resolveString(predicate.value, ctx)
1613
- };
1614
- }
1615
- case "singleton": {
1616
- return {
1617
- ...predicate,
1618
- assertion: { ...predicate.assertion, value: resolveValue(predicate.assertion.value, ctx) }
1619
- };
1620
- }
1621
- case "browser": {
1622
- return { ...predicate, value: resolveString(predicate.value, ctx) };
1623
- }
1624
- case "state": {
1625
- return {
1626
- ...predicate,
1627
- assertion: resolveAssertion(predicate.assertion, ctx),
1628
- key: whereMap(predicate.key, ctx)
1629
- };
1630
- }
1631
- case "not": {
1632
- return { ...predicate, predicate: resolvePredicate(predicate.predicate, ctx) };
1633
- }
1634
- case "when": {
1635
- return {
1636
- ...predicate,
1637
- branches: predicate.branches.map((row2) => ({
1638
- condition: row2.condition == null ? void 0 : resolveCondition(row2.condition, ctx),
1639
- consequence: row2.consequence.map((consequence) => resolvePredicate(consequence, ctx)),
1640
- name: row2.name
1641
- }))
1642
- };
1643
- }
1644
- case "and": {
1645
- return {
1646
- ...predicate,
1647
- predicates: predicate.predicates.map((p) => resolvePredicate(p, ctx))
1648
- };
1522
+ function resolveValidTest(workflow2, sim, name) {
1523
+ const test = resolveTest(workflow2, sim, name);
1524
+ return danglingRefHeads(test).length === 0 ? test : null;
1525
+ }
1526
+ function danglingRefHeads(test) {
1527
+ const known = /* @__PURE__ */ new Set([
1528
+ ...test.world.map((setup) => setup.as),
1529
+ ...Object.keys(test.params),
1530
+ ...test.steps.flatMap((step) => createdAliasesIn(step))
1531
+ ]);
1532
+ return [...new Set(collectRefObjects(test).map((ref) => headOf(ref)))].filter(
1533
+ (head) => !known.has(head)
1534
+ );
1535
+ }
1536
+ function createdAliasesIn(step) {
1537
+ return step.expect.flatMap((predicate) => {
1538
+ if (predicate.kind !== "state" || predicate.assertion.kind === "deleted") {
1539
+ return [];
1649
1540
  }
1650
- case "count": {
1651
- return predicate;
1541
+ return [predicate.assertion.as];
1542
+ });
1543
+ }
1544
+ function coverTarget({ acc, sims, target, whenIds, workflow: workflow2 }) {
1545
+ const key2 = choiceKey(whenIds, target.when, target.index);
1546
+ if (acc.covered.has(key2)) {
1547
+ return { covered: acc.covered, tests: acc.tests };
1548
+ }
1549
+ const picked = sims.filter((s) => s.choices.get(target.when) === target.index).reduce((found, s) => {
1550
+ if (found != null) {
1551
+ return found;
1652
1552
  }
1553
+ const test = resolveValidTest(workflow2, s, target.name);
1554
+ return test == null ? null : { sim: s, test };
1555
+ }, null);
1556
+ if (picked == null) {
1557
+ throw new Error(
1558
+ `workflow "${workflow2.name}": branch "${target.name}" is unreachable \u2014 no combination of optional entities and singleton values reaches it`
1559
+ );
1653
1560
  }
1561
+ const reached = [...picked.sim.choices].map(([when2, index]) => choiceKey(whenIds, when2, index));
1562
+ return {
1563
+ covered: /* @__PURE__ */ new Set([...acc.covered, ...reached]),
1564
+ tests: [...acc.tests, picked.test]
1565
+ };
1654
1566
  }
1655
- function resolveAssertion(assertion, ctx) {
1656
- if (assertion.kind === "deleted") {
1657
- return assertion;
1567
+ function choiceKey(whenIds, when2, index) {
1568
+ const id2 = whenIds.get(when2);
1569
+ if (id2 == null) {
1570
+ throw new Error("internal: when node missing from target index");
1658
1571
  }
1659
- const descriptor = ctx.captures.get(assertion);
1660
- if (descriptor == null) {
1661
- throw new Error("internal: capture assertion was not registered");
1572
+ return `${String(id2)}:${String(index)}`;
1573
+ }
1574
+ function requireMainTest(workflow2, sims) {
1575
+ const test = sims.reduce(
1576
+ (found, sim) => found ?? resolveValidTest(workflow2, sim, "main"),
1577
+ null
1578
+ );
1579
+ if (test == null) {
1580
+ throw new Error(
1581
+ `workflow "${workflow2.name}": no valid seed found \u2014 steps may reference optional entities no candidate provides, or a condition mentions values the solver cannot pin`
1582
+ );
1662
1583
  }
1663
- const as = aliasFor(ctx, descriptor);
1664
- return assertion.kind === "created" ? { as, kind: "created", props: setMap(assertion.props, ctx) } : { as, kind: "updated", props: updateMap(assertion.props, ctx) };
1584
+ return test;
1665
1585
  }
1666
- function updateMap(map, ctx) {
1667
- return Object.fromEntries(
1668
- Object.entries(map).map(([key2, value2]) => [
1669
- key2,
1670
- isChanged(value2) ? value2 : resolveValue(value2, ctx)
1671
- ])
1586
+ function assertUniqueBranchNames(workflow2) {
1587
+ const names = collectTargets(workflow2.steps).map((target) => target.name);
1588
+ const dup = names.find((name, index) => names.indexOf(name) !== index);
1589
+ if (dup != null) {
1590
+ throw new Error(
1591
+ `workflow "${workflow2.name}": branch name "${dup}" is used twice \u2014 branch names must be unique within a workflow`
1592
+ );
1593
+ }
1594
+ }
1595
+ function collectTargets(steps) {
1596
+ return steps.flatMap((step) => step.expect.flatMap((predicate) => targetsIn(predicate)));
1597
+ }
1598
+ function targetsIn(predicate) {
1599
+ if (predicate.kind === "not") {
1600
+ return targetsIn(predicate.predicate);
1601
+ }
1602
+ if (predicate.kind === "and") {
1603
+ return predicate.predicates.flatMap((p) => targetsIn(p));
1604
+ }
1605
+ if (predicate.kind !== "when") {
1606
+ return [];
1607
+ }
1608
+ return predicate.branches.flatMap((row2, index) => [
1609
+ { index, name: row2.name, when: predicate },
1610
+ ...row2.consequence.flatMap((consequence) => targetsIn(consequence))
1611
+ ]);
1612
+ }
1613
+ function proposeCandidates(workflow2, options) {
1614
+ const subsets = maybeSubsets(workflow2.maybe);
1615
+ const pinSets = pinAssignments(workflow2, options);
1616
+ if (subsets.length * pinSets.length > MAX_CANDIDATES) {
1617
+ throw new Error(
1618
+ `workflow "${workflow2.name}": too many optional entities and singleton values to solve \u2014 split the workflow or convert maybe(...) entities to of(...)`
1619
+ );
1620
+ }
1621
+ return subsets.flatMap((maybes) => pinSets.map((pins) => ({ maybes, pins })));
1622
+ }
1623
+ function maybeSubsets(maybe) {
1624
+ const masks = Array.from({ length: 1 << maybe.length }, (_, mask) => mask);
1625
+ return masks.map((mask) => ({ mask, size: maybe.filter((_, i) => (mask & 1 << i) !== 0).length })).toSorted((a, b) => a.size === b.size ? a.mask - b.mask : a.size - b.size).map(({ mask }) => maybe.filter((_, i) => (mask & 1 << i) !== 0));
1626
+ }
1627
+ function pinAssignments(workflow2, options) {
1628
+ const domains = pinDomains(workflow2, options);
1629
+ return Object.entries(domains).reduce(
1630
+ (acc, [param, domain]) => acc.flatMap((pins) => domain.map((value2) => ({ ...pins, [param]: value2 }))),
1631
+ [{}]
1672
1632
  );
1673
1633
  }
1674
- function whereMap(map, ctx) {
1675
- return Object.fromEntries(
1676
- Object.entries(map).map(([key2, value2]) => [
1677
- key2,
1678
- resolveWhere(value2, ctx)
1679
- ])
1634
+ function pinDomains(workflow2, options) {
1635
+ const literals = conditionSingletonLiterals(workflow2.steps);
1636
+ return Object.entries(workflow2.singletons).reduce((acc, [name, value2]) => {
1637
+ const param = paramRefOf(value2);
1638
+ const compared = literals.get(name);
1639
+ if (param == null || compared == null) {
1640
+ return acc;
1641
+ }
1642
+ const enumValues = options.singletonValues.get(name);
1643
+ return { ...acc, [param]: withComplements(compared, enumValues) };
1644
+ }, {});
1645
+ }
1646
+ function conditionSingletonLiterals(steps) {
1647
+ const pairs = steps.flatMap(
1648
+ (step) => step.expect.flatMap((predicate) => conditionLiteralsIn(predicate))
1649
+ );
1650
+ return pairs.reduce(
1651
+ (acc, [name, literal]) => new Map([...acc, [name, [.../* @__PURE__ */ new Set([...acc.get(name) ?? [], literal])]]]),
1652
+ /* @__PURE__ */ new Map()
1680
1653
  );
1681
1654
  }
1682
- function resolveWhere(value2, ctx) {
1683
- return isWithin2(value2) ? { ...value2, selection: { ...value2.selection, where: whereMap(value2.selection.where, ctx) } } : resolveValue(value2, ctx);
1655
+ function conditionLiteralsIn(predicate) {
1656
+ if (predicate.kind === "not") {
1657
+ return conditionLiteralsIn(predicate.predicate);
1658
+ }
1659
+ if (predicate.kind === "and") {
1660
+ return predicate.predicates.flatMap((p) => conditionLiteralsIn(p));
1661
+ }
1662
+ if (predicate.kind !== "when") {
1663
+ return [];
1664
+ }
1665
+ return predicate.branches.flatMap((row2) => [
1666
+ ...row2.condition == null ? [] : singletonLiteralsInCondition(row2.condition),
1667
+ ...row2.consequence.flatMap((consequence) => conditionLiteralsIn(consequence))
1668
+ ]);
1684
1669
  }
1685
- function resolveCondition(condition, ctx) {
1670
+ function singletonLiteralsInCondition(condition) {
1686
1671
  switch (condition.kind) {
1687
1672
  case "singleton": {
1688
- return {
1689
- ...condition,
1690
- assertion: {
1691
- ...condition.assertion,
1692
- value: resolveValue(condition.assertion.value, ctx)
1693
- }
1694
- };
1673
+ const value2 = condition.assertion.value;
1674
+ return value2 == null || typeof value2 !== "object" ? [[condition.singleton, value2]] : [];
1695
1675
  }
1696
1676
  case "count": {
1697
- return condition;
1677
+ return [];
1698
1678
  }
1699
1679
  case "not": {
1700
- return { ...condition, predicate: resolveCondition(condition.predicate, ctx) };
1680
+ return singletonLiteralsInCondition(condition.predicate);
1701
1681
  }
1702
1682
  case "and": {
1703
- return {
1704
- ...condition,
1705
- predicates: condition.predicates.map((p) => resolveCondition(p, ctx))
1706
- };
1683
+ return condition.predicates.flatMap((p) => singletonLiteralsInCondition(p));
1707
1684
  }
1708
1685
  }
1709
1686
  }
1710
-
1711
- // src/workflow.ts
1712
- function workflow(intent, fn) {
1713
- const sourcePath = captureSourcePath();
1714
- if (fn == null) {
1715
- return { spec: stubSpec(intent, sourcePath) };
1716
- }
1717
- const final = finalize(fn());
1718
- return {
1719
- spec: {
1720
- absent: final.absent,
1721
- exclusive: final.exclusive,
1722
- intent,
1723
- maybe: final.maybe,
1724
- name: slugify(intent),
1725
- params: final.params,
1726
- singletons: final.singletons,
1727
- sourcePath,
1728
- steps: final.steps,
1729
- stub: false,
1730
- tests: [],
1731
- world: final.world
1732
- }
1733
- };
1734
- }
1735
- function stubSpec(intent, sourcePath) {
1736
- return {
1737
- absent: [],
1738
- exclusive: [],
1739
- intent,
1740
- maybe: [],
1741
- name: slugify(intent),
1742
- params: {},
1743
- singletons: {},
1744
- sourcePath,
1745
- steps: [],
1746
- stub: true,
1747
- tests: [],
1748
- world: []
1749
- };
1750
- }
1751
- var WORKFLOWS_ANCHOR_PATTERN = /[/\\]\.ripplo[/\\]workflows[/\\]([^):]+?)(?::\d+:\d+\)?)?$/;
1752
- function captureSourcePath() {
1753
- const stack = new Error("capture").stack;
1754
- if (stack == null) {
1687
+ function paramRefOf(value2) {
1688
+ if (value2 == null || typeof value2 !== "object" || !("ref" in value2)) {
1755
1689
  return void 0;
1756
1690
  }
1757
- const match = stack.split("\n").map((line) => WORKFLOWS_ANCHOR_PATTERN.exec(line)).find((m) => m != null);
1758
- const captured = match?.[1];
1759
- return captured == null ? void 0 : captured.replaceAll("\\", "/");
1691
+ return value2.ref.includes(".") ? void 0 : value2.ref;
1760
1692
  }
1761
- function slugify(intent) {
1762
- const slug = intent.toLowerCase().replaceAll(/[^a-z0-9]+/g, " ").trim().split(" ").join("-");
1763
- if (slug.length === 0) {
1764
- throw new Error(`workflow intent "${intent}" slugifies to an empty string`);
1693
+ function withComplements(values, enumValues) {
1694
+ if (enumValues != null) {
1695
+ return [.../* @__PURE__ */ new Set([...values, ...enumValues])];
1765
1696
  }
1766
- return slug;
1767
- }
1768
-
1769
- // src/params.ts
1770
- function arbitrary(field2) {
1771
- const token = {
1772
- base: `${field2.entity}_${field2.field}`,
1773
- valueSpace: field2.valueSpaceName
1774
- };
1775
- return paramBinding(token);
1776
- }
1777
-
1778
- // src/engine.ts
1779
- import { err, ok, okAsync, Result, ResultAsync } from "neverthrow";
1780
- function createEngine(ripplo, impls, teardown) {
1781
- const entities = new Map(Object.entries(impls.entities));
1782
- const singletons = new Map(
1783
- Object.entries(impls.singletons).map(([name, impl]) => [
1784
- name,
1785
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- per-name impls typed at the call site; erase the contravariant scalar seed to the loose dispatch shape
1786
- impl
1787
- ])
1788
- );
1789
- assertDeclared(ripplo, [...entities.keys()], [...singletons.keys()]);
1790
- const entityImpl = (name) => lookup(entities.get(name), name);
1791
- const singletonImpl = (name) => lookup(singletons.get(name), name);
1792
- return {
1793
- read: (request, runId) => ResultAsync.combine([
1794
- ResultAsync.combine(
1795
- request.entities.map((name) => readEntity(entityImpl(name), name, runId))
1796
- ),
1797
- ResultAsync.combine(
1798
- request.singletons.map((name) => readSingleton(singletonImpl(name), name, runId))
1799
- )
1800
- ]).map(([entityPairs, singletonPairs]) => ({
1801
- entities: Object.fromEntries(entityPairs),
1802
- singletons: Object.fromEntries(singletonPairs)
1803
- })),
1804
- seed: (request, runId) => ResultAsync.fromPromise(teardown(runId), implFailure).andThen(() => seedSingletons(request.singletons, singletonImpl, runId)).andThen(() => seedSetups(request.entities, entityImpl, runId)),
1805
- teardown: (runId) => ResultAsync.fromPromise(teardown(runId), implFailure)
1806
- };
1807
- }
1808
- function assertDeclared(ripplo, entityNames, singletonNames) {
1809
- const backendEntities = new Set(
1810
- ripplo.entities.filter((e) => e.schema.source === "backend").map((e) => e.name)
1811
- );
1812
- const backendSingletons = new Set(
1813
- ripplo.singletons.filter((s) => s.schema.source === "backend").map((s) => s.name)
1814
- );
1815
- entityNames.forEach((name) => {
1816
- if (!backendEntities.has(name)) {
1817
- throw new Error(`engine impl "${name}" has no matching backend entity`);
1697
+ const complements = values.flatMap((value2) => {
1698
+ if (typeof value2 === "boolean") {
1699
+ return [!value2];
1700
+ }
1701
+ if (typeof value2 === "number") {
1702
+ return [value2 + 1];
1818
1703
  }
1819
- });
1820
- singletonNames.forEach((name) => {
1821
- if (!backendSingletons.has(name)) {
1822
- throw new Error(`engine impl "${name}" has no matching backend singleton`);
1704
+ if (typeof value2 === "string") {
1705
+ return [syntheticDistinct(value2, values)];
1823
1706
  }
1707
+ return [];
1824
1708
  });
1709
+ return [.../* @__PURE__ */ new Set([...values, ...complements])];
1825
1710
  }
1826
- function lookup(impl, name) {
1827
- return impl == null ? err({ message: `no engine impl for "${name}"` }) : ok(impl);
1711
+ function syntheticDistinct(seed, taken) {
1712
+ const candidate = `${seed}-alt`;
1713
+ return taken.includes(candidate) ? syntheticDistinct(candidate, taken) : candidate;
1828
1714
  }
1829
- function readEntity(impl, name, runId) {
1830
- return impl.asyncAndThen(
1831
- (i) => ResultAsync.fromPromise(i.read({ runId }), implFailure).map((rows) => [
1832
- name,
1833
- [...rows]
1834
- ])
1715
+ function simulate(workflow2, candidate) {
1716
+ const initial = {
1717
+ choices: /* @__PURE__ */ new Map(),
1718
+ state: {
1719
+ rows: [...workflow2.world, ...candidate.maybes],
1720
+ singles: seedSingles(workflow2.singletons, candidate.pins)
1721
+ }
1722
+ };
1723
+ const folded = workflow2.steps.reduce(
1724
+ (acc, step) => acc == null ? null : stepSim(acc, step),
1725
+ initial
1835
1726
  );
1727
+ return folded == null ? null : { candidate, choices: folded.choices };
1836
1728
  }
1837
- function implFailure(error) {
1838
- return { message: error instanceof Error ? error.message : String(error) };
1839
- }
1840
- function readSingleton(impl, name, runId) {
1841
- return impl.asyncAndThen(
1842
- (i) => ResultAsync.fromPromise(i.read({ runId }), implFailure).map((value2) => [
1843
- name,
1844
- value2
1845
- ])
1729
+ function seedSingles(singletons, pins) {
1730
+ return Object.fromEntries(
1731
+ Object.entries(singletons).map(([name, value2]) => {
1732
+ const param = paramRefOf(value2);
1733
+ return [name, param != null && param in pins ? pins[param] ?? null : value2];
1734
+ })
1846
1735
  );
1847
1736
  }
1848
- function seedSingletons(values, singletonImpl, runId) {
1849
- return ResultAsync.combine(
1850
- Object.entries(values).map(
1851
- ([name, value2]) => singletonImpl(name).asyncAndThen(
1852
- (impl) => ResultAsync.fromPromise(impl.seed({ runId, value: value2 }), implFailure)
1853
- )
1854
- )
1737
+ function stepSim(acc, step) {
1738
+ const rows = step.expect.filter((p) => p.kind === "state").reduce((current, effect) => applyEffect(current, effect), acc.state.rows);
1739
+ const preWhens = { rows, singles: acc.state.singles };
1740
+ const whens = step.expect.filter((p) => p.kind === "when");
1741
+ const resolved = whens.reduce((current, when2) => current == null ? null : foldWhen(current, when2, preWhens), {
1742
+ choices: acc.choices,
1743
+ sets: {}
1744
+ });
1745
+ if (resolved == null) {
1746
+ return null;
1747
+ }
1748
+ const immediate = singletonSets(step.expect);
1749
+ return {
1750
+ choices: resolved.choices,
1751
+ state: { rows, singles: { ...preWhens.singles, ...immediate, ...resolved.sets } }
1752
+ };
1753
+ }
1754
+ function foldWhen(acc, when2, state) {
1755
+ const picked = pickBranch(when2, state);
1756
+ if (picked === "unknown") {
1757
+ return null;
1758
+ }
1759
+ if (picked == null) {
1760
+ return acc;
1761
+ }
1762
+ const row2 = when2.branches[picked];
1763
+ if (row2 == null) {
1764
+ return acc;
1765
+ }
1766
+ const choices = new Map([...acc.choices, [when2, picked]]);
1767
+ return row2.consequence.reduce(
1768
+ (folded, consequence) => {
1769
+ if (folded == null) {
1770
+ return null;
1771
+ }
1772
+ if (consequence.kind === "when") {
1773
+ return foldWhen(folded, consequence, state);
1774
+ }
1775
+ return { choices: folded.choices, sets: { ...folded.sets, ...singletonSets([consequence]) } };
1776
+ },
1777
+ { choices, sets: acc.sets }
1855
1778
  );
1856
1779
  }
1857
- function seedSetups(specs, entityImpl, runId) {
1858
- const waves = dependencyWaves(specs);
1859
- return waves.reduce((accR, wave) => accR.andThen((acc) => seedWave(wave, entityImpl, acc, runId)), okAsync({ env: /* @__PURE__ */ new Map(), rows: [] })).map((fold) => orderRows(specs, fold.rows));
1780
+ function pickBranch(when2, state) {
1781
+ return when2.branches.reduce((picked, row2, index) => {
1782
+ if (picked !== void 0) {
1783
+ return picked;
1784
+ }
1785
+ if (row2.condition == null) {
1786
+ return index;
1787
+ }
1788
+ const holds = evalCondition(row2.condition, state);
1789
+ if (holds === "unknown") {
1790
+ return "unknown";
1791
+ }
1792
+ return holds ? index : void 0;
1793
+ }, void 0);
1860
1794
  }
1861
- function dependencyWaves(specs) {
1862
- return buildWaves([], specs);
1795
+ function evalCondition(condition, state) {
1796
+ switch (condition.kind) {
1797
+ case "count": {
1798
+ return state.rows.filter((row2) => row2.entity === condition.entity).length === condition.value;
1799
+ }
1800
+ case "singleton": {
1801
+ return compareValues(state.singles[condition.singleton], condition.assertion.value);
1802
+ }
1803
+ case "not": {
1804
+ return negate(evalCondition(condition.predicate, state));
1805
+ }
1806
+ case "and": {
1807
+ return conjoin(condition.predicates.map((p) => evalCondition(p, state)));
1808
+ }
1809
+ }
1863
1810
  }
1864
- function buildWaves(done, remaining) {
1865
- if (remaining.length === 0) {
1866
- return done;
1811
+ function compareValues(seeded, wanted) {
1812
+ if (seeded === void 0) {
1813
+ return "unknown";
1867
1814
  }
1868
- const seeded = new Set(done.flat().map((spec) => spec.as));
1869
- const ready = remaining.filter((spec) => refAliases(spec).every((alias) => seeded.has(alias)));
1870
- if (ready.length === 0) {
1871
- return [...done, remaining];
1815
+ const seededRef = isRefValue(seeded);
1816
+ const wantedRef = isRefValue(wanted);
1817
+ if (seededRef != null && wantedRef != null) {
1818
+ return seededRef === wantedRef ? true : "unknown";
1872
1819
  }
1873
- return buildWaves(
1874
- [...done, ready],
1875
- remaining.filter((spec) => !ready.includes(spec))
1876
- );
1820
+ if (seededRef != null || wantedRef != null || isTemplateValue(seeded) || isTemplateValue(wanted)) {
1821
+ return "unknown";
1822
+ }
1823
+ return sameSetValue(seeded, wanted);
1877
1824
  }
1878
- function refAliases(spec) {
1879
- return Object.values(spec.fields).flatMap((value2) => {
1880
- if (value2 === null || typeof value2 !== "object" || !("ref" in value2)) {
1881
- return [];
1882
- }
1883
- const lastDot = value2.ref.lastIndexOf(".");
1884
- return lastDot === -1 ? [] : [value2.ref.slice(0, lastDot)];
1885
- });
1825
+ function isTemplateValue(value2) {
1826
+ return value2 != null && typeof value2 === "object" && "template" in value2;
1886
1827
  }
1887
- function seedWave(wave, entityImpl, acc, runId) {
1888
- return ResultAsync.combine(
1889
- wave.map((spec) => seedSetup(entityImpl(spec.entity), acc, spec, runId))
1890
- ).map((folds) => mergeFolds(acc, folds));
1828
+ function negate(value2) {
1829
+ return value2 === "unknown" ? "unknown" : !value2;
1891
1830
  }
1892
- function seedSetup(impl, acc, spec, runId) {
1893
- return resolveFields(spec.fields, acc.env).asyncAndThen((fields) => runSeed(impl, fields, runId)).map(({ row: row2, session }) => foldRow(acc, spec, row2, session));
1831
+ function conjoin(values) {
1832
+ if (values.includes(false)) {
1833
+ return false;
1834
+ }
1835
+ return values.every((v2) => v2 === true) ? true : "unknown";
1894
1836
  }
1895
- function resolveFields(fields, env) {
1896
- return Result.combine(
1897
- Object.entries(fields).map(
1898
- ([field2, value2]) => resolveValue2(value2, env).map((cell2) => [field2, cell2])
1899
- )
1900
- ).map((entries) => Object.fromEntries(entries));
1837
+ function singletonSets(predicates) {
1838
+ return Object.fromEntries(
1839
+ predicates.flatMap((predicate) => {
1840
+ if (predicate.kind === "singleton") {
1841
+ return [[predicate.singleton, predicate.assertion.value]];
1842
+ }
1843
+ return [];
1844
+ })
1845
+ );
1901
1846
  }
1902
- function resolveValue2(value2, env) {
1903
- if (value2 === null || typeof value2 !== "object") {
1904
- return ok(value2);
1847
+ function applyEffect(rows, effect) {
1848
+ if (effect.assertion.kind === "created") {
1849
+ return [
1850
+ ...rows,
1851
+ { as: effect.assertion.as, entity: effect.entity, set: effect.assertion.props }
1852
+ ];
1905
1853
  }
1906
- if ("ref" in value2) {
1907
- return env.has(value2.ref) ? ok(env.get(value2.ref) ?? null) : err({ message: `setup ref "${value2.ref}" was not produced by an earlier entity` });
1854
+ if (effect.assertion.kind === "deleted") {
1855
+ return rows.filter((row2) => row2.entity !== effect.entity || !matchesKey(row2, effect.key, rows));
1908
1856
  }
1909
- throw new Error("internal: a setup value resolved to an unresolved template");
1857
+ return rows;
1910
1858
  }
1911
- function runSeed(impl, fields, runId) {
1912
- return impl.asyncAndThen((i) => ResultAsync.fromPromise(i.seed({ fields, runId }), implFailure));
1859
+ function matchesKey(row2, key2, rows) {
1860
+ return Object.entries(key2).every(([field2, want]) => matchesField({ field: field2, row: row2, rows, want }));
1913
1861
  }
1914
- function foldRow(acc, spec, row2, session) {
1915
- const next = Object.entries(row2).map(([field2, value2]) => [
1916
- `${spec.as}.${field2}`,
1917
- value2
1918
- ]);
1862
+ function matchesField({ field: field2, row: row2, rows, want }) {
1863
+ if (isWithinValue(want)) {
1864
+ const candidates = rows.filter(
1865
+ (candidate) => candidate.entity === want.selection.entity && matchesKey(candidate, want.selection.where, rows)
1866
+ ).map((candidate) => fieldValue(candidate, want.field));
1867
+ const own = fieldValue(row2, field2);
1868
+ return candidates.some((candidate) => valuesEqual(own, candidate));
1869
+ }
1870
+ return valuesEqual(fieldValue(row2, field2), want);
1871
+ }
1872
+ function isWithinValue(value2) {
1873
+ return value2 != null && typeof value2 === "object" && "kind" in value2;
1874
+ }
1875
+ function fieldValue(row2, field2) {
1876
+ return row2.set[field2] ?? { ref: `${row2.as}.${field2}` };
1877
+ }
1878
+ function valuesEqual(a, b) {
1879
+ const aRef = isRefValue(a);
1880
+ const bRef = isRefValue(b);
1881
+ if (aRef != null || bRef != null) {
1882
+ return aRef === bRef;
1883
+ }
1884
+ if (isTemplateValue(a) || isTemplateValue(b)) {
1885
+ return false;
1886
+ }
1887
+ return sameSetValue(a, b);
1888
+ }
1889
+ function resolveTest(workflow2, sim, name) {
1890
+ const steps = workflow2.steps.map((step) => ({
1891
+ action: step.action,
1892
+ expect: step.expect.flatMap((predicate) => resolvePredicate(predicate, sim.choices))
1893
+ }));
1894
+ const world = [...workflow2.world, ...sim.candidate.maybes];
1895
+ const singletons = seedSingles(workflow2.singletons, sim.candidate.pins);
1896
+ const used = usedRefHeads({ singletons, steps, workflow: workflow2, world });
1919
1897
  return {
1920
- env: new Map([...acc.env, ...next]),
1921
- rows: [...acc.rows, { as: spec.as, row: row2, session }]
1898
+ absent: workflow2.absent,
1899
+ exclusive: workflow2.exclusive,
1900
+ intent: workflow2.intent,
1901
+ name,
1902
+ params: Object.fromEntries(
1903
+ Object.entries(workflow2.params).filter(([param]) => used.has(param))
1904
+ ),
1905
+ singletons,
1906
+ slug: slugify(name),
1907
+ steps,
1908
+ workflow: workflow2.name,
1909
+ world
1922
1910
  };
1923
1911
  }
1924
- function mergeFolds(base, folds) {
1925
- return {
1926
- env: new Map([...base.env, ...folds.flatMap((fold) => [...fold.env])]),
1927
- rows: [...base.rows, ...folds.flatMap((fold) => fold.rows.slice(base.rows.length))]
1928
- };
1912
+ function resolvePredicate(predicate, choices) {
1913
+ if (predicate.kind === "not") {
1914
+ const inner = resolvePredicate(predicate.predicate, choices);
1915
+ return inner.map((p) => ({ kind: "not", predicate: p }));
1916
+ }
1917
+ if (predicate.kind === "and") {
1918
+ return [
1919
+ {
1920
+ kind: "and",
1921
+ predicates: predicate.predicates.flatMap((p) => resolvePredicate(p, choices))
1922
+ }
1923
+ ];
1924
+ }
1925
+ if (predicate.kind !== "when") {
1926
+ return [predicate];
1927
+ }
1928
+ const picked = choices.get(predicate);
1929
+ const row2 = picked == null ? void 0 : predicate.branches[picked];
1930
+ return row2 == null ? [] : row2.consequence.flatMap((c) => resolvePredicate(c, choices));
1929
1931
  }
1930
- function orderRows(specs, rows) {
1931
- const byAs = new Map(rows.map((row2) => [row2.as, row2]));
1932
- return specs.flatMap((spec) => {
1933
- const row2 = byAs.get(spec.as);
1934
- return row2 == null ? [] : [row2];
1932
+
1933
+ // src/build.ts
1934
+ function buildLockfile(input) {
1935
+ assertUniqueNames(input.entities);
1936
+ const lockfile = lockfileSchema.parse({
1937
+ entities: input.entities.map((handle) => handle.schema),
1938
+ singletons: input.singletons.map((handle) => handle.schema),
1939
+ valueSpaces: dedupeByName([
1940
+ ...input.entities.flatMap((handle) => handle.valueSpaces),
1941
+ ...input.singletons.flatMap((handle) => handle.valueSpaces)
1942
+ ]),
1943
+ workflows: input.workflows.map(
1944
+ (ripploWorkflow) => expandWorkflow(ripploWorkflow.spec, { singletonValues: singletonValuesOf(input.singletons) })
1945
+ )
1935
1946
  });
1947
+ assertNoContradictions(lockfile);
1948
+ return lockfile;
1936
1949
  }
1937
-
1938
- // src/client-engine.ts
1939
- import { z as z8 } from "zod";
1940
-
1941
- // ../spec/src/leaves.ts
1942
- import { z } from "zod";
1943
- var budgetSchema = z.enum(["fast", "slow", "async"]);
1944
- var valueRefSchema = z.object({ ref: z.string().min(1) });
1945
- var primitiveSchema = z.union([z.string(), z.number(), z.boolean()]);
1946
- var templateSchema = z.object({
1947
- template: z.array(z.union([z.string(), valueRefSchema])).min(1)
1948
- });
1949
- var setValueSchema = z.union([valueRefSchema, primitiveSchema, templateSchema, z.null()]);
1950
- var changedSchema = z.object({ kind: z.literal("changed") });
1951
- var updateValueSchema = z.union([setValueSchema, changedSchema]);
1952
- var stringValueSchema = z.union([z.string(), valueRefSchema, templateSchema]);
1953
- var roleLocatorSchema = z.object({
1954
- by: z.literal("role"),
1955
- name: z.union([stringValueSchema, z.undefined()]).optional().transform((value2) => value2),
1956
- role: z.string().min(1)
1957
- });
1958
- var testIdLocatorSchema = z.object({ by: z.literal("testId"), value: stringValueSchema });
1959
- var insideLocatorSchema = z.object({
1960
- by: z.literal("inside"),
1961
- scope: z.lazy(() => locatorSchema),
1962
- target: z.lazy(() => locatorSchema)
1963
- });
1964
- var locatorSchema = z.discriminatedUnion("by", [
1965
- roleLocatorSchema,
1966
- testIdLocatorSchema,
1967
- insideLocatorSchema
1968
- ]);
1969
- var primitiveTypeSchema = z.enum(["string", "number", "boolean"]);
1970
- var consistencyClassSchema = z.enum(["strict", "eventual"]);
1971
- var stringConstraintsSchema = z.object({
1972
- kind: z.literal("string"),
1973
- maxLength: z.number().int().positive().optional(),
1974
- minLength: z.number().int().nonnegative().optional(),
1975
- pattern: z.string().optional()
1976
- });
1977
- var numberConstraintsSchema = z.object({
1978
- kind: z.literal("number"),
1979
- max: z.number().int().optional(),
1980
- min: z.number().int().optional()
1981
- });
1982
- var datetimeConstraintsSchema = z.object({
1983
- kind: z.literal("datetime"),
1984
- maxOffsetDays: z.number().int(),
1985
- minOffsetDays: z.number().int()
1986
- });
1987
- var constraintsSchema = z.discriminatedUnion("kind", [
1988
- stringConstraintsSchema,
1989
- numberConstraintsSchema,
1990
- datetimeConstraintsSchema
1991
- ]);
1992
- var generatorSchema = z.enum([
1993
- "company.name",
1994
- "date.iso",
1995
- "internet.email",
1996
- "internet.url",
1997
- "person.fullName",
1998
- "lorem.slug",
1999
- "lorem.word"
2000
- ]);
2001
- var valueSpaceSchema = z.object({
2002
- constraints: constraintsSchema.optional(),
2003
- generator: generatorSchema,
2004
- name: z.string().min(1),
2005
- type: primitiveTypeSchema,
2006
- values: z.array(primitiveSchema).min(1).optional()
2007
- });
2008
- var propSpecSchema = z.object({
2009
- consistency: consistencyClassSchema.default("strict"),
2010
- optional: z.boolean(),
2011
- type: primitiveTypeSchema,
2012
- valueSpace: z.string().min(1).optional()
2013
- });
2014
- var sourceSchema = z.enum(["backend", "client"]);
2015
- var entitySchemaSchema = z.object({
2016
- description: z.string().optional(),
2017
- identity: z.array(z.string().min(1)).min(1),
2018
- identityKind: z.enum(["surrogate", "natural"]),
2019
- name: z.string().min(1),
2020
- props: z.record(z.string().min(1), propSpecSchema),
2021
- source: sourceSchema.default("backend")
2022
- });
2023
- var singletonSchemaSchema = z.object({
2024
- consistency: consistencyClassSchema.default("strict"),
2025
- default: primitiveSchema,
2026
- description: z.string().optional(),
2027
- name: z.string().min(1),
2028
- source: sourceSchema.default("backend"),
2029
- type: primitiveTypeSchema,
2030
- valueSpace: z.string().min(1).optional()
2031
- });
2032
- var browserSingletonSchema = z.enum(["url", "title", "viewport"]);
2033
-
2034
- // ../spec/src/predicate.ts
2035
- import { z as z2 } from "zod";
2036
- var stateAssertionSchema = z2.discriminatedUnion("kind", [
2037
- z2.object({
2038
- as: z2.string().min(1),
2039
- kind: z2.literal("created"),
2040
- props: z2.record(z2.string().min(1), setValueSchema)
2041
- }),
2042
- z2.object({
2043
- as: z2.string().min(1),
2044
- kind: z2.literal("updated"),
2045
- props: z2.record(z2.string().min(1), updateValueSchema)
2046
- }),
2047
- z2.object({ kind: z2.literal("deleted") })
2048
- ]);
2049
- var singletonAssertionSchema = z2.object({ kind: z2.literal("is"), value: setValueSchema });
2050
- var whereValueSchema = z2.lazy(
2051
- () => z2.union([setValueSchema, withinSchema])
2052
- );
2053
- var selectionSchema = z2.object({
2054
- entity: z2.string().min(1),
2055
- where: z2.record(z2.string().min(1), whereValueSchema)
2056
- });
2057
- var withinSchema = z2.object({
2058
- field: z2.string().min(1),
2059
- kind: z2.literal("within"),
2060
- selection: selectionSchema
2061
- });
2062
- var wait = z2.union([budgetSchema, z2.undefined()]).optional().transform((value2) => value2);
2063
- var singletonPredicateSchema = z2.object({
2064
- assertion: singletonAssertionSchema,
2065
- kind: z2.literal("singleton"),
2066
- singleton: z2.string().min(1),
2067
- wait
2068
- });
2069
- var countPredicateSchema = z2.object({
2070
- entity: z2.string().min(1),
2071
- kind: z2.literal("count"),
2072
- value: z2.number().int().nonnegative()
2073
- });
2074
- var conditionSchema = z2.lazy(
2075
- () => z2.discriminatedUnion("kind", [
2076
- singletonPredicateSchema,
2077
- countPredicateSchema,
2078
- z2.object({ kind: z2.literal("not"), predicate: conditionSchema }),
2079
- z2.object({ kind: z2.literal("and"), predicates: z2.array(conditionSchema) })
2080
- ])
2081
- );
2082
- var whenBranchSchema = z2.lazy(
2083
- () => z2.object({
2084
- condition: z2.union([conditionSchema, z2.undefined()]).optional().transform((value2) => value2),
2085
- consequence: z2.array(predicateSchema),
2086
- name: z2.string().min(1)
2087
- })
2088
- );
2089
- var predicateSchema = z2.lazy(
2090
- () => z2.discriminatedUnion("kind", [
2091
- z2.object({ kind: z2.literal("visible"), locator: locatorSchema, wait }),
2092
- z2.object({ kind: z2.literal("disabled"), locator: locatorSchema, wait }),
2093
- z2.object({ kind: z2.literal("enabled"), locator: locatorSchema, wait }),
2094
- z2.object({ kind: z2.literal("focused"), locator: locatorSchema, wait }),
2095
- z2.object({ kind: z2.literal("checked"), locator: locatorSchema, wait }),
2096
- z2.object({ kind: z2.literal("value"), locator: locatorSchema, value: stringValueSchema, wait }),
2097
- z2.object({ kind: z2.literal("text"), locator: locatorSchema, value: stringValueSchema, wait }),
2098
- singletonPredicateSchema,
2099
- z2.object({
2100
- kind: z2.literal("browser"),
2101
- name: browserSingletonSchema,
2102
- value: stringValueSchema,
2103
- wait
2104
- }),
2105
- z2.object({
2106
- assertion: stateAssertionSchema,
2107
- entity: z2.string().min(1),
2108
- key: z2.record(z2.string().min(1), whereValueSchema),
2109
- kind: z2.literal("state"),
2110
- wait
2111
- }),
2112
- z2.object({ kind: z2.literal("not"), predicate: predicateSchema }),
2113
- z2.object({ kind: z2.literal("and"), predicates: z2.array(predicateSchema) }),
2114
- countPredicateSchema,
2115
- z2.object({ branches: z2.array(whenBranchSchema), kind: z2.literal("when") })
2116
- ])
2117
- );
2118
-
2119
- // ../spec/src/codec.ts
2120
- import { z as z3 } from "zod";
2121
- var envelopeSchema = z3.object({
2122
- __codec: z3.string().min(1),
2123
- data: z3.unknown(),
2124
- version: z3.number().int().positive()
2125
- });
2126
- var CodecVersionError = class extends Error {
2127
- codec;
2128
- currentVersion;
2129
- gotVersion;
2130
- constructor(params) {
2131
- super(
2132
- `Unsupported ${params.codec} version ${String(params.gotVersion)} (current ${String(params.currentVersion)}). Upgrade Ripplo or rebuild with a compatible CLI.`
2133
- );
2134
- this.name = "CodecVersionError";
2135
- this.codec = params.codec;
2136
- this.currentVersion = params.currentVersion;
2137
- this.gotVersion = params.gotVersion;
2138
- }
2139
- };
2140
- var CodecMismatchError = class extends Error {
2141
- constructor(params) {
2142
- super(`Codec mismatch: expected "${params.expected}", got "${params.got}"`);
2143
- this.name = "CodecMismatchError";
1950
+ function assertUniqueNames(entities) {
1951
+ const names = entities.map((handle) => handle.schema.name);
1952
+ const duplicate = names.find((name, index) => names.indexOf(name) !== index);
1953
+ if (duplicate != null) {
1954
+ throw new Error(`duplicate entity name "${duplicate}" \u2014 each entity name must be unique`);
2144
1955
  }
2145
- };
2146
- function defineCodec({
2147
- name,
2148
- schema
2149
- }) {
1956
+ }
1957
+ function dedupeByName(spaces) {
1958
+ const byName = new Map(spaces.map((space) => [space.name, space]));
1959
+ return [...byName.values()];
1960
+ }
1961
+ function assertNoContradictions(lockfile) {
1962
+ lockfile.workflows.forEach((workflow2) => {
1963
+ assertNoContradiction(workflow2);
1964
+ assertNoDanglingRefs(workflow2);
1965
+ });
1966
+ }
1967
+ function assertNoContradiction(workflow2) {
1968
+ const setups = [...workflow2.world, ...workflow2.maybe];
1969
+ workflow2.absent.forEach((absence) => {
1970
+ const clash = setups.find(
1971
+ (setup) => setup.entity === absence.entity && whereMatches(absence.where, setup.set)
1972
+ );
1973
+ if (clash != null) {
1974
+ throw new Error(
1975
+ `test "${workflow2.name}": creates a "${absence.entity}" ("${clash.as}") that a none(${absence.entity}, \u2026) in its world forbids`
1976
+ );
1977
+ }
1978
+ });
1979
+ }
1980
+ function whereMatches(where, set) {
1981
+ return Object.entries(where).every(([field2, want]) => sameValue(want, set[field2]));
1982
+ }
1983
+ function sameValue(a, b) {
1984
+ return b !== void 0 && sameSetValue(a, b);
1985
+ }
1986
+ function assertNoDanglingRefs(workflow2) {
1987
+ const aliases = new Set([...workflow2.world, ...workflow2.maybe].map((setup) => setup.as));
1988
+ const paramKeys = new Set(Object.keys(workflow2.params));
1989
+ const fieldSets = [
1990
+ ...workflow2.world.map((setup) => setup.set),
1991
+ ...workflow2.maybe.map((setup) => setup.set),
1992
+ ...workflow2.absent.map((absence) => absence.where)
1993
+ ];
1994
+ fieldSets.forEach((set) => {
1995
+ assertSetRefs(workflow2.name, set, aliases, paramKeys);
1996
+ });
1997
+ }
1998
+ function assertSetRefs(workflowName, set, aliases, paramKeys) {
1999
+ Object.values(set).forEach((value2) => {
2000
+ if (!isRef(value2) || paramKeys.has(value2.ref)) {
2001
+ return;
2002
+ }
2003
+ const lastDot = value2.ref.lastIndexOf(".");
2004
+ if (lastDot === -1) {
2005
+ return;
2006
+ }
2007
+ const aliasPath = value2.ref.slice(0, lastDot);
2008
+ if (!aliases.has(aliasPath)) {
2009
+ throw new Error(
2010
+ `test "${workflowName}": ref "${value2.ref}" points at unknown alias "${aliasPath}"`
2011
+ );
2012
+ }
2013
+ });
2014
+ }
2015
+ function isRef(value2) {
2016
+ return value2 != null && typeof value2 === "object" && "ref" in value2;
2017
+ }
2018
+
2019
+ // src/ripplo.ts
2020
+ function createRipplo(input) {
2150
2021
  return {
2151
- currentVersion: 1,
2152
- name,
2153
- decode: (raw) => decode({ name, raw, schema }),
2154
- encode: (value2) => ({
2155
- __codec: name,
2156
- data: value2,
2157
- version: 1
2158
- })
2022
+ entities: input.entities,
2023
+ lockfile: buildLockfile({
2024
+ entities: input.entities,
2025
+ singletons: input.singletons,
2026
+ workflows: input.workflows
2027
+ }),
2028
+ singletons: input.singletons,
2029
+ workflows: input.workflows
2159
2030
  };
2160
2031
  }
2161
- function decode({
2162
- name,
2163
- raw,
2164
- schema
2165
- }) {
2166
- const envelope = envelopeSchema.parse(raw);
2167
- if (envelope.__codec !== name) {
2168
- throw new CodecMismatchError({ expected: name, got: envelope.__codec });
2169
- }
2170
- if (envelope.version !== 1) {
2171
- throw new CodecVersionError({ codec: name, currentVersion: 1, gotVersion: envelope.version });
2032
+
2033
+ // src/locators.ts
2034
+ function role(roleName, name, ...bindings) {
2035
+ return { by: "role", name: name == null ? void 0 : nameValue(name, bindings), role: roleName };
2036
+ }
2037
+ function inside(scope, target) {
2038
+ return { by: "inside", scope, target };
2039
+ }
2040
+ function testId(value2, ...bindings) {
2041
+ if (typeof value2 === "string") {
2042
+ return { by: "testId", value: value2 };
2172
2043
  }
2173
- return schema.parse(envelope.data);
2044
+ return { by: "testId", value: stringValueFromTemplate(value2, bindings) };
2174
2045
  }
2175
-
2176
- // ../spec/src/client-channel.ts
2177
- var CLIENT_MOUNT_KEY = "__ripplo__";
2178
- var CLIENT_SEED_KEY = "__ripplo_seed__";
2179
-
2180
- // ../spec/src/lockfile.ts
2181
- import { z as z4 } from "zod";
2182
- var actionSchema = z4.discriminatedUnion("kind", [
2183
- z4.object({ kind: z4.literal("goto"), url: stringValueSchema }),
2184
- z4.object({ kind: z4.literal("fill"), locator: locatorSchema, value: setValueSchema }),
2185
- z4.object({ kind: z4.literal("clear"), locator: locatorSchema }),
2186
- z4.object({ kind: z4.literal("click"), locator: locatorSchema }),
2187
- z4.object({ kind: z4.literal("dblclick"), locator: locatorSchema }),
2188
- z4.object({ kind: z4.literal("select"), locator: locatorSchema, value: setValueSchema }),
2189
- z4.object({ kind: z4.literal("check"), locator: locatorSchema }),
2190
- z4.object({ kind: z4.literal("uncheck"), locator: locatorSchema }),
2191
- z4.object({ kind: z4.literal("hover"), locator: locatorSchema }),
2192
- z4.object({
2193
- files: z4.array(z4.string().min(1)).min(1),
2194
- kind: z4.literal("upload"),
2195
- locator: locatorSchema
2196
- }),
2197
- z4.object({ key: z4.string().min(1), kind: z4.literal("press"), locator: locatorSchema.optional() })
2198
- ]);
2199
- var stepSchema = z4.object({
2200
- action: actionSchema,
2201
- expect: z4.array(predicateSchema).default([])
2202
- });
2203
- var paramSchema = z4.object({
2204
- valueSpace: z4.string().min(1)
2205
- });
2206
- var setupSchema = z4.object({
2207
- as: z4.string().min(1),
2208
- entity: z4.string().min(1),
2209
- set: z4.record(z4.string().min(1), setValueSchema)
2210
- });
2211
- var absenceSchema = z4.object({
2212
- entity: z4.string().min(1),
2213
- where: z4.record(z4.string().min(1), setValueSchema)
2214
- });
2215
- var resolvedTestSchema = z4.object({
2216
- absent: z4.array(absenceSchema).default([]),
2217
- exclusive: z4.array(z4.string().min(1)).default([]),
2218
- intent: z4.string().min(1),
2219
- name: z4.string().min(1),
2220
- params: z4.record(z4.string().min(1), paramSchema),
2221
- singletons: z4.record(z4.string().min(1), setValueSchema).default({}),
2222
- slug: z4.string().min(1),
2223
- steps: z4.array(stepSchema).default([]),
2224
- workflow: z4.string().min(1),
2225
- world: z4.array(setupSchema).default([])
2226
- });
2227
- var workflowSchema = z4.object({
2228
- absent: z4.array(absenceSchema).default([]),
2229
- exclusive: z4.array(z4.string().min(1)).default([]),
2230
- intent: z4.string().min(1),
2231
- maybe: z4.array(setupSchema).default([]),
2232
- name: z4.string().min(1),
2233
- params: z4.record(z4.string().min(1), paramSchema),
2234
- singletons: z4.record(z4.string().min(1), setValueSchema).default({}),
2235
- sourcePath: z4.string().min(1).optional(),
2236
- steps: z4.array(stepSchema).default([]),
2237
- stub: z4.boolean().default(false),
2238
- tests: z4.array(resolvedTestSchema).default([]),
2239
- world: z4.array(setupSchema).default([])
2240
- });
2241
- var fixtureEntrySchema = z4.object({
2242
- sha256: z4.string().regex(/^[0-9a-f]{64}$/u),
2243
- size: z4.number().int().nonnegative()
2244
- });
2245
- var lockfileSchema = z4.object({
2246
- entities: z4.array(entitySchemaSchema),
2247
- fixtures: z4.record(z4.string().min(1), fixtureEntrySchema).default({}),
2248
- singletons: z4.array(singletonSchemaSchema).default([]),
2249
- valueSpaces: z4.array(valueSpaceSchema),
2250
- workflows: z4.array(workflowSchema)
2251
- });
2252
- var lockfileCodec = defineCodec({ name: "ripplo-lockfile", schema: lockfileSchema });
2253
-
2254
- // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/esm/wrapper.js
2255
- var import__ = __toESM(require_safe_stable_stringify(), 1);
2256
- var configure = import__.default.configure;
2257
-
2258
- // ../spec/src/sync-payload.ts
2259
- import { z as z5 } from "zod";
2260
- var stepDescriptorSchema = z5.object({
2261
- index: z5.number().int().nonnegative(),
2262
- kind: z5.string(),
2263
- target: z5.string(),
2264
- value: z5.string()
2265
- });
2266
- function slugify2(name) {
2267
- return name.toLowerCase().replaceAll(/[^a-z0-9]/g, "-").split("-").filter((part) => part.length > 0).join("-");
2046
+ var alertdialog = named("alertdialog");
2047
+ var button = named("button");
2048
+ var cell = named("cell");
2049
+ var checkbox = named("checkbox");
2050
+ var columnheader = named("columnheader");
2051
+ var combobox = named("combobox");
2052
+ var dialog = named("dialog");
2053
+ var heading = named("heading");
2054
+ var img = named("img");
2055
+ var link = named("link");
2056
+ var listitem = named("listitem");
2057
+ var menuitem = named("menuitem");
2058
+ var option = named("option");
2059
+ var radio = named("radio");
2060
+ var row = named("row");
2061
+ var searchbox = named("searchbox");
2062
+ var slider = named("slider");
2063
+ var spinbutton = named("spinbutton");
2064
+ var switchControl = named("switch");
2065
+ var tab = named("tab");
2066
+ var textbox = named("textbox");
2067
+ var treeitem = named("treeitem");
2068
+ var alert = container("alert");
2069
+ var banner = container("banner");
2070
+ var complementary = container("complementary");
2071
+ var contentinfo = container("contentinfo");
2072
+ var form = container("form");
2073
+ var grid = container("grid");
2074
+ var group = container("group");
2075
+ var list = container("list");
2076
+ var main = container("main");
2077
+ var menu = container("menu");
2078
+ var navigation = container("navigation");
2079
+ var progressbar = container("progressbar");
2080
+ var radiogroup = container("radiogroup");
2081
+ var region = container("region");
2082
+ var status = container("status");
2083
+ var table = container("table");
2084
+ var tablist = container("tablist");
2085
+ var tabpanel = container("tabpanel");
2086
+ var toolbar = container("toolbar");
2087
+ function named(roleName) {
2088
+ return (name, ...bindings) => role(roleName, name, ...bindings);
2089
+ }
2090
+ function container(roleName) {
2091
+ return (name, ...bindings) => role(roleName, name, ...bindings);
2092
+ }
2093
+ function nameValue(name, bindings) {
2094
+ if (typeof name === "string") {
2095
+ return name;
2096
+ }
2097
+ if (isBinding(name)) {
2098
+ return name;
2099
+ }
2100
+ return stringValueFromTemplate(name, bindings);
2268
2101
  }
2269
2102
 
2270
- // ../spec/src/session.ts
2271
- import { z as z6 } from "zod";
2272
- var sameSiteSchema = z6.enum(["Strict", "Lax", "None"]);
2273
- var cookieSchema = z6.object({
2274
- domain: z6.string().min(1),
2275
- expires: z6.number(),
2276
- httpOnly: z6.boolean(),
2277
- name: z6.string().min(1),
2278
- path: z6.string().min(1),
2279
- sameSite: sameSiteSchema,
2280
- secure: z6.boolean(),
2281
- value: z6.string()
2282
- });
2283
- var originSchema = z6.object({
2284
- localStorage: z6.array(z6.object({ name: z6.string().min(1), value: z6.string() })),
2285
- origin: z6.string().min(1)
2286
- });
2287
- var sessionSchema = z6.object({
2288
- cookies: z6.array(cookieSchema),
2289
- headers: z6.record(z6.string().min(1), z6.string()).optional(),
2290
- origins: z6.array(originSchema)
2291
- });
2292
-
2293
- // ../spec/src/engine.ts
2294
- import { z as z7 } from "zod";
2295
- var cellSchema = z7.union([primitiveSchema, z7.null()]);
2296
- var rowSchema = z7.record(z7.string().min(1), cellSchema);
2297
- var setupSpecSchema = z7.object({
2298
- as: z7.string().min(1),
2299
- entity: z7.string().min(1),
2300
- fields: z7.record(z7.string().min(1), setValueSchema)
2301
- });
2302
- var setupRequestSchema = z7.object({
2303
- entities: z7.array(setupSpecSchema),
2304
- runId: z7.string().min(1),
2305
- singletons: z7.record(z7.string().min(1), cellSchema).default({})
2306
- });
2307
- var setupRowSchema = z7.object({
2308
- as: z7.string().min(1),
2309
- row: rowSchema,
2310
- session: sessionSchema.optional()
2311
- });
2312
- var setupResponseSchema = z7.object({
2313
- rows: z7.array(setupRowSchema)
2314
- });
2315
- var stateRequestSchema = z7.object({
2316
- entities: z7.array(z7.string().min(1)),
2317
- runId: z7.string().min(1),
2318
- singletons: z7.array(z7.string().min(1)).default([])
2319
- });
2320
- var stateResponseSchema = z7.object({
2321
- entities: z7.record(z7.string().min(1), z7.array(rowSchema)),
2322
- singletons: z7.record(z7.string().min(1), cellSchema).default({})
2323
- });
2324
- var teardownRequestSchema = z7.object({
2325
- runId: z7.string().min(1)
2326
- });
2327
- var teardownResponseSchema = z7.object({
2328
- ok: z7.literal(true)
2329
- });
2103
+ // src/singleton.ts
2104
+ function singleton(name, config) {
2105
+ const valueSpaceName = `singleton.${name}`;
2106
+ const { constraints, generator, primitive } = config.value;
2107
+ const schema = {
2108
+ consistency: config.consistency ?? "strict",
2109
+ default: config.default,
2110
+ description: config.description,
2111
+ name,
2112
+ source: config.source,
2113
+ type: primitive,
2114
+ valueSpace: valueSpaceName
2115
+ };
2116
+ return {
2117
+ is: isPredicate,
2118
+ name,
2119
+ schema,
2120
+ source: config.source,
2121
+ value: { __t: void 0, entity: name, field: "value", primitive, valueSpaceName },
2122
+ valueSpaces: [{ constraints, generator, name: valueSpaceName, type: primitive }],
2123
+ of: (value2) => ({
2124
+ __entity: { kind: "singletonState", singleton: name, value: toSetValue(value2) },
2125
+ is: isPredicate
2126
+ })
2127
+ };
2128
+ function isPredicate(value2) {
2129
+ return condLeaf({
2130
+ assertion: { kind: "is", value: value2 },
2131
+ kind: "singleton",
2132
+ singleton: name,
2133
+ wait: void 0
2134
+ });
2135
+ }
2136
+ }
2330
2137
 
2331
- // src/client-engine.ts
2332
- var seedSchema = z8.record(z8.string(), z8.union([z8.string(), z8.number(), z8.boolean()])).catch({});
2333
- function createClientEngine(_ripplo, impls) {
2334
- const singletons = new Map(
2335
- Object.entries(impls.singletons).map(([name, impl]) => [
2138
+ // src/builtins.ts
2139
+ function browserSingleton(name) {
2140
+ return {
2141
+ name,
2142
+ is: (strings, ...values) => leaf({
2143
+ kind: "browser",
2336
2144
  name,
2337
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- per-name impls typed at the call site; erase the contravariant scalar seed to the loose dispatch shape
2338
- impl
2339
- ])
2340
- );
2341
- const entities = new Map(Object.entries(impls.entities));
2342
- const seed = seedSchema.parse(Reflect.get(globalThis, CLIENT_SEED_KEY));
2343
- Object.entries(seed).forEach(([name, value2]) => singletons.get(name)?.seed(value2));
2145
+ value: stringValueFromTemplate(strings, values),
2146
+ wait: void 0
2147
+ })
2148
+ };
2149
+ }
2150
+ var url = browserSingleton("url");
2151
+ var title = browserSingleton("title");
2152
+ var viewport = browserSingleton("viewport");
2153
+
2154
+ // src/actions.ts
2155
+ function goto(strings, ...values) {
2156
+ return stepBuilder({ kind: "goto", url: stringValueFromTemplate(strings, values) }, [], []);
2157
+ }
2158
+ function click(locator) {
2159
+ return stepBuilder({ kind: "click", locator }, [], []);
2160
+ }
2161
+ function dblclick(locator) {
2162
+ return stepBuilder({ kind: "dblclick", locator }, [], []);
2163
+ }
2164
+ function fill(locator, binding) {
2165
+ return stepBuilder({ kind: "fill", locator, value: binding }, [], []);
2166
+ }
2167
+ function clear(locator) {
2168
+ return stepBuilder({ kind: "clear", locator }, [], []);
2169
+ }
2170
+ function select(locator, binding) {
2171
+ return stepBuilder({ kind: "select", locator, value: binding }, [], []);
2172
+ }
2173
+ function check(locator) {
2174
+ return stepBuilder({ kind: "check", locator }, [], []);
2175
+ }
2176
+ function uncheck(locator) {
2177
+ return stepBuilder({ kind: "uncheck", locator }, [], []);
2178
+ }
2179
+ function hover(locator) {
2180
+ return stepBuilder({ kind: "hover", locator }, [], []);
2181
+ }
2182
+ function upload(locator, files) {
2183
+ return stepBuilder({ files: [...files], kind: "upload", locator }, [], []);
2184
+ }
2185
+ function press(pressKey, locator) {
2186
+ return stepBuilder({ key: pressKey, kind: "press", locator }, [], []);
2187
+ }
2188
+ function stepBuilder(action, expected, captures) {
2344
2189
  return {
2345
- readEntity: (name) => entities.get(name)?.read() ?? [],
2346
- readSingleton: (name) => singletons.get(name)?.read() ?? null
2190
+ captures,
2191
+ step: { action, expect: [...expected] },
2192
+ expect: (...predicates) => stepBuilder(
2193
+ action,
2194
+ [...expected, ...predicates.map((p) => toPredicate(p))],
2195
+ [...captures, ...predicates.flatMap((p) => isCapturePredicate(p) ? [captureOf(p)] : [])]
2196
+ )
2347
2197
  };
2348
2198
  }
2349
- function mountClientEngine(ripplo, impls, { enabled: enabled2 }) {
2350
- if (!enabled2) {
2351
- return;
2199
+
2200
+ // src/coherence.ts
2201
+ function assertEntityCoherence(entities) {
2202
+ const counts = entities.reduce(
2203
+ (acc, d) => new Map([...acc, [d.entity, (acc.get(d.entity) ?? 0) + 1]]),
2204
+ /* @__PURE__ */ new Map()
2205
+ );
2206
+ const over = entities.find((d) => d.kind === "only" && (counts.get(d.entity) ?? 0) > 1);
2207
+ if (over != null) {
2208
+ throw new Error(
2209
+ `entity "${over.entity}" is declared only() but seeded ${String(counts.get(over.entity) ?? 0)} times \u2014 only() means a single exclusive instance; use of() for multiple instances`
2210
+ );
2352
2211
  }
2353
- Reflect.set(globalThis, CLIENT_MOUNT_KEY, createClientEngine(ripplo, impls));
2354
2212
  }
2355
2213
 
2356
- // src/expand-refs.ts
2357
- function usedRefHeads({
2358
- singletons,
2359
- steps,
2360
- workflow: workflow2,
2361
- world
2362
- }) {
2363
- const values = [
2364
- ...world.flatMap((setup) => Object.values(setup.set)),
2365
- ...workflow2.absent.flatMap((absence) => Object.values(absence.where)),
2366
- ...Object.values(singletons),
2367
- ...steps.flatMap((step) => stepRefValues(step))
2214
+ // src/finalize.ts
2215
+ function finalize(body) {
2216
+ const descriptors = [...new Set(body.given.map((item) => item.__entity))];
2217
+ const absences = descriptors.filter((d) => d.kind === "none");
2218
+ const singletonStates = descriptors.filter(
2219
+ (d) => d.kind === "singletonState"
2220
+ );
2221
+ const entities = topoSort(
2222
+ descriptors.filter(
2223
+ (d) => d.kind === "of" || d.kind === "only" || d.kind === "maybe"
2224
+ )
2225
+ );
2226
+ assertNoMaybeRefs({ absences, entities, singletonStates });
2227
+ assertEntityCoherence(entities);
2228
+ const captures = body.steps.flatMap((builder) => builder.captures);
2229
+ assertScopedConditions(body.steps, new Set(singletonStates.map((s) => s.singleton)));
2230
+ const aliases = assignAliases([...entities, ...captures.map((c) => c.descriptor)]);
2231
+ const { names, params } = assignParams(allBindings(body, entities, absences, singletonStates));
2232
+ const ctx = {
2233
+ aliases,
2234
+ captures: new Map(
2235
+ captures.map((c) => [c.assertion, c.descriptor])
2236
+ ),
2237
+ params: names
2238
+ };
2239
+ return {
2240
+ absent: absences.map((a) => ({ entity: a.entity, where: setMap(a.where, ctx) })),
2241
+ exclusive: [...new Set(entities.filter((d) => d.kind === "only").map((d) => d.entity))],
2242
+ maybe: setupsOf(entities, "maybe", ctx),
2243
+ params,
2244
+ singletons: resolveSingletons(singletonStates, ctx),
2245
+ steps: body.steps.map((builder) => resolveStep(builder.step, ctx)),
2246
+ world: entities.filter((d) => d.kind === "of" || d.kind === "only").map((d) => ({ as: aliasFor(ctx, d), entity: d.entity, set: setMap(d.props, ctx) }))
2247
+ };
2248
+ }
2249
+ function resolveSingletons(states, ctx) {
2250
+ return states.reduce((acc, state) => {
2251
+ const value2 = resolveValue(state.value, ctx);
2252
+ const existing = acc[state.singleton];
2253
+ if (existing !== void 0 && !sameSetValue(existing, value2)) {
2254
+ throw new Error(
2255
+ `singleton "${state.singleton}" is given two conflicting values \u2014 a test may set each singleton to one value`
2256
+ );
2257
+ }
2258
+ return { ...acc, [state.singleton]: value2 };
2259
+ }, {});
2260
+ }
2261
+ function assertNoMaybeRefs({ absences, entities, singletonStates }) {
2262
+ const offenders = [
2263
+ ...entities.filter((d) => d.kind === "of" || d.kind === "only").flatMap((d) => maybeBindings(Object.values(d.props), `${d.kind}(${d.entity})`)),
2264
+ ...absences.flatMap((a) => maybeBindings(Object.values(a.where), `none(${a.entity})`)),
2265
+ ...singletonStates.flatMap((s) => maybeBindings([s.value], `singleton "${s.singleton}"`))
2368
2266
  ];
2369
- return new Set(values.flatMap((value2) => refStrings(value2)).map((ref) => headOf(ref)));
2267
+ const first = offenders[0];
2268
+ if (first == null || first.binding.__bind.kind !== "field") {
2269
+ return;
2270
+ }
2271
+ const { descriptor, field: field2 } = first.binding.__bind;
2272
+ throw new Error(
2273
+ `${first.site} references ${descriptor.entity}.${field2} from a maybe(${descriptor.entity}) \u2014 maybe entities are not materialized at setup; reference them only in steps`
2274
+ );
2370
2275
  }
2371
- function collectRefObjects(node) {
2372
- if (Array.isArray(node)) {
2373
- return node.flatMap((item) => collectRefObjects(item));
2276
+ function maybeBindings(values, site) {
2277
+ return values.flatMap((value2) => valueBindings(value2)).filter((b) => b.__bind.kind === "field" && b.__bind.descriptor.kind === "maybe").map((binding) => ({ binding, site }));
2278
+ }
2279
+ function valueBindings(value2) {
2280
+ if (isBinding(value2)) {
2281
+ return [value2];
2374
2282
  }
2375
- if (node == null || typeof node !== "object") {
2376
- return [];
2283
+ if (isTemplate(value2)) {
2284
+ return value2.template.flatMap((segment) => isBinding(segment) ? [segment] : []);
2377
2285
  }
2378
- if ("ref" in node && typeof node.ref === "string" && Object.keys(node).length === 1) {
2379
- return [node.ref];
2286
+ return [];
2287
+ }
2288
+ function isTemplate(value2) {
2289
+ return typeof value2 === "object" && value2 !== null && "template" in value2;
2290
+ }
2291
+ function topoSort(entities) {
2292
+ return emitReady([], entities);
2293
+ }
2294
+ function emitReady(done, remaining) {
2295
+ if (remaining.length === 0) {
2296
+ return [...done];
2380
2297
  }
2381
- return Object.values(node).flatMap((value2) => collectRefObjects(value2));
2298
+ const ready = remaining.filter(
2299
+ (d) => depsOf(d, remaining).every((dep) => done.includes(dep) || dep === d)
2300
+ );
2301
+ if (ready.length === 0) {
2302
+ throw new Error("cyclic dependency between given entities");
2303
+ }
2304
+ return emitReady(
2305
+ [...done, ...ready],
2306
+ remaining.filter((d) => !ready.includes(d))
2307
+ );
2382
2308
  }
2383
- function isRefValue(value2) {
2384
- return value2 != null && typeof value2 === "object" && "ref" in value2 ? value2.ref : void 0;
2309
+ function depsOf(descriptor, among) {
2310
+ return Object.values(descriptor.props).flatMap((v2) => valueBindings(v2)).flatMap((b) => b.__bind.kind === "field" ? [b.__bind.descriptor] : []).filter((target) => target !== descriptor && among.includes(target));
2385
2311
  }
2386
- function headOf(ref) {
2387
- const dot = ref.indexOf(".");
2388
- return dot === -1 ? ref : ref.slice(0, dot);
2312
+ function assertScopedConditions(steps, givenSingletons) {
2313
+ steps.flatMap((builder) => builder.step.expect).forEach((predicate) => {
2314
+ assertPredicateScoped(predicate, givenSingletons);
2315
+ });
2389
2316
  }
2390
- function stepRefValues(step) {
2391
- return collectRefObjects(step).map((ref) => ({ ref }));
2317
+ function assertPredicateScoped(predicate, given) {
2318
+ if (predicate.kind === "not") {
2319
+ assertPredicateScoped(predicate.predicate, given);
2320
+ return;
2321
+ }
2322
+ if (predicate.kind !== "when") {
2323
+ return;
2324
+ }
2325
+ predicate.branches.forEach((row2) => {
2326
+ const names = row2.condition == null ? [] : conditionSingletons(row2.condition);
2327
+ names.forEach((name) => {
2328
+ if (!given.has(name)) {
2329
+ throw new Error(
2330
+ `when() conditions on singleton "${name}", which is not in the workflow's given \u2014 add ${name}.of(...) to given`
2331
+ );
2332
+ }
2333
+ });
2334
+ row2.consequence.forEach((consequence) => {
2335
+ assertPredicateScoped(consequence, given);
2336
+ });
2337
+ });
2392
2338
  }
2393
- function refStrings(value2) {
2394
- const ref = isRefValue(value2);
2395
- if (ref != null) {
2396
- return [ref];
2339
+ function conditionSingletons(predicate) {
2340
+ if (predicate.kind === "singleton") {
2341
+ return [predicate.singleton];
2397
2342
  }
2398
- if (value2 != null && typeof value2 === "object" && "template" in value2) {
2399
- return value2.template.flatMap((segment) => typeof segment === "string" ? [] : [segment.ref]);
2343
+ if (predicate.kind === "not") {
2344
+ return conditionSingletons(predicate.predicate);
2345
+ }
2346
+ if (predicate.kind === "and") {
2347
+ return predicate.predicates.flatMap((p) => conditionSingletons(p));
2400
2348
  }
2401
2349
  return [];
2402
2350
  }
2403
-
2404
- // src/expand.ts
2405
- var MAX_CANDIDATES = 4096;
2406
- function singletonValuesOf(singletons) {
2407
- return new Map(
2408
- singletons.flatMap((singleton2) => {
2409
- const space = singleton2.valueSpaces.find((s) => s.name === singleton2.schema.valueSpace);
2410
- return space?.values == null ? [] : [[singleton2.schema.name, space.values]];
2411
- })
2351
+ function assignAliases(entities) {
2352
+ return entities.reduce(
2353
+ (acc, d) => {
2354
+ const ordinal = acc.counts[d.entity] ?? 0;
2355
+ return {
2356
+ aliases: new Map([...acc.aliases, [d, `${d.entity}_${String(ordinal)}`]]),
2357
+ counts: { ...acc.counts, [d.entity]: ordinal + 1 }
2358
+ };
2359
+ },
2360
+ { aliases: /* @__PURE__ */ new Map(), counts: {} }
2361
+ ).aliases;
2362
+ }
2363
+ function assignParams(bindings) {
2364
+ const unique = [...new Set(bindings.filter((b) => b.__bind.kind === "param"))];
2365
+ const assigned = unique.reduce(
2366
+ (acc, b) => {
2367
+ const token = tokenOf(b);
2368
+ const ordinal = acc.counts[token.base] ?? 0;
2369
+ const name = ordinal === 0 ? token.base : `${token.base}_${String(ordinal - 1)}`;
2370
+ if (name in acc.params) {
2371
+ throw new Error(
2372
+ `param name "${name}" collides with an existing param \u2014 rename the field so deduplicated param names stay unique`
2373
+ );
2374
+ }
2375
+ return {
2376
+ counts: { ...acc.counts, [token.base]: ordinal + 1 },
2377
+ names: new Map([...acc.names, [b, name]]),
2378
+ params: { ...acc.params, [name]: { valueSpace: token.valueSpace } }
2379
+ };
2380
+ },
2381
+ { counts: {}, names: /* @__PURE__ */ new Map(), params: {} }
2412
2382
  );
2383
+ return { names: assigned.names, params: assigned.params };
2413
2384
  }
2414
- function expandWorkflow(workflow2, options) {
2415
- if (workflow2.stub) {
2416
- return { ...workflow2, tests: [] };
2417
- }
2418
- assertUniqueBranchNames(workflow2);
2419
- const targets = collectTargets(workflow2.steps);
2420
- const whenIds = new Map(targets.map((target, index) => [target.when, index]));
2421
- const sims = proposeCandidates(workflow2, options).map((candidate) => simulate(workflow2, candidate)).filter((sim) => sim != null);
2422
- if (targets.length === 0) {
2423
- return { ...workflow2, tests: [requireMainTest(workflow2, sims)] };
2385
+ function tokenOf(binding) {
2386
+ if (binding.__bind.kind !== "param") {
2387
+ throw new Error("internal: expected a param binding");
2424
2388
  }
2425
- const { tests } = targets.reduce(
2426
- (acc, target) => coverTarget({ acc, sims, target, whenIds, workflow: workflow2 }),
2427
- { covered: /* @__PURE__ */ new Set(), tests: [] }
2428
- );
2429
- return { ...workflow2, tests };
2389
+ return binding.__bind.token;
2430
2390
  }
2431
- function resolveValidTest(workflow2, sim, name) {
2432
- const test = resolveTest(workflow2, sim, name);
2433
- return danglingRefHeads(test).length === 0 ? test : null;
2391
+ function allBindings(body, entities, absences, singletonStates) {
2392
+ return [
2393
+ ...entities.flatMap((d) => Object.values(d.props).flatMap((v2) => valueBindings(v2))),
2394
+ ...absences.flatMap((a) => Object.values(a.where).flatMap((v2) => valueBindings(v2))),
2395
+ ...singletonStates.flatMap((s) => valueBindings(s.value)),
2396
+ ...body.steps.flatMap((builder) => stepBindings(builder.step))
2397
+ ];
2434
2398
  }
2435
- function danglingRefHeads(test) {
2436
- const known = /* @__PURE__ */ new Set([
2437
- ...test.world.map((setup) => setup.as),
2438
- ...Object.keys(test.params),
2439
- ...test.steps.flatMap((step) => createdAliasesIn(step))
2440
- ]);
2441
- return [...new Set(collectRefObjects(test).map((ref) => headOf(ref)))].filter(
2442
- (head) => !known.has(head)
2443
- );
2399
+ function stepBindings(step) {
2400
+ return [...actionBindings(step.action), ...step.expect.flatMap((p) => predicateBindings(p))];
2444
2401
  }
2445
- function createdAliasesIn(step) {
2446
- return step.expect.flatMap((predicate) => {
2447
- if (predicate.kind !== "state" || predicate.assertion.kind === "deleted") {
2448
- return [];
2449
- }
2450
- return [predicate.assertion.as];
2451
- });
2402
+ function actionBindings(action) {
2403
+ if (action.kind === "goto") {
2404
+ return valueBindings(action.url);
2405
+ }
2406
+ if (action.kind === "fill" || action.kind === "select") {
2407
+ return [...locatorBindings(action.locator), ...valueBindings(action.value)];
2408
+ }
2409
+ if (action.kind === "press") {
2410
+ return action.locator == null ? [] : locatorBindings(action.locator);
2411
+ }
2412
+ if (action.kind === "upload") {
2413
+ return locatorBindings(action.locator);
2414
+ }
2415
+ return locatorBindings(action.locator);
2452
2416
  }
2453
- function coverTarget({ acc, sims, target, whenIds, workflow: workflow2 }) {
2454
- const key2 = choiceKey(whenIds, target.when, target.index);
2455
- if (acc.covered.has(key2)) {
2456
- return { covered: acc.covered, tests: acc.tests };
2417
+ function locatorBindings(locator) {
2418
+ if (locator.by === "inside") {
2419
+ return [...locatorBindings(locator.scope), ...locatorBindings(locator.target)];
2457
2420
  }
2458
- const picked = sims.filter((s) => s.choices.get(target.when) === target.index).reduce((found, s) => {
2459
- if (found != null) {
2460
- return found;
2461
- }
2462
- const test = resolveValidTest(workflow2, s, target.name);
2463
- return test == null ? null : { sim: s, test };
2464
- }, null);
2465
- if (picked == null) {
2466
- throw new Error(
2467
- `workflow "${workflow2.name}": branch "${target.name}" is unreachable \u2014 no combination of optional entities and singleton values reaches it`
2468
- );
2421
+ if (locator.by === "role") {
2422
+ return locator.name == null ? [] : valueBindings(locator.name);
2469
2423
  }
2470
- const reached = [...picked.sim.choices].map(([when2, index]) => choiceKey(whenIds, when2, index));
2471
- return {
2472
- covered: /* @__PURE__ */ new Set([...acc.covered, ...reached]),
2473
- tests: [...acc.tests, picked.test]
2474
- };
2424
+ return valueBindings(locator.value);
2475
2425
  }
2476
- function choiceKey(whenIds, when2, index) {
2477
- const id2 = whenIds.get(when2);
2478
- if (id2 == null) {
2479
- throw new Error("internal: when node missing from target index");
2426
+ function predicateBindings(predicate) {
2427
+ switch (predicate.kind) {
2428
+ case "visible":
2429
+ case "disabled":
2430
+ case "enabled":
2431
+ case "focused":
2432
+ case "checked": {
2433
+ return locatorBindings(predicate.locator);
2434
+ }
2435
+ case "value":
2436
+ case "text": {
2437
+ return [...locatorBindings(predicate.locator), ...valueBindings(predicate.value)];
2438
+ }
2439
+ case "singleton": {
2440
+ return valueBindings(predicate.assertion.value);
2441
+ }
2442
+ case "browser": {
2443
+ return valueBindings(predicate.value);
2444
+ }
2445
+ case "state": {
2446
+ return [
2447
+ ...predicate.assertion.kind === "deleted" ? [] : Object.values(predicate.assertion.props).flatMap(
2448
+ (v2) => isChanged(v2) ? [] : valueBindings(v2)
2449
+ ),
2450
+ ...Object.values(predicate.key).flatMap((v2) => whereBindings(v2))
2451
+ ];
2452
+ }
2453
+ case "not": {
2454
+ return predicateBindings(predicate.predicate);
2455
+ }
2456
+ case "when": {
2457
+ return predicate.branches.flatMap((row2) => [
2458
+ ...row2.condition == null ? [] : predicateBindings(row2.condition),
2459
+ ...row2.consequence.flatMap((consequence) => predicateBindings(consequence))
2460
+ ]);
2461
+ }
2462
+ case "and": {
2463
+ return predicate.predicates.flatMap((p) => predicateBindings(p));
2464
+ }
2465
+ case "count": {
2466
+ return [];
2467
+ }
2480
2468
  }
2481
- return `${String(id2)}:${String(index)}`;
2482
2469
  }
2483
- function requireMainTest(workflow2, sims) {
2484
- const test = sims.reduce(
2485
- (found, sim) => found ?? resolveValidTest(workflow2, sim, "main"),
2486
- null
2487
- );
2488
- if (test == null) {
2489
- throw new Error(
2490
- `workflow "${workflow2.name}": no valid seed found \u2014 steps may reference optional entities no candidate provides, or a condition mentions values the solver cannot pin`
2491
- );
2492
- }
2493
- return test;
2470
+ function whereBindings(value2) {
2471
+ return isWithin2(value2) ? Object.values(value2.selection.where).flatMap((v2) => whereBindings(v2)) : valueBindings(value2);
2494
2472
  }
2495
- function assertUniqueBranchNames(workflow2) {
2496
- const names = collectTargets(workflow2.steps).map((target) => target.name);
2497
- const dup = names.find((name, index) => names.indexOf(name) !== index);
2498
- if (dup != null) {
2499
- throw new Error(
2500
- `workflow "${workflow2.name}": branch name "${dup}" is used twice \u2014 branch names must be unique within a workflow`
2501
- );
2502
- }
2473
+ function isWithin2(value2) {
2474
+ return typeof value2 === "object" && value2 !== null && "kind" in value2;
2503
2475
  }
2504
- function collectTargets(steps) {
2505
- return steps.flatMap((step) => step.expect.flatMap((predicate) => targetsIn(predicate)));
2476
+ function setMap(map, ctx) {
2477
+ return Object.fromEntries(
2478
+ Object.entries(map).map(([key2, value2]) => [key2, resolveValue(value2, ctx)])
2479
+ );
2506
2480
  }
2507
- function targetsIn(predicate) {
2508
- if (predicate.kind === "not") {
2509
- return targetsIn(predicate.predicate);
2510
- }
2511
- if (predicate.kind === "and") {
2512
- return predicate.predicates.flatMap((p) => targetsIn(p));
2481
+ function resolveValue(value2, ctx) {
2482
+ if (isBinding(value2)) {
2483
+ return resolveBinding(value2, ctx);
2513
2484
  }
2514
- if (predicate.kind !== "when") {
2515
- return [];
2485
+ if (isTemplate(value2)) {
2486
+ return resolveTemplate(value2, ctx);
2516
2487
  }
2517
- return predicate.branches.flatMap((row2, index) => [
2518
- { index, name: row2.name, when: predicate },
2519
- ...row2.consequence.flatMap((consequence) => targetsIn(consequence))
2520
- ]);
2488
+ return value2;
2521
2489
  }
2522
- function proposeCandidates(workflow2, options) {
2523
- const subsets = maybeSubsets(workflow2.maybe);
2524
- const pinSets = pinAssignments(workflow2, options);
2525
- if (subsets.length * pinSets.length > MAX_CANDIDATES) {
2490
+ function resolveBinding(binding, ctx) {
2491
+ const bind = binding.__bind;
2492
+ if (bind.kind === "param") {
2493
+ const name = ctx.params.get(binding);
2494
+ if (name == null) {
2495
+ throw new Error("internal: param binding was not collected");
2496
+ }
2497
+ return { ref: name };
2498
+ }
2499
+ const alias = ctx.aliases.get(bind.descriptor);
2500
+ if (alias == null) {
2526
2501
  throw new Error(
2527
- `workflow "${workflow2.name}": too many optional entities and singleton values to solve \u2014 split the workflow or convert maybe(...) entities to of(...)`
2502
+ `references a "${bind.descriptor.entity}" entity that is not included in the test's given`
2528
2503
  );
2529
2504
  }
2530
- return subsets.flatMap((maybes) => pinSets.map((pins) => ({ maybes, pins })));
2505
+ return { ref: `${alias}.${bind.field}` };
2531
2506
  }
2532
- function maybeSubsets(maybe) {
2533
- const masks = Array.from({ length: 1 << maybe.length }, (_, mask) => mask);
2534
- return masks.map((mask) => ({ mask, size: maybe.filter((_, i) => (mask & 1 << i) !== 0).length })).toSorted((a, b) => a.size === b.size ? a.mask - b.mask : a.size - b.size).map(({ mask }) => maybe.filter((_, i) => (mask & 1 << i) !== 0));
2507
+ function resolveTemplate(template, ctx) {
2508
+ return {
2509
+ template: template.template.map(
2510
+ (segment) => isBinding(segment) ? resolveBinding(segment, ctx) : segment
2511
+ )
2512
+ };
2535
2513
  }
2536
- function pinAssignments(workflow2, options) {
2537
- const domains = pinDomains(workflow2, options);
2538
- return Object.entries(domains).reduce(
2539
- (acc, [param, domain]) => acc.flatMap((pins) => domain.map((value2) => ({ ...pins, [param]: value2 }))),
2540
- [{}]
2541
- );
2514
+ function setupsOf(entities, kind, ctx) {
2515
+ return entities.filter((d) => d.kind === kind).map((d) => ({ as: aliasFor(ctx, d), entity: d.entity, set: setMap(d.props, ctx) }));
2542
2516
  }
2543
- function pinDomains(workflow2, options) {
2544
- const literals = conditionSingletonLiterals(workflow2.steps);
2545
- return Object.entries(workflow2.singletons).reduce((acc, [name, value2]) => {
2546
- const param = paramRefOf(value2);
2547
- const compared = literals.get(name);
2548
- if (param == null || compared == null) {
2549
- return acc;
2550
- }
2551
- const enumValues = options.singletonValues.get(name);
2552
- return { ...acc, [param]: withComplements(compared, enumValues) };
2553
- }, {});
2517
+ function aliasFor(ctx, descriptor) {
2518
+ const alias = ctx.aliases.get(descriptor);
2519
+ if (alias == null) {
2520
+ throw new Error(`internal: no alias for ${descriptor.entity}`);
2521
+ }
2522
+ return alias;
2554
2523
  }
2555
- function conditionSingletonLiterals(steps) {
2556
- const pairs = steps.flatMap(
2557
- (step) => step.expect.flatMap((predicate) => conditionLiteralsIn(predicate))
2558
- );
2559
- return pairs.reduce(
2560
- (acc, [name, literal]) => new Map([...acc, [name, [.../* @__PURE__ */ new Set([...acc.get(name) ?? [], literal])]]]),
2561
- /* @__PURE__ */ new Map()
2562
- );
2524
+ function resolveStep(step, ctx) {
2525
+ return {
2526
+ action: resolveAction(step.action, ctx),
2527
+ expect: step.expect.map((predicate) => resolvePredicate2(predicate, ctx))
2528
+ };
2563
2529
  }
2564
- function conditionLiteralsIn(predicate) {
2565
- if (predicate.kind === "not") {
2566
- return conditionLiteralsIn(predicate.predicate);
2530
+ function resolveAction(action, ctx) {
2531
+ if (action.kind === "goto") {
2532
+ return { ...action, url: resolveString(action.url, ctx) };
2567
2533
  }
2568
- if (predicate.kind === "and") {
2569
- return predicate.predicates.flatMap((p) => conditionLiteralsIn(p));
2534
+ if (action.kind === "fill" || action.kind === "select") {
2535
+ return {
2536
+ ...action,
2537
+ locator: resolveLocator(action.locator, ctx),
2538
+ value: resolveValue(action.value, ctx)
2539
+ };
2570
2540
  }
2571
- if (predicate.kind !== "when") {
2572
- return [];
2541
+ if (action.kind === "press") {
2542
+ return {
2543
+ ...action,
2544
+ locator: action.locator == null ? void 0 : resolveLocator(action.locator, ctx)
2545
+ };
2573
2546
  }
2574
- return predicate.branches.flatMap((row2) => [
2575
- ...row2.condition == null ? [] : singletonLiteralsInCondition(row2.condition),
2576
- ...row2.consequence.flatMap((consequence) => conditionLiteralsIn(consequence))
2577
- ]);
2578
- }
2579
- function singletonLiteralsInCondition(condition) {
2580
- switch (condition.kind) {
2581
- case "singleton": {
2582
- const value2 = condition.assertion.value;
2583
- return value2 == null || typeof value2 !== "object" ? [[condition.singleton, value2]] : [];
2584
- }
2585
- case "count": {
2586
- return [];
2587
- }
2588
- case "not": {
2589
- return singletonLiteralsInCondition(condition.predicate);
2590
- }
2591
- case "and": {
2592
- return condition.predicates.flatMap((p) => singletonLiteralsInCondition(p));
2593
- }
2547
+ if (action.kind === "upload") {
2548
+ return { ...action, locator: resolveLocator(action.locator, ctx) };
2594
2549
  }
2550
+ return { ...action, locator: resolveLocator(action.locator, ctx) };
2595
2551
  }
2596
- function paramRefOf(value2) {
2597
- if (value2 == null || typeof value2 !== "object" || !("ref" in value2)) {
2598
- return void 0;
2552
+ function resolveString(value2, ctx) {
2553
+ if (isBinding(value2)) {
2554
+ return resolveBinding(value2, ctx);
2599
2555
  }
2600
- return value2.ref.includes(".") ? void 0 : value2.ref;
2556
+ if (isTemplate(value2)) {
2557
+ return resolveTemplate(value2, ctx);
2558
+ }
2559
+ return value2;
2601
2560
  }
2602
- function withComplements(values, enumValues) {
2603
- if (enumValues != null) {
2604
- return [.../* @__PURE__ */ new Set([...values, ...enumValues])];
2561
+ function resolveLocator(locator, ctx) {
2562
+ if (locator.by === "inside") {
2563
+ return {
2564
+ by: "inside",
2565
+ scope: resolveLocator(locator.scope, ctx),
2566
+ target: resolveLocator(locator.target, ctx)
2567
+ };
2605
2568
  }
2606
- const complements = values.flatMap((value2) => {
2607
- if (typeof value2 === "boolean") {
2608
- return [!value2];
2569
+ if (locator.by === "role") {
2570
+ return locator.name == null ? locator : { ...locator, name: resolveString(locator.name, ctx) };
2571
+ }
2572
+ return { ...locator, value: resolveString(locator.value, ctx) };
2573
+ }
2574
+ function resolvePredicate2(predicate, ctx) {
2575
+ switch (predicate.kind) {
2576
+ case "visible":
2577
+ case "disabled":
2578
+ case "enabled":
2579
+ case "focused":
2580
+ case "checked": {
2581
+ return { ...predicate, locator: resolveLocator(predicate.locator, ctx) };
2609
2582
  }
2610
- if (typeof value2 === "number") {
2611
- return [value2 + 1];
2583
+ case "value":
2584
+ case "text": {
2585
+ return {
2586
+ ...predicate,
2587
+ locator: resolveLocator(predicate.locator, ctx),
2588
+ value: resolveString(predicate.value, ctx)
2589
+ };
2612
2590
  }
2613
- if (typeof value2 === "string") {
2614
- return [syntheticDistinct(value2, values)];
2591
+ case "singleton": {
2592
+ return {
2593
+ ...predicate,
2594
+ assertion: { ...predicate.assertion, value: resolveValue(predicate.assertion.value, ctx) }
2595
+ };
2615
2596
  }
2616
- return [];
2617
- });
2618
- return [.../* @__PURE__ */ new Set([...values, ...complements])];
2619
- }
2620
- function syntheticDistinct(seed, taken) {
2621
- const candidate = `${seed}-alt`;
2622
- return taken.includes(candidate) ? syntheticDistinct(candidate, taken) : candidate;
2623
- }
2624
- function simulate(workflow2, candidate) {
2625
- const initial = {
2626
- choices: /* @__PURE__ */ new Map(),
2627
- state: {
2628
- rows: [...workflow2.world, ...candidate.maybes],
2629
- singles: seedSingles(workflow2.singletons, candidate.pins)
2597
+ case "browser": {
2598
+ return { ...predicate, value: resolveString(predicate.value, ctx) };
2599
+ }
2600
+ case "state": {
2601
+ return {
2602
+ ...predicate,
2603
+ assertion: resolveAssertion(predicate.assertion, ctx),
2604
+ key: whereMap(predicate.key, ctx)
2605
+ };
2606
+ }
2607
+ case "not": {
2608
+ return { ...predicate, predicate: resolvePredicate2(predicate.predicate, ctx) };
2609
+ }
2610
+ case "when": {
2611
+ return {
2612
+ ...predicate,
2613
+ branches: predicate.branches.map((row2) => ({
2614
+ condition: row2.condition == null ? void 0 : resolveCondition(row2.condition, ctx),
2615
+ consequence: row2.consequence.map((consequence) => resolvePredicate2(consequence, ctx)),
2616
+ name: row2.name
2617
+ }))
2618
+ };
2619
+ }
2620
+ case "and": {
2621
+ return {
2622
+ ...predicate,
2623
+ predicates: predicate.predicates.map((p) => resolvePredicate2(p, ctx))
2624
+ };
2625
+ }
2626
+ case "count": {
2627
+ return predicate;
2630
2628
  }
2631
- };
2632
- const folded = workflow2.steps.reduce(
2633
- (acc, step) => acc == null ? null : stepSim(acc, step),
2634
- initial
2635
- );
2636
- return folded == null ? null : { candidate, choices: folded.choices };
2637
- }
2638
- function seedSingles(singletons, pins) {
2639
- return Object.fromEntries(
2640
- Object.entries(singletons).map(([name, value2]) => {
2641
- const param = paramRefOf(value2);
2642
- return [name, param != null && param in pins ? pins[param] ?? null : value2];
2643
- })
2644
- );
2645
- }
2646
- function stepSim(acc, step) {
2647
- const rows = step.expect.filter((p) => p.kind === "state").reduce((current, effect) => applyEffect(current, effect), acc.state.rows);
2648
- const preWhens = { rows, singles: acc.state.singles };
2649
- const whens = step.expect.filter((p) => p.kind === "when");
2650
- const resolved = whens.reduce((current, when2) => current == null ? null : foldWhen(current, when2, preWhens), {
2651
- choices: acc.choices,
2652
- sets: {}
2653
- });
2654
- if (resolved == null) {
2655
- return null;
2656
2629
  }
2657
- const immediate = singletonSets(step.expect);
2658
- return {
2659
- choices: resolved.choices,
2660
- state: { rows, singles: { ...preWhens.singles, ...immediate, ...resolved.sets } }
2661
- };
2662
2630
  }
2663
- function foldWhen(acc, when2, state) {
2664
- const picked = pickBranch(when2, state);
2665
- if (picked === "unknown") {
2666
- return null;
2667
- }
2668
- if (picked == null) {
2669
- return acc;
2631
+ function resolveAssertion(assertion, ctx) {
2632
+ if (assertion.kind === "deleted") {
2633
+ return assertion;
2670
2634
  }
2671
- const row2 = when2.branches[picked];
2672
- if (row2 == null) {
2673
- return acc;
2635
+ const descriptor = ctx.captures.get(assertion);
2636
+ if (descriptor == null) {
2637
+ throw new Error("internal: capture assertion was not registered");
2674
2638
  }
2675
- const choices = new Map([...acc.choices, [when2, picked]]);
2676
- return row2.consequence.reduce(
2677
- (folded, consequence) => {
2678
- if (folded == null) {
2679
- return null;
2680
- }
2681
- if (consequence.kind === "when") {
2682
- return foldWhen(folded, consequence, state);
2683
- }
2684
- return { choices: folded.choices, sets: { ...folded.sets, ...singletonSets([consequence]) } };
2685
- },
2686
- { choices, sets: acc.sets }
2639
+ const as = aliasFor(ctx, descriptor);
2640
+ return assertion.kind === "created" ? { as, kind: "created", props: setMap(assertion.props, ctx) } : { as, kind: "updated", props: updateMap(assertion.props, ctx) };
2641
+ }
2642
+ function updateMap(map, ctx) {
2643
+ return Object.fromEntries(
2644
+ Object.entries(map).map(([key2, value2]) => [
2645
+ key2,
2646
+ isChanged(value2) ? value2 : resolveValue(value2, ctx)
2647
+ ])
2687
2648
  );
2688
2649
  }
2689
- function pickBranch(when2, state) {
2690
- return when2.branches.reduce((picked, row2, index) => {
2691
- if (picked !== void 0) {
2692
- return picked;
2693
- }
2694
- if (row2.condition == null) {
2695
- return index;
2696
- }
2697
- const holds = evalCondition(row2.condition, state);
2698
- if (holds === "unknown") {
2699
- return "unknown";
2700
- }
2701
- return holds ? index : void 0;
2702
- }, void 0);
2650
+ function whereMap(map, ctx) {
2651
+ return Object.fromEntries(
2652
+ Object.entries(map).map(([key2, value2]) => [
2653
+ key2,
2654
+ resolveWhere(value2, ctx)
2655
+ ])
2656
+ );
2703
2657
  }
2704
- function evalCondition(condition, state) {
2658
+ function resolveWhere(value2, ctx) {
2659
+ return isWithin2(value2) ? { ...value2, selection: { ...value2.selection, where: whereMap(value2.selection.where, ctx) } } : resolveValue(value2, ctx);
2660
+ }
2661
+ function resolveCondition(condition, ctx) {
2705
2662
  switch (condition.kind) {
2706
- case "count": {
2707
- return state.rows.filter((row2) => row2.entity === condition.entity).length === condition.value;
2708
- }
2709
2663
  case "singleton": {
2710
- return compareValues(state.singles[condition.singleton], condition.assertion.value);
2664
+ return {
2665
+ ...condition,
2666
+ assertion: {
2667
+ ...condition.assertion,
2668
+ value: resolveValue(condition.assertion.value, ctx)
2669
+ }
2670
+ };
2671
+ }
2672
+ case "count": {
2673
+ return condition;
2711
2674
  }
2712
2675
  case "not": {
2713
- return negate(evalCondition(condition.predicate, state));
2676
+ return { ...condition, predicate: resolveCondition(condition.predicate, ctx) };
2714
2677
  }
2715
2678
  case "and": {
2716
- return conjoin(condition.predicates.map((p) => evalCondition(p, state)));
2679
+ return {
2680
+ ...condition,
2681
+ predicates: condition.predicates.map((p) => resolveCondition(p, ctx))
2682
+ };
2717
2683
  }
2718
2684
  }
2719
2685
  }
2720
- function compareValues(seeded, wanted) {
2721
- if (seeded === void 0) {
2722
- return "unknown";
2723
- }
2724
- const seededRef = isRefValue(seeded);
2725
- const wantedRef = isRefValue(wanted);
2726
- if (seededRef != null && wantedRef != null) {
2727
- return seededRef === wantedRef ? true : "unknown";
2728
- }
2729
- if (seededRef != null || wantedRef != null || isTemplateValue(seeded) || isTemplateValue(wanted)) {
2730
- return "unknown";
2686
+
2687
+ // src/workflow.ts
2688
+ function workflow(intent, fn) {
2689
+ const sourcePath = captureSourcePath();
2690
+ if (fn == null) {
2691
+ return { spec: stubSpec(intent, sourcePath) };
2731
2692
  }
2732
- return sameSetValue(seeded, wanted);
2693
+ const final = finalize(fn());
2694
+ return {
2695
+ spec: {
2696
+ absent: final.absent,
2697
+ exclusive: final.exclusive,
2698
+ intent,
2699
+ maybe: final.maybe,
2700
+ name: slugify2(intent),
2701
+ params: final.params,
2702
+ singletons: final.singletons,
2703
+ sourcePath,
2704
+ steps: final.steps,
2705
+ stub: false,
2706
+ tests: [],
2707
+ world: final.world
2708
+ }
2709
+ };
2733
2710
  }
2734
- function isTemplateValue(value2) {
2735
- return value2 != null && typeof value2 === "object" && "template" in value2;
2711
+ function stubSpec(intent, sourcePath) {
2712
+ return {
2713
+ absent: [],
2714
+ exclusive: [],
2715
+ intent,
2716
+ maybe: [],
2717
+ name: slugify2(intent),
2718
+ params: {},
2719
+ singletons: {},
2720
+ sourcePath,
2721
+ steps: [],
2722
+ stub: true,
2723
+ tests: [],
2724
+ world: []
2725
+ };
2736
2726
  }
2737
- function negate(value2) {
2738
- return value2 === "unknown" ? "unknown" : !value2;
2727
+ var WORKFLOWS_ANCHOR_PATTERN = /[/\\]\.ripplo[/\\]workflows[/\\]([^):]+?)(?::\d+:\d+\)?)?$/;
2728
+ function captureSourcePath() {
2729
+ const stack = new Error("capture").stack;
2730
+ if (stack == null) {
2731
+ return void 0;
2732
+ }
2733
+ const match = stack.split("\n").map((line) => WORKFLOWS_ANCHOR_PATTERN.exec(line)).find((m) => m != null);
2734
+ const captured = match?.[1];
2735
+ return captured == null ? void 0 : captured.replaceAll("\\", "/");
2739
2736
  }
2740
- function conjoin(values) {
2741
- if (values.includes(false)) {
2742
- return false;
2737
+ function slugify2(intent) {
2738
+ const slug = intent.toLowerCase().replaceAll(/[^a-z0-9]+/g, " ").trim().split(" ").join("-");
2739
+ if (slug.length === 0) {
2740
+ throw new Error(`workflow intent "${intent}" slugifies to an empty string`);
2743
2741
  }
2744
- return values.every((v2) => v2 === true) ? true : "unknown";
2742
+ return slug;
2745
2743
  }
2746
- function singletonSets(predicates) {
2747
- return Object.fromEntries(
2748
- predicates.flatMap((predicate) => {
2749
- if (predicate.kind === "singleton") {
2750
- return [[predicate.singleton, predicate.assertion.value]];
2751
- }
2752
- return [];
2753
- })
2744
+
2745
+ // src/params.ts
2746
+ function arbitrary(field2) {
2747
+ const token = {
2748
+ base: `${field2.entity}_${field2.field}`,
2749
+ valueSpace: field2.valueSpaceName
2750
+ };
2751
+ return paramBinding(token);
2752
+ }
2753
+
2754
+ // src/engine.ts
2755
+ import { err, ok, okAsync, Result, ResultAsync } from "neverthrow";
2756
+ function createEngine(ripplo, impls, teardown) {
2757
+ const entities = new Map(Object.entries(impls.entities));
2758
+ const singletons = new Map(
2759
+ Object.entries(impls.singletons).map(([name, impl]) => [
2760
+ name,
2761
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- per-name impls typed at the call site; erase the contravariant scalar seed to the loose dispatch shape
2762
+ impl
2763
+ ])
2754
2764
  );
2765
+ assertDeclared(ripplo, [...entities.keys()], [...singletons.keys()]);
2766
+ const entityImpl = (name) => lookup(entities.get(name), name);
2767
+ const singletonImpl = (name) => lookup(singletons.get(name), name);
2768
+ return {
2769
+ read: (request, runId) => ResultAsync.combine([
2770
+ ResultAsync.combine(
2771
+ request.entities.map((name) => readEntity(entityImpl(name), name, runId))
2772
+ ),
2773
+ ResultAsync.combine(
2774
+ request.singletons.map((name) => readSingleton(singletonImpl(name), name, runId))
2775
+ )
2776
+ ]).map(([entityPairs, singletonPairs]) => ({
2777
+ entities: Object.fromEntries(entityPairs),
2778
+ singletons: Object.fromEntries(singletonPairs)
2779
+ })),
2780
+ seed: (request, runId) => ResultAsync.fromPromise(teardown(runId), implFailure).andThen(() => seedSingletons(request.singletons, singletonImpl, runId)).andThen(() => seedSetups(request.entities, entityImpl, runId)),
2781
+ teardown: (runId) => ResultAsync.fromPromise(teardown(runId), implFailure)
2782
+ };
2755
2783
  }
2756
- function applyEffect(rows, effect) {
2757
- if (effect.assertion.kind === "created") {
2758
- return [
2759
- ...rows,
2760
- { as: effect.assertion.as, entity: effect.entity, set: effect.assertion.props }
2761
- ];
2762
- }
2763
- if (effect.assertion.kind === "deleted") {
2764
- return rows.filter((row2) => row2.entity !== effect.entity || !matchesKey(row2, effect.key, rows));
2765
- }
2766
- return rows;
2784
+ function assertDeclared(ripplo, entityNames, singletonNames) {
2785
+ const backendEntities = new Set(
2786
+ ripplo.entities.filter((e) => e.schema.source === "backend").map((e) => e.name)
2787
+ );
2788
+ const backendSingletons = new Set(
2789
+ ripplo.singletons.filter((s) => s.schema.source === "backend").map((s) => s.name)
2790
+ );
2791
+ entityNames.forEach((name) => {
2792
+ if (!backendEntities.has(name)) {
2793
+ throw new Error(`engine impl "${name}" has no matching backend entity`);
2794
+ }
2795
+ });
2796
+ singletonNames.forEach((name) => {
2797
+ if (!backendSingletons.has(name)) {
2798
+ throw new Error(`engine impl "${name}" has no matching backend singleton`);
2799
+ }
2800
+ });
2767
2801
  }
2768
- function matchesKey(row2, key2, rows) {
2769
- return Object.entries(key2).every(([field2, want]) => matchesField({ field: field2, row: row2, rows, want }));
2802
+ function lookup(impl, name) {
2803
+ return impl == null ? err({ message: `no engine impl for "${name}"` }) : ok(impl);
2770
2804
  }
2771
- function matchesField({ field: field2, row: row2, rows, want }) {
2772
- if (isWithinValue(want)) {
2773
- const candidates = rows.filter(
2774
- (candidate) => candidate.entity === want.selection.entity && matchesKey(candidate, want.selection.where, rows)
2775
- ).map((candidate) => fieldValue(candidate, want.field));
2776
- const own = fieldValue(row2, field2);
2777
- return candidates.some((candidate) => valuesEqual(own, candidate));
2778
- }
2779
- return valuesEqual(fieldValue(row2, field2), want);
2805
+ function readEntity(impl, name, runId) {
2806
+ return impl.asyncAndThen(
2807
+ (i) => ResultAsync.fromPromise(i.read({ runId }), implFailure).map((rows) => [
2808
+ name,
2809
+ [...rows]
2810
+ ])
2811
+ );
2780
2812
  }
2781
- function isWithinValue(value2) {
2782
- return value2 != null && typeof value2 === "object" && "kind" in value2;
2813
+ function implFailure(error) {
2814
+ return { message: error instanceof Error ? error.message : String(error) };
2783
2815
  }
2784
- function fieldValue(row2, field2) {
2785
- return row2.set[field2] ?? { ref: `${row2.as}.${field2}` };
2816
+ function readSingleton(impl, name, runId) {
2817
+ return impl.asyncAndThen(
2818
+ (i) => ResultAsync.fromPromise(i.read({ runId }), implFailure).map((value2) => [
2819
+ name,
2820
+ value2
2821
+ ])
2822
+ );
2786
2823
  }
2787
- function valuesEqual(a, b) {
2788
- const aRef = isRefValue(a);
2789
- const bRef = isRefValue(b);
2790
- if (aRef != null || bRef != null) {
2791
- return aRef === bRef;
2792
- }
2793
- if (isTemplateValue(a) || isTemplateValue(b)) {
2794
- return false;
2795
- }
2796
- return sameSetValue(a, b);
2824
+ function seedSingletons(values, singletonImpl, runId) {
2825
+ return ResultAsync.combine(
2826
+ Object.entries(values).map(
2827
+ ([name, value2]) => singletonImpl(name).asyncAndThen(
2828
+ (impl) => ResultAsync.fromPromise(impl.seed({ runId, value: value2 }), implFailure)
2829
+ )
2830
+ )
2831
+ );
2797
2832
  }
2798
- function resolveTest(workflow2, sim, name) {
2799
- const steps = workflow2.steps.map((step) => ({
2800
- action: step.action,
2801
- expect: step.expect.flatMap((predicate) => resolvePredicate2(predicate, sim.choices))
2802
- }));
2803
- const world = [...workflow2.world, ...sim.candidate.maybes];
2804
- const singletons = seedSingles(workflow2.singletons, sim.candidate.pins);
2805
- const used = usedRefHeads({ singletons, steps, workflow: workflow2, world });
2806
- return {
2807
- absent: workflow2.absent,
2808
- exclusive: workflow2.exclusive,
2809
- intent: workflow2.intent,
2810
- name,
2811
- params: Object.fromEntries(
2812
- Object.entries(workflow2.params).filter(([param]) => used.has(param))
2813
- ),
2814
- singletons,
2815
- slug: slugify2(name),
2816
- steps,
2817
- workflow: workflow2.name,
2818
- world
2819
- };
2833
+ function seedSetups(specs, entityImpl, runId) {
2834
+ const waves = dependencyWaves(specs);
2835
+ return waves.reduce((accR, wave) => accR.andThen((acc) => seedWave(wave, entityImpl, acc, runId)), okAsync({ env: /* @__PURE__ */ new Map(), rows: [] })).map((fold) => orderRows(specs, fold.rows));
2820
2836
  }
2821
- function resolvePredicate2(predicate, choices) {
2822
- if (predicate.kind === "not") {
2823
- const inner = resolvePredicate2(predicate.predicate, choices);
2824
- return inner.map((p) => ({ kind: "not", predicate: p }));
2825
- }
2826
- if (predicate.kind === "and") {
2827
- return [
2828
- {
2829
- kind: "and",
2830
- predicates: predicate.predicates.flatMap((p) => resolvePredicate2(p, choices))
2831
- }
2832
- ];
2837
+ function dependencyWaves(specs) {
2838
+ return buildWaves([], specs);
2839
+ }
2840
+ function buildWaves(done, remaining) {
2841
+ if (remaining.length === 0) {
2842
+ return done;
2833
2843
  }
2834
- if (predicate.kind !== "when") {
2835
- return [predicate];
2844
+ const seeded = new Set(done.flat().map((spec) => spec.as));
2845
+ const ready = remaining.filter((spec) => refAliases(spec).every((alias) => seeded.has(alias)));
2846
+ if (ready.length === 0) {
2847
+ return [...done, remaining];
2836
2848
  }
2837
- const picked = choices.get(predicate);
2838
- const row2 = picked == null ? void 0 : predicate.branches[picked];
2839
- return row2 == null ? [] : row2.consequence.flatMap((c) => resolvePredicate2(c, choices));
2849
+ return buildWaves(
2850
+ [...done, ready],
2851
+ remaining.filter((spec) => !ready.includes(spec))
2852
+ );
2840
2853
  }
2841
-
2842
- // src/build.ts
2843
- function buildLockfile(input) {
2844
- assertUniqueNames(input.entities);
2845
- const lockfile = lockfileSchema.parse({
2846
- entities: input.entities.map((handle) => handle.schema),
2847
- singletons: input.singletons.map((handle) => handle.schema),
2848
- valueSpaces: dedupeByName([
2849
- ...input.entities.flatMap((handle) => handle.valueSpaces),
2850
- ...input.singletons.flatMap((handle) => handle.valueSpaces)
2851
- ]),
2852
- workflows: input.workflows.map(
2853
- (ripploWorkflow) => expandWorkflow(ripploWorkflow.spec, { singletonValues: singletonValuesOf(input.singletons) })
2854
- )
2854
+ function refAliases(spec) {
2855
+ return Object.values(spec.fields).flatMap((value2) => {
2856
+ if (value2 === null || typeof value2 !== "object" || !("ref" in value2)) {
2857
+ return [];
2858
+ }
2859
+ const lastDot = value2.ref.lastIndexOf(".");
2860
+ return lastDot === -1 ? [] : [value2.ref.slice(0, lastDot)];
2855
2861
  });
2856
- assertNoContradictions(lockfile);
2857
- return lockfile;
2858
2862
  }
2859
- function assertUniqueNames(entities) {
2860
- const names = entities.map((handle) => handle.schema.name);
2861
- const duplicate = names.find((name, index) => names.indexOf(name) !== index);
2862
- if (duplicate != null) {
2863
- throw new Error(`duplicate entity name "${duplicate}" \u2014 each entity name must be unique`);
2864
- }
2863
+ function seedWave(wave, entityImpl, acc, runId) {
2864
+ return ResultAsync.combine(
2865
+ wave.map((spec) => seedSetup(entityImpl(spec.entity), acc, spec, runId))
2866
+ ).map((folds) => mergeFolds(acc, folds));
2865
2867
  }
2866
- function dedupeByName(spaces) {
2867
- const byName = new Map(spaces.map((space) => [space.name, space]));
2868
- return [...byName.values()];
2868
+ function seedSetup(impl, acc, spec, runId) {
2869
+ return resolveFields(spec.fields, acc.env).asyncAndThen((fields) => runSeed(impl, fields, runId)).map(({ row: row2, session }) => foldRow(acc, spec, row2, session));
2869
2870
  }
2870
- function assertNoContradictions(lockfile) {
2871
- lockfile.workflows.forEach((workflow2) => {
2872
- assertNoContradiction(workflow2);
2873
- assertNoDanglingRefs(workflow2);
2874
- });
2871
+ function resolveFields(fields, env) {
2872
+ return Result.combine(
2873
+ Object.entries(fields).map(
2874
+ ([field2, value2]) => resolveValue2(value2, env).map((cell2) => [field2, cell2])
2875
+ )
2876
+ ).map((entries) => Object.fromEntries(entries));
2875
2877
  }
2876
- function assertNoContradiction(workflow2) {
2877
- const setups = [...workflow2.world, ...workflow2.maybe];
2878
- workflow2.absent.forEach((absence) => {
2879
- const clash = setups.find(
2880
- (setup) => setup.entity === absence.entity && whereMatches(absence.where, setup.set)
2881
- );
2882
- if (clash != null) {
2883
- throw new Error(
2884
- `test "${workflow2.name}": creates a "${absence.entity}" ("${clash.as}") that a none(${absence.entity}, \u2026) in its world forbids`
2885
- );
2886
- }
2887
- });
2878
+ function resolveValue2(value2, env) {
2879
+ if (value2 === null || typeof value2 !== "object") {
2880
+ return ok(value2);
2881
+ }
2882
+ if ("ref" in value2) {
2883
+ return env.has(value2.ref) ? ok(env.get(value2.ref) ?? null) : err({ message: `setup ref "${value2.ref}" was not produced by an earlier entity` });
2884
+ }
2885
+ throw new Error("internal: a setup value resolved to an unresolved template");
2888
2886
  }
2889
- function whereMatches(where, set) {
2890
- return Object.entries(where).every(([field2, want]) => sameValue(want, set[field2]));
2887
+ function runSeed(impl, fields, runId) {
2888
+ return impl.asyncAndThen((i) => ResultAsync.fromPromise(i.seed({ fields, runId }), implFailure));
2891
2889
  }
2892
- function sameValue(a, b) {
2893
- return b !== void 0 && sameSetValue(a, b);
2890
+ function foldRow(acc, spec, row2, session) {
2891
+ const next = Object.entries(row2).map(([field2, value2]) => [
2892
+ `${spec.as}.${field2}`,
2893
+ value2
2894
+ ]);
2895
+ return {
2896
+ env: new Map([...acc.env, ...next]),
2897
+ rows: [...acc.rows, { as: spec.as, row: row2, session }]
2898
+ };
2894
2899
  }
2895
- function assertNoDanglingRefs(workflow2) {
2896
- const aliases = new Set([...workflow2.world, ...workflow2.maybe].map((setup) => setup.as));
2897
- const paramKeys = new Set(Object.keys(workflow2.params));
2898
- const fieldSets = [
2899
- ...workflow2.world.map((setup) => setup.set),
2900
- ...workflow2.maybe.map((setup) => setup.set),
2901
- ...workflow2.absent.map((absence) => absence.where)
2902
- ];
2903
- fieldSets.forEach((set) => {
2904
- assertSetRefs(workflow2.name, set, aliases, paramKeys);
2905
- });
2900
+ function mergeFolds(base, folds) {
2901
+ return {
2902
+ env: new Map([...base.env, ...folds.flatMap((fold) => [...fold.env])]),
2903
+ rows: [...base.rows, ...folds.flatMap((fold) => fold.rows.slice(base.rows.length))]
2904
+ };
2906
2905
  }
2907
- function assertSetRefs(workflowName, set, aliases, paramKeys) {
2908
- Object.values(set).forEach((value2) => {
2909
- if (!isRef(value2) || paramKeys.has(value2.ref)) {
2910
- return;
2911
- }
2912
- const lastDot = value2.ref.lastIndexOf(".");
2913
- if (lastDot === -1) {
2914
- return;
2915
- }
2916
- const aliasPath = value2.ref.slice(0, lastDot);
2917
- if (!aliases.has(aliasPath)) {
2918
- throw new Error(
2919
- `test "${workflowName}": ref "${value2.ref}" points at unknown alias "${aliasPath}"`
2920
- );
2921
- }
2906
+ function orderRows(specs, rows) {
2907
+ const byAs = new Map(rows.map((row2) => [row2.as, row2]));
2908
+ return specs.flatMap((spec) => {
2909
+ const row2 = byAs.get(spec.as);
2910
+ return row2 == null ? [] : [row2];
2922
2911
  });
2923
2912
  }
2924
- function isRef(value2) {
2925
- return value2 != null && typeof value2 === "object" && "ref" in value2;
2926
- }
2927
2913
 
2928
- // src/ripplo.ts
2929
- function createRipplo(input) {
2914
+ // src/client-engine.ts
2915
+ import { z as z8 } from "zod";
2916
+ var seedSchema = z8.record(z8.string(), z8.union([z8.string(), z8.number(), z8.boolean()])).catch({});
2917
+ function createClientEngine(_ripplo, impls) {
2918
+ const singletons = new Map(
2919
+ Object.entries(impls.singletons).map(([name, impl]) => [
2920
+ name,
2921
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- per-name impls typed at the call site; erase the contravariant scalar seed to the loose dispatch shape
2922
+ impl
2923
+ ])
2924
+ );
2925
+ const entities = new Map(Object.entries(impls.entities));
2926
+ const seed = seedSchema.parse(Reflect.get(globalThis, CLIENT_SEED_KEY));
2927
+ Object.entries(seed).forEach(([name, value2]) => singletons.get(name)?.seed(value2));
2930
2928
  return {
2931
- entities: input.entities,
2932
- lockfile: buildLockfile({
2933
- entities: input.entities,
2934
- singletons: input.singletons,
2935
- workflows: input.workflows
2936
- }),
2937
- singletons: input.singletons,
2938
- workflows: input.workflows
2929
+ readEntity: (name) => entities.get(name)?.read() ?? [],
2930
+ readSingleton: (name) => singletons.get(name)?.read() ?? null
2939
2931
  };
2940
2932
  }
2933
+ function mountClientEngine(ripplo, impls, { enabled: enabled2 }) {
2934
+ if (!enabled2) {
2935
+ return;
2936
+ }
2937
+ Reflect.set(globalThis, CLIENT_MOUNT_KEY, createClientEngine(ripplo, impls));
2938
+ }
2941
2939
  export {
2942
2940
  CLIENT_MOUNT_KEY,
2943
2941
  CLIENT_SEED_KEY,