@wenathlan/extension 1.1.33 → 1.1.34

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.
@@ -128,6 +128,116 @@ var sessionmemory = class {
128
128
  async setdialogpolicy(policy) {
129
129
  return this.adapter.set("dialogpolicy", policy);
130
130
  }
131
+ /** Stores one observation capture under its version so every observation version stays available. */
132
+ async setobservation(record2) {
133
+ return this.adapter.set(`observation${record2.version}`, record2);
134
+ }
135
+ /** Returns one stored observation version. */
136
+ async getobservation(version) {
137
+ return this.adapter.get(`observation${version}`);
138
+ }
139
+ /** Returns the observation retention window; an absent setting keeps every capture. */
140
+ async observationretention() {
141
+ return (await this.getsettings())?.observationretention;
142
+ }
143
+ /** Stores one accessibility tree capture; retention is a user setting and an absent setting keeps every tree. */
144
+ async adda11ytree(capture) {
145
+ const records = await this.geta11ytrees();
146
+ const combined = [capture, ...records];
147
+ const retention = await this.observationretention();
148
+ await this.adapter.set("a11ytrees", retention === void 0 ? combined : combined.slice(0, retention));
149
+ }
150
+ /** Returns every stored accessibility tree capture, newest first. */
151
+ async geta11ytrees() {
152
+ return await this.adapter.get("a11ytrees") ?? [];
153
+ }
154
+ /** Stores one reader article capture; retention is a user setting and an absent setting keeps every article. */
155
+ async addreaderarticle(capture) {
156
+ const records = await this.getreaderarticles();
157
+ const combined = [capture, ...records];
158
+ const retention = await this.observationretention();
159
+ await this.adapter.set("readerarticles", retention === void 0 ? combined : combined.slice(0, retention));
160
+ }
161
+ /** Returns every stored reader article capture, newest first. */
162
+ async getreaderarticles() {
163
+ return await this.adapter.get("readerarticles") ?? [];
164
+ }
165
+ /** Records one dom mutation observed inside a reviewed watch. */
166
+ async addmutationevent(event) {
167
+ const records = await this.getmutationevents();
168
+ await this.adapter.set("mutationevents", [event, ...records]);
169
+ }
170
+ /** Returns the mutation event stream of every reviewed watch. */
171
+ async getmutationevents() {
172
+ return await this.adapter.get("mutationevents") ?? [];
173
+ }
174
+ /** Records one focus change observed inside a reviewed watch. */
175
+ async addfocusevent(event) {
176
+ const records = await this.getfocusevents();
177
+ await this.adapter.set("focusevents", [event, ...records]);
178
+ }
179
+ /** Returns the focus event stream of every reviewed watch. */
180
+ async getfocusevents() {
181
+ return await this.adapter.get("focusevents") ?? [];
182
+ }
183
+ /** Records one consent banner observed by a reviewed banner watch. */
184
+ async addbanner(event) {
185
+ const records = await this.getbanners();
186
+ await this.adapter.set("banners", [event, ...records]);
187
+ }
188
+ /** Returns every consent banner report observed so far. */
189
+ async getbanners() {
190
+ return await this.adapter.get("banners") ?? [];
191
+ }
192
+ /** Records one snapshot diff between two observation versions. */
193
+ async adddiff(diff) {
194
+ const records = await this.getdiffs();
195
+ await this.adapter.set("diffs", [diff, ...records]);
196
+ }
197
+ /** Returns every stored snapshot diff, newest first. */
198
+ async getdiffs() {
199
+ return await this.adapter.get("diffs") ?? [];
200
+ }
201
+ /** Records one derived selector with its stability score for reuse. */
202
+ async addselector(selector) {
203
+ const records = await this.getselectors();
204
+ await this.adapter.set("selectors", [selector, ...records]);
205
+ }
206
+ /** Returns every stored derived selector with its stability score, newest first. */
207
+ async getselectors() {
208
+ return await this.adapter.get("selectors") ?? [];
209
+ }
210
+ /** Records one detected template class or section fingerprint for its origin. */
211
+ async addtemplate(profile) {
212
+ const records = await this.gettemplates();
213
+ await this.adapter.set("templates", [profile, ...records]);
214
+ }
215
+ /** Returns every stored template class and section fingerprint, newest first. */
216
+ async gettemplates() {
217
+ return await this.adapter.get("templates") ?? [];
218
+ }
219
+ /** Records one watch registration so it survives service worker restarts. */
220
+ async addwatch(watch) {
221
+ const records = await this.getwatches();
222
+ await this.adapter.set("watches", [watch, ...records]);
223
+ }
224
+ /** Returns every watch registration, newest first, including closed windows. */
225
+ async getwatches() {
226
+ return await this.adapter.get("watches") ?? [];
227
+ }
228
+ /** Closes one watch registration by watch id once its reviewed lifetime window ends. */
229
+ async closewatch(watchid, closedat) {
230
+ const records = await this.getwatches();
231
+ await this.adapter.set("watches", records.map((watch) => watch.watchid === watchid && watch.closedat === void 0 ? { ...watch, closedat } : watch));
232
+ }
233
+ /** Returns the live page signals of language, template, scroll lock and banner state. */
234
+ async getsignals() {
235
+ return this.adapter.get("signals");
236
+ }
237
+ /** Replaces the live page signals after an observation step refreshes them. */
238
+ async setsignals(signals) {
239
+ return this.adapter.set("signals", signals);
240
+ }
131
241
  };
132
242
  function randomid() {
133
243
  return crypto.randomUUID();
@@ -136,9 +246,10 @@ function randomid() {
136
246
  // policy.ts
137
247
  var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select", "presskey", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "reload", "back", "forward", "writestorage", "setattribute", "removeattribute", "evaluate", "tabcreate", "tabactivate", "tabclose", "tabreload", "windowcreate", "windowclose", "windowresize", "downloadfile", "clickpoint", "shiftclick", "dismissdialog", "enterframe", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor"]);
138
248
  var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover", "clickdeep", "rightclick", "doubleclick", "scrollpage", "scrollby", "scrollend", "scrolltop", "fullscreen", "zoomset", "movepointer", "clicktext", "clickaria", "clickname", "expanddetails", "pierceshadow", "retryaction"]);
139
- var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot", "mapclicks", "verifyvisible", "verifyenabled", "resolvexpath"]);
249
+ var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot", "mapclicks", "verifyvisible", "verifyenabled", "resolvexpath", "a11ytree", "readvisible", "readertree", "detectlists", "detecttables", "readjson", "watchmutate", "waitquiet", "watchbanner", "detectinfinitescroll", "detectvirtual", "detectlazy", "readscrollpos", "readlang", "readoutline", "countpages", "listshadow", "listframes", "classifypage", "fingerprintsection", "diffsnapshots", "readselection", "watchfocus", "detectsticky", "detectscrolllock", "readopengraph", "detectlanguage", "deriveselector"]);
140
250
  var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
141
- var targetactions = /* @__PURE__ */ new Set(["inspect", "focus", "click", "type", "scroll", "select", "hover", "clickdeep", "rightclick", "doubleclick", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "highlight", "setattribute", "removeattribute", "waitfor", "shiftclick", "typetime", "appendtext", "setvalue", "typeedit", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "expanddetails", "verifyvisible", "verifyenabled", "pierceshadow"]);
251
+ var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus"]);
252
+ var targetactions = /* @__PURE__ */ new Set(["inspect", "focus", "click", "type", "scroll", "select", "hover", "clickdeep", "rightclick", "doubleclick", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "highlight", "setattribute", "removeattribute", "waitfor", "shiftclick", "typetime", "appendtext", "setvalue", "typeedit", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "expanddetails", "verifyvisible", "verifyenabled", "pierceshadow", "deriveselector", "fingerprintsection"]);
142
253
  var valueactions = /* @__PURE__ */ new Set(["presskey", "drag", "drop", "upload", "readattribute", "removeattribute", "waittext", "evaluate", "zoomset", "tabactivate", "tabclose", "tabreload", "windowclose", "windowresize", "tabcreate", "windowcreate", "downloadfile", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "chooseradio", "setslider", "setdate", "setcolor"]);
143
254
  function normalizeendpoint(value) {
144
255
  const endpoint = new URL(value.trim());
@@ -362,6 +473,24 @@ function validatestep(step, origin) {
362
473
  if (!nonnegativeoption(retry, "settle")) return { allowed: false, reason: "The reviewed retry settle window must be zero or a positive number of milliseconds." };
363
474
  if (!nonnegativeoption(retry, "tolerance")) return { allowed: false, reason: "The reviewed retry movement tolerance must be zero or a positive number of pixels." };
364
475
  }
476
+ if (watchactions.has(step.kind)) {
477
+ if (typeof options.lifetime !== "number" || !Number.isFinite(options.lifetime) || options.lifetime <= 0) return { allowed: false, reason: "A reviewed watch lifetime window in milliseconds is required in options." };
478
+ if (options.scopes !== void 0 && (!Array.isArray(options.scopes) || !options.scopes.every((scope) => isnonempty(scope)))) return { allowed: false, reason: "The reviewed watch scopes must be a list of non-empty selectors." };
479
+ if (options.events !== void 0 && (!Array.isArray(options.events) || !options.events.every((event) => isnonempty(event)))) return { allowed: false, reason: "The reviewed watch event kinds must be a list of non-empty strings." };
480
+ if (!nonnegativeoption(options, "poll")) return { allowed: false, reason: "The reviewed watch poll interval must be zero or a positive number of milliseconds." };
481
+ }
482
+ if (step.kind === "waitquiet") {
483
+ const rule = options.quietrule;
484
+ if (!rule || typeof rule !== "object" || Array.isArray(rule)) return { allowed: false, reason: "A reviewed quietrule with an idle threshold is required in options." };
485
+ const quiet = rule;
486
+ if (typeof quiet.idle !== "number" || !Number.isFinite(quiet.idle) || quiet.idle <= 0) return { allowed: false, reason: "The reviewed quiet idle threshold must be a positive number of milliseconds with no code ceiling." };
487
+ if (!nonnegativeoption(quiet, "poll")) return { allowed: false, reason: "The reviewed quiet poll interval must be zero or a positive number of milliseconds." };
488
+ if (!nonnegativeoption(quiet, "timeout")) return { allowed: false, reason: "The reviewed quiet timeout must be zero or a positive number of milliseconds." };
489
+ }
490
+ if (step.kind === "diffsnapshots") {
491
+ const versions = options.versions;
492
+ if (!Array.isArray(versions) || versions.length !== 2 || !versions.every((version) => typeof version === "number" && Number.isInteger(version) && version >= 1)) return { allowed: false, reason: "Two reviewed observation version numbers are required in options." };
493
+ }
365
494
  return { allowed: true };
366
495
  }
367
496
  function sessiongate(input) {
@@ -378,6 +507,7 @@ function canexecute(input) {
378
507
  if (!input.plan || input.plan.state !== "approved") return { allowed: false, reason: "The plan has not received explicit approval." };
379
508
  if (input.plan.expiresat <= now) return { allowed: false, reason: "The approved plan has expired." };
380
509
  if ((input.step.kind === "pierceshadow" || input.step.kind === "enterframe") && !origingranted(input.session, input.origin)) return { allowed: false, reason: "The shadow or frame step is outside the session origin grants." };
510
+ if (input.step.kind === "readjson" && !origingranted(input.session, input.origin)) return { allowed: false, reason: "The json state read is outside the session origin grants." };
381
511
  return validatestep(input.step, input.origin);
382
512
  }
383
513
  function canpreview(input) {
@@ -420,9 +550,17 @@ function resetforplan(progress, plan, now) {
420
550
  const snapshot2 = { planid: progress.planid, completedsteps: progress.completedsteps, ...progress.outcomes ? { outcomes: progress.outcomes } : {}, updatedat: progress.updatedat };
421
551
  return { planid: plan.id, completedsteps: [], outcomes: [], prior: [...progress.prior ?? [], snapshot2], updatedat: now };
422
552
  }
553
+ function watchclosed(startedat, lifetime, now) {
554
+ return now >= startedat + lifetime;
555
+ }
556
+ function recordwatchcompletion(progress, planid, stepid, startedat, lifetime, now) {
557
+ const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
558
+ if (!watchclosed(startedat, lifetime, now)) return base;
559
+ return recordstep(base, planid, stepid, now);
560
+ }
423
561
 
424
562
  // version.ts
425
- var packageversion = "1.1.33";
563
+ var packageversion = "1.1.34";
426
564
 
427
565
  // types.ts
428
566
  var protocolversion = packageversion;
@@ -489,6 +627,19 @@ function mapresponse(input) {
489
627
  function heldkeysreport(input) {
490
628
  return { version: protocolversion, tabid: input.tabid, heldkeys: input.holds };
491
629
  }
630
+ function observationresponse(input) {
631
+ return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, observation: input.observation });
632
+ }
633
+ function signalsreport(input) {
634
+ const signals = input.signals;
635
+ return {
636
+ version: protocolversion,
637
+ ...signals && signals.language !== void 0 ? { language: signals.language } : {},
638
+ ...signals && signals.template !== void 0 ? { template: signals.template } : {},
639
+ ...signals && signals.scrolllocked !== void 0 ? { scrolllocked: signals.scrolllocked } : {},
640
+ ...signals && signals.banner !== void 0 ? { banner: signals.banner } : {}
641
+ };
642
+ }
492
643
 
493
644
  // extension/browsertabs.ts
494
645
  var browserkinds = /* @__PURE__ */ new Set(["tablist", "tabcreate", "tabactivate", "tabclose", "tabreload", "tabsnapshot", "windowlist", "windowcreate", "windowclose", "zoomset", "windowresize", "downloadfile"]);
@@ -710,6 +861,8 @@ function heldkeys(holds, tabid2) {
710
861
  var sessionduration = 15 * 60 * 1e3;
711
862
  var freshcheckkinds = /* @__PURE__ */ new Set(["focus", "inspect", "click", "type", "scroll", "select", "hover"]);
712
863
  var pointerkinds = /* @__PURE__ */ new Set(["movepointer", "clickpoint", "shiftclick", "clicktext", "clickaria", "clickname", "pierceshadow"]);
864
+ var watchstepkinds = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus"]);
865
+ var observationstepkinds = /* @__PURE__ */ new Set(["a11ytree", "readvisible", "readertree", "detectlists", "detecttables", "readjson", "detectinfinitescroll", "detectvirtual", "detectlazy", "readscrollpos", "readlang", "readoutline", "countpages", "listshadow", "listframes", "classifypage", "fingerprintsection", "readselection", "detectsticky", "detectscrolllock", "readopengraph", "detectlanguage", "deriveselector"]);
713
866
  var chromestorage = {
714
867
  async get(key) {
715
868
  return (await chrome.storage.local.get(key))[key];
@@ -753,7 +906,10 @@ async function snapshot(tabid2) {
753
906
  } });
754
907
  const value = result[0]?.result;
755
908
  if (!value) throw new Error("The page did not return an observation.");
756
- return value;
909
+ const observationcapture = value;
910
+ const version = await memory.nextobservationversion();
911
+ await memory.setobservation({ version, observation: observationcapture });
912
+ return observationcapture;
757
913
  }
758
914
  function plandialogpolicy(plan) {
759
915
  const step = plan?.steps.find((candidate) => candidate.kind === "dismissdialog");
@@ -874,6 +1030,9 @@ function stepauditkind(step, ok) {
874
1030
  if (step.kind === "keyhold" || step.kind === "keyrelease") return "hold";
875
1031
  if (step.kind === "retryaction") return "retry";
876
1032
  if (pointerkinds.has(step.kind)) return "pointer";
1033
+ if (watchstepkinds.has(step.kind)) return "watch";
1034
+ if (step.kind === "diffsnapshots") return "diff";
1035
+ if (observationstepkinds.has(step.kind)) return "observation";
877
1036
  return ok ? "action" : "error";
878
1037
  }
879
1038
  function resolvedinnerstep(step, plan) {
@@ -960,6 +1119,141 @@ async function executeenterframe(step, plan, tabid2, origin) {
960
1119
  const derived = { ...step, options: JSON.stringify({ ...options, kind: inner.kind, ...inner.target ? { target: inner.target } : {}, ...inner.value ? { value: inner.value } : {}, ...inneroptions ? { options: inneroptions } : {} }) };
961
1120
  return dispatchpagestep(derived, tabid2, origin, plan);
962
1121
  }
1122
+ function observationnodes(observationcapture) {
1123
+ return observationcapture.interactive.map((entry) => ({ selector: entry.selector, tag: entry.role, text: entry.label, attributes: {} }));
1124
+ }
1125
+ function detailarray(details, key) {
1126
+ const value = details?.[key];
1127
+ return Array.isArray(value) ? value : [];
1128
+ }
1129
+ async function executediffsnapshots(step, session, plan, tabid2, origin) {
1130
+ const options = stepoptions2(step);
1131
+ const versions = Array.isArray(options.versions) ? options.versions.filter((item) => typeof item === "number") : [];
1132
+ const baseversion = versions[0];
1133
+ const targetversion = versions[1];
1134
+ if (baseversion === void 0 || targetversion === void 0) throw new Error("Two reviewed observation versions are required.");
1135
+ const base = await memory.getobservation(baseversion);
1136
+ const target = await memory.getobservation(targetversion);
1137
+ if (!base || !target) throw new Error("A reviewed observation version has not been captured yet.");
1138
+ const derived = { ...step, options: JSON.stringify({ ...options, base: observationnodes(base.observation), target: observationnodes(target.observation) }) };
1139
+ const output = await dispatchpagestep(derived, tabid2, origin, plan) ?? { ok: false, summary: "The snapshot diff returned no result." };
1140
+ const diff = {
1141
+ baseversion,
1142
+ targetversion,
1143
+ added: detailarray(output.details, "added"),
1144
+ removed: detailarray(output.details, "removed"),
1145
+ changed: detailarray(output.details, "changed"),
1146
+ at: Date.now()
1147
+ };
1148
+ await memory.adddiff(diff);
1149
+ await audit("diff", `Diffed observation versions ${baseversion} and ${targetversion}: ${diff.added.length} added, ${diff.removed.length} removed and ${diff.changed.length} changed nodes.`, { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id });
1150
+ return { ok: Boolean(output.ok), summary: output.summary, details: { ...output.details ?? {}, versions: [baseversion, targetversion] } };
1151
+ }
1152
+ async function executewatchstep(step, session, plan, tabid2, origin) {
1153
+ const options = stepoptions2(step);
1154
+ const watchid = typeof options.watchid === "string" && options.watchid.trim() ? options.watchid : randomid();
1155
+ const scopes = Array.isArray(options.scopes) ? options.scopes.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
1156
+ const events = Array.isArray(options.events) ? options.events.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
1157
+ const lifetime = typeof options.lifetime === "number" && Number.isFinite(options.lifetime) && options.lifetime > 0 ? options.lifetime : 0;
1158
+ const watch = { watchid, kind: step.kind, stepid: step.id, sessionid: session.id, origin, scopes, events, startedat: Date.now(), lifetime };
1159
+ await memory.addwatch(watch);
1160
+ await audit("watch", `Watch ${step.kind} registered under id ${watchid} for the reviewed lifetime of ${lifetime} milliseconds inside ${scopes.length > 0 ? `scopes ${scopes.join(", ")}` : "the whole document"}.`, { sessionid: session.id, planid: plan.id, stepid: step.id });
1161
+ const output = await dispatchpagestep(step, tabid2, origin, plan) ?? { ok: false, summary: "The watch returned no result." };
1162
+ await memory.closewatch(watchid, Date.now());
1163
+ if (step.kind === "watchmutate") {
1164
+ for (const entry of detailarray(output.details, "events")) {
1165
+ if (!entry || typeof entry !== "object") continue;
1166
+ const record2 = entry;
1167
+ await memory.addmutationevent({ ...record2, sessionid: session.id });
1168
+ }
1169
+ }
1170
+ if (step.kind === "watchfocus") {
1171
+ for (const entry of detailarray(output.details, "events")) {
1172
+ if (!entry || typeof entry !== "object") continue;
1173
+ const record2 = entry;
1174
+ await memory.addfocusevent({ ...record2, sessionid: session.id });
1175
+ }
1176
+ }
1177
+ if (step.kind === "watchbanner") {
1178
+ for (const entry of detailarray(output.details, "banners")) {
1179
+ if (!entry || typeof entry !== "object") continue;
1180
+ const record2 = entry;
1181
+ await memory.addbanner({ ...record2, sessionid: session.id });
1182
+ }
1183
+ }
1184
+ await audit("watch", `Watch ${watchid} closed after its reviewed lifetime of ${lifetime} milliseconds.`, { sessionid: session.id, planid: plan.id, stepid: step.id });
1185
+ return { output, watch };
1186
+ }
1187
+ async function refreshsignals(step, output) {
1188
+ const current = await memory.getsignals();
1189
+ const next = current ? { ...current, refreshedat: Date.now() } : { refreshedat: Date.now() };
1190
+ let changed = false;
1191
+ if (step.kind === "readlang" || step.kind === "detectlanguage") {
1192
+ const language = output?.details?.language;
1193
+ if (typeof language === "string" && language) {
1194
+ next.language = language;
1195
+ changed = true;
1196
+ }
1197
+ }
1198
+ if (step.kind === "classifypage") {
1199
+ const template = output?.details?.template;
1200
+ if (typeof template === "string" && template) {
1201
+ next.template = template;
1202
+ changed = true;
1203
+ }
1204
+ }
1205
+ if (step.kind === "detectscrolllock") {
1206
+ const locked = output?.details?.locked;
1207
+ if (typeof locked === "boolean") {
1208
+ next.scrolllocked = locked;
1209
+ changed = true;
1210
+ }
1211
+ }
1212
+ if (step.kind === "watchbanner") {
1213
+ const banners = detailarray(output?.details, "banners");
1214
+ const first = banners[0];
1215
+ if (first && typeof first.kind === "string") {
1216
+ next.banner = first.kind;
1217
+ changed = true;
1218
+ }
1219
+ }
1220
+ if (changed) await memory.setsignals(next);
1221
+ }
1222
+ async function recordevidence(step, output, session, plan, origin) {
1223
+ const extra = { ...session ? { sessionid: session.id } : {}, ...plan ? { planid: plan.id } : {}, stepid: step.id };
1224
+ if (step.kind === "a11ytree" && output?.details?.tree && typeof output.details.tree === "object") {
1225
+ const version = await memory.nextobservationversion();
1226
+ await memory.adda11ytree({ version, tree: output.details.tree, capturedat: Date.now() });
1227
+ await audit("observation", `Accessibility tree captured under observation version ${version} with ${output.details.nodecount ?? "an unknown count of"} nodes.`, extra);
1228
+ }
1229
+ if (step.kind === "readertree" && output?.details?.article && typeof output.details.article === "object") {
1230
+ const version = await memory.nextobservationversion();
1231
+ const article = output.details.article;
1232
+ await memory.addreaderarticle({ version, article, capturedat: Date.now() });
1233
+ await audit("observation", `Reader article captured under observation version ${version} with ${article.blocks.length} blocks and ${article.words} words.`, extra);
1234
+ }
1235
+ if (step.kind === "classifypage" && typeof output?.details?.template === "string") {
1236
+ const fingerprint = typeof output.details.fingerprint === "string" ? output.details.fingerprint : "";
1237
+ const profile = { origin, template: output.details.template, fingerprint, at: Date.now() };
1238
+ await memory.addtemplate(profile);
1239
+ await audit("observation", `Page template ${profile.template} classified for ${origin}${fingerprint ? ` with fingerprint ${fingerprint}` : ""}.`, extra);
1240
+ }
1241
+ if (step.kind === "fingerprintsection" && typeof output?.details?.fingerprint === "string") {
1242
+ const profile = { origin, template: "", fingerprint: output.details.fingerprint, ...typeof output.details.section === "string" ? { section: output.details.section } : {}, at: Date.now() };
1243
+ await memory.addtemplate(profile);
1244
+ await audit("observation", `Section fingerprint ${profile.fingerprint} computed for ${origin}.`, extra);
1245
+ }
1246
+ if (step.kind === "deriveselector") {
1247
+ const candidates = detailarray(output?.details, "candidates");
1248
+ const best = candidates[0];
1249
+ if (best && typeof best.selector === "string" && best.selector) {
1250
+ const record2 = { stepid: step.id, selector: best.selector, strategy: best.strategy, score: best.score, at: Date.now() };
1251
+ await memory.addselector(record2);
1252
+ await audit("observation", `Derived selector ${record2.selector} through the ${record2.strategy} strategy with stability ${record2.score}.`, extra);
1253
+ }
1254
+ }
1255
+ await refreshsignals(step, output);
1256
+ }
963
1257
  async function executestep(stepid) {
964
1258
  const session = await memory.getsession();
965
1259
  const plan = await memory.getplan();
@@ -969,6 +1263,7 @@ async function executestep(stepid) {
969
1263
  const gate = canexecute({ session, plan, step, tabid: tab.id, origin });
970
1264
  if (!gate.allowed) throw new Error(gate.reason);
971
1265
  let output;
1266
+ let watchwindow;
972
1267
  if (isbrowserkind(step.kind)) {
973
1268
  const capability = requiredcapability(step.kind);
974
1269
  if (capability) {
@@ -988,6 +1283,13 @@ async function executestep(stepid) {
988
1283
  output = await executemapclicks(step, plan, tab.id, origin);
989
1284
  } else if (step.kind === "enterframe") {
990
1285
  output = await executeenterframe(step, plan, tab.id, origin);
1286
+ } else if (watchstepkinds.has(step.kind)) {
1287
+ if (!session || !plan || plan.state !== "approved") throw new Error("Watch kinds refuse to run outside an approved session plan.");
1288
+ const watched = await executewatchstep(step, session, plan, tab.id, origin);
1289
+ output = watched.output;
1290
+ watchwindow = { startedat: watched.watch.startedat, lifetime: watched.watch.lifetime };
1291
+ } else if (step.kind === "diffsnapshots") {
1292
+ output = await executediffsnapshots(step, session, plan, tab.id, origin);
991
1293
  } else {
992
1294
  if (step.target && freshcheckkinds.has(step.kind)) {
993
1295
  const fresh = await snapshot(tab.id);
@@ -995,6 +1297,7 @@ async function executestep(stepid) {
995
1297
  }
996
1298
  output = await dispatchpagestep(step, tab.id, origin, plan);
997
1299
  }
1300
+ await recordevidence(step, output, session, plan, origin);
998
1301
  const summary = output?.summary ?? "The page action returned no result.";
999
1302
  const resolved = output?.details?.resolvedtarget;
1000
1303
  if (resolved) {
@@ -1005,7 +1308,8 @@ async function executestep(stepid) {
1005
1308
  await audit(auditkind, summary, { ...session ? { sessionid: session.id } : {}, ...plan ? { planid: plan.id } : {}, stepid });
1006
1309
  await memory.addoutcome(outcome);
1007
1310
  if (output?.ok && plan) {
1008
- const completed = recordstep(await memory.getprogress(), plan.id, stepid, Date.now());
1311
+ const base = await memory.getprogress();
1312
+ const completed = watchwindow ? recordwatchcompletion(base, plan.id, stepid, watchwindow.startedat, watchwindow.lifetime, Date.now()) : recordstep(base, plan.id, stepid, Date.now());
1009
1313
  const tracked = recordoutcome(completed, plan.id, outcome, Date.now());
1010
1314
  await memory.setprogress(tracked);
1011
1315
  if (iscomplete(tracked, plan) && plan.state === "approved") {
@@ -1083,7 +1387,10 @@ async function handlerequest(message, sender) {
1083
1387
  const holds = heldkeys(await memory.getholds());
1084
1388
  const observationversion = await memory.getobservationversion();
1085
1389
  const map = observationversion !== void 0 && observationversion > 0 ? await memory.getmap(observationversion) : void 0;
1086
- return { config: await memory.getconfig(), session, plan, progress: plan && progress?.planid === plan.id ? progress : void 0, diagnostic: await memory.getdiagnostic(), audit: await memory.getaudit(), capabilities: await refreshcapabilities(), outcomes: await memory.getoutcomes(), holds: heldkeysreport({ tabid: session?.tabid ?? 0, holds }), dialogs: await memory.getdialogs(), retries: await memory.getretries(), ...map ? { map } : {} };
1390
+ const a11y = (await memory.geta11ytrees())[0];
1391
+ const reader = (await memory.getreaderarticles())[0];
1392
+ const signals = await memory.getsignals();
1393
+ return { config: await memory.getconfig(), session, plan, progress: plan && progress?.planid === plan.id ? progress : void 0, diagnostic: await memory.getdiagnostic(), audit: await memory.getaudit(), capabilities: await refreshcapabilities(), outcomes: await memory.getoutcomes(), holds: heldkeysreport({ tabid: session?.tabid ?? 0, holds }), dialogs: await memory.getdialogs(), retries: await memory.getretries(), ...signals ? { signals: signalsreport({ signals }) } : { signals: signalsreport({}) }, banners: await memory.getbanners(), mutationevents: await memory.getmutationevents(), focusevents: await memory.getfocusevents(), diffs: await memory.getdiffs(), selectors: await memory.getselectors(), ...a11y ? { a11y } : {}, ...reader ? { reader } : {}, ...map ? { map } : {} };
1087
1394
  }
1088
1395
  case "capabilities":
1089
1396
  return refreshcapabilities();
@@ -1135,6 +1442,14 @@ async function handlerequest(message, sender) {
1135
1442
  if (!map) throw new Error("No clickable map has been captured yet.");
1136
1443
  return JSON.parse(mapresponse({ map, plan }));
1137
1444
  }
1445
+ case "observation": {
1446
+ const plan = await memory.getplan();
1447
+ if (!plan) throw new Error("No plan is available for an observation envelope.");
1448
+ const version = await memory.getobservationversion();
1449
+ const record2 = version !== void 0 ? await memory.getobservation(version) : void 0;
1450
+ if (!record2) throw new Error("No observation has been captured yet.");
1451
+ return JSON.parse(observationresponse({ observation: record2.observation, plan }));
1452
+ }
1138
1453
  case "pausesession":
1139
1454
  return pausesession();
1140
1455
  case "resumesession":
@@ -1155,6 +1470,16 @@ chrome.runtime.onMessage.addListener((message, sender, sendresponse) => {
1155
1470
  handlerequest(message, sender).then((value) => sendresponse({ ok: true, value })).catch((error) => sendresponse({ ok: false, error: error instanceof Error ? error.message : String(error) }));
1156
1471
  return true;
1157
1472
  });
1473
+ async function reconcilewatches() {
1474
+ for (const watch of await memory.getwatches()) {
1475
+ if (watch.closedat !== void 0) continue;
1476
+ if (!watchclosed(watch.startedat, watch.lifetime, Date.now())) continue;
1477
+ await memory.closewatch(watch.watchid, Date.now());
1478
+ await audit("watch", `Watch ${watch.watchid} of ${watch.kind} closed on service worker restart after its reviewed lifetime of ${watch.lifetime} milliseconds.`, { sessionid: watch.sessionid, stepid: watch.stepid });
1479
+ }
1480
+ }
1481
+ reconcilewatches().catch(() => {
1482
+ });
1158
1483
  chrome.runtime.onConnect.addListener((port) => {
1159
1484
  if (port.name !== "devthinksidepanel" || port.sender?.id !== chrome.runtime.id || !port.sender.url?.startsWith(chrome.runtime.getURL(""))) return port.disconnect();
1160
1485
  port.onMessage.addListener((message) => {