opensteer 0.4.14 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -360,7 +360,7 @@ var import_net = require("net");
360
360
  var import_fs4 = require("fs");
361
361
 
362
362
  // src/opensteer.ts
363
- var import_crypto2 = require("crypto");
363
+ var import_crypto = require("crypto");
364
364
 
365
365
  // src/browser/pool.ts
366
366
  var import_playwright = require("playwright");
@@ -2142,7 +2142,6 @@ var LocalSelectorStorage = class {
2142
2142
 
2143
2143
  // src/html/pipeline.ts
2144
2144
  var cheerio3 = __toESM(require("cheerio"), 1);
2145
- var import_crypto = require("crypto");
2146
2145
 
2147
2146
  // src/html/serializer.ts
2148
2147
  var cheerio = __toESM(require("cheerio"), 1);
@@ -2415,9 +2414,6 @@ var ENSURE_NAME_SHIM_SCRIPT = `
2415
2414
  `;
2416
2415
  var OS_FRAME_TOKEN_KEY = "__opensteerFrameToken";
2417
2416
  var OS_INSTANCE_TOKEN_KEY = "__opensteerInstanceToken";
2418
- var OS_COUNTER_OWNER_KEY = "__opensteerCounterOwner";
2419
- var OS_COUNTER_VALUE_KEY = "__opensteerCounterValue";
2420
- var OS_COUNTER_NEXT_KEY = "__opensteerCounterNext";
2421
2417
 
2422
2418
  // src/element-path/build.ts
2423
2419
  var MAX_ATTRIBUTE_VALUE_LENGTH = 300;
@@ -4207,567 +4203,178 @@ function cleanForAction(html) {
4207
4203
  return compactHtml(htmlOut);
4208
4204
  }
4209
4205
 
4210
- // src/extract-value-normalization.ts
4211
- var URL_LIST_ATTRIBUTES = /* @__PURE__ */ new Set(["srcset", "imagesrcset", "ping"]);
4212
- function normalizeExtractedValue(raw, attribute) {
4213
- if (raw == null) return null;
4214
- const rawText = String(raw);
4215
- if (!rawText.trim()) return null;
4216
- const normalizedAttribute = String(attribute || "").trim().toLowerCase();
4217
- if (URL_LIST_ATTRIBUTES.has(normalizedAttribute)) {
4218
- const singleValue = pickSingleListAttributeValue(
4219
- normalizedAttribute,
4220
- rawText
4221
- ).trim();
4222
- return singleValue || null;
4223
- }
4224
- const text = rawText.replace(/\s+/g, " ").trim();
4225
- return text || null;
4226
- }
4227
- function pickSingleListAttributeValue(attribute, raw) {
4228
- if (attribute === "ping") {
4229
- const firstUrl = raw.trim().split(/\s+/)[0] || "";
4230
- return firstUrl.trim();
4231
- }
4232
- if (attribute === "srcset" || attribute === "imagesrcset") {
4233
- const picked = pickBestSrcsetCandidate(raw);
4234
- if (picked) return picked;
4235
- return pickFirstSrcsetToken(raw) || "";
4236
- }
4237
- return raw.trim();
4238
- }
4239
- function pickBestSrcsetCandidate(raw) {
4240
- const candidates = parseSrcsetCandidates(raw);
4241
- if (!candidates.length) return null;
4242
- const widthCandidates = candidates.filter(
4243
- (candidate) => typeof candidate.width === "number" && Number.isFinite(candidate.width) && candidate.width > 0
4244
- );
4245
- if (widthCandidates.length) {
4246
- return widthCandidates.reduce(
4247
- (best, candidate) => candidate.width > best.width ? candidate : best
4248
- ).url;
4249
- }
4250
- const densityCandidates = candidates.filter(
4251
- (candidate) => typeof candidate.density === "number" && Number.isFinite(candidate.density) && candidate.density > 0
4252
- );
4253
- if (densityCandidates.length) {
4254
- return densityCandidates.reduce(
4255
- (best, candidate) => candidate.density > best.density ? candidate : best
4256
- ).url;
4257
- }
4258
- return candidates[0]?.url || null;
4259
- }
4260
- function parseSrcsetCandidates(raw) {
4261
- const text = String(raw || "").trim();
4262
- if (!text) return [];
4263
- const out = [];
4264
- let index = 0;
4265
- while (index < text.length) {
4266
- index = skipSeparators(text, index);
4267
- if (index >= text.length) break;
4268
- const urlToken = readUrlToken(text, index);
4269
- index = urlToken.nextIndex;
4270
- const url = urlToken.value.trim();
4271
- if (!url) continue;
4272
- index = skipWhitespace(text, index);
4273
- const descriptors = [];
4274
- while (index < text.length && text[index] !== ",") {
4275
- const descriptorToken = readDescriptorToken(text, index);
4276
- if (!descriptorToken.value) {
4277
- index = descriptorToken.nextIndex;
4278
- continue;
4279
- }
4280
- descriptors.push(descriptorToken.value);
4281
- index = descriptorToken.nextIndex;
4282
- index = skipWhitespace(text, index);
4283
- }
4284
- if (index < text.length && text[index] === ",") {
4285
- index += 1;
4286
- }
4287
- let width = null;
4288
- let density = null;
4289
- for (const descriptor of descriptors) {
4290
- const token = descriptor.trim().toLowerCase();
4291
- if (!token) continue;
4292
- const widthMatch = token.match(/^(\d+)w$/);
4293
- if (widthMatch) {
4294
- const parsed = Number.parseInt(widthMatch[1], 10);
4295
- if (Number.isFinite(parsed)) {
4296
- width = parsed;
4297
- }
4298
- continue;
4299
- }
4300
- const densityMatch = token.match(/^(\d*\.?\d+)x$/);
4301
- if (densityMatch) {
4302
- const parsed = Number.parseFloat(densityMatch[1]);
4303
- if (Number.isFinite(parsed)) {
4304
- density = parsed;
4305
- }
4306
- }
4307
- }
4308
- out.push({
4309
- url,
4310
- width,
4311
- density
4312
- });
4313
- }
4314
- return out;
4315
- }
4316
- function pickFirstSrcsetToken(raw) {
4317
- const candidate = parseSrcsetCandidates(raw)[0];
4318
- if (candidate?.url) {
4319
- return candidate.url;
4320
- }
4321
- const text = String(raw || "");
4322
- const start = skipSeparators(text, 0);
4323
- if (start >= text.length) return null;
4324
- const firstToken = readUrlToken(text, start).value.trim();
4325
- return firstToken || null;
4326
- }
4327
- function skipWhitespace(value, index) {
4328
- let cursor = index;
4329
- while (cursor < value.length && /\s/.test(value[cursor])) {
4330
- cursor += 1;
4331
- }
4332
- return cursor;
4333
- }
4334
- function skipSeparators(value, index) {
4335
- let cursor = skipWhitespace(value, index);
4336
- while (cursor < value.length && value[cursor] === ",") {
4337
- cursor += 1;
4338
- cursor = skipWhitespace(value, cursor);
4339
- }
4340
- return cursor;
4341
- }
4342
- function readUrlToken(value, index) {
4343
- let cursor = index;
4344
- let out = "";
4345
- const isDataUrl = value.slice(index, index + 5).toLowerCase().startsWith("data:");
4346
- while (cursor < value.length) {
4347
- const char = value[cursor];
4348
- if (/\s/.test(char)) {
4349
- break;
4350
- }
4351
- if (char === "," && !isDataUrl) {
4352
- break;
4353
- }
4354
- out += char;
4355
- cursor += 1;
4356
- }
4357
- if (isDataUrl && out.endsWith(",") && cursor < value.length) {
4358
- out = out.slice(0, -1);
4206
+ // src/html/pipeline.ts
4207
+ function applyCleaner(mode, html) {
4208
+ switch (mode) {
4209
+ case "clickable":
4210
+ return cleanForClickable(html);
4211
+ case "scrollable":
4212
+ return cleanForScrollable(html);
4213
+ case "extraction":
4214
+ return cleanForExtraction(html);
4215
+ case "full":
4216
+ return cleanForFull(html);
4217
+ case "action":
4218
+ default:
4219
+ return cleanForAction(html);
4359
4220
  }
4360
- return {
4361
- value: out,
4362
- nextIndex: cursor
4363
- };
4364
4221
  }
4365
- function readDescriptorToken(value, index) {
4366
- let cursor = skipWhitespace(value, index);
4367
- let out = "";
4368
- while (cursor < value.length) {
4369
- const char = value[cursor];
4370
- if (char === "," || /\s/.test(char)) {
4371
- break;
4222
+ async function assignCounters(page, html, nodePaths, nodeMeta) {
4223
+ const $ = cheerio3.load(html, { xmlMode: false });
4224
+ const counterIndex = /* @__PURE__ */ new Map();
4225
+ let nextCounter = 1;
4226
+ const assignedByNodeId = /* @__PURE__ */ new Map();
4227
+ $("*").each(function() {
4228
+ const el = $(this);
4229
+ const nodeId = el.attr(OS_NODE_ID_ATTR);
4230
+ if (!nodeId) return;
4231
+ const counter = nextCounter++;
4232
+ assignedByNodeId.set(nodeId, counter);
4233
+ const path5 = nodePaths.get(nodeId);
4234
+ el.attr("c", String(counter));
4235
+ el.removeAttr(OS_NODE_ID_ATTR);
4236
+ if (path5) {
4237
+ counterIndex.set(counter, cloneElementPath(path5));
4372
4238
  }
4373
- out += char;
4374
- cursor += 1;
4239
+ });
4240
+ try {
4241
+ await syncLiveCounters(page, nodeMeta, assignedByNodeId);
4242
+ } catch (error) {
4243
+ await clearLiveCounters(page);
4244
+ throw error;
4375
4245
  }
4246
+ $(`[${OS_NODE_ID_ATTR}]`).removeAttr(OS_NODE_ID_ATTR);
4376
4247
  return {
4377
- value: out.trim(),
4378
- nextIndex: cursor
4248
+ html: $.html(),
4249
+ counterIndex
4379
4250
  };
4380
4251
  }
4381
-
4382
- // src/html/counter-runtime.ts
4383
- var CounterResolutionError = class extends Error {
4384
- code;
4385
- constructor(code, message) {
4386
- super(message);
4387
- this.name = "CounterResolutionError";
4388
- this.code = code;
4389
- }
4390
- };
4391
- async function ensureLiveCounters(page, nodeMeta, nodeIds) {
4392
- const out = /* @__PURE__ */ new Map();
4393
- if (!nodeIds.length) return out;
4394
- const grouped = /* @__PURE__ */ new Map();
4395
- for (const nodeId of nodeIds) {
4252
+ async function syncLiveCounters(page, nodeMeta, assignedByNodeId) {
4253
+ await clearLiveCounters(page);
4254
+ if (!assignedByNodeId.size) return;
4255
+ const groupedByFrame = /* @__PURE__ */ new Map();
4256
+ for (const [nodeId, counter] of assignedByNodeId.entries()) {
4396
4257
  const meta = nodeMeta.get(nodeId);
4397
- if (!meta) {
4398
- throw new CounterResolutionError(
4399
- "ERR_COUNTER_STALE_OR_NOT_FOUND",
4400
- `Missing metadata for node ${nodeId}. Run snapshot() again.`
4401
- );
4402
- }
4403
- const list = grouped.get(meta.frameToken) || [];
4258
+ if (!meta?.frameToken) continue;
4259
+ const list = groupedByFrame.get(meta.frameToken) || [];
4404
4260
  list.push({
4405
4261
  nodeId,
4406
- instanceToken: meta.instanceToken
4262
+ counter
4407
4263
  });
4408
- grouped.set(meta.frameToken, list);
4264
+ groupedByFrame.set(meta.frameToken, list);
4409
4265
  }
4266
+ if (!groupedByFrame.size) return;
4267
+ const failures = [];
4410
4268
  const framesByToken = await mapFramesByToken(page);
4411
- let nextCounter = await readGlobalNextCounter(page);
4412
- const usedCounters = /* @__PURE__ */ new Map();
4413
- for (const [frameToken, entries] of grouped.entries()) {
4269
+ for (const [frameToken, entries] of groupedByFrame.entries()) {
4414
4270
  const frame = framesByToken.get(frameToken);
4415
4271
  if (!frame) {
4416
- throw new CounterResolutionError(
4417
- "ERR_COUNTER_FRAME_UNAVAILABLE",
4418
- `Counter frame ${frameToken} is unavailable. Run snapshot() again.`
4419
- );
4272
+ for (const entry of entries) {
4273
+ failures.push({
4274
+ nodeId: entry.nodeId,
4275
+ counter: entry.counter,
4276
+ frameToken,
4277
+ reason: "frame_missing"
4278
+ });
4279
+ }
4280
+ continue;
4420
4281
  }
4421
- const result = await frame.evaluate(
4422
- ({
4423
- entries: entries2,
4424
- nodeAttr,
4425
- instanceTokenKey,
4426
- counterOwnerKey,
4427
- counterValueKey,
4428
- startCounter
4429
- }) => {
4430
- const helpers = {
4431
- pushNode(map, node) {
4432
- const nodeId = node.getAttribute(nodeAttr);
4433
- if (!nodeId) return;
4434
- const list = map.get(nodeId) || [];
4435
- list.push(node);
4436
- map.set(nodeId, list);
4437
- },
4438
- walk(map, root) {
4282
+ try {
4283
+ const unresolved = await frame.evaluate(
4284
+ ({ entries: entries2, nodeAttr }) => {
4285
+ const index = /* @__PURE__ */ new Map();
4286
+ const unresolved2 = [];
4287
+ const walk = (root) => {
4439
4288
  const children = Array.from(root.children);
4440
4289
  for (const child of children) {
4441
- helpers.pushNode(map, child);
4442
- helpers.walk(map, child);
4290
+ const nodeId = child.getAttribute(nodeAttr);
4291
+ if (nodeId) {
4292
+ const list = index.get(nodeId) || [];
4293
+ list.push(child);
4294
+ index.set(nodeId, list);
4295
+ }
4296
+ walk(child);
4443
4297
  if (child.shadowRoot) {
4444
- helpers.walk(map, child.shadowRoot);
4298
+ walk(child.shadowRoot);
4445
4299
  }
4446
4300
  }
4447
- },
4448
- buildNodeIndex() {
4449
- const map = /* @__PURE__ */ new Map();
4450
- helpers.walk(map, document);
4451
- return map;
4452
- }
4453
- };
4454
- const index = helpers.buildNodeIndex();
4455
- const assigned = [];
4456
- const failures = [];
4457
- let next = Math.max(1, Number(startCounter || 1));
4458
- for (const entry of entries2) {
4459
- const matches = index.get(entry.nodeId) || [];
4460
- if (!matches.length) {
4461
- failures.push({
4462
- nodeId: entry.nodeId,
4463
- reason: "missing"
4464
- });
4465
- continue;
4466
- }
4467
- if (matches.length !== 1) {
4468
- failures.push({
4469
- nodeId: entry.nodeId,
4470
- reason: "ambiguous"
4471
- });
4472
- continue;
4473
- }
4474
- const target = matches[0];
4475
- if (target[instanceTokenKey] !== entry.instanceToken) {
4476
- failures.push({
4477
- nodeId: entry.nodeId,
4478
- reason: "instance_mismatch"
4479
- });
4480
- continue;
4481
- }
4482
- const owned = target[counterOwnerKey] === true;
4483
- const runtimeCounter = Number(target[counterValueKey] || 0);
4484
- if (owned && Number.isFinite(runtimeCounter) && runtimeCounter > 0) {
4485
- target.setAttribute("c", String(runtimeCounter));
4486
- assigned.push({
4487
- nodeId: entry.nodeId,
4488
- counter: runtimeCounter
4489
- });
4490
- continue;
4301
+ };
4302
+ walk(document);
4303
+ for (const entry of entries2) {
4304
+ const matches = index.get(entry.nodeId) || [];
4305
+ if (matches.length !== 1) {
4306
+ unresolved2.push({
4307
+ nodeId: entry.nodeId,
4308
+ counter: entry.counter,
4309
+ matches: matches.length
4310
+ });
4311
+ continue;
4312
+ }
4313
+ matches[0].setAttribute("c", String(entry.counter));
4491
4314
  }
4492
- const counter = next++;
4493
- target.setAttribute("c", String(counter));
4494
- Object.defineProperty(target, counterOwnerKey, {
4495
- value: true,
4496
- writable: true,
4497
- configurable: true
4498
- });
4499
- Object.defineProperty(target, counterValueKey, {
4500
- value: counter,
4501
- writable: true,
4502
- configurable: true
4503
- });
4504
- assigned.push({ nodeId: entry.nodeId, counter });
4315
+ return unresolved2;
4316
+ },
4317
+ {
4318
+ entries,
4319
+ nodeAttr: OS_NODE_ID_ATTR
4505
4320
  }
4506
- return {
4507
- assigned,
4508
- failures,
4509
- nextCounter: next
4510
- };
4511
- },
4512
- {
4513
- entries,
4514
- nodeAttr: OS_NODE_ID_ATTR,
4515
- instanceTokenKey: OS_INSTANCE_TOKEN_KEY,
4516
- counterOwnerKey: OS_COUNTER_OWNER_KEY,
4517
- counterValueKey: OS_COUNTER_VALUE_KEY,
4518
- startCounter: nextCounter
4519
- }
4520
- );
4521
- if (result.failures.length) {
4522
- const first = result.failures[0];
4523
- throw buildCounterFailureError(first.nodeId, first.reason);
4524
- }
4525
- nextCounter = result.nextCounter;
4526
- for (const item of result.assigned) {
4527
- const existingNode = usedCounters.get(item.counter);
4528
- if (existingNode && existingNode !== item.nodeId) {
4529
- throw new CounterResolutionError(
4530
- "ERR_COUNTER_AMBIGUOUS",
4531
- `Counter ${item.counter} is assigned to multiple nodes (${existingNode}, ${item.nodeId}). Run snapshot() again.`
4532
- );
4321
+ );
4322
+ for (const entry of unresolved) {
4323
+ failures.push({
4324
+ nodeId: entry.nodeId,
4325
+ counter: entry.counter,
4326
+ frameToken,
4327
+ reason: "match_count",
4328
+ matches: entry.matches
4329
+ });
4330
+ }
4331
+ } catch {
4332
+ for (const entry of entries) {
4333
+ failures.push({
4334
+ nodeId: entry.nodeId,
4335
+ counter: entry.counter,
4336
+ frameToken,
4337
+ reason: "frame_unavailable"
4338
+ });
4533
4339
  }
4534
- usedCounters.set(item.counter, item.nodeId);
4535
- out.set(item.nodeId, item.counter);
4536
4340
  }
4537
4341
  }
4538
- await writeGlobalNextCounter(page, nextCounter);
4539
- return out;
4540
- }
4541
- async function resolveCounterElement(page, snapshot, counter) {
4542
- const binding = readBinding(snapshot, counter);
4543
- const framesByToken = await mapFramesByToken(page);
4544
- const frame = framesByToken.get(binding.frameToken);
4545
- if (!frame) {
4546
- throw new CounterResolutionError(
4547
- "ERR_COUNTER_FRAME_UNAVAILABLE",
4548
- `Counter ${counter} frame is unavailable. Run snapshot() again.`
4549
- );
4550
- }
4551
- const status = await frame.evaluate(
4552
- ({
4553
- nodeId,
4554
- instanceToken,
4555
- counter: counter2,
4556
- nodeAttr,
4557
- instanceTokenKey,
4558
- counterOwnerKey,
4559
- counterValueKey
4560
- }) => {
4561
- const helpers = {
4562
- walk(map, root) {
4563
- const children = Array.from(root.children);
4564
- for (const child of children) {
4565
- const id = child.getAttribute(nodeAttr);
4566
- if (id) {
4567
- const list = map.get(id) || [];
4568
- list.push(child);
4569
- map.set(id, list);
4570
- }
4571
- helpers.walk(map, child);
4572
- if (child.shadowRoot) {
4573
- helpers.walk(map, child.shadowRoot);
4574
- }
4575
- }
4576
- },
4577
- buildNodeIndex() {
4578
- const map = /* @__PURE__ */ new Map();
4579
- helpers.walk(map, document);
4580
- return map;
4581
- }
4582
- };
4583
- const matches = helpers.buildNodeIndex().get(nodeId) || [];
4584
- if (!matches.length) return "missing";
4585
- if (matches.length !== 1) return "ambiguous";
4586
- const target = matches[0];
4587
- if (target[instanceTokenKey] !== instanceToken) {
4588
- return "instance_mismatch";
4589
- }
4590
- if (target[counterOwnerKey] !== true) {
4591
- return "instance_mismatch";
4342
+ if (failures.length) {
4343
+ const preview = failures.slice(0, 3).map((failure) => {
4344
+ const base = `counter ${failure.counter} (nodeId "${failure.nodeId}") in frame "${failure.frameToken}"`;
4345
+ if (failure.reason === "frame_missing") {
4346
+ return `${base} could not be synchronized because the frame is missing.`;
4592
4347
  }
4593
- if (Number(target[counterValueKey] || 0) !== counter2) {
4594
- return "instance_mismatch";
4348
+ if (failure.reason === "frame_unavailable") {
4349
+ return `${base} could not be synchronized because frame evaluation failed.`;
4595
4350
  }
4596
- if (target.getAttribute("c") !== String(counter2)) {
4597
- return "instance_mismatch";
4598
- }
4599
- return "ok";
4600
- },
4601
- {
4602
- nodeId: binding.nodeId,
4603
- instanceToken: binding.instanceToken,
4604
- counter,
4605
- nodeAttr: OS_NODE_ID_ATTR,
4606
- instanceTokenKey: OS_INSTANCE_TOKEN_KEY,
4607
- counterOwnerKey: OS_COUNTER_OWNER_KEY,
4608
- counterValueKey: OS_COUNTER_VALUE_KEY
4609
- }
4610
- );
4611
- if (status !== "ok") {
4612
- throw buildCounterFailureError(binding.nodeId, status);
4351
+ return `${base} expected exactly one live node but found ${failure.matches ?? 0}.`;
4352
+ });
4353
+ const remaining = failures.length > 3 ? ` (+${failures.length - 3} more)` : "";
4354
+ throw new Error(
4355
+ `Failed to synchronize snapshot counters with the live DOM: ${preview.join(" ")}${remaining}`
4356
+ );
4613
4357
  }
4614
- const handle = await frame.evaluateHandle(
4615
- ({ nodeId, nodeAttr }) => {
4616
- const helpers = {
4617
- walk(matches, root) {
4358
+ }
4359
+ async function clearLiveCounters(page) {
4360
+ for (const frame of page.frames()) {
4361
+ try {
4362
+ await frame.evaluate(() => {
4363
+ const walk = (root) => {
4618
4364
  const children = Array.from(root.children);
4619
4365
  for (const child of children) {
4620
- if (child.getAttribute(nodeAttr) === nodeId) {
4621
- matches.push(child);
4622
- }
4623
- helpers.walk(matches, child);
4366
+ child.removeAttribute("c");
4367
+ walk(child);
4624
4368
  if (child.shadowRoot) {
4625
- helpers.walk(matches, child.shadowRoot);
4626
- }
4627
- }
4628
- },
4629
- findUniqueNode() {
4630
- const matches = [];
4631
- helpers.walk(matches, document);
4632
- if (matches.length !== 1) return null;
4633
- return matches[0];
4634
- }
4635
- };
4636
- return helpers.findUniqueNode();
4637
- },
4638
- {
4639
- nodeId: binding.nodeId,
4640
- nodeAttr: OS_NODE_ID_ATTR
4641
- }
4642
- );
4643
- const element = handle.asElement();
4644
- if (!element) {
4645
- await handle.dispose();
4646
- throw new CounterResolutionError(
4647
- "ERR_COUNTER_STALE_OR_NOT_FOUND",
4648
- `Counter ${counter} became stale. Run snapshot() again.`
4649
- );
4650
- }
4651
- return element;
4652
- }
4653
- async function resolveCountersBatch(page, snapshot, requests) {
4654
- const out = {};
4655
- if (!requests.length) return out;
4656
- const grouped = /* @__PURE__ */ new Map();
4657
- for (const request of requests) {
4658
- const binding = readBinding(snapshot, request.counter);
4659
- const list = grouped.get(binding.frameToken) || [];
4660
- list.push({
4661
- ...request,
4662
- ...binding
4663
- });
4664
- grouped.set(binding.frameToken, list);
4665
- }
4666
- const framesByToken = await mapFramesByToken(page);
4667
- for (const [frameToken, entries] of grouped.entries()) {
4668
- const frame = framesByToken.get(frameToken);
4669
- if (!frame) {
4670
- throw new CounterResolutionError(
4671
- "ERR_COUNTER_FRAME_UNAVAILABLE",
4672
- `Counter frame ${frameToken} is unavailable. Run snapshot() again.`
4673
- );
4674
- }
4675
- const result = await frame.evaluate(
4676
- ({
4677
- entries: entries2,
4678
- nodeAttr,
4679
- instanceTokenKey,
4680
- counterOwnerKey,
4681
- counterValueKey
4682
- }) => {
4683
- const values = [];
4684
- const failures = [];
4685
- const helpers = {
4686
- walk(map, root) {
4687
- const children = Array.from(root.children);
4688
- for (const child of children) {
4689
- const id = child.getAttribute(nodeAttr);
4690
- if (id) {
4691
- const list = map.get(id) || [];
4692
- list.push(child);
4693
- map.set(id, list);
4694
- }
4695
- helpers.walk(map, child);
4696
- if (child.shadowRoot) {
4697
- helpers.walk(map, child.shadowRoot);
4698
- }
4369
+ walk(child.shadowRoot);
4699
4370
  }
4700
- },
4701
- buildNodeIndex() {
4702
- const map = /* @__PURE__ */ new Map();
4703
- helpers.walk(map, document);
4704
- return map;
4705
- },
4706
- readRawValue(element, attribute) {
4707
- if (attribute) {
4708
- return element.getAttribute(attribute);
4709
- }
4710
- return element.textContent;
4711
- }
4712
- };
4713
- const index = helpers.buildNodeIndex();
4714
- for (const entry of entries2) {
4715
- const matches = index.get(entry.nodeId) || [];
4716
- if (!matches.length) {
4717
- failures.push({
4718
- nodeId: entry.nodeId,
4719
- reason: "missing"
4720
- });
4721
- continue;
4722
- }
4723
- if (matches.length !== 1) {
4724
- failures.push({
4725
- nodeId: entry.nodeId,
4726
- reason: "ambiguous"
4727
- });
4728
- continue;
4729
- }
4730
- const target = matches[0];
4731
- if (target[instanceTokenKey] !== entry.instanceToken || target[counterOwnerKey] !== true || Number(target[counterValueKey] || 0) !== entry.counter || target.getAttribute("c") !== String(entry.counter)) {
4732
- failures.push({
4733
- nodeId: entry.nodeId,
4734
- reason: "instance_mismatch"
4735
- });
4736
- continue;
4737
4371
  }
4738
- values.push({
4739
- key: entry.key,
4740
- value: helpers.readRawValue(target, entry.attribute)
4741
- });
4742
- }
4743
- return {
4744
- values,
4745
- failures
4746
4372
  };
4747
- },
4748
- {
4749
- entries,
4750
- nodeAttr: OS_NODE_ID_ATTR,
4751
- instanceTokenKey: OS_INSTANCE_TOKEN_KEY,
4752
- counterOwnerKey: OS_COUNTER_OWNER_KEY,
4753
- counterValueKey: OS_COUNTER_VALUE_KEY
4754
- }
4755
- );
4756
- if (result.failures.length) {
4757
- const first = result.failures[0];
4758
- throw buildCounterFailureError(first.nodeId, first.reason);
4759
- }
4760
- const attributeByKey = new Map(
4761
- entries.map((entry) => [entry.key, entry.attribute])
4762
- );
4763
- for (const item of result.values) {
4764
- out[item.key] = normalizeExtractedValue(
4765
- item.value,
4766
- attributeByKey.get(item.key)
4767
- );
4373
+ walk(document);
4374
+ });
4375
+ } catch {
4768
4376
  }
4769
4377
  }
4770
- return out;
4771
4378
  }
4772
4379
  async function mapFramesByToken(page) {
4773
4380
  const out = /* @__PURE__ */ new Map();
@@ -4789,180 +4396,6 @@ async function readFrameToken(frame) {
4789
4396
  return null;
4790
4397
  }
4791
4398
  }
4792
- async function readGlobalNextCounter(page) {
4793
- const current = await page.mainFrame().evaluate((counterNextKey) => {
4794
- const win = window;
4795
- return Number(win[counterNextKey] || 0);
4796
- }, OS_COUNTER_NEXT_KEY).catch(() => 0);
4797
- if (Number.isFinite(current) && current > 0) {
4798
- return current;
4799
- }
4800
- let max = 0;
4801
- for (const frame of page.frames()) {
4802
- try {
4803
- const frameMax = await frame.evaluate(
4804
- ({ nodeAttr, counterOwnerKey, counterValueKey }) => {
4805
- let localMax = 0;
4806
- const helpers = {
4807
- walk(root) {
4808
- const children = Array.from(
4809
- root.children
4810
- );
4811
- for (const child of children) {
4812
- const candidate = child;
4813
- const hasNodeId = child.hasAttribute(nodeAttr);
4814
- const owned = candidate[counterOwnerKey] === true;
4815
- if (hasNodeId && owned) {
4816
- const value = Number(
4817
- candidate[counterValueKey] || 0
4818
- );
4819
- if (Number.isFinite(value) && value > localMax) {
4820
- localMax = value;
4821
- }
4822
- }
4823
- helpers.walk(child);
4824
- if (child.shadowRoot) {
4825
- helpers.walk(child.shadowRoot);
4826
- }
4827
- }
4828
- }
4829
- };
4830
- helpers.walk(document);
4831
- return localMax;
4832
- },
4833
- {
4834
- nodeAttr: OS_NODE_ID_ATTR,
4835
- counterOwnerKey: OS_COUNTER_OWNER_KEY,
4836
- counterValueKey: OS_COUNTER_VALUE_KEY
4837
- }
4838
- );
4839
- if (frameMax > max) {
4840
- max = frameMax;
4841
- }
4842
- } catch {
4843
- }
4844
- }
4845
- const next = max + 1;
4846
- await writeGlobalNextCounter(page, next);
4847
- return next;
4848
- }
4849
- async function writeGlobalNextCounter(page, nextCounter) {
4850
- await page.mainFrame().evaluate(
4851
- ({ counterNextKey, nextCounter: nextCounter2 }) => {
4852
- const win = window;
4853
- win[counterNextKey] = nextCounter2;
4854
- },
4855
- {
4856
- counterNextKey: OS_COUNTER_NEXT_KEY,
4857
- nextCounter
4858
- }
4859
- ).catch(() => void 0);
4860
- }
4861
- function readBinding(snapshot, counter) {
4862
- if (!snapshot.counterBindings) {
4863
- throw new CounterResolutionError(
4864
- "ERR_COUNTER_NOT_FOUND",
4865
- `Counter ${counter} is unavailable because this snapshot has no counter bindings. Run snapshot() with counters first.`
4866
- );
4867
- }
4868
- const binding = snapshot.counterBindings.get(counter);
4869
- if (!binding) {
4870
- throw new CounterResolutionError(
4871
- "ERR_COUNTER_NOT_FOUND",
4872
- `Counter ${counter} was not found in the current snapshot. Run snapshot() again.`
4873
- );
4874
- }
4875
- if (binding.sessionId !== snapshot.snapshotSessionId) {
4876
- throw new CounterResolutionError(
4877
- "ERR_COUNTER_STALE_OR_NOT_FOUND",
4878
- `Counter ${counter} is stale for this snapshot session. Run snapshot() again.`
4879
- );
4880
- }
4881
- return binding;
4882
- }
4883
- function buildCounterFailureError(nodeId, reason) {
4884
- if (reason === "ambiguous") {
4885
- return new CounterResolutionError(
4886
- "ERR_COUNTER_AMBIGUOUS",
4887
- `Counter target is ambiguous for node ${nodeId}. Run snapshot() again.`
4888
- );
4889
- }
4890
- return new CounterResolutionError(
4891
- "ERR_COUNTER_STALE_OR_NOT_FOUND",
4892
- `Counter target is stale or missing for node ${nodeId}. Run snapshot() again.`
4893
- );
4894
- }
4895
-
4896
- // src/html/pipeline.ts
4897
- function applyCleaner(mode, html) {
4898
- switch (mode) {
4899
- case "clickable":
4900
- return cleanForClickable(html);
4901
- case "scrollable":
4902
- return cleanForScrollable(html);
4903
- case "extraction":
4904
- return cleanForExtraction(html);
4905
- case "full":
4906
- return cleanForFull(html);
4907
- case "action":
4908
- default:
4909
- return cleanForAction(html);
4910
- }
4911
- }
4912
- async function assignCounters(page, html, nodePaths, nodeMeta, snapshotSessionId) {
4913
- const $ = cheerio3.load(html, { xmlMode: false });
4914
- const counterIndex = /* @__PURE__ */ new Map();
4915
- const counterBindings = /* @__PURE__ */ new Map();
4916
- const orderedNodeIds = [];
4917
- $("*").each(function() {
4918
- const el = $(this);
4919
- const nodeId = el.attr(OS_NODE_ID_ATTR);
4920
- if (!nodeId) return;
4921
- orderedNodeIds.push(nodeId);
4922
- });
4923
- const countersByNodeId = await ensureLiveCounters(
4924
- page,
4925
- nodeMeta,
4926
- orderedNodeIds
4927
- );
4928
- $("*").each(function() {
4929
- const el = $(this);
4930
- const nodeId = el.attr(OS_NODE_ID_ATTR);
4931
- if (!nodeId) return;
4932
- const path5 = nodePaths.get(nodeId);
4933
- const meta = nodeMeta.get(nodeId);
4934
- const counter = countersByNodeId.get(nodeId);
4935
- if (counter == null || !Number.isFinite(counter)) {
4936
- throw new Error(
4937
- `Counter assignment failed for node ${nodeId}. Run snapshot() again.`
4938
- );
4939
- }
4940
- if (counterBindings.has(counter) && counterBindings.get(counter)?.nodeId !== nodeId) {
4941
- throw new Error(
4942
- `Counter ${counter} was assigned to multiple nodes. Run snapshot() again.`
4943
- );
4944
- }
4945
- el.attr("c", String(counter));
4946
- el.removeAttr(OS_NODE_ID_ATTR);
4947
- if (path5) {
4948
- counterIndex.set(counter, cloneElementPath(path5));
4949
- }
4950
- if (meta) {
4951
- counterBindings.set(counter, {
4952
- sessionId: snapshotSessionId,
4953
- frameToken: meta.frameToken,
4954
- nodeId,
4955
- instanceToken: meta.instanceToken
4956
- });
4957
- }
4958
- });
4959
- $(`[${OS_NODE_ID_ATTR}]`).removeAttr(OS_NODE_ID_ATTR);
4960
- return {
4961
- html: $.html(),
4962
- counterIndex,
4963
- counterBindings
4964
- };
4965
- }
4966
4399
  function stripNodeIds(html) {
4967
4400
  if (!html.includes(OS_NODE_ID_ATTR)) return html;
4968
4401
  const $ = cheerio3.load(html, { xmlMode: false });
@@ -4970,7 +4403,6 @@ function stripNodeIds(html) {
4970
4403
  return $.html();
4971
4404
  }
4972
4405
  async function prepareSnapshot(page, options = {}) {
4973
- const snapshotSessionId = (0, import_crypto.randomUUID)();
4974
4406
  const mode = options.mode ?? "action";
4975
4407
  const withCounters = options.withCounters ?? true;
4976
4408
  const shouldMarkInteractive = options.markInteractive ?? true;
@@ -4983,18 +4415,15 @@ async function prepareSnapshot(page, options = {}) {
4983
4415
  const reducedHtml = applyCleaner(mode, processedHtml);
4984
4416
  let cleanedHtml = reducedHtml;
4985
4417
  let counterIndex = null;
4986
- let counterBindings = null;
4987
4418
  if (withCounters) {
4988
4419
  const counted = await assignCounters(
4989
4420
  page,
4990
4421
  reducedHtml,
4991
4422
  serialized.nodePaths,
4992
- serialized.nodeMeta,
4993
- snapshotSessionId
4423
+ serialized.nodeMeta
4994
4424
  );
4995
4425
  cleanedHtml = counted.html;
4996
4426
  counterIndex = counted.counterIndex;
4997
- counterBindings = counted.counterBindings;
4998
4427
  } else {
4999
4428
  cleanedHtml = stripNodeIds(cleanedHtml);
5000
4429
  }
@@ -5003,15 +4432,13 @@ async function prepareSnapshot(page, options = {}) {
5003
4432
  cleanedHtml = $unwrap("body").html()?.trim() || cleanedHtml;
5004
4433
  }
5005
4434
  return {
5006
- snapshotSessionId,
5007
4435
  mode,
5008
4436
  url: page.url(),
5009
4437
  rawHtml,
5010
4438
  processedHtml,
5011
4439
  reducedHtml,
5012
4440
  cleanedHtml,
5013
- counterIndex,
5014
- counterBindings
4441
+ counterIndex
5015
4442
  };
5016
4443
  }
5017
4444
 
@@ -5185,143 +4612,544 @@ function selectInRoot(root, selectors) {
5185
4612
  mode: "unique"
5186
4613
  };
5187
4614
  }
5188
- if (count > 1 && !fallback) {
5189
- fallback = {
5190
- selector,
5191
- count,
5192
- mode: "fallback"
5193
- };
4615
+ if (count > 1 && !fallback) {
4616
+ fallback = {
4617
+ selector,
4618
+ count,
4619
+ mode: "fallback"
4620
+ };
4621
+ }
4622
+ }
4623
+ return fallback;
4624
+ }
4625
+ function countInDocument(selectors) {
4626
+ const out = [];
4627
+ for (const selector of selectors) {
4628
+ if (!selector) continue;
4629
+ let count = 0;
4630
+ try {
4631
+ count = document.querySelectorAll(selector).length;
4632
+ } catch {
4633
+ count = 0;
4634
+ }
4635
+ out.push({ selector, count });
4636
+ }
4637
+ return out;
4638
+ }
4639
+ function countInRoot(root, selectors) {
4640
+ if (!(root instanceof ShadowRoot)) return [];
4641
+ const out = [];
4642
+ for (const selector of selectors) {
4643
+ if (!selector) continue;
4644
+ let count = 0;
4645
+ try {
4646
+ count = root.querySelectorAll(selector).length;
4647
+ } catch {
4648
+ count = 0;
4649
+ }
4650
+ out.push({ selector, count });
4651
+ }
4652
+ return out;
4653
+ }
4654
+ function isPathDebugEnabled() {
4655
+ const value = process.env.OPENSTEER_DEBUG_PATH || process.env.OPENSTEER_DEBUG || process.env.DEBUG_SELECTORS;
4656
+ if (!value) return false;
4657
+ const normalized = value.trim().toLowerCase();
4658
+ return normalized === "1" || normalized === "true";
4659
+ }
4660
+ function debugPath(message, data) {
4661
+ if (!isPathDebugEnabled()) return;
4662
+ if (data !== void 0) {
4663
+ console.log(`[opensteer:path] ${message}`, data);
4664
+ } else {
4665
+ console.log(`[opensteer:path] ${message}`);
4666
+ }
4667
+ }
4668
+ async function disposeHandle(handle) {
4669
+ if (!handle) return;
4670
+ try {
4671
+ await handle.dispose();
4672
+ } catch {
4673
+ }
4674
+ }
4675
+
4676
+ // src/actions/actionability-probe.ts
4677
+ async function probeActionabilityState(element) {
4678
+ try {
4679
+ return await element.evaluate((target) => {
4680
+ if (!(target instanceof Element)) {
4681
+ return {
4682
+ connected: false,
4683
+ visible: null,
4684
+ enabled: null,
4685
+ editable: null,
4686
+ blocker: null
4687
+ };
4688
+ }
4689
+ const connected = target.isConnected;
4690
+ if (!connected) {
4691
+ return {
4692
+ connected: false,
4693
+ visible: null,
4694
+ enabled: null,
4695
+ editable: null,
4696
+ blocker: null
4697
+ };
4698
+ }
4699
+ const style = window.getComputedStyle(target);
4700
+ const rect = target.getBoundingClientRect();
4701
+ const hasBox = rect.width > 0 && rect.height > 0;
4702
+ const opacity = Number.parseFloat(style.opacity || "1");
4703
+ const isVisible = hasBox && style.display !== "none" && style.visibility !== "hidden" && style.visibility !== "collapse" && (!Number.isFinite(opacity) || opacity > 0);
4704
+ let enabled = null;
4705
+ if (target instanceof HTMLButtonElement || target instanceof HTMLInputElement || target instanceof HTMLSelectElement || target instanceof HTMLTextAreaElement || target instanceof HTMLOptionElement || target instanceof HTMLOptGroupElement || target instanceof HTMLFieldSetElement) {
4706
+ enabled = !target.disabled;
4707
+ }
4708
+ let editable = null;
4709
+ if (target instanceof HTMLInputElement) {
4710
+ editable = !target.readOnly && !target.disabled;
4711
+ } else if (target instanceof HTMLTextAreaElement) {
4712
+ editable = !target.readOnly && !target.disabled;
4713
+ } else if (target instanceof HTMLSelectElement) {
4714
+ editable = !target.disabled;
4715
+ } else if (target instanceof HTMLElement && target.isContentEditable) {
4716
+ editable = true;
4717
+ }
4718
+ let blocker = null;
4719
+ if (hasBox && window.innerWidth > 0 && window.innerHeight > 0) {
4720
+ const x = Math.min(
4721
+ Math.max(rect.left + rect.width / 2, 0),
4722
+ window.innerWidth - 1
4723
+ );
4724
+ const y = Math.min(
4725
+ Math.max(rect.top + rect.height / 2, 0),
4726
+ window.innerHeight - 1
4727
+ );
4728
+ const top = document.elementFromPoint(x, y);
4729
+ if (top && top !== target && !target.contains(top)) {
4730
+ const classes = String(top.className || "").split(/\s+/).map((value) => value.trim()).filter(Boolean).slice(0, 5);
4731
+ blocker = {
4732
+ tag: top.tagName.toLowerCase(),
4733
+ id: top.id || null,
4734
+ classes,
4735
+ role: top.getAttribute("role"),
4736
+ text: (top.textContent || "").trim().slice(0, 80) || null
4737
+ };
4738
+ }
4739
+ }
4740
+ return {
4741
+ connected,
4742
+ visible: isVisible,
4743
+ enabled,
4744
+ editable,
4745
+ blocker
4746
+ };
4747
+ });
4748
+ } catch {
4749
+ return null;
4750
+ }
4751
+ }
4752
+
4753
+ // src/extract-value-normalization.ts
4754
+ var URL_LIST_ATTRIBUTES = /* @__PURE__ */ new Set(["srcset", "imagesrcset", "ping"]);
4755
+ function normalizeExtractedValue(raw, attribute) {
4756
+ if (raw == null) return null;
4757
+ const rawText = String(raw);
4758
+ if (!rawText.trim()) return null;
4759
+ const normalizedAttribute = String(attribute || "").trim().toLowerCase();
4760
+ if (URL_LIST_ATTRIBUTES.has(normalizedAttribute)) {
4761
+ const singleValue = pickSingleListAttributeValue(
4762
+ normalizedAttribute,
4763
+ rawText
4764
+ ).trim();
4765
+ return singleValue || null;
4766
+ }
4767
+ const text = rawText.replace(/\s+/g, " ").trim();
4768
+ return text || null;
4769
+ }
4770
+ function pickSingleListAttributeValue(attribute, raw) {
4771
+ if (attribute === "ping") {
4772
+ const firstUrl = raw.trim().split(/\s+/)[0] || "";
4773
+ return firstUrl.trim();
4774
+ }
4775
+ if (attribute === "srcset" || attribute === "imagesrcset") {
4776
+ const picked = pickBestSrcsetCandidate(raw);
4777
+ if (picked) return picked;
4778
+ return pickFirstSrcsetToken(raw) || "";
4779
+ }
4780
+ return raw.trim();
4781
+ }
4782
+ function pickBestSrcsetCandidate(raw) {
4783
+ const candidates = parseSrcsetCandidates(raw);
4784
+ if (!candidates.length) return null;
4785
+ const widthCandidates = candidates.filter(
4786
+ (candidate) => typeof candidate.width === "number" && Number.isFinite(candidate.width) && candidate.width > 0
4787
+ );
4788
+ if (widthCandidates.length) {
4789
+ return widthCandidates.reduce(
4790
+ (best, candidate) => candidate.width > best.width ? candidate : best
4791
+ ).url;
4792
+ }
4793
+ const densityCandidates = candidates.filter(
4794
+ (candidate) => typeof candidate.density === "number" && Number.isFinite(candidate.density) && candidate.density > 0
4795
+ );
4796
+ if (densityCandidates.length) {
4797
+ return densityCandidates.reduce(
4798
+ (best, candidate) => candidate.density > best.density ? candidate : best
4799
+ ).url;
4800
+ }
4801
+ return candidates[0]?.url || null;
4802
+ }
4803
+ function parseSrcsetCandidates(raw) {
4804
+ const text = String(raw || "").trim();
4805
+ if (!text) return [];
4806
+ const out = [];
4807
+ let index = 0;
4808
+ while (index < text.length) {
4809
+ index = skipSeparators(text, index);
4810
+ if (index >= text.length) break;
4811
+ const urlToken = readUrlToken(text, index);
4812
+ index = urlToken.nextIndex;
4813
+ const url = urlToken.value.trim();
4814
+ if (!url) continue;
4815
+ index = skipWhitespace(text, index);
4816
+ const descriptors = [];
4817
+ while (index < text.length && text[index] !== ",") {
4818
+ const descriptorToken = readDescriptorToken(text, index);
4819
+ if (!descriptorToken.value) {
4820
+ index = descriptorToken.nextIndex;
4821
+ continue;
4822
+ }
4823
+ descriptors.push(descriptorToken.value);
4824
+ index = descriptorToken.nextIndex;
4825
+ index = skipWhitespace(text, index);
4826
+ }
4827
+ if (index < text.length && text[index] === ",") {
4828
+ index += 1;
4829
+ }
4830
+ let width = null;
4831
+ let density = null;
4832
+ for (const descriptor of descriptors) {
4833
+ const token = descriptor.trim().toLowerCase();
4834
+ if (!token) continue;
4835
+ const widthMatch = token.match(/^(\d+)w$/);
4836
+ if (widthMatch) {
4837
+ const parsed = Number.parseInt(widthMatch[1], 10);
4838
+ if (Number.isFinite(parsed)) {
4839
+ width = parsed;
4840
+ }
4841
+ continue;
4842
+ }
4843
+ const densityMatch = token.match(/^(\d*\.?\d+)x$/);
4844
+ if (densityMatch) {
4845
+ const parsed = Number.parseFloat(densityMatch[1]);
4846
+ if (Number.isFinite(parsed)) {
4847
+ density = parsed;
4848
+ }
4849
+ }
4850
+ }
4851
+ out.push({
4852
+ url,
4853
+ width,
4854
+ density
4855
+ });
4856
+ }
4857
+ return out;
4858
+ }
4859
+ function pickFirstSrcsetToken(raw) {
4860
+ const candidate = parseSrcsetCandidates(raw)[0];
4861
+ if (candidate?.url) {
4862
+ return candidate.url;
4863
+ }
4864
+ const text = String(raw || "");
4865
+ const start = skipSeparators(text, 0);
4866
+ if (start >= text.length) return null;
4867
+ const firstToken = readUrlToken(text, start).value.trim();
4868
+ return firstToken || null;
4869
+ }
4870
+ function skipWhitespace(value, index) {
4871
+ let cursor = index;
4872
+ while (cursor < value.length && /\s/.test(value[cursor])) {
4873
+ cursor += 1;
4874
+ }
4875
+ return cursor;
4876
+ }
4877
+ function skipSeparators(value, index) {
4878
+ let cursor = skipWhitespace(value, index);
4879
+ while (cursor < value.length && value[cursor] === ",") {
4880
+ cursor += 1;
4881
+ cursor = skipWhitespace(value, cursor);
4882
+ }
4883
+ return cursor;
4884
+ }
4885
+ function readUrlToken(value, index) {
4886
+ let cursor = index;
4887
+ let out = "";
4888
+ const isDataUrl = value.slice(index, index + 5).toLowerCase().startsWith("data:");
4889
+ while (cursor < value.length) {
4890
+ const char = value[cursor];
4891
+ if (/\s/.test(char)) {
4892
+ break;
4893
+ }
4894
+ if (char === "," && !isDataUrl) {
4895
+ break;
4896
+ }
4897
+ out += char;
4898
+ cursor += 1;
4899
+ }
4900
+ if (isDataUrl && out.endsWith(",") && cursor < value.length) {
4901
+ out = out.slice(0, -1);
4902
+ }
4903
+ return {
4904
+ value: out,
4905
+ nextIndex: cursor
4906
+ };
4907
+ }
4908
+ function readDescriptorToken(value, index) {
4909
+ let cursor = skipWhitespace(value, index);
4910
+ let out = "";
4911
+ while (cursor < value.length) {
4912
+ const char = value[cursor];
4913
+ if (char === "," || /\s/.test(char)) {
4914
+ break;
4915
+ }
4916
+ out += char;
4917
+ cursor += 1;
4918
+ }
4919
+ return {
4920
+ value: out.trim(),
4921
+ nextIndex: cursor
4922
+ };
4923
+ }
4924
+
4925
+ // src/html/counter-runtime.ts
4926
+ var CounterResolutionError = class extends Error {
4927
+ code;
4928
+ constructor(code, message) {
4929
+ super(message);
4930
+ this.name = "CounterResolutionError";
4931
+ this.code = code;
4932
+ }
4933
+ };
4934
+ async function resolveCounterElement(page, counter) {
4935
+ const normalized = normalizeCounter(counter);
4936
+ if (normalized == null) {
4937
+ throw buildCounterNotFoundError(counter);
4938
+ }
4939
+ const scan = await scanCounterOccurrences(page, [normalized]);
4940
+ const entry = scan.get(normalized);
4941
+ if (!entry || entry.count <= 0 || !entry.frame) {
4942
+ throw buildCounterNotFoundError(counter);
4943
+ }
4944
+ if (entry.count > 1) {
4945
+ throw buildCounterAmbiguousError(counter);
4946
+ }
4947
+ const handle = await resolveUniqueHandleInFrame(entry.frame, normalized);
4948
+ const element = handle.asElement();
4949
+ if (!element) {
4950
+ await handle.dispose();
4951
+ throw buildCounterNotFoundError(counter);
4952
+ }
4953
+ return element;
4954
+ }
4955
+ async function resolveCountersBatch(page, requests) {
4956
+ const out = {};
4957
+ if (!requests.length) return out;
4958
+ const counters = dedupeCounters(requests);
4959
+ const scan = await scanCounterOccurrences(page, counters);
4960
+ for (const counter of counters) {
4961
+ const entry = scan.get(counter);
4962
+ if (entry.count > 1) {
4963
+ throw buildCounterAmbiguousError(counter);
4964
+ }
4965
+ }
4966
+ const valueCache = /* @__PURE__ */ new Map();
4967
+ for (const request of requests) {
4968
+ const normalized = normalizeCounter(request.counter);
4969
+ if (normalized == null) {
4970
+ out[request.key] = null;
4971
+ continue;
4972
+ }
4973
+ const entry = scan.get(normalized);
4974
+ if (!entry || entry.count <= 0 || !entry.frame) {
4975
+ out[request.key] = null;
4976
+ continue;
4977
+ }
4978
+ const cacheKey = `${normalized}:${request.attribute || ""}`;
4979
+ if (valueCache.has(cacheKey)) {
4980
+ out[request.key] = valueCache.get(cacheKey);
4981
+ continue;
4982
+ }
4983
+ const read = await readCounterValueInFrame(
4984
+ entry.frame,
4985
+ normalized,
4986
+ request.attribute
4987
+ );
4988
+ if (read.status === "ambiguous") {
4989
+ throw buildCounterAmbiguousError(normalized);
4990
+ }
4991
+ if (read.status === "missing") {
4992
+ valueCache.set(cacheKey, null);
4993
+ out[request.key] = null;
4994
+ continue;
5194
4995
  }
4996
+ const normalizedValue = normalizeExtractedValue(
4997
+ read.value ?? null,
4998
+ request.attribute
4999
+ );
5000
+ valueCache.set(cacheKey, normalizedValue);
5001
+ out[request.key] = normalizedValue;
5195
5002
  }
5196
- return fallback;
5003
+ return out;
5197
5004
  }
5198
- function countInDocument(selectors) {
5005
+ function dedupeCounters(requests) {
5006
+ const seen = /* @__PURE__ */ new Set();
5199
5007
  const out = [];
5200
- for (const selector of selectors) {
5201
- if (!selector) continue;
5202
- let count = 0;
5203
- try {
5204
- count = document.querySelectorAll(selector).length;
5205
- } catch {
5206
- count = 0;
5207
- }
5208
- out.push({ selector, count });
5008
+ for (const request of requests) {
5009
+ const normalized = normalizeCounter(request.counter);
5010
+ if (normalized == null || seen.has(normalized)) continue;
5011
+ seen.add(normalized);
5012
+ out.push(normalized);
5209
5013
  }
5210
5014
  return out;
5211
5015
  }
5212
- function countInRoot(root, selectors) {
5213
- if (!(root instanceof ShadowRoot)) return [];
5214
- const out = [];
5215
- for (const selector of selectors) {
5216
- if (!selector) continue;
5217
- let count = 0;
5016
+ function normalizeCounter(counter) {
5017
+ if (!Number.isFinite(counter)) return null;
5018
+ if (!Number.isInteger(counter)) return null;
5019
+ if (counter <= 0) return null;
5020
+ return counter;
5021
+ }
5022
+ async function scanCounterOccurrences(page, counters) {
5023
+ const out = /* @__PURE__ */ new Map();
5024
+ for (const counter of counters) {
5025
+ out.set(counter, {
5026
+ count: 0,
5027
+ frame: null
5028
+ });
5029
+ }
5030
+ if (!counters.length) return out;
5031
+ for (const frame of page.frames()) {
5032
+ let frameCounts;
5218
5033
  try {
5219
- count = root.querySelectorAll(selector).length;
5034
+ frameCounts = await frame.evaluate((candidates) => {
5035
+ const keys = new Set(candidates.map((value) => String(value)));
5036
+ const counts = {};
5037
+ for (const key of keys) {
5038
+ counts[key] = 0;
5039
+ }
5040
+ const walk = (root) => {
5041
+ const children = Array.from(root.children);
5042
+ for (const child of children) {
5043
+ const value = child.getAttribute("c");
5044
+ if (value && keys.has(value)) {
5045
+ counts[value] = (counts[value] || 0) + 1;
5046
+ }
5047
+ walk(child);
5048
+ if (child.shadowRoot) {
5049
+ walk(child.shadowRoot);
5050
+ }
5051
+ }
5052
+ };
5053
+ walk(document);
5054
+ return counts;
5055
+ }, counters);
5220
5056
  } catch {
5221
- count = 0;
5057
+ continue;
5058
+ }
5059
+ for (const [rawCounter, rawCount] of Object.entries(frameCounts)) {
5060
+ const counter = Number.parseInt(rawCounter, 10);
5061
+ if (!Number.isFinite(counter)) continue;
5062
+ const count = Number(rawCount || 0);
5063
+ if (!Number.isFinite(count) || count <= 0) continue;
5064
+ const entry = out.get(counter);
5065
+ entry.count += count;
5066
+ if (!entry.frame) {
5067
+ entry.frame = frame;
5068
+ }
5222
5069
  }
5223
- out.push({ selector, count });
5224
5070
  }
5225
5071
  return out;
5226
5072
  }
5227
- function isPathDebugEnabled() {
5228
- const value = process.env.OPENSTEER_DEBUG_PATH || process.env.OPENSTEER_DEBUG || process.env.DEBUG_SELECTORS;
5229
- if (!value) return false;
5230
- const normalized = value.trim().toLowerCase();
5231
- return normalized === "1" || normalized === "true";
5232
- }
5233
- function debugPath(message, data) {
5234
- if (!isPathDebugEnabled()) return;
5235
- if (data !== void 0) {
5236
- console.log(`[opensteer:path] ${message}`, data);
5237
- } else {
5238
- console.log(`[opensteer:path] ${message}`);
5239
- }
5240
- }
5241
- async function disposeHandle(handle) {
5242
- if (!handle) return;
5243
- try {
5244
- await handle.dispose();
5245
- } catch {
5246
- }
5073
+ async function resolveUniqueHandleInFrame(frame, counter) {
5074
+ return frame.evaluateHandle((targetCounter) => {
5075
+ const matches = [];
5076
+ const walk = (root) => {
5077
+ const children = Array.from(root.children);
5078
+ for (const child of children) {
5079
+ if (child.getAttribute("c") === targetCounter) {
5080
+ matches.push(child);
5081
+ }
5082
+ walk(child);
5083
+ if (child.shadowRoot) {
5084
+ walk(child.shadowRoot);
5085
+ }
5086
+ }
5087
+ };
5088
+ walk(document);
5089
+ if (matches.length !== 1) {
5090
+ return null;
5091
+ }
5092
+ return matches[0];
5093
+ }, String(counter));
5247
5094
  }
5248
-
5249
- // src/actions/actionability-probe.ts
5250
- async function probeActionabilityState(element) {
5095
+ async function readCounterValueInFrame(frame, counter, attribute) {
5251
5096
  try {
5252
- return await element.evaluate((target) => {
5253
- if (!(target instanceof Element)) {
5254
- return {
5255
- connected: false,
5256
- visible: null,
5257
- enabled: null,
5258
- editable: null,
5259
- blocker: null
5260
- };
5261
- }
5262
- const connected = target.isConnected;
5263
- if (!connected) {
5264
- return {
5265
- connected: false,
5266
- visible: null,
5267
- enabled: null,
5268
- editable: null,
5269
- blocker: null
5097
+ return await frame.evaluate(
5098
+ ({ targetCounter, attribute: attribute2 }) => {
5099
+ const matches = [];
5100
+ const walk = (root) => {
5101
+ const children = Array.from(root.children);
5102
+ for (const child of children) {
5103
+ if (child.getAttribute("c") === targetCounter) {
5104
+ matches.push(child);
5105
+ }
5106
+ walk(child);
5107
+ if (child.shadowRoot) {
5108
+ walk(child.shadowRoot);
5109
+ }
5110
+ }
5270
5111
  };
5271
- }
5272
- const style = window.getComputedStyle(target);
5273
- const rect = target.getBoundingClientRect();
5274
- const hasBox = rect.width > 0 && rect.height > 0;
5275
- const opacity = Number.parseFloat(style.opacity || "1");
5276
- const isVisible = hasBox && style.display !== "none" && style.visibility !== "hidden" && style.visibility !== "collapse" && (!Number.isFinite(opacity) || opacity > 0);
5277
- let enabled = null;
5278
- if (target instanceof HTMLButtonElement || target instanceof HTMLInputElement || target instanceof HTMLSelectElement || target instanceof HTMLTextAreaElement || target instanceof HTMLOptionElement || target instanceof HTMLOptGroupElement || target instanceof HTMLFieldSetElement) {
5279
- enabled = !target.disabled;
5280
- }
5281
- let editable = null;
5282
- if (target instanceof HTMLInputElement) {
5283
- editable = !target.readOnly && !target.disabled;
5284
- } else if (target instanceof HTMLTextAreaElement) {
5285
- editable = !target.readOnly && !target.disabled;
5286
- } else if (target instanceof HTMLSelectElement) {
5287
- editable = !target.disabled;
5288
- } else if (target instanceof HTMLElement && target.isContentEditable) {
5289
- editable = true;
5290
- }
5291
- let blocker = null;
5292
- if (hasBox && window.innerWidth > 0 && window.innerHeight > 0) {
5293
- const x = Math.min(
5294
- Math.max(rect.left + rect.width / 2, 0),
5295
- window.innerWidth - 1
5296
- );
5297
- const y = Math.min(
5298
- Math.max(rect.top + rect.height / 2, 0),
5299
- window.innerHeight - 1
5300
- );
5301
- const top = document.elementFromPoint(x, y);
5302
- if (top && top !== target && !target.contains(top)) {
5303
- const classes = String(top.className || "").split(/\s+/).map((value) => value.trim()).filter(Boolean).slice(0, 5);
5304
- blocker = {
5305
- tag: top.tagName.toLowerCase(),
5306
- id: top.id || null,
5307
- classes,
5308
- role: top.getAttribute("role"),
5309
- text: (top.textContent || "").trim().slice(0, 80) || null
5112
+ walk(document);
5113
+ if (!matches.length) {
5114
+ return {
5115
+ status: "missing"
5116
+ };
5117
+ }
5118
+ if (matches.length > 1) {
5119
+ return {
5120
+ status: "ambiguous"
5310
5121
  };
5311
5122
  }
5123
+ const target = matches[0];
5124
+ const value = attribute2 ? target.getAttribute(attribute2) : target.textContent;
5125
+ return {
5126
+ status: "ok",
5127
+ value
5128
+ };
5129
+ },
5130
+ {
5131
+ targetCounter: String(counter),
5132
+ attribute
5312
5133
  }
5313
- return {
5314
- connected,
5315
- visible: isVisible,
5316
- enabled,
5317
- editable,
5318
- blocker
5319
- };
5320
- });
5134
+ );
5321
5135
  } catch {
5322
- return null;
5136
+ return {
5137
+ status: "missing"
5138
+ };
5323
5139
  }
5324
5140
  }
5141
+ function buildCounterNotFoundError(counter) {
5142
+ return new CounterResolutionError(
5143
+ "ERR_COUNTER_NOT_FOUND",
5144
+ `Counter ${counter} was not found in the live DOM.`
5145
+ );
5146
+ }
5147
+ function buildCounterAmbiguousError(counter) {
5148
+ return new CounterResolutionError(
5149
+ "ERR_COUNTER_AMBIGUOUS",
5150
+ `Counter ${counter} matches multiple live elements.`
5151
+ );
5152
+ }
5325
5153
 
5326
5154
  // src/actions/failure-classifier.ts
5327
5155
  var ACTION_FAILURE_CODES = [
@@ -5437,13 +5265,6 @@ function classifyTypedError(error) {
5437
5265
  classificationSource: "typed_error"
5438
5266
  });
5439
5267
  }
5440
- if (error.code === "ERR_COUNTER_FRAME_UNAVAILABLE") {
5441
- return buildFailure({
5442
- code: "TARGET_UNAVAILABLE",
5443
- message: error.message,
5444
- classificationSource: "typed_error"
5445
- });
5446
- }
5447
5268
  if (error.code === "ERR_COUNTER_AMBIGUOUS") {
5448
5269
  return buildFailure({
5449
5270
  code: "TARGET_AMBIGUOUS",
@@ -5451,13 +5272,6 @@ function classifyTypedError(error) {
5451
5272
  classificationSource: "typed_error"
5452
5273
  });
5453
5274
  }
5454
- if (error.code === "ERR_COUNTER_STALE_OR_NOT_FOUND") {
5455
- return buildFailure({
5456
- code: "TARGET_STALE",
5457
- message: error.message,
5458
- classificationSource: "typed_error"
5459
- });
5460
- }
5461
5275
  }
5462
5276
  return null;
5463
5277
  }
@@ -10721,7 +10535,7 @@ var Opensteer = class _Opensteer {
10721
10535
  let persistPath = null;
10722
10536
  try {
10723
10537
  if (storageKey && resolution.shouldPersist) {
10724
- persistPath = await this.buildPathFromResolvedHandle(
10538
+ persistPath = await this.tryBuildPathFromResolvedHandle(
10725
10539
  handle,
10726
10540
  "hover",
10727
10541
  resolution.counter
@@ -10820,7 +10634,7 @@ var Opensteer = class _Opensteer {
10820
10634
  let persistPath = null;
10821
10635
  try {
10822
10636
  if (storageKey && resolution.shouldPersist) {
10823
- persistPath = await this.buildPathFromResolvedHandle(
10637
+ persistPath = await this.tryBuildPathFromResolvedHandle(
10824
10638
  handle,
10825
10639
  "input",
10826
10640
  resolution.counter
@@ -10923,7 +10737,7 @@ var Opensteer = class _Opensteer {
10923
10737
  let persistPath = null;
10924
10738
  try {
10925
10739
  if (storageKey && resolution.shouldPersist) {
10926
- persistPath = await this.buildPathFromResolvedHandle(
10740
+ persistPath = await this.tryBuildPathFromResolvedHandle(
10927
10741
  handle,
10928
10742
  "select",
10929
10743
  resolution.counter
@@ -11033,7 +10847,7 @@ var Opensteer = class _Opensteer {
11033
10847
  let persistPath = null;
11034
10848
  try {
11035
10849
  if (storageKey && resolution.shouldPersist) {
11036
- persistPath = await this.buildPathFromResolvedHandle(
10850
+ persistPath = await this.tryBuildPathFromResolvedHandle(
11037
10851
  handle,
11038
10852
  "scroll",
11039
10853
  resolution.counter
@@ -11329,17 +11143,19 @@ var Opensteer = class _Opensteer {
11329
11143
  const handle = await this.resolveCounterHandle(resolution.counter);
11330
11144
  try {
11331
11145
  if (storageKey && resolution.shouldPersist) {
11332
- const persistPath = await this.buildPathFromResolvedHandle(
11146
+ const persistPath = await this.tryBuildPathFromResolvedHandle(
11333
11147
  handle,
11334
11148
  method,
11335
11149
  resolution.counter
11336
11150
  );
11337
- this.persistPath(
11338
- storageKey,
11339
- method,
11340
- options.description,
11341
- persistPath
11342
- );
11151
+ if (persistPath) {
11152
+ this.persistPath(
11153
+ storageKey,
11154
+ method,
11155
+ options.description,
11156
+ persistPath
11157
+ );
11158
+ }
11343
11159
  }
11344
11160
  return await counterFn(handle);
11345
11161
  } catch (err) {
@@ -11377,7 +11193,7 @@ var Opensteer = class _Opensteer {
11377
11193
  let persistPath = null;
11378
11194
  try {
11379
11195
  if (storageKey && resolution.shouldPersist) {
11380
- persistPath = await this.buildPathFromResolvedHandle(
11196
+ persistPath = await this.tryBuildPathFromResolvedHandle(
11381
11197
  handle,
11382
11198
  "uploadFile",
11383
11199
  resolution.counter
@@ -11653,7 +11469,7 @@ var Opensteer = class _Opensteer {
11653
11469
  let persistPath = null;
11654
11470
  try {
11655
11471
  if (storageKey && resolution.shouldPersist) {
11656
- persistPath = await this.buildPathFromResolvedHandle(
11472
+ persistPath = await this.tryBuildPathFromResolvedHandle(
11657
11473
  handle,
11658
11474
  "click",
11659
11475
  resolution.counter
@@ -11749,17 +11565,6 @@ var Opensteer = class _Opensteer {
11749
11565
  }
11750
11566
  }
11751
11567
  if (options.element != null) {
11752
- const pathFromElement = await this.tryBuildPathFromCounter(
11753
- options.element
11754
- );
11755
- if (pathFromElement) {
11756
- return {
11757
- path: pathFromElement,
11758
- counter: null,
11759
- shouldPersist: Boolean(storageKey),
11760
- source: "element"
11761
- };
11762
- }
11763
11568
  return {
11764
11569
  path: null,
11765
11570
  counter: options.element,
@@ -11787,17 +11592,6 @@ var Opensteer = class _Opensteer {
11787
11592
  options.description
11788
11593
  );
11789
11594
  if (resolved?.counter != null) {
11790
- const pathFromAiCounter = await this.tryBuildPathFromCounter(
11791
- resolved.counter
11792
- );
11793
- if (pathFromAiCounter) {
11794
- return {
11795
- path: pathFromAiCounter,
11796
- counter: null,
11797
- shouldPersist: Boolean(storageKey),
11798
- source: "ai"
11799
- };
11800
- }
11801
11595
  return {
11802
11596
  path: null,
11803
11597
  counter: resolved.counter,
@@ -11879,23 +11673,22 @@ var Opensteer = class _Opensteer {
11879
11673
  try {
11880
11674
  const builtPath = await buildElementPathFromHandle(handle);
11881
11675
  if (builtPath) {
11882
- return this.withIndexedIframeContext(builtPath, indexedPath);
11676
+ const withFrameContext = await this.withHandleIframeContext(
11677
+ handle,
11678
+ builtPath
11679
+ );
11680
+ return this.withIndexedIframeContext(
11681
+ withFrameContext,
11682
+ indexedPath
11683
+ );
11883
11684
  }
11884
11685
  return indexedPath;
11885
11686
  } finally {
11886
11687
  await handle.dispose();
11887
11688
  }
11888
11689
  }
11889
- async tryBuildPathFromCounter(counter) {
11890
- try {
11891
- return await this.buildPathFromElement(counter);
11892
- } catch {
11893
- return null;
11894
- }
11895
- }
11896
11690
  async resolveCounterHandle(element) {
11897
- const snapshot = await this.ensureSnapshotWithCounters();
11898
- return resolveCounterElement(this.page, snapshot, element);
11691
+ return resolveCounterElement(this.page, element);
11899
11692
  }
11900
11693
  async resolveCounterHandleForAction(action, description, element) {
11901
11694
  try {
@@ -11919,8 +11712,12 @@ var Opensteer = class _Opensteer {
11919
11712
  const indexedPath = await this.readPathFromCounterIndex(counter);
11920
11713
  const builtPath = await buildElementPathFromHandle(handle);
11921
11714
  if (builtPath) {
11715
+ const withFrameContext = await this.withHandleIframeContext(
11716
+ handle,
11717
+ builtPath
11718
+ );
11922
11719
  const normalized = this.withIndexedIframeContext(
11923
- builtPath,
11720
+ withFrameContext,
11924
11721
  indexedPath
11925
11722
  );
11926
11723
  if (normalized.nodes.length) return normalized;
@@ -11930,15 +11727,34 @@ var Opensteer = class _Opensteer {
11930
11727
  `Unable to build element path from counter ${counter} during ${action}.`
11931
11728
  );
11932
11729
  }
11730
+ async tryBuildPathFromResolvedHandle(handle, action, counter) {
11731
+ try {
11732
+ return await this.buildPathFromResolvedHandle(handle, action, counter);
11733
+ } catch (error) {
11734
+ this.logDebugError(
11735
+ `path persistence skipped for ${action} counter ${counter}`,
11736
+ error
11737
+ );
11738
+ return null;
11739
+ }
11740
+ }
11933
11741
  withIndexedIframeContext(builtPath, indexedPath) {
11934
11742
  const normalizedBuilt = this.normalizePath(builtPath);
11935
11743
  if (!indexedPath) return normalizedBuilt;
11936
11744
  const iframePrefix = collectIframeContextPrefix(indexedPath);
11937
11745
  if (!iframePrefix.length) return normalizedBuilt;
11746
+ const builtContext = cloneContextHops(normalizedBuilt.context);
11747
+ const overlap = measureContextOverlap(iframePrefix, builtContext);
11748
+ const missingPrefix = cloneContextHops(
11749
+ iframePrefix.slice(0, iframePrefix.length - overlap)
11750
+ );
11751
+ if (!missingPrefix.length) {
11752
+ return normalizedBuilt;
11753
+ }
11938
11754
  const merged = {
11939
11755
  context: [
11940
- ...cloneContextHops(iframePrefix),
11941
- ...cloneContextHops(normalizedBuilt.context)
11756
+ ...missingPrefix,
11757
+ ...builtContext
11942
11758
  ],
11943
11759
  nodes: cloneElementPath(normalizedBuilt).nodes
11944
11760
  };
@@ -11948,9 +11764,48 @@ var Opensteer = class _Opensteer {
11948
11764
  if (fallback.nodes.length) return fallback;
11949
11765
  return normalizedBuilt;
11950
11766
  }
11767
+ async withHandleIframeContext(handle, path5) {
11768
+ const ownFrame = await handle.ownerFrame();
11769
+ if (!ownFrame) {
11770
+ return this.normalizePath(path5);
11771
+ }
11772
+ let frame = ownFrame;
11773
+ let prefix2 = [];
11774
+ while (frame && frame !== this.page.mainFrame()) {
11775
+ const parent = frame.parentFrame();
11776
+ if (!parent) break;
11777
+ const frameElement = await frame.frameElement().catch(() => null);
11778
+ if (!frameElement) break;
11779
+ try {
11780
+ const frameElementPath = await buildElementPathFromHandle(frameElement);
11781
+ if (frameElementPath?.nodes.length) {
11782
+ const segment = [
11783
+ ...cloneContextHops(frameElementPath.context),
11784
+ {
11785
+ kind: "iframe",
11786
+ host: cloneElementPath(frameElementPath).nodes
11787
+ }
11788
+ ];
11789
+ prefix2 = [...segment, ...prefix2];
11790
+ }
11791
+ } finally {
11792
+ await frameElement.dispose().catch(() => void 0);
11793
+ }
11794
+ frame = parent;
11795
+ }
11796
+ if (!prefix2.length) {
11797
+ return this.normalizePath(path5);
11798
+ }
11799
+ return this.normalizePath({
11800
+ context: [...prefix2, ...cloneContextHops(path5.context)],
11801
+ nodes: cloneElementPath(path5).nodes
11802
+ });
11803
+ }
11951
11804
  async readPathFromCounterIndex(counter) {
11952
- const snapshot = await this.ensureSnapshotWithCounters();
11953
- const indexed = snapshot.counterIndex?.get(counter);
11805
+ if (!this.snapshotCache || this.snapshotCache.url !== this.page.url() || !this.snapshotCache.counterIndex) {
11806
+ return null;
11807
+ }
11808
+ const indexed = this.snapshotCache.counterIndex.get(counter);
11954
11809
  if (!indexed) return null;
11955
11810
  const normalized = this.normalizePath(indexed);
11956
11811
  if (!normalized.nodes.length) return null;
@@ -11961,15 +11816,6 @@ var Opensteer = class _Opensteer {
11961
11816
  if (!path5) return null;
11962
11817
  return this.normalizePath(path5);
11963
11818
  }
11964
- async ensureSnapshotWithCounters() {
11965
- if (!this.snapshotCache || !this.snapshotCache.counterBindings || this.snapshotCache.url !== this.page.url()) {
11966
- await this.snapshot({
11967
- mode: "full",
11968
- withCounters: true
11969
- });
11970
- }
11971
- return this.snapshotCache;
11972
- }
11973
11819
  persistPath(id, method, description, path5) {
11974
11820
  const now = Date.now();
11975
11821
  const safeFile = this.storage.getSelectorFileName(id);
@@ -12219,17 +12065,6 @@ var Opensteer = class _Opensteer {
12219
12065
  return;
12220
12066
  }
12221
12067
  if (normalized.element != null) {
12222
- const path5 = await this.tryBuildPathFromCounter(
12223
- normalized.element
12224
- );
12225
- if (path5) {
12226
- fields.push({
12227
- key: fieldKey,
12228
- path: path5,
12229
- attribute: normalized.attribute
12230
- });
12231
- return;
12232
- }
12233
12068
  fields.push({
12234
12069
  key: fieldKey,
12235
12070
  counter: normalized.element,
@@ -12274,15 +12109,6 @@ var Opensteer = class _Opensteer {
12274
12109
  continue;
12275
12110
  }
12276
12111
  if (fieldPlan.element != null) {
12277
- const path6 = await this.tryBuildPathFromCounter(fieldPlan.element);
12278
- if (path6) {
12279
- fields.push({
12280
- key,
12281
- path: path6,
12282
- attribute: fieldPlan.attribute
12283
- });
12284
- continue;
12285
- }
12286
12112
  fields.push({
12287
12113
  key,
12288
12114
  counter: fieldPlan.element,
@@ -12337,12 +12163,7 @@ var Opensteer = class _Opensteer {
12337
12163
  }
12338
12164
  }
12339
12165
  if (counterRequests.length) {
12340
- const snapshot = await this.ensureSnapshotWithCounters();
12341
- const counterValues = await resolveCountersBatch(
12342
- this.page,
12343
- snapshot,
12344
- counterRequests
12345
- );
12166
+ const counterValues = await resolveCountersBatch(this.page, counterRequests);
12346
12167
  Object.assign(result, counterValues);
12347
12168
  }
12348
12169
  if (pathFields.length) {
@@ -12372,7 +12193,7 @@ var Opensteer = class _Opensteer {
12372
12193
  const path5 = await this.buildPathFromElement(field.counter);
12373
12194
  if (!path5) {
12374
12195
  throw new Error(
12375
- `Unable to build element path from counter ${field.counter} for extraction field "${field.key}".`
12196
+ `Unable to persist extraction schema field "${field.key}": counter ${field.counter} could not be converted into a stable element path.`
12376
12197
  );
12377
12198
  }
12378
12199
  resolved.push({
@@ -12394,7 +12215,7 @@ var Opensteer = class _Opensteer {
12394
12215
  }
12395
12216
  resolveStorageKey(description) {
12396
12217
  if (!description) return null;
12397
- return (0, import_crypto2.createHash)("sha256").update(description).digest("hex").slice(0, 16);
12218
+ return (0, import_crypto.createHash)("sha256").update(description).digest("hex").slice(0, 16);
12398
12219
  }
12399
12220
  normalizePath(path5) {
12400
12221
  return sanitizeElementPath(path5);
@@ -12418,6 +12239,33 @@ function collectIframeContextPrefix(path5) {
12418
12239
  if (lastIframeIndex < 0) return [];
12419
12240
  return cloneContextHops(context.slice(0, lastIframeIndex + 1));
12420
12241
  }
12242
+ function measureContextOverlap(indexedPrefix, builtContext) {
12243
+ const maxOverlap = Math.min(indexedPrefix.length, builtContext.length);
12244
+ for (let size = maxOverlap; size > 0; size -= 1) {
12245
+ if (matchesContextPrefix(indexedPrefix, builtContext, size, true)) {
12246
+ return size;
12247
+ }
12248
+ }
12249
+ for (let size = maxOverlap; size > 0; size -= 1) {
12250
+ if (matchesContextPrefix(indexedPrefix, builtContext, size, false)) {
12251
+ return size;
12252
+ }
12253
+ }
12254
+ return 0;
12255
+ }
12256
+ function matchesContextPrefix(indexedPrefix, builtContext, size, strictHost) {
12257
+ for (let idx = 0; idx < size; idx += 1) {
12258
+ const left = indexedPrefix[indexedPrefix.length - size + idx];
12259
+ const right = builtContext[idx];
12260
+ if (left.kind !== right.kind) {
12261
+ return false;
12262
+ }
12263
+ if (strictHost && JSON.stringify(left.host) !== JSON.stringify(right.host)) {
12264
+ return false;
12265
+ }
12266
+ }
12267
+ return true;
12268
+ }
12421
12269
  function normalizeSchemaValue(value) {
12422
12270
  if (!value) return null;
12423
12271
  if (typeof value !== "object" || Array.isArray(value)) {
@@ -12439,7 +12287,7 @@ function normalizeExtractSource(source) {
12439
12287
  }
12440
12288
  function computeSchemaHash(schema) {
12441
12289
  const stable = stableStringify(schema);
12442
- return (0, import_crypto2.createHash)("sha256").update(stable).digest("hex");
12290
+ return (0, import_crypto.createHash)("sha256").update(stable).digest("hex");
12443
12291
  }
12444
12292
  function buildPathMap(fields) {
12445
12293
  const out = {};
@@ -12675,7 +12523,7 @@ function isInternalOrBlankPageUrl(url) {
12675
12523
  }
12676
12524
  function buildLocalRunId(namespace) {
12677
12525
  const normalized = namespace.trim() || "default";
12678
- return `${normalized}-${Date.now().toString(36)}-${(0, import_crypto2.randomUUID)().slice(0, 8)}`;
12526
+ return `${normalized}-${Date.now().toString(36)}-${(0, import_crypto.randomUUID)().slice(0, 8)}`;
12679
12527
  }
12680
12528
 
12681
12529
  // src/cli/paths.ts