arcy.js 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1422 @@
1
+ import { FLOW_CONTRACT, FLOW_GLOBAL, createFlowRunState, createPreviewRunState, uuidV4 } from './chunk-FR6SJSDU.js';
2
+ import { readFlowDetail, normalizeOptionReply, validateFillReply, fetchFlowRun } from './chunk-CQ2DESAJ.js';
3
+ import { matchTarget } from './chunk-NIHUDSWK.js';
4
+ import { collectElementsDeep, isInteractive, captureEventFingerprint } from './chunk-NY3NXM2V.js';
5
+
6
+ /* arcy.js — https://arcyai.com */
7
+
8
+ // src/fingerprint/manifest.ts
9
+ var LIVE_MANIFEST_MAX_ELEMENTS = 40;
10
+ function captureLiveManifest(doc = document) {
11
+ try {
12
+ const candidates = collectElementsDeep(doc);
13
+ const manifest = [];
14
+ for (const el of candidates) {
15
+ if (manifest.length >= LIVE_MANIFEST_MAX_ELEMENTS) break;
16
+ if (!isInteractive(el)) continue;
17
+ manifest.push(captureEventFingerprint(el));
18
+ }
19
+ return manifest;
20
+ } catch {
21
+ return [];
22
+ }
23
+ }
24
+
25
+ // src/flow/condition.ts
26
+ var MAX_CONDITION_DEPTH = 3;
27
+ var CONDITION_TYPES = [
28
+ "attribute",
29
+ "current_page",
30
+ "element",
31
+ "text_input",
32
+ "user_fills_input",
33
+ "current_time",
34
+ "user_idle",
35
+ "visitor",
36
+ "always_true",
37
+ "group"
38
+ ];
39
+ function isTargetFingerprint(value) {
40
+ if (!value || typeof value !== "object") return false;
41
+ const v = value;
42
+ return v.v === 1 && !!v.core && typeof v.core === "object" && typeof v.core.tag === "string" && (v.selector === null || typeof v.selector === "string") && typeof v.ordinal === "number" && typeof v.precision === "number";
43
+ }
44
+ function parseNode(value, depth) {
45
+ if (depth > MAX_CONDITION_DEPTH) return null;
46
+ if (!value || typeof value !== "object") return null;
47
+ const v = value;
48
+ const type = v.type;
49
+ if (typeof type !== "string" || !CONDITION_TYPES.includes(type)) {
50
+ return null;
51
+ }
52
+ switch (type) {
53
+ case "attribute": {
54
+ if (typeof v.codeName !== "string" || typeof v.operator !== "string") return null;
55
+ return {
56
+ type: "attribute",
57
+ codeName: v.codeName,
58
+ operator: v.operator,
59
+ value: v.value
60
+ };
61
+ }
62
+ case "current_page": {
63
+ if (!Array.isArray(v.matches) || !Array.isArray(v.doesNotMatch)) return null;
64
+ return {
65
+ type: "current_page",
66
+ matches: v.matches.filter((m) => typeof m === "string"),
67
+ doesNotMatch: v.doesNotMatch.filter(
68
+ (m) => typeof m === "string"
69
+ )
70
+ };
71
+ }
72
+ case "element": {
73
+ if (!isTargetFingerprint(v.target) || typeof v.state !== "string") return null;
74
+ return {
75
+ type: "element",
76
+ target: v.target,
77
+ state: v.state
78
+ };
79
+ }
80
+ case "text_input": {
81
+ if (!isTargetFingerprint(v.target) || typeof v.operator !== "string" || typeof v.value !== "string") {
82
+ return null;
83
+ }
84
+ return {
85
+ type: "text_input",
86
+ target: v.target,
87
+ operator: v.operator,
88
+ value: v.value
89
+ };
90
+ }
91
+ case "user_fills_input": {
92
+ if (!isTargetFingerprint(v.target)) return null;
93
+ return { type: "user_fills_input", target: v.target };
94
+ }
95
+ case "current_time": {
96
+ const from = v.from;
97
+ const to = v.to;
98
+ if (from !== null && typeof from !== "string" || to !== null && typeof to !== "string") {
99
+ return null;
100
+ }
101
+ return { type: "current_time", from: from ?? null, to: to ?? null };
102
+ }
103
+ case "user_idle": {
104
+ const seconds = v.seconds;
105
+ if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds < 1 || seconds > 600) {
106
+ return null;
107
+ }
108
+ return { type: "user_idle", seconds };
109
+ }
110
+ case "visitor": {
111
+ if (v.state !== "first_visit" && v.state !== "returning") return null;
112
+ return { type: "visitor", state: v.state };
113
+ }
114
+ case "always_true":
115
+ return { type: "always_true" };
116
+ case "group": {
117
+ if (!Array.isArray(v.children) || v.operator !== "and" && v.operator !== "or") {
118
+ return null;
119
+ }
120
+ const children = [];
121
+ for (const child of v.children) {
122
+ const parsed = parseNode(child, depth + 1);
123
+ if (parsed) children.push(parsed);
124
+ }
125
+ return { type: "group", operator: v.operator, children };
126
+ }
127
+ default:
128
+ return null;
129
+ }
130
+ }
131
+ function parseTriggerCondition(value) {
132
+ if (!value || typeof value !== "object") return null;
133
+ const v = value;
134
+ if (v.v !== 1) return null;
135
+ const node = parseNode(v.node, 0);
136
+ return node ? { v: 1, node } : null;
137
+ }
138
+ function parseHHMM(value) {
139
+ const match = /^(\d{2}):(\d{2})$/.exec(value);
140
+ if (!match) return null;
141
+ const hours = Number(match[1]);
142
+ const minutes = Number(match[2]);
143
+ if (hours > 23 || minutes > 59) return null;
144
+ return hours * 60 + minutes;
145
+ }
146
+ function evaluateCurrentTime(node, now) {
147
+ if (node.from === null && node.to === null) return true;
148
+ const nowMinutes = now.getHours() * 60 + now.getMinutes();
149
+ const from = node.from !== null ? parseHHMM(node.from) : null;
150
+ const to = node.to !== null ? parseHHMM(node.to) : null;
151
+ if (node.from !== null && from === null) return false;
152
+ if (node.to !== null && to === null) return false;
153
+ if (from !== null && to === null) return nowMinutes >= from;
154
+ if (from === null && to !== null) return nowMinutes <= to;
155
+ if (from !== null && to !== null) {
156
+ if (from <= to) return nowMinutes >= from && nowMinutes <= to;
157
+ return nowMinutes >= from || nowMinutes <= to;
158
+ }
159
+ return true;
160
+ }
161
+ function globMatches(pattern, value) {
162
+ const escaped = pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*");
163
+ try {
164
+ return new RegExp(`^${escaped}$`).test(value);
165
+ } catch {
166
+ return false;
167
+ }
168
+ }
169
+ function evaluateCurrentPage(node, href) {
170
+ if (node.doesNotMatch.some((pattern) => globMatches(pattern, href))) return false;
171
+ if (node.matches.length === 0) return true;
172
+ return node.matches.some((pattern) => globMatches(pattern, href));
173
+ }
174
+ function compareText(operator, live, expected) {
175
+ if (live === void 0) return false;
176
+ switch (operator) {
177
+ case "is":
178
+ return live === expected;
179
+ case "is_not":
180
+ return live !== expected;
181
+ case "contains":
182
+ return live.includes(expected);
183
+ case "does_not_contain":
184
+ return !live.includes(expected);
185
+ case "starts_with":
186
+ return live.startsWith(expected);
187
+ case "ends_with":
188
+ return live.endsWith(expected);
189
+ // D1297's catalogue fill-out. The empty pair reads the LIVE value only,
190
+ // so an authored `value` is ignored rather than misread.
191
+ case "is_empty":
192
+ return live === "";
193
+ case "is_not_empty":
194
+ return live !== "";
195
+ // Numeric compares parse both sides; either side not a number means the
196
+ // condition cannot hold (never throw, never coerce NaN into an answer).
197
+ case "greater_than":
198
+ case "less_than": {
199
+ const liveNumber = Number(live);
200
+ const expectedNumber = Number(expected);
201
+ if (live.trim() === "" || !Number.isFinite(liveNumber) || !Number.isFinite(expectedNumber)) {
202
+ return false;
203
+ }
204
+ return operator === "greater_than" ? liveNumber > expectedNumber : liveNumber < expectedNumber;
205
+ }
206
+ // The same `*` glob `current_page` uses, applied to a field value.
207
+ case "matches_pattern":
208
+ return globMatches(expected, live);
209
+ default:
210
+ return false;
211
+ }
212
+ }
213
+ function isElementVisible(el, doc) {
214
+ try {
215
+ if (el.getClientRects().length === 0) return false;
216
+ } catch {
217
+ return false;
218
+ }
219
+ try {
220
+ const style = doc.defaultView?.getComputedStyle(el);
221
+ if (style && style.visibility === "hidden") return false;
222
+ } catch {
223
+ }
224
+ return true;
225
+ }
226
+ function isElementFocused(el, doc) {
227
+ try {
228
+ let active = doc.activeElement;
229
+ while (active) {
230
+ if (active === el || el.contains(active)) return true;
231
+ const root = active.shadowRoot;
232
+ active = root?.activeElement ?? null;
233
+ }
234
+ } catch {
235
+ }
236
+ return false;
237
+ }
238
+ function evaluateElement(node, ctx) {
239
+ const result = matchTarget(node.target, ctx.doc);
240
+ switch (node.state) {
241
+ case "is_present":
242
+ return result.status === "matched";
243
+ case "is_not_present":
244
+ return result.status === "not_found";
245
+ case "is_disabled":
246
+ case "is_not_disabled": {
247
+ if (result.status !== "matched") return false;
248
+ const disabled = "disabled" in result.element && result.element.disabled === true;
249
+ return node.state === "is_disabled" ? disabled : !disabled;
250
+ }
251
+ case "is_clicked":
252
+ return ctx.wasClicked(node.target);
253
+ case "is_not_clicked":
254
+ return !ctx.wasClicked(node.target);
255
+ case "is_visible":
256
+ return result.status === "matched" && isElementVisible(result.element, ctx.doc);
257
+ case "is_not_visible":
258
+ return result.status !== "matched" || !isElementVisible(result.element, ctx.doc);
259
+ case "is_checked":
260
+ case "is_not_checked": {
261
+ if (result.status !== "matched") return false;
262
+ const checked = "checked" in result.element && result.element.checked === true;
263
+ return node.state === "is_checked" ? checked : !checked;
264
+ }
265
+ case "is_focused":
266
+ return result.status === "matched" && isElementFocused(result.element, ctx.doc);
267
+ case "is_not_focused":
268
+ return result.status !== "matched" || !isElementFocused(result.element, ctx.doc);
269
+ default:
270
+ return false;
271
+ }
272
+ }
273
+ function evaluateAttribute(node, ctx) {
274
+ const live = ctx.getTrait?.(node.codeName);
275
+ switch (node.operator) {
276
+ case "has_any_value":
277
+ return live !== void 0 && live !== null && live !== "";
278
+ case "is_empty":
279
+ return live === void 0 || live === null || live === "";
280
+ }
281
+ if (live === void 0) return false;
282
+ const liveText = String(live);
283
+ const expected = node.value === void 0 ? "" : String(node.value);
284
+ return compareText(node.operator, liveText, expected);
285
+ }
286
+ function evaluateCondition(node, ctx) {
287
+ switch (node.type) {
288
+ case "always_true":
289
+ return true;
290
+ case "current_time":
291
+ return evaluateCurrentTime(node, ctx.now());
292
+ case "current_page":
293
+ return evaluateCurrentPage(node, ctx.href());
294
+ case "element":
295
+ return evaluateElement(node, ctx);
296
+ case "attribute":
297
+ return evaluateAttribute(node, ctx);
298
+ case "text_input": {
299
+ const live = ctx.readInputValue(node.target);
300
+ return compareText(node.operator, live, node.value);
301
+ }
302
+ case "user_fills_input": {
303
+ const live = ctx.readInputValue(node.target);
304
+ const initial = ctx.initialInputValue(node.target);
305
+ return live !== void 0 && live !== "" && live !== initial;
306
+ }
307
+ case "user_idle": {
308
+ const idle = ctx.idleSeconds?.();
309
+ return idle !== void 0 && idle >= node.seconds;
310
+ }
311
+ case "visitor": {
312
+ const first = ctx.isFirstVisit?.();
313
+ if (first === void 0) return false;
314
+ return node.state === "first_visit" ? first : !first;
315
+ }
316
+ case "group": {
317
+ if (node.children.length === 0) return false;
318
+ return node.operator === "and" ? node.children.every((child) => evaluateCondition(child, ctx)) : node.children.some((child) => evaluateCondition(child, ctx));
319
+ }
320
+ default:
321
+ return false;
322
+ }
323
+ }
324
+ function needsIntervalCheck(node) {
325
+ if (node.type === "current_time" || node.type === "user_idle") return true;
326
+ if (node.type === "group") return node.children.some(needsIntervalCheck);
327
+ return false;
328
+ }
329
+ function needsIdleCheck(node) {
330
+ if (node.type === "user_idle") return true;
331
+ if (node.type === "group") return node.children.some(needsIdleCheck);
332
+ return false;
333
+ }
334
+
335
+ // src/flow/runtime.ts
336
+ var TRIGGER_THROTTLE_MS = 250;
337
+ var TIME_CHECK_INTERVAL_MS = 15e3;
338
+ var IDLE_CHECK_INTERVAL_MS = 1e3;
339
+ var ACTIVITY_EVENTS = [
340
+ "pointerdown",
341
+ "pointermove",
342
+ "keydown",
343
+ "wheel",
344
+ "scroll",
345
+ "touchstart"
346
+ ];
347
+ var ELEMENT_WAIT_POLL_MS = 250;
348
+ var PAGE_READY_MAX_MS = 3e3;
349
+ var STEP_SETTLE_MS = 250;
350
+ var noopTrack = () => {
351
+ };
352
+ var defaultExecutor = () => Promise.resolve({ status: "wait" });
353
+ function findStepIndex(flow, cvid) {
354
+ return flow.steps.findIndex((step) => step.cvid === cvid);
355
+ }
356
+ function findElementTargets(node) {
357
+ if (!node) return [];
358
+ if (node.type === "element") return [node.target];
359
+ if (node.type === "group") return node.children.flatMap((child) => findElementTargets(child));
360
+ return [];
361
+ }
362
+ var ActiveRun = class {
363
+ constructor(flow, stepIndex, startedAt, flowSessionId, doc, win, runState, executor, getTrait, isFirstVisit, track, now, setTimeoutImpl, clearTimeoutImpl, setIntervalImpl, clearIntervalImpl) {
364
+ this.flow = flow;
365
+ this.stepIndex = stepIndex;
366
+ this.startedAt = startedAt;
367
+ this.flowSessionId = flowSessionId;
368
+ this.doc = doc;
369
+ this.win = win;
370
+ this.runState = runState;
371
+ this.executor = executor;
372
+ this.getTrait = getTrait;
373
+ this.isFirstVisit = isFirstVisit;
374
+ this.track = track;
375
+ this.now = now;
376
+ this.setTimeoutImpl = setTimeoutImpl;
377
+ this.clearTimeoutImpl = clearTimeoutImpl;
378
+ this.setIntervalImpl = setIntervalImpl;
379
+ this.clearIntervalImpl = clearIntervalImpl;
380
+ this.stopped = false;
381
+ this.dwellTimer = null;
382
+ this.observer = null;
383
+ this.intervalHandle = null;
384
+ this.throttleHandle = null;
385
+ this.lastCheck = 0;
386
+ this.trueSince = /* @__PURE__ */ new Map();
387
+ this.clicked = /* @__PURE__ */ new Set();
388
+ this.initialValues = /* @__PURE__ */ new Map();
389
+ this.clickListener = null;
390
+ /** When the end user last did anything, for `user_idle` (D1299). Reset at
391
+ * each step's watch start, so a step mounted on an already-quiet page
392
+ * starts its idle clock at zero rather than firing instantly. */
393
+ this.lastActivityAt = 0;
394
+ /** The activity listeners `startWatch` installs when a trigger tree holds
395
+ * a `user_idle` leaf. They stamp `lastActivityAt` and nothing else: the
396
+ * idle interval is what re-evaluates. */
397
+ this.activityListeners = [];
398
+ /** ADR 0166: true while a fill question is awaiting the end user's
399
+ * answer. Suspends trigger evaluation for the duration. */
400
+ this.inputPending = false;
401
+ /** Extra capture-phase listeners `startWatch` installs alongside the
402
+ * click listener (D1297): typing, focus moves and checkbox flips change
403
+ * nothing a MutationObserver can see, yet `text_input`, `is_focused`
404
+ * and `is_checked` conditions all turn on them. */
405
+ this.inputListeners = [];
406
+ /** Parsed once per step, keyed by trigger cvid, so every condition
407
+ * evaluation and every `is_clicked`/`user_fills_input` cache lookup
408
+ * within one step's lifetime shares the SAME `TargetFingerprint` object
409
+ * identity. `condition.ts`'s parse is otherwise pure and re-parsing on
410
+ * every tick would mint a fresh object each time, silently breaking the
411
+ * `Map`/`Set` identity these caches rely on. */
412
+ this.parsedTriggers = /* @__PURE__ */ new Map();
413
+ /** Read by anything else that wants to know why a run ended, alongside
414
+ * the `finish()` telemetry emission below. */
415
+ this.lastStopReason = null;
416
+ /** 26.4: the bar's status surface. Set by `beginRun` rather than passed
417
+ * through the constructor, which already carries fifteen positional
418
+ * arguments. Never throws into the run (ADR 0050). */
419
+ this.onStatus = null;
420
+ /** D1085: the shell's apology surface, fired once per run that ended on
421
+ * an error. Set by `beginRun`, same reasoning as `onStatus`. */
422
+ this.onFailure = null;
423
+ /** ADR 0166/D1294: withdraws a fill question still waiting in the chat
424
+ * surface the moment the run ends, whatever ended it. Set by `beginRun`,
425
+ * same reasoning as `onStatus`. Never throws into the run (ADR 0050). */
426
+ this.onCancelFill = null;
427
+ }
428
+ resolveElement(target, toleranceMs = 0) {
429
+ return matchTarget(target, this.doc);
430
+ }
431
+ /** The wait window (D1085): the flow's own `waitSeconds`, polled. Resolves
432
+ * early the moment the element is matched and connected, or with the last
433
+ * failed result once the window closes. A stopped or jumped-away run
434
+ * resolves immediately with whatever the DOM says right now; the caller's
435
+ * own staleness guard discards the outcome either way. */
436
+ waitForElement(target) {
437
+ const check = () => {
438
+ const result = matchTarget(target, this.doc);
439
+ const found = result.status === "matched" && result.element.isConnected;
440
+ return { result, found };
441
+ };
442
+ const first = check();
443
+ const windowMs = Math.max(0, this.flow.waitSeconds * 1e3);
444
+ if (first.found || windowMs === 0) return Promise.resolve(first.result);
445
+ const startIndex = this.stepIndex;
446
+ const deadline = this.now() + windowMs;
447
+ return new Promise((resolve) => {
448
+ const tick = () => {
449
+ const attempt = check();
450
+ if (attempt.found || this.stopped || this.stepIndex !== startIndex || this.now() >= deadline) {
451
+ resolve(attempt.result);
452
+ return;
453
+ }
454
+ this.setTimeoutImpl(tick, ELEMENT_WAIT_POLL_MS);
455
+ };
456
+ this.setTimeoutImpl(tick, ELEMENT_WAIT_POLL_MS);
457
+ });
458
+ }
459
+ conditionContext() {
460
+ return {
461
+ doc: this.doc,
462
+ now: () => new Date(this.now()),
463
+ href: () => this.win.location?.href ?? "",
464
+ getTrait: this.getTrait,
465
+ idleSeconds: () => Math.max(0, (this.now() - this.lastActivityAt) / 1e3),
466
+ isFirstVisit: this.isFirstVisit,
467
+ wasClicked: (target) => this.clicked.has(target),
468
+ readInputValue: (target) => {
469
+ const result = matchTarget(target, this.doc);
470
+ if (result.status !== "matched") return void 0;
471
+ const el = result.element;
472
+ return typeof el.value === "string" ? el.value : void 0;
473
+ },
474
+ initialInputValue: (target) => {
475
+ if (!this.initialValues.has(target)) {
476
+ const result = matchTarget(target, this.doc);
477
+ const el = result.status === "matched" ? result.element : null;
478
+ this.initialValues.set(
479
+ target,
480
+ el && typeof el.value === "string" ? el.value : void 0
481
+ );
482
+ }
483
+ return this.initialValues.get(target);
484
+ }
485
+ };
486
+ }
487
+ currentStep() {
488
+ return this.flow.steps[this.stepIndex];
489
+ }
490
+ persist() {
491
+ const step = this.currentStep();
492
+ if (!step) return;
493
+ this.runState.save({
494
+ flowId: this.flow.cvid,
495
+ currentStepCvid: step.cvid,
496
+ flowSessionId: this.flowSessionId,
497
+ startedAt: this.startedAt
498
+ });
499
+ }
500
+ teardownWatch() {
501
+ this.observer?.disconnect();
502
+ this.observer = null;
503
+ if (this.intervalHandle !== null) {
504
+ this.clearIntervalImpl(this.intervalHandle);
505
+ this.intervalHandle = null;
506
+ }
507
+ if (this.throttleHandle !== null) {
508
+ this.clearTimeoutImpl(this.throttleHandle);
509
+ this.throttleHandle = null;
510
+ }
511
+ if (this.dwellTimer !== null) {
512
+ this.clearTimeoutImpl(this.dwellTimer);
513
+ this.dwellTimer = null;
514
+ }
515
+ if (this.clickListener) {
516
+ try {
517
+ this.doc.removeEventListener("click", this.clickListener, true);
518
+ } catch {
519
+ }
520
+ this.clickListener = null;
521
+ }
522
+ for (const [name, listener] of this.inputListeners) {
523
+ try {
524
+ this.doc.removeEventListener(name, listener, true);
525
+ } catch {
526
+ }
527
+ }
528
+ this.inputListeners = [];
529
+ for (const [name, listener] of this.activityListeners) {
530
+ try {
531
+ this.doc.removeEventListener(name, listener, true);
532
+ } catch {
533
+ }
534
+ }
535
+ this.activityListeners = [];
536
+ this.inputPending = false;
537
+ this.trueSince.clear();
538
+ this.clicked.clear();
539
+ this.initialValues.clear();
540
+ }
541
+ jumpTo(cvid) {
542
+ const index = findStepIndex(this.flow, cvid);
543
+ if (index === -1) {
544
+ this.finish("error", { reason: "step_not_found" });
545
+ return;
546
+ }
547
+ this.stopStep();
548
+ this.stepIndex = index;
549
+ this.persist();
550
+ this.enterStep();
551
+ }
552
+ emitStatus(status) {
553
+ try {
554
+ this.onStatus?.(status);
555
+ } catch {
556
+ }
557
+ }
558
+ /** The end user pressed stop on the bar. The same ending a `dismiss_flow`
559
+ * trigger produces, so one abandoned run is one `flow_abandoned` event
560
+ * however it was abandoned. */
561
+ dismiss() {
562
+ if (this.stopped) return;
563
+ this.finish("dismissed");
564
+ }
565
+ emit(name, payload) {
566
+ try {
567
+ this.track(
568
+ name,
569
+ {
570
+ flowId: this.flow.cvid,
571
+ flowSessionId: this.flowSessionId,
572
+ stepCvid: this.currentStep()?.cvid
573
+ },
574
+ payload
575
+ );
576
+ } catch {
577
+ }
578
+ }
579
+ finish(reason, payload) {
580
+ this.lastStopReason = reason;
581
+ const eventName = reason === "completed" ? "flow_completed" : reason === "dismissed" ? "flow_abandoned" : "flow_error";
582
+ this.emit(eventName, payload);
583
+ try {
584
+ this.onCancelFill?.();
585
+ } catch {
586
+ }
587
+ this.stopStep();
588
+ this.stopped = true;
589
+ this.runState.clear();
590
+ if (reason === "completed") {
591
+ this.emitStatus({
592
+ name: this.flow.publicName ?? this.flow.name,
593
+ step: this.flow.steps.length,
594
+ total: this.flow.steps.length,
595
+ phase: "done"
596
+ });
597
+ }
598
+ this.emitStatus(null);
599
+ if (reason === "error") {
600
+ try {
601
+ this.onFailure?.();
602
+ } catch {
603
+ }
604
+ }
605
+ }
606
+ stopStep() {
607
+ this.teardownWatch();
608
+ }
609
+ fireTrigger(trigger) {
610
+ if (trigger.actionType === "dismiss_flow") {
611
+ this.finish("dismissed");
612
+ return;
613
+ }
614
+ if (trigger.actionType === "go_to_step" && trigger.targetStepCvid) {
615
+ this.jumpTo(trigger.targetStepCvid);
616
+ }
617
+ }
618
+ parsedTrigger(trigger) {
619
+ if (!this.parsedTriggers.has(trigger.cvid)) {
620
+ this.parsedTriggers.set(trigger.cvid, parseTriggerCondition(trigger.condition));
621
+ }
622
+ return this.parsedTriggers.get(trigger.cvid) ?? null;
623
+ }
624
+ checkTriggers(step, ctx) {
625
+ if (this.stopped) return;
626
+ if (this.inputPending) return;
627
+ const now = this.now();
628
+ for (const trigger of [...step.triggers].sort((a, b) => a.position - b.position)) {
629
+ const parsed = this.parsedTrigger(trigger);
630
+ if (!parsed) continue;
631
+ const isTrue = evaluateCondition(parsed.node, ctx);
632
+ const key = trigger.cvid;
633
+ if (!isTrue) {
634
+ this.trueSince.delete(key);
635
+ continue;
636
+ }
637
+ const since = this.trueSince.get(key) ?? now;
638
+ if (!this.trueSince.has(key)) this.trueSince.set(key, since);
639
+ const waitMs = (trigger.waitSeconds ?? 0) * 1e3;
640
+ if (now - since >= waitMs) {
641
+ this.fireTrigger(trigger);
642
+ return;
643
+ }
644
+ }
645
+ }
646
+ scheduleCheck(step) {
647
+ if (this.stopped || this.throttleHandle !== null) return;
648
+ const wait = Math.max(0, TRIGGER_THROTTLE_MS - (this.now() - this.lastCheck));
649
+ this.throttleHandle = this.setTimeoutImpl(() => {
650
+ this.throttleHandle = null;
651
+ this.lastCheck = this.now();
652
+ this.checkTriggers(step, this.conditionContext());
653
+ }, wait);
654
+ }
655
+ startWatch(step) {
656
+ const anyNeedsInterval = step.triggers.some((trigger) => {
657
+ const parsed = this.parsedTrigger(trigger);
658
+ return parsed ? needsIntervalCheck(parsed.node) : false;
659
+ });
660
+ const anyNeedsIdle = step.triggers.some((trigger) => {
661
+ const parsed = this.parsedTrigger(trigger);
662
+ return parsed ? needsIdleCheck(parsed.node) : false;
663
+ });
664
+ this.lastActivityAt = this.now();
665
+ if (typeof MutationObserver === "function") {
666
+ try {
667
+ this.observer = new MutationObserver(() => this.scheduleCheck(step));
668
+ this.observer.observe(this.doc.documentElement, {
669
+ childList: true,
670
+ subtree: true,
671
+ attributes: true,
672
+ characterData: true
673
+ });
674
+ } catch {
675
+ }
676
+ }
677
+ if (anyNeedsInterval) {
678
+ try {
679
+ this.intervalHandle = this.setIntervalImpl(
680
+ () => this.scheduleCheck(step),
681
+ anyNeedsIdle ? IDLE_CHECK_INTERVAL_MS : TIME_CHECK_INTERVAL_MS
682
+ );
683
+ } catch {
684
+ }
685
+ }
686
+ if (anyNeedsIdle) {
687
+ for (const name of ACTIVITY_EVENTS) {
688
+ const listener = () => {
689
+ this.lastActivityAt = this.now();
690
+ };
691
+ try {
692
+ this.doc.addEventListener(name, listener, { capture: true, passive: true });
693
+ this.activityListeners.push([name, listener]);
694
+ } catch {
695
+ }
696
+ }
697
+ }
698
+ this.clickListener = (event) => {
699
+ for (const trigger of step.triggers) {
700
+ const parsed = this.parsedTrigger(trigger);
701
+ const target = findElementTargets(parsed?.node);
702
+ for (const t of target) {
703
+ const result = matchTarget(t, this.doc);
704
+ if (result.status === "matched" && event.target instanceof Node && (result.element === event.target || result.element.contains(event.target))) {
705
+ this.clicked.add(t);
706
+ }
707
+ }
708
+ }
709
+ this.scheduleCheck(step);
710
+ };
711
+ try {
712
+ this.doc.addEventListener("click", this.clickListener, true);
713
+ } catch {
714
+ this.clickListener = null;
715
+ }
716
+ for (const name of ["input", "change", "focusin", "focusout"]) {
717
+ const listener = () => this.scheduleCheck(step);
718
+ try {
719
+ this.doc.addEventListener(name, listener, true);
720
+ this.inputListeners.push([name, listener]);
721
+ } catch {
722
+ }
723
+ }
724
+ this.checkTriggers(step, this.conditionContext());
725
+ }
726
+ async runExecutor(step) {
727
+ const startIndex = this.stepIndex;
728
+ try {
729
+ const outcome = await this.executor(step, {
730
+ doc: this.doc,
731
+ win: this.win,
732
+ resolveElement: (target, toleranceMs) => this.resolveElement(target, toleranceMs),
733
+ waitForElement: (target) => this.waitForElement(target),
734
+ setInputPending: (pending) => {
735
+ this.inputPending = pending;
736
+ }
737
+ });
738
+ if (this.stopped || this.stepIndex !== startIndex) return;
739
+ if (outcome.status === "advance") {
740
+ this.emit("flow_step_completed");
741
+ if (step.isCompletion) {
742
+ this.finish("completed");
743
+ return;
744
+ }
745
+ const nextIndex = this.stepIndex + 1;
746
+ if (nextIndex >= this.flow.steps.length) {
747
+ this.finish("completed");
748
+ return;
749
+ }
750
+ this.stopStep();
751
+ this.stepIndex = nextIndex;
752
+ this.persist();
753
+ this.enterStep();
754
+ return;
755
+ }
756
+ if (outcome.status === "navigated") {
757
+ this.emit("flow_step_completed");
758
+ if (outcome.navigateTarget === "new_tab") {
759
+ this.finish("completed");
760
+ return;
761
+ }
762
+ this.stopStep();
763
+ if (step.isCompletion) {
764
+ this.finish("completed");
765
+ return;
766
+ }
767
+ const nextIndex = this.stepIndex + 1;
768
+ if (nextIndex >= this.flow.steps.length) {
769
+ this.finish("completed");
770
+ return;
771
+ }
772
+ this.stepIndex = nextIndex;
773
+ this.persist();
774
+ return;
775
+ }
776
+ if (outcome.status === "error") {
777
+ this.finish(
778
+ "error",
779
+ outcome.reason === "element_not_found" ? { reason: outcome.reason, liveManifest: captureLiveManifest(this.doc) } : { reason: outcome.reason }
780
+ );
781
+ return;
782
+ }
783
+ } catch {
784
+ }
785
+ }
786
+ enterStep() {
787
+ const step = this.currentStep();
788
+ if (!step || this.stopped) {
789
+ this.finish(step ? "completed" : "error", step ? void 0 : { reason: "missing_step" });
790
+ return;
791
+ }
792
+ this.persist();
793
+ this.emit("flow_step_entered");
794
+ this.emitStatus({
795
+ name: this.flow.publicName ?? this.flow.name,
796
+ step: this.stepIndex + 1,
797
+ total: this.flow.steps.length,
798
+ phase: "running"
799
+ });
800
+ const begin = () => {
801
+ if (this.stopped) return;
802
+ this.startWatch(step);
803
+ void this.runExecutor(step);
804
+ };
805
+ this.waitForPageReady(begin);
806
+ }
807
+ /** Holds until the document is past parsing, then one `STEP_SETTLE_MS`
808
+ * beat, then runs `begin`. Gives up waiting on `readyState` after
809
+ * `PAGE_READY_MAX_MS` and starts the step regardless: a page that slow is
810
+ * `waitForElement`'s problem, and it has the flow's full wait window for
811
+ * it. Uses the same `dwellTimer` slot as the old dwell, so `stopStep`
812
+ * cancels a pending entry exactly as before. */
813
+ waitForPageReady(begin) {
814
+ const deadline = this.now() + PAGE_READY_MAX_MS;
815
+ const settleThenBegin = () => {
816
+ this.dwellTimer = this.setTimeoutImpl(() => {
817
+ this.dwellTimer = null;
818
+ begin();
819
+ }, STEP_SETTLE_MS);
820
+ };
821
+ const check = () => {
822
+ if (this.stopped) return;
823
+ if (this.doc.readyState !== "loading" || this.now() >= deadline) {
824
+ settleThenBegin();
825
+ return;
826
+ }
827
+ this.dwellTimer = this.setTimeoutImpl(() => {
828
+ this.dwellTimer = null;
829
+ check();
830
+ }, ELEMENT_WAIT_POLL_MS);
831
+ };
832
+ check();
833
+ }
834
+ begin() {
835
+ this.enterStep();
836
+ }
837
+ stop() {
838
+ if (this.stopped) return;
839
+ try {
840
+ this.onCancelFill?.();
841
+ } catch {
842
+ }
843
+ this.stopStep();
844
+ this.stopped = true;
845
+ }
846
+ };
847
+ function createFlowRuntime(options) {
848
+ const {
849
+ context,
850
+ executor = defaultExecutor,
851
+ track = context.track ?? noopTrack,
852
+ now = () => Date.now(),
853
+ setTimeoutImpl = (fn, ms) => setTimeout(fn, ms),
854
+ clearTimeoutImpl = (handle) => clearTimeout(handle),
855
+ setIntervalImpl = (fn, ms) => setInterval(fn, ms),
856
+ clearIntervalImpl = (handle) => clearInterval(handle)
857
+ } = options;
858
+ const doc = context.doc ?? document;
859
+ const win = context.win ?? window;
860
+ const runState = createFlowRunState(context.token);
861
+ const apiOptions = {
862
+ apiBase: context.apiBase,
863
+ sessionToken: context.sessionToken,
864
+ sessionId: context.sessionId
865
+ };
866
+ let active = null;
867
+ async function loadFlow(flowCvid) {
868
+ const result = await fetchFlowRun(flowCvid, apiOptions);
869
+ return result.ok ? result.flow : null;
870
+ }
871
+ function beginRun(flow, stepIndex, startedAt, flowSessionId, overrides = {}) {
872
+ active?.stop();
873
+ active = new ActiveRun(
874
+ flow,
875
+ stepIndex,
876
+ startedAt,
877
+ flowSessionId,
878
+ doc,
879
+ win,
880
+ overrides.runState ?? runState,
881
+ executor,
882
+ context.getTrait,
883
+ context.isFirstVisit,
884
+ overrides.track ?? track,
885
+ now,
886
+ setTimeoutImpl,
887
+ clearTimeoutImpl,
888
+ setIntervalImpl,
889
+ clearIntervalImpl
890
+ );
891
+ active.onStatus = (status) => context.onStatus?.(status);
892
+ active.onFailure = () => context.onFailure?.();
893
+ active.onCancelFill = () => context.cancelFill?.();
894
+ active.begin();
895
+ }
896
+ return {
897
+ async start(flowCvid) {
898
+ const flow = await loadFlow(flowCvid);
899
+ if (!flow || flow.steps.length === 0) return;
900
+ const flowSessionId = uuidV4();
901
+ try {
902
+ track("flow_started", { flowId: flow.cvid, flowSessionId });
903
+ } catch {
904
+ }
905
+ beginRun(flow, 0, now(), flowSessionId);
906
+ },
907
+ async resume() {
908
+ const pending = runState.read();
909
+ if (!pending) return false;
910
+ const flow = await loadFlow(pending.flowId);
911
+ if (!flow || flow.steps.length === 0) {
912
+ runState.clear();
913
+ return false;
914
+ }
915
+ const stepIndex = findStepIndex(flow, pending.currentStepCvid);
916
+ if (stepIndex === -1) {
917
+ runState.clear();
918
+ return false;
919
+ }
920
+ beginRun(flow, stepIndex, pending.startedAt, pending.flowSessionId);
921
+ return true;
922
+ },
923
+ stop() {
924
+ active?.dismiss();
925
+ active = null;
926
+ },
927
+ // ── Preview (ADR 0137, D1081) ────────────────────────────────────────
928
+ // A real run through the exact same ActiveRun, differing in only two
929
+ // injected seams: run state persists to `arcy.preview.<token>`
930
+ // (sessionStorage, carrying the draft itself, since the spent nonce
931
+ // cannot serve it twice), and telemetry is `noopTrack`, so a preview
932
+ // never touches funnel numbers even if the page-level telemetry stop
933
+ // were somehow missed. Everything else - executor, promptFill, status
934
+ // surface, failure stop - is the production path, which is the whole
935
+ // point of previewing.
936
+ async startPreview(raw) {
937
+ const flow = readFlowDetail(raw);
938
+ if (!flow || flow.steps.length === 0) return;
939
+ const previewState = createPreviewRunState(context.token);
940
+ beginRun(flow, 0, now(), uuidV4(), {
941
+ runState: previewStateAdapter(previewState, raw),
942
+ track: noopTrack
943
+ });
944
+ },
945
+ async resumePreview() {
946
+ const previewState = createPreviewRunState(context.token);
947
+ const pending = previewState.read();
948
+ if (!pending) return false;
949
+ const flow = readFlowDetail(pending.flow);
950
+ if (!flow || flow.steps.length === 0) {
951
+ previewState.clear();
952
+ return false;
953
+ }
954
+ const stepIndex = findStepIndex(flow, pending.currentStepCvid);
955
+ if (stepIndex === -1) {
956
+ previewState.clear();
957
+ return false;
958
+ }
959
+ beginRun(flow, stepIndex, pending.startedAt, pending.flowSessionId, {
960
+ runState: previewStateAdapter(previewState, pending.flow),
961
+ track: noopTrack
962
+ });
963
+ return true;
964
+ },
965
+ destroy() {
966
+ active?.stop();
967
+ active = null;
968
+ }
969
+ };
970
+ }
971
+ function previewStateAdapter(previewState, rawFlow) {
972
+ return {
973
+ // ActiveRun only ever writes; reads happen in resumePreview() above.
974
+ read: () => null,
975
+ save: (run) => previewState.save({ ...run, flow: rawFlow }),
976
+ clear: () => previewState.clear()
977
+ };
978
+ }
979
+
980
+ // src/flow/executor.ts
981
+ var SCROLL_MARGIN = 24;
982
+ function scrollOffsetFor(rect, viewportHeight, occlusion) {
983
+ const floor = viewportHeight - Math.max(0, occlusion);
984
+ if (!(viewportHeight > 0) || floor <= 0) return 0;
985
+ if (rect.bottom > floor) {
986
+ const needed = rect.bottom - floor + SCROLL_MARGIN;
987
+ const room = Math.max(0, rect.top - SCROLL_MARGIN);
988
+ return Math.min(needed, room);
989
+ }
990
+ if (rect.top < 0) return rect.top - SCROLL_MARGIN;
991
+ return 0;
992
+ }
993
+ function scrollIntoViewAboveBar(el, ctx, options) {
994
+ try {
995
+ const occlusion = options.getBottomOcclusion?.() ?? 0;
996
+ const rect = el.getBoundingClientRect();
997
+ const height = ctx.doc.documentElement?.clientHeight || ctx.win.innerHeight || 0;
998
+ const delta = scrollOffsetFor(rect, height, occlusion);
999
+ if (delta === 0) return;
1000
+ ctx.win.scrollBy({ top: delta, behavior: "smooth" });
1001
+ } catch {
1002
+ }
1003
+ }
1004
+ function dispatchClickSequence(el, doc) {
1005
+ const win = doc.defaultView;
1006
+ const PointerCtor = win?.PointerEvent;
1007
+ const rect = (() => {
1008
+ try {
1009
+ return el.getBoundingClientRect();
1010
+ } catch {
1011
+ return null;
1012
+ }
1013
+ })();
1014
+ const point = {
1015
+ bubbles: true,
1016
+ cancelable: true,
1017
+ clientX: rect ? rect.left + rect.width / 2 : 0,
1018
+ clientY: rect ? rect.top + rect.height / 2 : 0
1019
+ };
1020
+ const firePointer = (type) => {
1021
+ try {
1022
+ if (typeof PointerCtor === "function") {
1023
+ el.dispatchEvent(new PointerCtor(type, { ...point, pointerType: "mouse" }));
1024
+ }
1025
+ } catch {
1026
+ }
1027
+ };
1028
+ const fireMouse = (type) => {
1029
+ try {
1030
+ el.dispatchEvent(new MouseEvent(type, point));
1031
+ } catch {
1032
+ }
1033
+ };
1034
+ firePointer("pointerover");
1035
+ firePointer("pointerenter");
1036
+ firePointer("pointerdown");
1037
+ fireMouse("mousedown");
1038
+ try {
1039
+ if (typeof el.focus === "function") el.focus();
1040
+ } catch {
1041
+ }
1042
+ firePointer("pointerup");
1043
+ fireMouse("mouseup");
1044
+ fireMouse("click");
1045
+ }
1046
+ function setNativeValue(el, value) {
1047
+ const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : el instanceof HTMLInputElement ? HTMLInputElement.prototype : el instanceof HTMLSelectElement ? HTMLSelectElement.prototype : null;
1048
+ if (!proto) return false;
1049
+ const descriptor = Object.getOwnPropertyDescriptor(proto, "value");
1050
+ const setter = descriptor?.set;
1051
+ if (!setter) return false;
1052
+ try {
1053
+ setter.call(el, value);
1054
+ return true;
1055
+ } catch {
1056
+ return false;
1057
+ }
1058
+ }
1059
+ function dispatchInputChange(el, doc) {
1060
+ const win = doc.defaultView;
1061
+ try {
1062
+ el.dispatchEvent(new (win?.Event ?? Event)("input", { bubbles: true }));
1063
+ } catch {
1064
+ }
1065
+ try {
1066
+ el.dispatchEvent(new (win?.Event ?? Event)("change", { bubbles: true }));
1067
+ } catch {
1068
+ }
1069
+ }
1070
+ async function dispatchClickStep(step, ctx, options) {
1071
+ if (!isTargetFingerprint(step.target)) {
1072
+ return { status: "error", reason: "invalid_target" };
1073
+ }
1074
+ const result = await ctx.waitForElement(step.target);
1075
+ if (result.status !== "matched" || !result.element.isConnected) {
1076
+ return { status: "error", reason: "element_not_found" };
1077
+ }
1078
+ scrollIntoViewAboveBar(result.element, ctx, options);
1079
+ try {
1080
+ dispatchClickSequence(result.element, ctx.doc);
1081
+ } catch {
1082
+ return { status: "error", reason: "dispatch_failed" };
1083
+ }
1084
+ return { status: "advance" };
1085
+ }
1086
+ var AUTHORED_INPUT_TYPES = [
1087
+ "text",
1088
+ "number",
1089
+ "email",
1090
+ "date",
1091
+ "url",
1092
+ "tel"
1093
+ ];
1094
+ function readAuthoredInputType(value) {
1095
+ return AUTHORED_INPUT_TYPES.includes(value) ? value : null;
1096
+ }
1097
+ var INPUT_TYPE_TO_KIND = {
1098
+ text: "text",
1099
+ search: "text",
1100
+ number: "number",
1101
+ email: "email",
1102
+ url: "url",
1103
+ tel: "tel",
1104
+ password: "password",
1105
+ date: "date",
1106
+ time: "time",
1107
+ "datetime-local": "datetime",
1108
+ month: "month",
1109
+ week: "week",
1110
+ checkbox: "checkbox",
1111
+ radio: "radio",
1112
+ range: "range"
1113
+ };
1114
+ function readAttr(el, name) {
1115
+ try {
1116
+ const value = el.getAttribute(name);
1117
+ return value === null || value === "" ? null : value;
1118
+ } catch {
1119
+ return null;
1120
+ }
1121
+ }
1122
+ function selectOptions(el) {
1123
+ const options = [];
1124
+ for (let i = 0; i < el.options.length; i += 1) {
1125
+ const option = el.options[i];
1126
+ if (!option || option.disabled) continue;
1127
+ const label = (option.label || option.textContent || "").trim();
1128
+ if (option.value === "") continue;
1129
+ options.push({ value: option.value, label: label || option.value });
1130
+ }
1131
+ return options;
1132
+ }
1133
+ function radioOptions(el, doc) {
1134
+ const name = el.getAttribute("name");
1135
+ const scope = el.form ?? doc;
1136
+ const group = [];
1137
+ if (name) {
1138
+ try {
1139
+ const selector = `input[type="radio"][name="${CSS.escape(name)}"]`;
1140
+ scope.querySelectorAll(selector).forEach((node) => {
1141
+ if (node instanceof HTMLInputElement) group.push(node);
1142
+ });
1143
+ } catch {
1144
+ }
1145
+ }
1146
+ if (group.length === 0) group.push(el);
1147
+ return group.map((radio) => ({
1148
+ value: radio.value,
1149
+ label: radioLabel(radio) || radio.value
1150
+ }));
1151
+ }
1152
+ function controlLabelText(el) {
1153
+ try {
1154
+ const labels = el.labels;
1155
+ if (labels && labels.length > 0) {
1156
+ return (labels[0]?.textContent ?? "").trim();
1157
+ }
1158
+ } catch {
1159
+ }
1160
+ try {
1161
+ const closest = el.closest("label");
1162
+ if (closest) return (closest.textContent ?? "").trim();
1163
+ } catch {
1164
+ }
1165
+ return "";
1166
+ }
1167
+ function radioLabel(el) {
1168
+ return controlLabelText(el);
1169
+ }
1170
+ function readFillSpec(el, authored, doc) {
1171
+ let kind = "custom";
1172
+ let options = null;
1173
+ let required = false;
1174
+ let min = null;
1175
+ let max = null;
1176
+ let step = null;
1177
+ let maxLength = null;
1178
+ let pattern = null;
1179
+ if (el instanceof HTMLSelectElement) {
1180
+ kind = "select";
1181
+ options = selectOptions(el);
1182
+ required = el.required || !el.querySelector('option[value=""]');
1183
+ } else if (el instanceof HTMLTextAreaElement) {
1184
+ kind = "textarea";
1185
+ required = el.required;
1186
+ if (el.maxLength > 0) maxLength = el.maxLength;
1187
+ } else if (el instanceof HTMLInputElement) {
1188
+ const type = (readAttr(el, "type") ?? "text").toLowerCase();
1189
+ kind = INPUT_TYPE_TO_KIND[type] ?? "text";
1190
+ required = el.required;
1191
+ min = readAttr(el, "min");
1192
+ max = readAttr(el, "max");
1193
+ step = readAttr(el, "step");
1194
+ pattern = readAttr(el, "pattern");
1195
+ if (el.maxLength > 0) maxLength = el.maxLength;
1196
+ if (kind === "radio") options = radioOptions(el, doc);
1197
+ if (kind === "checkbox") {
1198
+ options = [{ value: "true", label: controlLabelText(el) }];
1199
+ }
1200
+ }
1201
+ if (authored && authored !== "text" && (kind === "text" || kind === "custom" || kind === "textarea")) {
1202
+ kind = authored;
1203
+ }
1204
+ return { kind, required, options, min, max, step, maxLength, pattern };
1205
+ }
1206
+ function findEditable(el) {
1207
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
1208
+ return el;
1209
+ }
1210
+ if (el instanceof HTMLElement && el.isContentEditable) return el;
1211
+ try {
1212
+ const inner = el.querySelector(
1213
+ 'input:not([type="hidden"]), textarea, select, [contenteditable="true"], [contenteditable=""]'
1214
+ );
1215
+ return inner instanceof HTMLElement ? inner : null;
1216
+ } catch {
1217
+ return null;
1218
+ }
1219
+ }
1220
+ function dispatchKeyboardShadow(el, doc) {
1221
+ const win = doc.defaultView;
1222
+ const Ctor = win?.KeyboardEvent;
1223
+ if (typeof Ctor !== "function") return;
1224
+ for (const type of ["keydown", "keyup"]) {
1225
+ try {
1226
+ el.dispatchEvent(new Ctor(type, { bubbles: true, cancelable: true }));
1227
+ } catch {
1228
+ }
1229
+ }
1230
+ }
1231
+ function applyFillReply(el, reply, kind, doc) {
1232
+ if (kind === "checkbox" && el instanceof HTMLInputElement) {
1233
+ const desired = reply === "true";
1234
+ if (el.checked !== desired) dispatchClickSequence(el, doc);
1235
+ if (el.checked !== desired) {
1236
+ el.checked = desired;
1237
+ dispatchInputChange(el, doc);
1238
+ }
1239
+ return true;
1240
+ }
1241
+ if (kind === "radio" && el instanceof HTMLInputElement) {
1242
+ const scope = el.form ?? doc;
1243
+ const name = el.getAttribute("name");
1244
+ let chosen = null;
1245
+ if (name) {
1246
+ try {
1247
+ const selector = `input[type="radio"][name="${CSS.escape(name)}"]`;
1248
+ scope.querySelectorAll(selector).forEach((node) => {
1249
+ if (node instanceof HTMLInputElement && node.value === reply) {
1250
+ chosen = node;
1251
+ }
1252
+ });
1253
+ } catch {
1254
+ }
1255
+ }
1256
+ const target = chosen ?? (el.value === reply ? el : null);
1257
+ if (!target) return false;
1258
+ dispatchClickSequence(target, doc);
1259
+ const settled = target;
1260
+ if (!settled.checked) {
1261
+ settled.checked = true;
1262
+ dispatchInputChange(settled, doc);
1263
+ }
1264
+ return true;
1265
+ }
1266
+ const editable = findEditable(el);
1267
+ if (!editable) return false;
1268
+ if (editable instanceof HTMLElement && editable.isContentEditable) {
1269
+ try {
1270
+ editable.focus();
1271
+ } catch {
1272
+ }
1273
+ try {
1274
+ editable.textContent = reply;
1275
+ } catch {
1276
+ return false;
1277
+ }
1278
+ dispatchInputChange(editable, doc);
1279
+ return true;
1280
+ }
1281
+ try {
1282
+ editable.focus();
1283
+ } catch {
1284
+ }
1285
+ if (!setNativeValue(editable, reply)) return false;
1286
+ dispatchKeyboardShadow(editable, doc);
1287
+ dispatchInputChange(editable, doc);
1288
+ return true;
1289
+ }
1290
+ async function dispatchFillStep(step, ctx, options) {
1291
+ if (!isTargetFingerprint(step.target)) {
1292
+ return { status: "error", reason: "invalid_target" };
1293
+ }
1294
+ if (!options.promptFill) {
1295
+ return { status: "wait" };
1296
+ }
1297
+ const result = await ctx.waitForElement(step.target);
1298
+ if (result.status !== "matched" || !result.element.isConnected) {
1299
+ return { status: "error", reason: "element_not_found" };
1300
+ }
1301
+ const spec = readFillSpec(
1302
+ result.element,
1303
+ readAuthoredInputType(step.fillInputType),
1304
+ ctx.doc
1305
+ );
1306
+ const question = (step.fillDescription ?? "").trim() || step.label || "";
1307
+ let errorCode = null;
1308
+ let lastInvalid = null;
1309
+ let reply;
1310
+ for (; ; ) {
1311
+ let answer;
1312
+ ctx.setInputPending(true);
1313
+ try {
1314
+ answer = await options.promptFill({ ...spec, question, errorCode });
1315
+ } catch {
1316
+ return { status: "error", reason: "prompt_failed" };
1317
+ } finally {
1318
+ ctx.setInputPending(false);
1319
+ }
1320
+ if (answer === null) {
1321
+ return { status: "error", reason: "fill_cancelled" };
1322
+ }
1323
+ const normalized = spec.kind === "select" || spec.kind === "radio" ? normalizeOptionReply(answer, spec.options) : spec.kind === "password" ? (
1324
+ // Never trimmed: a password's whitespace is the user's own.
1325
+ answer
1326
+ ) : answer.trim();
1327
+ errorCode = validateFillReply(normalized, { ...spec, question, errorCode: null });
1328
+ if (errorCode === null) {
1329
+ reply = normalized;
1330
+ break;
1331
+ }
1332
+ if (lastInvalid !== null && lastInvalid === normalized) {
1333
+ return { status: "error", reason: "fill_invalid" };
1334
+ }
1335
+ lastInvalid = normalized;
1336
+ }
1337
+ const fresh = await ctx.waitForElement(step.target);
1338
+ if (fresh.status !== "matched" || !fresh.element.isConnected) {
1339
+ return { status: "error", reason: "element_not_found" };
1340
+ }
1341
+ const freshSpec = readFillSpec(
1342
+ fresh.element,
1343
+ readAuthoredInputType(step.fillInputType),
1344
+ ctx.doc
1345
+ );
1346
+ if (freshSpec.kind !== spec.kind) {
1347
+ return { status: "error", reason: "unfillable_target" };
1348
+ }
1349
+ scrollIntoViewAboveBar(fresh.element, ctx, options);
1350
+ if (!applyFillReply(fresh.element, reply, freshSpec.kind, ctx.doc)) {
1351
+ return { status: "error", reason: "unfillable_target" };
1352
+ }
1353
+ return { status: "advance" };
1354
+ }
1355
+ function isSafeNavigateUrl(url) {
1356
+ if (url.startsWith("/") && !url.startsWith("//")) return true;
1357
+ try {
1358
+ const parsed = new URL(url);
1359
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
1360
+ } catch {
1361
+ return false;
1362
+ }
1363
+ }
1364
+ async function dispatchNavigateStep(step, ctx) {
1365
+ if (!step.navigateUrl || !step.navigateTarget) {
1366
+ return { status: "error", reason: "invalid_navigate_step" };
1367
+ }
1368
+ if (!isSafeNavigateUrl(step.navigateUrl)) {
1369
+ return { status: "error", reason: "invalid_navigate_step" };
1370
+ }
1371
+ try {
1372
+ if (step.navigateTarget === "new_tab") {
1373
+ ctx.win.open(step.navigateUrl, "_blank", "noopener,noreferrer");
1374
+ } else {
1375
+ ctx.win.location.assign(step.navigateUrl);
1376
+ }
1377
+ } catch {
1378
+ return { status: "error", reason: "navigate_failed" };
1379
+ }
1380
+ return { status: "navigated", navigateTarget: step.navigateTarget };
1381
+ }
1382
+ function createDomExecutor(options = {}) {
1383
+ return async (step, ctx) => {
1384
+ try {
1385
+ switch (step.type) {
1386
+ case "click":
1387
+ return await dispatchClickStep(step, ctx, options);
1388
+ case "fill":
1389
+ return await dispatchFillStep(step, ctx, options);
1390
+ case "navigate":
1391
+ return await dispatchNavigateStep(step, ctx);
1392
+ default:
1393
+ return { status: "error", reason: "unknown_step_type" };
1394
+ }
1395
+ } catch {
1396
+ return { status: "error", reason: "executor_threw" };
1397
+ }
1398
+ };
1399
+ }
1400
+
1401
+ // src/flow/index.ts
1402
+ function mount(context) {
1403
+ try {
1404
+ const executor = createDomExecutor({
1405
+ promptFill: context.promptFill,
1406
+ // 26.4: the bar covers the bottom of the screen, so a target under it
1407
+ // is scrolled into view before it is clicked or filled.
1408
+ getBottomOcclusion: context.getBottomOcclusion
1409
+ });
1410
+ return createFlowRuntime({ context, executor });
1411
+ } catch {
1412
+ return null;
1413
+ }
1414
+ }
1415
+ var registration = { contract: FLOW_CONTRACT, mount };
1416
+ if (typeof window !== "undefined") {
1417
+ try {
1418
+ ;
1419
+ window[FLOW_GLOBAL] = registration;
1420
+ } catch {
1421
+ }
1422
+ }