things-api 0.17.0 → 0.18.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.
- package/README.md +1 -1
- package/deputy/VERSION +1 -0
- package/deputy/helpers-Info.plist +23 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/CodeResources +0 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/Info.plist +20 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/MacOS/things-reader +0 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/_CodeSignature/CodeResources +115 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/Info.plist +23 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/MacOS/things-deputy +0 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/_CodeSignature/CodeResources +125 -0
- package/deputy/reader/Info.plist +20 -0
- package/deputy/reader/entitlements.plist +15 -0
- package/deputy/reader/main.swift +491 -0
- package/deputy/src/main.swift +133 -0
- package/deputy/src/osascript.swift +102 -0
- package/deputy/src/server.swift +350 -0
- package/deputy/src/sqlite.swift +121 -0
- package/dist/cli/commands/doctor.js +55 -2
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/helpers.d.ts +17 -0
- package/dist/cli/commands/helpers.js +148 -0
- package/dist/cli/commands/helpers.js.map +1 -0
- package/dist/cli/commands/op-result.d.ts +15 -0
- package/dist/cli/commands/op-result.js +39 -0
- package/dist/cli/commands/op-result.js.map +1 -0
- package/dist/cli/commands/writes.js +22 -3
- package/dist/cli/commands/writes.js.map +1 -1
- package/dist/cli/help.js +4 -0
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/helpers-check.d.ts +10 -0
- package/dist/cli/helpers-check.js +59 -0
- package/dist/cli/helpers-check.js.map +1 -0
- package/dist/cli/main.js +22 -1
- package/dist/cli/main.js.map +1 -1
- package/dist/client.js +16 -2
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +31 -0
- package/dist/config.js +9 -0
- package/dist/config.js.map +1 -1
- package/dist/contracts.d.ts +1 -1
- package/dist/contracts.js +1 -1
- package/dist/db/baselines/db-v26.js +8 -4
- package/dist/db/baselines/db-v26.js.map +1 -1
- package/dist/db/baselines/db-v27.d.ts +42 -0
- package/dist/db/baselines/db-v27.js +47 -0
- package/dist/db/baselines/db-v27.js.map +1 -0
- package/dist/db/baselines/index.js +2 -1
- package/dist/db/baselines/index.js.map +1 -1
- package/dist/db/locate.d.ts +4 -1
- package/dist/db/locate.js +3 -1
- package/dist/db/locate.js.map +1 -1
- package/dist/db/schema.d.ts +1 -1
- package/dist/db/schema.js +9 -0
- package/dist/db/schema.js.map +1 -1
- package/dist/deputy/bridge-worker.d.ts +1 -0
- package/dist/deputy/bridge-worker.js +79 -0
- package/dist/deputy/bridge-worker.js.map +1 -0
- package/dist/deputy/bridge.d.ts +14 -0
- package/dist/deputy/bridge.js +70 -0
- package/dist/deputy/bridge.js.map +1 -0
- package/dist/deputy/client.d.ts +19 -0
- package/dist/deputy/client.js +104 -0
- package/dist/deputy/client.js.map +1 -0
- package/dist/deputy/db-facade.d.ts +18 -0
- package/dist/deputy/db-facade.js +71 -0
- package/dist/deputy/db-facade.js.map +1 -0
- package/dist/deputy/files.d.ts +1 -0
- package/dist/deputy/files.js +16 -0
- package/dist/deputy/files.js.map +1 -0
- package/dist/deputy/install.d.ts +105 -0
- package/dist/deputy/install.js +467 -0
- package/dist/deputy/install.js.map +1 -0
- package/dist/deputy/notice.d.ts +6 -0
- package/dist/deputy/notice.js +28 -0
- package/dist/deputy/notice.js.map +1 -0
- package/dist/deputy/notices.d.ts +31 -0
- package/dist/deputy/notices.js +94 -0
- package/dist/deputy/notices.js.map +1 -0
- package/dist/deputy/osa.d.ts +21 -0
- package/dist/deputy/osa.js +72 -0
- package/dist/deputy/osa.js.map +1 -0
- package/dist/deputy/protocol.d.ts +86 -0
- package/dist/deputy/protocol.js +106 -0
- package/dist/deputy/protocol.js.map +1 -0
- package/dist/deputy/routing.d.ts +63 -0
- package/dist/deputy/routing.js +338 -0
- package/dist/deputy/routing.js.map +1 -0
- package/dist/deputy/shortcuts-exec.d.ts +10 -0
- package/dist/deputy/shortcuts-exec.js +50 -0
- package/dist/deputy/shortcuts-exec.js.map +1 -0
- package/dist/diagnose.d.ts +43 -1
- package/dist/diagnose.js +64 -2
- package/dist/diagnose.js.map +1 -1
- package/dist/index.d.ts +13 -2
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +25 -7
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/mappers.d.ts +12 -3
- package/dist/model/mappers.js +6 -1
- package/dist/model/mappers.js.map +1 -1
- package/dist/model/recurrence.d.ts +5 -1
- package/dist/model/recurrence.js +19 -1
- package/dist/model/recurrence.js.map +1 -1
- package/dist/model/template-projection.d.ts +26 -0
- package/dist/model/template-projection.js +113 -0
- package/dist/model/template-projection.js.map +1 -0
- package/dist/op-result.d.ts +46 -0
- package/dist/op-result.js +156 -0
- package/dist/op-result.js.map +1 -0
- package/dist/read/detail.js +7 -1
- package/dist/read/detail.js.map +1 -1
- package/dist/read/queries.js +8 -1
- package/dist/read/queries.js.map +1 -1
- package/dist/read/views.d.ts +6 -4
- package/dist/read/views.js +60 -18
- package/dist/read/views.js.map +1 -1
- package/dist/sync-health.js +6 -2
- package/dist/sync-health.js.map +1 -1
- package/dist/write/accessibility-probe.js +4 -2
- package/dist/write/accessibility-probe.js.map +1 -1
- package/dist/write/automation-probe.js +5 -1
- package/dist/write/automation-probe.js.map +1 -1
- package/dist/write/availability.js +7 -3
- package/dist/write/availability.js.map +1 -1
- package/dist/write/commands.js +58 -13
- package/dist/write/commands.js.map +1 -1
- package/dist/write/experimental.d.ts +19 -0
- package/dist/write/experimental.js +50 -0
- package/dist/write/experimental.js.map +1 -1
- package/dist/write/move.js +14 -6
- package/dist/write/move.js.map +1 -1
- package/dist/write/operations.d.ts +28 -4
- package/dist/write/pipeline.d.ts +2 -0
- package/dist/write/pipeline.js +59 -2
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/pre-state.js +33 -15
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/promote-clone.js +187 -19
- package/dist/write/promote-clone.js.map +1 -1
- package/dist/write/reorder.js +76 -27
- package/dist/write/reorder.js.map +1 -1
- package/dist/write/repeat-anchor.d.ts +30 -0
- package/dist/write/repeat-anchor.js +166 -1
- package/dist/write/repeat-anchor.js.map +1 -1
- package/dist/write/repeat-rule.js +10 -0
- package/dist/write/repeat-rule.js.map +1 -1
- package/dist/write/vectors/applescript.js +7 -16
- package/dist/write/vectors/applescript.js.map +1 -1
- package/dist/write/vectors/session-reachability.d.ts +55 -0
- package/dist/write/vectors/session-reachability.js +56 -8
- package/dist/write/vectors/session-reachability.js.map +1 -1
- package/dist/write/vectors/shortcuts.js +5 -14
- package/dist/write/vectors/shortcuts.js.map +1 -1
- package/dist/write/vectors/simulator.d.ts +16 -1
- package/dist/write/vectors/simulator.js +61 -10
- package/dist/write/vectors/simulator.js.map +1 -1
- package/dist/write/vectors/types.d.ts +67 -1
- package/dist/write/vectors/ui-recipes.js +128 -38
- package/dist/write/vectors/ui-recipes.js.map +1 -1
- package/dist/write/vectors/ui.d.ts +76 -0
- package/dist/write/vectors/ui.js +443 -37
- package/dist/write/vectors/ui.js.map +1 -1
- package/dist/write/verify/poller.d.ts +13 -0
- package/dist/write/verify/poller.js +38 -0
- package/dist/write/verify/poller.js.map +1 -1
- package/package.json +8 -1
- package/scripts/build-helpers.sh +104 -0
- package/scripts/deputy-cert-setup.sh +60 -0
- package/skills/things-cli/SKILL.md +8 -3
- package/skills/things-cli/references/errors.md +4 -0
- package/skills/things-cli/references/repeating.md +69 -0
package/dist/write/vectors/ui.js
CHANGED
|
@@ -27,15 +27,22 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import { execFile } from "node:child_process";
|
|
29
29
|
import { DEFAULT_UI_DRIVE_BUDGET_MS } from "../../config.js";
|
|
30
|
+
import { osaExec } from "../../deputy/osa.js";
|
|
30
31
|
import { noteInflightStep, trace, traceActive, tracePath } from "../../trace/tracer.js";
|
|
31
32
|
import { UI_DRIVE_OPS } from "../operations.js";
|
|
32
33
|
import { escapeAppleScript } from "./applescript.js";
|
|
33
|
-
import { H_UI_SESSION_UNREACHABLE, probeSessionReachability, } from "./session-reachability.js";
|
|
34
|
+
import { createReachabilityCache, H_UI_SESSION_UNREACHABLE, probeSessionReachability, } from "./session-reachability.js";
|
|
34
35
|
import { certificationOf } from "./ui-certification.js";
|
|
35
36
|
import { driveSidebarAreaReorder, jxaSidebarSnapshotScript } from "./ui-drag.js";
|
|
36
37
|
/** GUI driving can stall on an unanswered sheet; give each step headroom. */
|
|
37
38
|
const STEP_TIMEOUT_MS = 15_000;
|
|
38
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Poll interval while waiting for a dynamic element (sheet/popover). KEPT at 300ms
|
|
41
|
+
* after the PERF2 audit: the control a mode switch reveals takes ~462ms to appear
|
|
42
|
+
* on the golden (S5b, [docs/lab/perf2-step-latency.md]), which EXCEEDS this
|
|
43
|
+
* interval — so a 300ms poll catches it on its second round; a finer interval
|
|
44
|
+
* would only add osascript hops for a marginal detection gain (UIC6 confirmed).
|
|
45
|
+
*/
|
|
39
46
|
const WAIT_POLL_MS = 300;
|
|
40
47
|
/**
|
|
41
48
|
* How long `resolveStepPath` polls a candidate-addressed control before failing
|
|
@@ -48,9 +55,22 @@ const RESOLVE_CANDIDATE_TIMEOUT_MS = 5_000;
|
|
|
48
55
|
* Settle after the reveal/activate preamble so the menu bar repopulates for the
|
|
49
56
|
* newly-selected target before the canary reads it (UIC1: the Items ▸ Repeat
|
|
50
57
|
* submenu appears only once a repeating item is selected, and the update is not
|
|
51
|
-
* instantaneous).
|
|
58
|
+
* instantaneous). TRIMMED 1500 → 1000 by the PERF2 audit (S5a,
|
|
59
|
+
* [docs/lab/perf2-step-latency.md]): on a warm running app under DEFAULT macOS
|
|
60
|
+
* animations the menu repopulates in ~92ms median / 116ms max (N=10) — a ~13×
|
|
61
|
+
* margin at 1500. Menu-bar repopulation is a LOCAL UI operation (not a DB-commit /
|
|
62
|
+
* sync-bound one), so it does not scale with DB size the way the OK commit does;
|
|
63
|
+
* 1000ms keeps ~8.6× the golden max as host headroom. Under-margining only ever
|
|
64
|
+
* costs a fail-closed spurious drive refusal (the canary miss), never a bad write.
|
|
65
|
+
*/
|
|
66
|
+
const SETTLE_AFTER_REVEAL_MS = 1000;
|
|
67
|
+
/**
|
|
68
|
+
* A shape-dependent step reached without the dialog having been measured — a
|
|
69
|
+
* recipe bug (the `probe-dialog-shape` step is missing or ran after its
|
|
70
|
+
* dependants). Refused, never guessed: the two shapes address DIFFERENT controls
|
|
71
|
+
* at the same index.
|
|
52
72
|
*/
|
|
53
|
-
const
|
|
73
|
+
const SHAPE_UNPROBED = "the Repeat dialog's shape was never measured, so this control's address is unknown (recipe bug)";
|
|
54
74
|
const SE = `tell application "System Events" to tell process "Things3"`;
|
|
55
75
|
/** resolve-element: does the element exist right now? Returns "true"/"false". */
|
|
56
76
|
export function axResolveScript(path) {
|
|
@@ -178,6 +198,270 @@ export function axSelectPopupCandidatesScript(path, values) {
|
|
|
178
198
|
error "none of the candidate menu items exist: " & {${list}}
|
|
179
199
|
end tell`;
|
|
180
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* probe-dialog-shape: MEASURE which Repeat dialog is open (RDLG2) — the whole
|
|
203
|
+
* version fork, decided by STRUCTURE rather than by the app version, so the
|
|
204
|
+
* recipe self-selects on any host and a future redesign refuses instead of
|
|
205
|
+
* silently pressing the wrong control:
|
|
206
|
+
*
|
|
207
|
+
* - `next-popup` (Things 3.23+) — the first occurrence is an `AXPopUpButton`
|
|
208
|
+
* listing the rule's own upcoming occurrences. It sits between Ends and every
|
|
209
|
+
* per-frequency control, so weekday / monthly / yearly pop-ups are one index
|
|
210
|
+
* further along.
|
|
211
|
+
* - `legacy` (Things ≤ 3.22) — the first occurrence is a free-form
|
|
212
|
+
* `AXDateTimeArea`, and the per-frequency controls follow Ends directly.
|
|
213
|
+
*
|
|
214
|
+
* The discriminator is the CONTROL CLASS on the `Next:` row, not the presence of
|
|
215
|
+
* the label: RDLG2d measured Things 3.22.14 and found it carries the same
|
|
216
|
+
* `Next:` static text (and the same occurrence-preview line) as 3.23 — only the
|
|
217
|
+
* control beside it changed. So the probe reads the label's row position and asks
|
|
218
|
+
* what kind of control shares that row, which also keeps it independent of the
|
|
219
|
+
* dialog's Ends state (an `Ends: on date` bound adds a SECOND date area, on a
|
|
220
|
+
* different row, in both shapes). Each branch is a POSITIVE match, so an
|
|
221
|
+
* unrecognized third dialog returns "unknown" and the driver refuses. Labels are
|
|
222
|
+
* pinned English, exactly like every other selector here.
|
|
223
|
+
*/
|
|
224
|
+
export function axProbeDialogShapeScript(groupPath, rowTolerance = 8) {
|
|
225
|
+
const tol = Math.max(1, Math.trunc(rowTolerance));
|
|
226
|
+
return `${SE}
|
|
227
|
+
set g to (${groupPath})
|
|
228
|
+
set nextY to missing value
|
|
229
|
+
set nStatic to (count of static texts of g)
|
|
230
|
+
repeat with i from 1 to nStatic
|
|
231
|
+
set v to ""
|
|
232
|
+
try
|
|
233
|
+
set v to (value of static text i of g) as text
|
|
234
|
+
end try
|
|
235
|
+
if v is "Next:" then
|
|
236
|
+
set p to position of static text i of g
|
|
237
|
+
set nextY to item 2 of p
|
|
238
|
+
end if
|
|
239
|
+
end repeat
|
|
240
|
+
if nextY is missing value then return "unknown"
|
|
241
|
+
set nPop to (count of pop up buttons of g)
|
|
242
|
+
repeat with i from 1 to nPop
|
|
243
|
+
set p to position of pop up button i of g
|
|
244
|
+
set dy to (item 2 of p) - nextY
|
|
245
|
+
if dy < 0 then set dy to -dy
|
|
246
|
+
if dy <= ${tol} then return "next-popup"
|
|
247
|
+
end repeat
|
|
248
|
+
try
|
|
249
|
+
set areas to (every UI element of g whose role is "AXDateTimeArea")
|
|
250
|
+
repeat with i from 1 to (count of areas)
|
|
251
|
+
set p to position of (item i of areas)
|
|
252
|
+
set dy to (item 2 of p) - nextY
|
|
253
|
+
if dy < 0 then set dy to -dy
|
|
254
|
+
if dy <= ${tol} then return "legacy"
|
|
255
|
+
end repeat
|
|
256
|
+
end try
|
|
257
|
+
return "unknown"
|
|
258
|
+
end tell`;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* select-next-occurrence: set the first occurrence through the Things 3.23
|
|
262
|
+
* `Next:` POP-UP (RDLG2). 3.23 replaced the free-form first-occurrence date area
|
|
263
|
+
* with a bounded MENU — `Today`, then the rule's own upcoming occurrences, then a
|
|
264
|
+
* `More…` item whose submenu carries the next hundred, cascading further the same
|
|
265
|
+
* way. Two consequences this script encodes:
|
|
266
|
+
*
|
|
267
|
+
* - a requested date is reachable ONLY if the rule itself produces it (or it is
|
|
268
|
+
* today): the menu offers nothing else, so an OFF-RULE first occurrence — free
|
|
269
|
+
* to set on ≤3.22 — is UNEXPRESSIBLE in this dialog and must fail closed with
|
|
270
|
+
* a named reason rather than land some neighbouring date;
|
|
271
|
+
* - item titles are localized (`Sun, Jul 12, 2026`), so the match is made by
|
|
272
|
+
* PARSING each title to a date (with a leading-weekday retry) and comparing
|
|
273
|
+
* calendar components — never by rebuilding the app's display string.
|
|
274
|
+
*
|
|
275
|
+
* The cascade is walked to a bounded depth; the click is verified by reading the
|
|
276
|
+
* pop-up's value back and requiring it to equal the clicked item's own title
|
|
277
|
+
* (the fail-closed read-back the ANCH2/YANCH1 date drives established).
|
|
278
|
+
*/
|
|
279
|
+
export function axSelectNextOccurrenceScript(popupPath, isoDate, maxLevels = 6) {
|
|
280
|
+
const [y, m, d] = isoDate.split("-").map((part) => Number(part));
|
|
281
|
+
const levels = Math.max(1, Math.trunc(maxLevels));
|
|
282
|
+
return `on parsedYMD(t)
|
|
283
|
+
set s to t as text
|
|
284
|
+
try
|
|
285
|
+
set theDate to date s
|
|
286
|
+
return {year of theDate, (month of theDate) as integer, day of theDate}
|
|
287
|
+
end try
|
|
288
|
+
try
|
|
289
|
+
set ofs to offset of ", " in s
|
|
290
|
+
if ofs > 0 then
|
|
291
|
+
set theDate to date (text (ofs + 2) thru -1 of s)
|
|
292
|
+
return {year of theDate, (month of theDate) as integer, day of theDate}
|
|
293
|
+
end if
|
|
294
|
+
end try
|
|
295
|
+
return missing value
|
|
296
|
+
end parsedYMD
|
|
297
|
+
|
|
298
|
+
set wantY to ${y}
|
|
299
|
+
set wantM to ${m}
|
|
300
|
+
set wantD to ${d}
|
|
301
|
+
set rightNow to current date
|
|
302
|
+
set isToday to ((year of rightNow) is wantY and ((month of rightNow) as integer) is wantM and (day of rightNow) is wantD)
|
|
303
|
+
${SE}
|
|
304
|
+
set pu to (${popupPath})
|
|
305
|
+
repeat 20 times
|
|
306
|
+
if (exists menu 1 of pu) then exit repeat
|
|
307
|
+
click pu
|
|
308
|
+
delay 0.3
|
|
309
|
+
end repeat
|
|
310
|
+
set theMenu to menu 1 of pu
|
|
311
|
+
set clickedTitle to ""
|
|
312
|
+
set levelsSeen to 0
|
|
313
|
+
if isToday then
|
|
314
|
+
set nms to name of every menu item of theMenu
|
|
315
|
+
if (count of nms) > 0 then
|
|
316
|
+
set t1 to item 1 of nms
|
|
317
|
+
if t1 is not missing value then
|
|
318
|
+
if (my parsedYMD(t1)) is missing value then
|
|
319
|
+
set clickedTitle to t1 as text
|
|
320
|
+
click menu item 1 of theMenu
|
|
321
|
+
end if
|
|
322
|
+
end if
|
|
323
|
+
end if
|
|
324
|
+
end if
|
|
325
|
+
repeat ${levels} times
|
|
326
|
+
if clickedTitle is not "" then exit repeat
|
|
327
|
+
set levelsSeen to levelsSeen + 1
|
|
328
|
+
set nms to name of every menu item of theMenu
|
|
329
|
+
set hit to 0
|
|
330
|
+
repeat with i from 1 to (count of nms)
|
|
331
|
+
set nm to item i of nms
|
|
332
|
+
if nm is not missing value then
|
|
333
|
+
set ymd to my parsedYMD(nm)
|
|
334
|
+
if ymd is not missing value then
|
|
335
|
+
if (item 1 of ymd) is wantY and (item 2 of ymd) is wantM and (item 3 of ymd) is wantD then
|
|
336
|
+
set hit to i
|
|
337
|
+
exit repeat
|
|
338
|
+
end if
|
|
339
|
+
end if
|
|
340
|
+
end if
|
|
341
|
+
end repeat
|
|
342
|
+
if hit > 0 then
|
|
343
|
+
set clickedTitle to (item hit of nms) as text
|
|
344
|
+
click menu item hit of theMenu
|
|
345
|
+
exit repeat
|
|
346
|
+
end if
|
|
347
|
+
set lastI to (count of nms)
|
|
348
|
+
if lastI is 0 then exit repeat
|
|
349
|
+
if (item lastI of nms) is missing value then exit repeat
|
|
350
|
+
set deeper to missing value
|
|
351
|
+
try
|
|
352
|
+
set deeper to menu 1 of menu item lastI of theMenu
|
|
353
|
+
end try
|
|
354
|
+
if deeper is missing value then
|
|
355
|
+
try
|
|
356
|
+
click menu item lastI of theMenu
|
|
357
|
+
delay 0.5
|
|
358
|
+
set deeper to menu 1 of menu item lastI of theMenu
|
|
359
|
+
end try
|
|
360
|
+
end if
|
|
361
|
+
if deeper is missing value then exit repeat
|
|
362
|
+
set theMenu to deeper
|
|
363
|
+
end repeat
|
|
364
|
+
if clickedTitle is "" then
|
|
365
|
+
key code 53
|
|
366
|
+
error "select-next-occurrence: this Repeat dialog offers only the rule's own upcoming occurrences (and today) as the first occurrence, and ${isoDate} is not one of them — searched " & levelsSeen & " level(s) of the Next: menu. Ask for a date the rule actually produces, or change the rule."
|
|
367
|
+
end if
|
|
368
|
+
delay 0.4
|
|
369
|
+
set shown to (value of pu) as text
|
|
370
|
+
if shown is not clickedTitle then
|
|
371
|
+
error "select-next-occurrence: the Next: pop-up committed \\"" & shown & "\\", not the requested \\"" & clickedTitle & "\\" — the selection did not take"
|
|
372
|
+
end if
|
|
373
|
+
return "OK"
|
|
374
|
+
end tell`;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* converge-weekdays: drive the weekly dialog's weekday ROWS onto an exact target
|
|
378
|
+
* set through a deterministic closed loop (RDLG2 — the RRD1 fix).
|
|
379
|
+
*
|
|
380
|
+
* The shipped drive set the FIRST weekday row and then pressed "+" and re-drove
|
|
381
|
+
* THE SAME row index per extra weekday, which on a PRE-POPULATED reschedule
|
|
382
|
+
* dialog left the rule's existing weekdays untouched: `{mon,wed}` retargeted to
|
|
383
|
+
* `{tue,thu,sat}` committed `{mon,tue,thu,sat}` (VMQ1 cell 2Tb — caught only by
|
|
384
|
+
* the write pipeline's verify). It also had no way to SHRINK a set.
|
|
385
|
+
*
|
|
386
|
+
* The loop instead: (1) read the live row count; (2) press the row-add button —
|
|
387
|
+
* the smaller-x button of a weekday row, resolved from live geometry rather than
|
|
388
|
+
* a pinned index, because the row buttons enumerate in an unstable order — until
|
|
389
|
+
* there are at least as many rows as target weekdays; (3) assign EVERY row from
|
|
390
|
+
* the target set, cycling, so a surplus row duplicates a target weekday instead
|
|
391
|
+
* of keeping a stale one (the app stores the weekdays as a SET, so duplicates
|
|
392
|
+
* collapse on commit — this is what makes shrinking possible without the
|
|
393
|
+
* remove button); (4) read every row back and require the set to match exactly.
|
|
394
|
+
* Anything else errors — the pipeline re-verifies against the DB regardless.
|
|
395
|
+
*
|
|
396
|
+
* `base` is the 1-based group pop-up index of the first weekday row (2 on the
|
|
397
|
+
* legacy dialog, 3 once the 3.23 `Next:` pop-up sits in front of them).
|
|
398
|
+
*/
|
|
399
|
+
export function axConvergeWeekdaysScript(groupPath, base, titles) {
|
|
400
|
+
const list = titles.map((t) => `"${escapeAppleScript(t)}"`).join(", ");
|
|
401
|
+
const b = Math.max(1, Math.trunc(base));
|
|
402
|
+
return `set wantList to {${list}}
|
|
403
|
+
set baseIx to ${b}
|
|
404
|
+
${SE}
|
|
405
|
+
set g to (${groupPath})
|
|
406
|
+
set k to (count of wantList)
|
|
407
|
+
repeat 14 times
|
|
408
|
+
set n to (count of pop up buttons of g) - baseIx + 1
|
|
409
|
+
if n >= k then exit repeat
|
|
410
|
+
set nb to (count of buttons of g)
|
|
411
|
+
if nb is 0 then error "converge-weekdays: the dialog exposes no weekday row button, so a second weekday cannot be added"
|
|
412
|
+
set bestI to 0
|
|
413
|
+
set bestX to 1000000
|
|
414
|
+
repeat with i from 1 to nb
|
|
415
|
+
set p to position of button i of g
|
|
416
|
+
set px to item 1 of p
|
|
417
|
+
if px < bestX then
|
|
418
|
+
set bestX to px
|
|
419
|
+
set bestI to i
|
|
420
|
+
end if
|
|
421
|
+
end repeat
|
|
422
|
+
click button bestI of g
|
|
423
|
+
delay 0.5
|
|
424
|
+
end repeat
|
|
425
|
+
set n to (count of pop up buttons of g) - baseIx + 1
|
|
426
|
+
if n < k then error "converge-weekdays: the dialog would not grow to " & k & " weekday row(s) — it stopped at " & n
|
|
427
|
+
repeat with i from 1 to n
|
|
428
|
+
set wi to ((i - 1) mod k) + 1
|
|
429
|
+
set wantVal to item wi of wantList
|
|
430
|
+
set pu to pop up button (baseIx + i - 1) of g
|
|
431
|
+
if ((value of pu) as text) is not wantVal then
|
|
432
|
+
repeat 20 times
|
|
433
|
+
if (exists menu 1 of pu) then exit repeat
|
|
434
|
+
click pu
|
|
435
|
+
delay 0.3
|
|
436
|
+
end repeat
|
|
437
|
+
if not (exists menu item wantVal of menu 1 of pu) then
|
|
438
|
+
key code 53
|
|
439
|
+
error "converge-weekdays: the weekday pop-up offers no item \\"" & wantVal & "\\" (the app may not be in English)"
|
|
440
|
+
end if
|
|
441
|
+
click menu item wantVal of menu 1 of pu
|
|
442
|
+
delay 0.4
|
|
443
|
+
end if
|
|
444
|
+
end repeat
|
|
445
|
+
set absent to ""
|
|
446
|
+
repeat with wi from 1 to k
|
|
447
|
+
set wantVal to item wi of wantList
|
|
448
|
+
set seen to false
|
|
449
|
+
repeat with i from 1 to n
|
|
450
|
+
if ((value of pop up button (baseIx + i - 1) of g) as text) is wantVal then set seen to true
|
|
451
|
+
end repeat
|
|
452
|
+
if not seen then set absent to absent & wantVal & " "
|
|
453
|
+
end repeat
|
|
454
|
+
set strays to ""
|
|
455
|
+
repeat with i from 1 to n
|
|
456
|
+
set v to (value of pop up button (baseIx + i - 1) of g) as text
|
|
457
|
+
if wantList does not contain v then set strays to strays & v & " "
|
|
458
|
+
end repeat
|
|
459
|
+
if absent is not "" or strays is not "" then
|
|
460
|
+
error "converge-weekdays: the weekday rows did not converge — missing: " & absent & "| unexpected: " & strays
|
|
461
|
+
end if
|
|
462
|
+
return "OK"
|
|
463
|
+
end tell`;
|
|
464
|
+
}
|
|
181
465
|
/**
|
|
182
466
|
* select-row: select a PROJECT row by title, purely via AX (UIC4-a). Walks the
|
|
183
467
|
* content table's rows, issues the row `select` action on each (which REPLACES
|
|
@@ -400,8 +684,11 @@ async function clearDialog(run) {
|
|
|
400
684
|
* Reachable now → proceed (disclosed); still not →
|
|
401
685
|
* block with the Space remediation.
|
|
402
686
|
*/
|
|
403
|
-
async function ensureWindowReachable(run) {
|
|
404
|
-
|
|
687
|
+
async function ensureWindowReachable(run, reachCache) {
|
|
688
|
+
// First probe MAY be served from the pre-seed gate's memo (PERF1) — but only a
|
|
689
|
+
// reachable verdict is ever memoized, so every refusal/relocation below is still
|
|
690
|
+
// decided on a fresh probe (see ReachabilityProbeCache).
|
|
691
|
+
const first = await reachCache.probe(run, STEP_TIMEOUT_MS);
|
|
405
692
|
if (first.reachable)
|
|
406
693
|
return { ok: true, relocated: false };
|
|
407
694
|
if (first.scope === "session")
|
|
@@ -413,6 +700,9 @@ async function ensureWindowReachable(run) {
|
|
|
413
700
|
label: "move the Things window to the current desktop",
|
|
414
701
|
script: axCloseReopenActivateScript(),
|
|
415
702
|
}, STEP_TIMEOUT_MS);
|
|
703
|
+
// The relocation just changed window state — drop any memo and re-probe LIVE
|
|
704
|
+
// (a closed-loop verify of the maneuver, never a cached verdict).
|
|
705
|
+
reachCache.invalidate();
|
|
416
706
|
const second = await probeSessionReachability(run, STEP_TIMEOUT_MS);
|
|
417
707
|
if (second.reachable)
|
|
418
708
|
return { ok: true, relocated: true };
|
|
@@ -506,6 +796,24 @@ function attr(el,name){ var out=Ref(); if($.AXUIElementCopyAttributeValue(el,$(n
|
|
|
506
796
|
function rolestr(el){ var v=attr(el,'AXRole'); return v? v.js : ''; }
|
|
507
797
|
function kids(el){ var c=attr(el,'AXChildren'); if(!c) return []; var a=[]; for(var i=0;i<c.count;i++) a.push(c.objectAtIndex(i)); return a; }
|
|
508
798
|
function collect(el,role,depth,out){ if(depth<0) return; if(rolestr(el)===role) out.push(el); var ks=kids(el); for(var i=0;i<ks.length;i++) collect(ks[i],role,depth-1,out); }
|
|
799
|
+
function subrole(el){ var v=attr(el,'AXSubrole'); return v? v.js : ''; }
|
|
800
|
+
function windowsOf(el){ var c=attr(el,'AXWindows'); if(!c) return []; var a=[]; for(var i=0;i<c.count;i++) a.push(c.objectAtIndex(i)); return a; }
|
|
801
|
+
function sizeWH(el){ var s=attr(el,'AXSize'); if(!s) return null; var d=ObjC.castRefToObject($.CFCopyDescription(s)).js; var mw=String(d).match(/w:([-0-9.]+)/); var mh=String(d).match(/h:([-0-9.]+)/); return (mw&&mh)? {w:+mw[1], h:+mh[1]} : null; }
|
|
802
|
+
// Resolve the Repeat-dialog SHELL so the AXDateTimeArea collect walks only its
|
|
803
|
+
// small subtree — never the app-wide tree, whose main-window list content is the
|
|
804
|
+
// 4.4s app-root descent PERF2 removed (docs/lab/perf2-step-latency.md). The dialog
|
|
805
|
+
// presents in TWO shapes (ui-recipes DIALOG_SHELLS, UIC4-a), tried in the SAME
|
|
806
|
+
// priority order the System-Events pathCandidates use: an attached AXSheet on the
|
|
807
|
+
// standard window (Things frontmost), then a detached top-level AXUnknown window
|
|
808
|
+
// that is not the 40x40 utility window (Things backgrounded). null when neither is
|
|
809
|
+
// present — the caller then falls through to the same named "presents 0 date
|
|
810
|
+
// area(s)" error the app-root walk threw when the dialog was absent.
|
|
811
|
+
function findShell(app){
|
|
812
|
+
var wins=windowsOf(app);
|
|
813
|
+
for(var i=0;i<wins.length;i++){ if(subrole(wins[i])==='AXStandardWindow'){ var sh=[]; collect(wins[i],'AXSheet',3,sh); if(sh.length) return sh[0]; } }
|
|
814
|
+
for(var i=0;i<wins.length;i++){ if(subrole(wins[i])==='AXUnknown'){ var wh=sizeWH(wins[i]); if(!wh || !(wh.w===40 && wh.h===40)) return wins[i]; } }
|
|
815
|
+
return null;
|
|
816
|
+
}
|
|
509
817
|
function posY(el){ var p=attr(el,'AXPosition'); if(!p) return 0; var d=ObjC.castRefToObject($.CFCopyDescription(p)).js; var m=String(d).match(/y:([-0-9.]+)/); return m? +m[1] : 0; }
|
|
510
818
|
function timeOfDay(el){ var v=attr(el,'AXValue'); if(!v) return -1; var cal=$.NSCalendar.currentCalendar; return cal.componentFromDate($.NSCalendarUnitHour,v)*60 + cal.componentFromDate($.NSCalendarUnitMinute,v); }
|
|
511
819
|
function pick(areas,target){
|
|
@@ -528,11 +836,15 @@ function run(){
|
|
|
528
836
|
var app=$.AXUIElementCreateApplication(apps.objectAtIndex(0).processIdentifier);
|
|
529
837
|
var target=${JSON.stringify(target)};
|
|
530
838
|
var spec=${JSON.stringify(spec)};
|
|
531
|
-
// Poll for the addressed area
|
|
532
|
-
//
|
|
533
|
-
//
|
|
839
|
+
// Poll for the addressed area WITHIN THE DIALOG SHELL (PERF2): resolve the sheet
|
|
840
|
+
// / detached editor first, then collect only its subtree — the app-root descent
|
|
841
|
+
// this replaced cost ~4.4s on the busy host by walking the main window's list
|
|
842
|
+
// content. collect is wrapped so a stale-element ObjC exception during traversal
|
|
843
|
+
// cannot bubble as a raw -2700; pick guards the empty set, so dt is null (never a
|
|
844
|
+
// crash) when the target is absent. When no shell resolves (dialog absent), areas
|
|
845
|
+
// stays empty and the loop falls through to the SAME named error below.
|
|
534
846
|
var areas=[]; var dt=null;
|
|
535
|
-
for(var t=0;t<20 && !dt;t++){ areas=[]; try{ collect(
|
|
847
|
+
for(var t=0;t<20 && !dt;t++){ areas=[]; try{ var shell=findShell(app); if(shell) collect(shell,'AXDateTimeArea',16,areas); }catch(e){ areas=[]; } dt=pick(areas,target); if(!dt) $.NSThread.sleepForTimeInterval(0.1); }
|
|
536
848
|
if(!dt) throw new Error('set-datetime '+target+': this Repeat-dialog state presents '+areas.length+' date area(s) ['+inv(areas)+'] but none is the '+target+' control — the requested first occurrence / bound cannot be set in this dialog shape');
|
|
537
849
|
var cal=$.NSCalendar.currentCalendar;
|
|
538
850
|
var d;
|
|
@@ -566,6 +878,20 @@ function run(){
|
|
|
566
878
|
return 'OK';
|
|
567
879
|
}`;
|
|
568
880
|
}
|
|
881
|
+
/**
|
|
882
|
+
* The converge-weekdays step encodes both of its inputs in `value` as
|
|
883
|
+
* `"<base>|<Weekday>,<Weekday>…"`: the base is the group pop-up index of the
|
|
884
|
+
* FIRST weekday row, which the dialog SHAPE decides (2 legacy / 3 next-popup),
|
|
885
|
+
* so it rides the same shape-selected `value` the driver merges in.
|
|
886
|
+
*/
|
|
887
|
+
export function weekdayBaseOf(value) {
|
|
888
|
+
const base = Number(value.split("|", 1)[0]);
|
|
889
|
+
return Number.isFinite(base) && base > 0 ? Math.trunc(base) : 2;
|
|
890
|
+
}
|
|
891
|
+
export function weekdayTitlesOf(value) {
|
|
892
|
+
const rest = value.slice(value.indexOf("|") + 1);
|
|
893
|
+
return rest.split(",").filter((t) => t !== "");
|
|
894
|
+
}
|
|
569
895
|
/** Parse a resolve-frame "x y w h" line into the frame's center point. */
|
|
570
896
|
export function parseFrameCenter(stdout) {
|
|
571
897
|
const nums = stdout.trim().split(/\s+/).map(Number);
|
|
@@ -577,30 +903,30 @@ export function parseFrameCenter(stdout) {
|
|
|
577
903
|
function revealUrl(uuid) {
|
|
578
904
|
return `things:///show?id=${encodeURIComponent(uuid)}`;
|
|
579
905
|
}
|
|
580
|
-
function defaultRun(command, timeoutMs) {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
[bin, args] = ["osascript", ["-e", command.script ?? ""]];
|
|
593
|
-
}
|
|
594
|
-
execFile(bin, [...args], { timeout: timeoutMs }, (err, stdout, stderr) => {
|
|
595
|
-
const timedOut = err !== null && err.killed === true;
|
|
596
|
-
resolve({
|
|
597
|
-
ok: err === null,
|
|
598
|
-
stdout: String(stdout),
|
|
599
|
-
stderr: String(stderr),
|
|
600
|
-
...(timedOut && { timedOut: true }),
|
|
906
|
+
async function defaultRun(command, timeoutMs) {
|
|
907
|
+
if (command.primitive === "reveal") {
|
|
908
|
+
// `open` is consent-free (LaunchServices, no AppleEvent) — never routed.
|
|
909
|
+
return new Promise((resolve) => {
|
|
910
|
+
execFile("open", [command.url ?? ""], { timeout: timeoutMs }, (err, stdout, stderr) => {
|
|
911
|
+
const timedOut = err !== null && err.killed === true;
|
|
912
|
+
resolve({
|
|
913
|
+
ok: err === null,
|
|
914
|
+
stdout: String(stdout),
|
|
915
|
+
stderr: String(stderr),
|
|
916
|
+
...(timedOut && { timedOut: true }),
|
|
917
|
+
});
|
|
601
918
|
});
|
|
602
919
|
});
|
|
603
|
-
}
|
|
920
|
+
}
|
|
921
|
+
// JXA (ObjC bridge) for the mouse-synthesis primitive; one stable shape.
|
|
922
|
+
const lang = command.lang === "javascript" ? "javascript" : "applescript";
|
|
923
|
+
const res = await osaExec(command.script ?? "", { lang, timeoutMs });
|
|
924
|
+
return {
|
|
925
|
+
ok: res.exitCode === 0 && res.timedOut !== true,
|
|
926
|
+
stdout: res.stdout,
|
|
927
|
+
stderr: res.stderr,
|
|
928
|
+
...(res.timedOut === true && { timedOut: true }),
|
|
929
|
+
};
|
|
604
930
|
}
|
|
605
931
|
/**
|
|
606
932
|
* Wrap the dispatch seam so every osascript hop is recorded. The last-dispatched
|
|
@@ -730,6 +1056,26 @@ export function commandForStep(step, targetUuid) {
|
|
|
730
1056
|
label: step.label,
|
|
731
1057
|
script: axEnsureCheckboxScript(step.path ?? "", step.checkboxTarget === true),
|
|
732
1058
|
};
|
|
1059
|
+
case "probe-dialog-shape":
|
|
1060
|
+
return {
|
|
1061
|
+
primitive: "probe-dialog-shape",
|
|
1062
|
+
label: step.label,
|
|
1063
|
+
script: axProbeDialogShapeScript(step.path ?? ""),
|
|
1064
|
+
};
|
|
1065
|
+
case "select-next-occurrence":
|
|
1066
|
+
return {
|
|
1067
|
+
primitive: "select-next-occurrence",
|
|
1068
|
+
label: step.label,
|
|
1069
|
+
script: axSelectNextOccurrenceScript(step.path ?? "", step.value ?? ""),
|
|
1070
|
+
};
|
|
1071
|
+
case "converge-weekdays":
|
|
1072
|
+
return {
|
|
1073
|
+
primitive: "converge-weekdays",
|
|
1074
|
+
label: step.label,
|
|
1075
|
+
// `value` is "<base index>|<Weekday>,<Weekday>…" — the base index is the
|
|
1076
|
+
// shape-selected group pop-up index of the first weekday row (RDLG2).
|
|
1077
|
+
script: axConvergeWeekdaysScript(step.path ?? "", weekdayBaseOf(step.value ?? ""), weekdayTitlesOf(step.value ?? "")),
|
|
1078
|
+
};
|
|
733
1079
|
case "wait":
|
|
734
1080
|
return { primitive: "wait", label: step.label, script: axResolveScript(step.path ?? "") };
|
|
735
1081
|
case "select-row":
|
|
@@ -816,7 +1162,7 @@ async function driveClickElement(step, run) {
|
|
|
816
1162
|
}
|
|
817
1163
|
return { ok: true };
|
|
818
1164
|
}
|
|
819
|
-
async function drive(recipe, run, aux, budgetMs = DEFAULT_UI_DRIVE_BUDGET_MS) {
|
|
1165
|
+
async function drive(recipe, run, aux, budgetMs = DEFAULT_UI_DRIVE_BUDGET_MS, reachCache = createReachabilityCache()) {
|
|
820
1166
|
const done = [];
|
|
821
1167
|
// The overall-drive WATCHDOG (TRACE1 #487). A drive can outlast the caller's
|
|
822
1168
|
// own timeout on a slow production database (large + Things-Cloud syncing
|
|
@@ -908,7 +1254,7 @@ async function drive(recipe, run, aux, budgetMs = DEFAULT_UI_DRIVE_BUDGET_MS) {
|
|
|
908
1254
|
// full-screen session REFUSES (blocked, zero mutation); a window merely on
|
|
909
1255
|
// another Space is RELOCATED back and disclosed. Menu-only recipes skip this.
|
|
910
1256
|
if (recipe.needsWindowReachability === true) {
|
|
911
|
-
const reach = await ensureWindowReachable(run);
|
|
1257
|
+
const reach = await ensureWindowReachable(run, reachCache);
|
|
912
1258
|
if (!reach.ok)
|
|
913
1259
|
return blockedReachability(reach.verdict);
|
|
914
1260
|
if (reach.relocated) {
|
|
@@ -943,8 +1289,39 @@ async function drive(recipe, run, aux, budgetMs = DEFAULT_UI_DRIVE_BUDGET_MS) {
|
|
|
943
1289
|
}
|
|
944
1290
|
}
|
|
945
1291
|
// 2. Execute the remaining steps in order; a dynamic element is waited-for.
|
|
1292
|
+
//
|
|
1293
|
+
// `dialogShape` is the Repeat dialog's MEASURED structure (RDLG2), set by the
|
|
1294
|
+
// recipe's `probe-dialog-shape` step and consumed by the steps that address a
|
|
1295
|
+
// control the 3.23 redesign moved or replaced. It stays null on every recipe
|
|
1296
|
+
// that never probes (no shape-dependent step), and any step that needs it while
|
|
1297
|
+
// it is null fails closed rather than guessing an index.
|
|
1298
|
+
let dialogShape = null;
|
|
946
1299
|
for (let i = idx; i < recipe.steps.length; i += 1) {
|
|
947
1300
|
let step = recipe.steps[i];
|
|
1301
|
+
// Shape-gated step: the recipe emits BOTH the legacy and the 3.23 drive for a
|
|
1302
|
+
// control whose CLASS changed, and only the matching one runs.
|
|
1303
|
+
if (step.onlyShape !== undefined) {
|
|
1304
|
+
if (dialogShape === null) {
|
|
1305
|
+
const clear = await clearDialog(run);
|
|
1306
|
+
return partial(step.label, SHAPE_UNPROBED, clear);
|
|
1307
|
+
}
|
|
1308
|
+
if (step.onlyShape !== dialogShape)
|
|
1309
|
+
continue;
|
|
1310
|
+
}
|
|
1311
|
+
// Shape-selected paths/values (the +1 index shift the 3.23 "Next:" pop-up
|
|
1312
|
+
// introduced, and the weekday-row base index).
|
|
1313
|
+
if (step.shaped !== undefined) {
|
|
1314
|
+
if (dialogShape === null) {
|
|
1315
|
+
const clear = await clearDialog(run);
|
|
1316
|
+
return partial(step.label, SHAPE_UNPROBED, clear);
|
|
1317
|
+
}
|
|
1318
|
+
const override = step.shaped[dialogShape];
|
|
1319
|
+
if (override === undefined) {
|
|
1320
|
+
const clear = await clearDialog(run);
|
|
1321
|
+
return partial(step.label, `this step has no drive for the "${dialogShape}" Repeat dialog (recipe bug)`, clear);
|
|
1322
|
+
}
|
|
1323
|
+
step = { ...step, ...override };
|
|
1324
|
+
}
|
|
948
1325
|
// Overall-drive watchdog: if the budget is spent, stop at THIS step boundary
|
|
949
1326
|
// (the per-step execFile timeouts keep the boundary close), clear any open
|
|
950
1327
|
// dialog, and return the honest uncertain-outcome timeout (TRACE1 #487).
|
|
@@ -991,6 +1368,28 @@ async function drive(recipe, run, aux, budgetMs = DEFAULT_UI_DRIVE_BUDGET_MS) {
|
|
|
991
1368
|
step = { ...step, path: effective };
|
|
992
1369
|
}
|
|
993
1370
|
const command = commandForStep(step, recipe.targetUuid);
|
|
1371
|
+
if (step.primitive === "probe-dialog-shape") {
|
|
1372
|
+
// MEASURE the dialog (RDLG2) before any shape-dependent control is touched.
|
|
1373
|
+
// A shape we do not recognize refuses the drive with the dialog cleared —
|
|
1374
|
+
// the same fail-closed posture as a canary miss, and for the same reason:
|
|
1375
|
+
// pressing a structural index into an unknown tree is how a GUI driver
|
|
1376
|
+
// writes the wrong rule.
|
|
1377
|
+
const res = await run(command, STEP_TIMEOUT_MS);
|
|
1378
|
+
const verdict = res.stdout.trim();
|
|
1379
|
+
if (!res.ok || (verdict !== "next-popup" && verdict !== "legacy")) {
|
|
1380
|
+
const clear = await clearDialog(run);
|
|
1381
|
+
return partial(step.label, res.ok
|
|
1382
|
+
? 'its first-occurrence row ("Next:") holds neither an occurrence pop-up nor a date ' +
|
|
1383
|
+
"field, so the dialog matched neither known shape — a Things update has redesigned " +
|
|
1384
|
+
"it again; nothing was entered into the rule"
|
|
1385
|
+
: res.timedOut === true
|
|
1386
|
+
? "the dialog-shape probe timed out"
|
|
1387
|
+
: res.stderr.trim() || "the dialog-shape probe failed", clear);
|
|
1388
|
+
}
|
|
1389
|
+
dialogShape = verdict;
|
|
1390
|
+
done.push(`${step.label} (${verdict})`);
|
|
1391
|
+
continue;
|
|
1392
|
+
}
|
|
994
1393
|
if (step.primitive === "select-row" || step.primitive === "select-heading-row") {
|
|
995
1394
|
// Pure-AX row selection with readback verification (UIC4-a / HEADCERT1):
|
|
996
1395
|
// "OK" only when the intended row selected (title readback for a project
|
|
@@ -1147,6 +1546,12 @@ export function createUiVector(config, run = defaultRun, aux = {}) {
|
|
|
1147
1546
|
// in-flight marker (for the signal handler) and, when tracing is on, records a
|
|
1148
1547
|
// start/end pair with timing/outcome (TRACE1 #487).
|
|
1149
1548
|
const tracedRun = tracingRun(run);
|
|
1549
|
+
// Intra-invocation reachability memo (PERF1), shared between the pre-seed gate
|
|
1550
|
+
// (probeReachability) and the in-drive gate (ensureWindowReachable) so a promote
|
|
1551
|
+
// composite does not probe the session — seconds-long on a busy desktop — twice.
|
|
1552
|
+
// The vector is rebuilt per client-open, so this is naturally scoped to one CLI
|
|
1553
|
+
// invocation; the memo's own TTL bounds reuse for a long-lived programmatic client.
|
|
1554
|
+
const reachCache = createReachabilityCache();
|
|
1150
1555
|
return {
|
|
1151
1556
|
id: "ui",
|
|
1152
1557
|
matrix: enabled ? enabledMatrix() : disabledMatrix(),
|
|
@@ -1157,13 +1562,14 @@ export function createUiVector(config, run = defaultRun, aux = {}) {
|
|
|
1157
1562
|
if (invocation.recipe === undefined) {
|
|
1158
1563
|
return refusal("ui invocation carried no recipe (compile bug).");
|
|
1159
1564
|
}
|
|
1160
|
-
return drive(invocation.recipe, tracedRun, aux, budgetMs);
|
|
1565
|
+
return drive(invocation.recipe, tracedRun, aux, budgetMs, reachCache);
|
|
1161
1566
|
},
|
|
1162
1567
|
// Pre-seed gate seam for the promote orchestrators (SESSGATE, #480): probe the
|
|
1163
1568
|
// live session BEFORE they seed a row, so a locked/full-screen session refuses
|
|
1164
1569
|
// with zero mutation. Present regardless of `enabled` (the orchestrator has
|
|
1165
|
-
// already cleared the H-UI-DRIVE ack by the time it consults this).
|
|
1166
|
-
|
|
1570
|
+
// already cleared the H-UI-DRIVE ack by the time it consults this). Populates
|
|
1571
|
+
// the memo the in-drive gate reuses (PERF1).
|
|
1572
|
+
probeReachability: () => reachCache.probe(tracedRun, STEP_TIMEOUT_MS),
|
|
1167
1573
|
};
|
|
1168
1574
|
}
|
|
1169
1575
|
//# sourceMappingURL=ui.js.map
|