things-api 0.19.0 → 0.19.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/deputy/prebuilt/Things API Helper.app/Contents/CodeResources +0 -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/MacOS/things-deputy +0 -0
- package/dist/audit/schema.d.ts +19 -2
- package/dist/audit/schema.js.map +1 -1
- package/dist/capability.d.ts +166 -4
- package/dist/capability.js +303 -13
- package/dist/capability.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts +13 -1
- package/dist/cli/commands/doctor.js +52 -22
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/helpers.js +14 -3
- package/dist/cli/commands/helpers.js.map +1 -1
- package/dist/cli/commands/mcp.js +8 -0
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/repeat-flags.js +34 -2
- package/dist/cli/commands/repeat-flags.js.map +1 -1
- package/dist/cli/commands/setup.js +16 -3
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/ui-state.d.ts +11 -0
- package/dist/cli/commands/ui-state.js +27 -0
- package/dist/cli/commands/ui-state.js.map +1 -0
- package/dist/cli/commands/writes.js +102 -67
- package/dist/cli/commands/writes.js.map +1 -1
- package/dist/cli/help.js +2 -0
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/interrupt.d.ts +54 -6
- package/dist/cli/interrupt.js +78 -12
- package/dist/cli/interrupt.js.map +1 -1
- package/dist/cli/main.js +10 -5
- package/dist/cli/main.js.map +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/contracts.d.ts +4 -4
- package/dist/contracts.js +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/deputy/install.d.ts +15 -1
- package/dist/deputy/install.js +46 -6
- package/dist/deputy/install.js.map +1 -1
- package/dist/deputy/osa.d.ts +42 -2
- package/dist/deputy/osa.js +79 -5
- package/dist/deputy/osa.js.map +1 -1
- package/dist/deputy/routing.d.ts +34 -0
- package/dist/deputy/routing.js +95 -0
- package/dist/deputy/routing.js.map +1 -1
- package/dist/deputy/wake.d.ts +61 -0
- package/dist/deputy/wake.js +149 -0
- package/dist/deputy/wake.js.map +1 -0
- package/dist/diagnose.d.ts +6 -4
- package/dist/diagnose.js +20 -4
- package/dist/diagnose.js.map +1 -1
- package/dist/direct-setup.d.ts +31 -5
- package/dist/direct-setup.js +200 -26
- package/dist/direct-setup.js.map +1 -1
- package/dist/host-access.d.ts +12 -0
- package/dist/host-access.js +12 -0
- package/dist/host-access.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +23 -9
- package/dist/mcp/server.js.map +1 -1
- package/dist/op-result.js +8 -1
- package/dist/op-result.js.map +1 -1
- package/dist/session-grant.js +16 -1
- package/dist/session-grant.js.map +1 -1
- package/dist/surface-copy.d.ts +11 -3
- package/dist/surface-copy.js +11 -3
- package/dist/surface-copy.js.map +1 -1
- package/dist/ui-state.d.ts +46 -0
- package/dist/ui-state.js +82 -0
- package/dist/ui-state.js.map +1 -0
- package/dist/wizard.js +8 -1
- package/dist/wizard.js.map +1 -1
- package/dist/write/accessibility-probe.js +4 -1
- package/dist/write/accessibility-probe.js.map +1 -1
- package/dist/write/automation-probe.js +7 -1
- package/dist/write/automation-probe.js.map +1 -1
- package/dist/write/availability.d.ts +0 -15
- package/dist/write/availability.js +7 -62
- package/dist/write/availability.js.map +1 -1
- package/dist/write/batch.js +17 -6
- package/dist/write/batch.js.map +1 -1
- package/dist/write/commands.d.ts +10 -6
- package/dist/write/commands.js +120 -31
- package/dist/write/commands.js.map +1 -1
- package/dist/write/failure-hints.d.ts +36 -7
- package/dist/write/failure-hints.js +33 -14
- package/dist/write/failure-hints.js.map +1 -1
- package/dist/write/field-limits.d.ts +100 -0
- package/dist/write/field-limits.js +144 -0
- package/dist/write/field-limits.js.map +1 -0
- package/dist/write/guards.js +3 -2
- package/dist/write/guards.js.map +1 -1
- package/dist/write/move.js +9 -12
- package/dist/write/move.js.map +1 -1
- package/dist/write/operations.d.ts +15 -8
- package/dist/write/operations.js +9 -0
- package/dist/write/operations.js.map +1 -1
- package/dist/write/opid.d.ts +67 -23
- package/dist/write/opid.js +141 -19
- package/dist/write/opid.js.map +1 -1
- package/dist/write/param-schema.d.ts +17 -44
- package/dist/write/param-schema.js +74 -7
- package/dist/write/param-schema.js.map +1 -1
- package/dist/write/pipeline.d.ts +38 -7
- package/dist/write/pipeline.js +134 -18
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/pre-state.d.ts +30 -16
- package/dist/write/pre-state.js +88 -20
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/promote-clone.d.ts +26 -0
- package/dist/write/promote-clone.js +333 -41
- package/dist/write/promote-clone.js.map +1 -1
- package/dist/write/repeat-collateral.d.ts +31 -0
- package/dist/write/repeat-collateral.js +153 -0
- package/dist/write/repeat-collateral.js.map +1 -0
- package/dist/write/resolution-timestamps.js +25 -2
- package/dist/write/resolution-timestamps.js.map +1 -1
- package/dist/write/template-mutation.js +163 -16
- package/dist/write/template-mutation.js.map +1 -1
- package/dist/write/vectors/applescript.js +3 -10
- package/dist/write/vectors/applescript.js.map +1 -1
- package/dist/write/vectors/simulator.js +8 -2
- package/dist/write/vectors/simulator.js.map +1 -1
- package/dist/write/vectors/types.d.ts +236 -4
- package/dist/write/vectors/ui-certification.js +63 -13
- package/dist/write/vectors/ui-certification.js.map +1 -1
- package/dist/write/vectors/ui-chord.d.ts +144 -0
- package/dist/write/vectors/ui-chord.js +428 -0
- package/dist/write/vectors/ui-chord.js.map +1 -0
- package/dist/write/vectors/ui-drag.d.ts +8 -4
- package/dist/write/vectors/ui-drag.js +2 -0
- package/dist/write/vectors/ui-drag.js.map +1 -1
- package/dist/write/vectors/ui-recipes.d.ts +51 -3
- package/dist/write/vectors/ui-recipes.js +452 -44
- package/dist/write/vectors/ui-recipes.js.map +1 -1
- package/dist/write/vectors/ui-state.d.ts +157 -0
- package/dist/write/vectors/ui-state.js +237 -0
- package/dist/write/vectors/ui-state.js.map +1 -0
- package/dist/write/vectors/ui.d.ts +386 -13
- package/dist/write/vectors/ui.js +1543 -124
- package/dist/write/vectors/ui.js.map +1 -1
- package/dist/write/vectors/url-scheme.js +3 -0
- package/dist/write/vectors/url-scheme.js.map +1 -1
- package/dist/write/verify/delta.d.ts +41 -0
- package/dist/write/verify/delta.js +36 -2
- package/dist/write/verify/delta.js.map +1 -1
- package/dist/write/verify/poller.d.ts +4 -1
- package/dist/write/verify/poller.js +14 -0
- package/dist/write/verify/poller.js.map +1 -1
- package/package.json +1 -1
- package/schema/envelope.schema.json +2 -1
- package/skills/things-cli/SKILL.md +2 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ThingsApiConfig } from "../../config.ts";
|
|
2
2
|
import { type UiDriveAux } from "./ui-drag.ts";
|
|
3
|
-
import
|
|
3
|
+
import { type UiSheetKind, type UiState } from "./ui-state.ts";
|
|
4
|
+
import type { UiClearOutcome, UiPrimitive, UiStep, WriteVector } from "./types.ts";
|
|
4
5
|
/**
|
|
5
6
|
* Command-level primitives. Extends the recipe `UiPrimitive` set with the
|
|
6
7
|
* INTERNAL sub-steps composite recipe steps decompose into: a `click-element`
|
|
@@ -10,7 +11,16 @@ import type { UiPrimitive, UiStep, WriteVector } from "./types.ts";
|
|
|
10
11
|
* every subprocess call behind the injectable `run` seam makes the
|
|
11
12
|
* orchestration unit-testable without a GUI.
|
|
12
13
|
*/
|
|
13
|
-
export type UiCommandPrimitive = UiPrimitive | "resolve-frame" | "click-point" | "sidebar-snapshot" | "sidebar-scroll" | "sidebar-drag" | "sidebar-held-drag"
|
|
14
|
+
export type UiCommandPrimitive = UiPrimitive | "resolve-frame" | "click-point" | "sidebar-snapshot" | "sidebar-scroll" | "sidebar-drag" | "sidebar-held-drag"
|
|
15
|
+
/** One modifier-bearing key event pair posted straight at the Things process (ui-chord.ts). */
|
|
16
|
+
| "chord-post"
|
|
17
|
+
/**
|
|
18
|
+
* The cleanup ladder's first rung: press the open dialog's own Cancel button
|
|
19
|
+
* (issue #620). Its own primitive rather than a `press` so a recipe's
|
|
20
|
+
* actuations and the cleanup's are never confused — in a trace, in a test, or
|
|
21
|
+
* in the completed-steps trail.
|
|
22
|
+
*/
|
|
23
|
+
| "dismiss-dialog";
|
|
14
24
|
/** A single primitive dispatch — one stable shape per primitive. */
|
|
15
25
|
export interface UiCommand {
|
|
16
26
|
primitive: UiCommandPrimitive;
|
|
@@ -36,23 +46,220 @@ export interface UiRunResult {
|
|
|
36
46
|
* (CLAUDE.md safety rails — the production app is never a valid target).
|
|
37
47
|
*/
|
|
38
48
|
export type UiRunner = (command: UiCommand, timeoutMs: number) => Promise<UiRunResult>;
|
|
49
|
+
/**
|
|
50
|
+
* The IN-SCRIPT half of the per-step focus guard (issue #620).
|
|
51
|
+
*
|
|
52
|
+
* A synthetic keystroke is not addressed at an element — System Events hands it
|
|
53
|
+
* to whatever application owns the screen at that instant. So every script that
|
|
54
|
+
* types re-asserts, in the same osascript hop that will do the typing, that
|
|
55
|
+
* Things is still frontmost; the drive-level census (see {@link guardedRun})
|
|
56
|
+
* runs a moment earlier and cannot close the last few milliseconds. The
|
|
57
|
+
* assertion FAILS CLOSED and names the application that owns the screen
|
|
58
|
+
* instead — never the contents of its window.
|
|
59
|
+
*
|
|
60
|
+
* This is the cheapest possible check: one System Events property read, no
|
|
61
|
+
* sleeps, no polling (UI-automation determinism doctrine).
|
|
62
|
+
*/
|
|
63
|
+
export declare const AX_FOCUS_GUARD_HANDLERS = "on fgFrontApp()\n\tset frontName to \"\"\n\ttry\n\t\ttell application \"System Events\" to set frontName to (name of first application process whose frontmost is true) as text\n\tend try\n\treturn frontName\nend fgFrontApp\n\non fgAssertFront(what)\n\tset f to my fgFrontApp()\n\tif f is \"Things3\" then return true\n\tif f is \"\" then\n\t\terror \"refused to \" & what & \": the frontmost application could not be read, so there is no proof the keystrokes would reach Things \u2014 nothing was typed\"\n\tend if\n\terror \"refused to \" & what & \": \" & f & \" is frontmost, not Things \u2014 a keystroke goes to whatever owns the screen, so nothing was typed\"\nend fgAssertFront";
|
|
39
64
|
/** resolve-element: does the element exist right now? Returns "true"/"false". */
|
|
40
65
|
export declare function axResolveScript(path: string): string;
|
|
41
66
|
/** press: AXPress the element. */
|
|
42
67
|
export declare function axPressScript(path: string): string;
|
|
43
68
|
/**
|
|
44
69
|
* set-field-value: enter a value into the dialog's numeric text field (interval,
|
|
45
|
-
* ends-count, start-days-earlier)
|
|
46
|
-
* value, and Tabs to commit — because
|
|
47
|
-
*
|
|
48
|
-
* number (the field shows "5" but the
|
|
49
|
-
* exactly like `set value` on a pop-up,
|
|
50
|
-
* case used the default interval 1).
|
|
51
|
-
* needs; Tab (not Return, which would
|
|
52
|
-
*
|
|
53
|
-
* reveal/activate preamble puts Things
|
|
70
|
+
* ends-count, start-days-earlier) — and into the Move… picker's filter field. It
|
|
71
|
+
* FOCUSES the field, TYPES the value, and Tabs to commit — because
|
|
72
|
+
* `set value of <field>` writes the field's displayed text WITHOUT firing the
|
|
73
|
+
* edit, so the app's binding keeps the old number (the field shows "5" but the
|
|
74
|
+
* rule stays interval 1 — a silent no-op exactly like `set value` on a pop-up,
|
|
75
|
+
* UIC6; it went unnoticed while every base case used the default interval 1).
|
|
76
|
+
* Real keystrokes fire the change the binding needs; Tab (not Return, which would
|
|
77
|
+
* fire the default OK button) commits and moves focus. Foreground-bound
|
|
78
|
+
* (keystrokes reach the frontmost app) — the reveal/activate preamble puts Things
|
|
79
|
+
* there. One stable command shape.
|
|
80
|
+
*
|
|
81
|
+
* NO SELECT-ALL KEYSTROKE (BEEP1, 2026-08-25, docs/lab/beep1-numeric-field-beep.md).
|
|
82
|
+
* The primitive used to send ⌘A before typing, and that ONE keystroke was the
|
|
83
|
+
* audible macOS alert beep every numeric-field drive fired on the live host:
|
|
84
|
+
* Things' `Edit ▸ Select All` menu item exists and is DISABLED while the Repeat
|
|
85
|
+
* sheet is up, AppKit dispatches ⌘A as a menu key equivalent FIRST, the disabled
|
|
86
|
+
* item swallows it, nothing handles it → NSBeep. It is a menu-dispatch fact, not
|
|
87
|
+
* a focus race: the beep survives a verified first responder and a 1.5 s settle,
|
|
88
|
+
* while Tab and the digits themselves are silent. ⌘A was also REDUNDANT —
|
|
89
|
+
* `set focused of tf to true` installs the field editor with the ENTIRE content
|
|
90
|
+
* selected (measured: `AXSelectedTextRange` length goes 0 → the full value
|
|
91
|
+
* length, on both a 1- and a 2-character value), so typing replaces the old value
|
|
92
|
+
* outright, including the shrinking case (12 → 3) that a stale caret would have
|
|
93
|
+
* corrupted into "123". Dropping the keystroke is therefore silent AND correct on
|
|
94
|
+
* all three fields.
|
|
95
|
+
*
|
|
96
|
+
* CLOSED-LOOP (determinism doctrine): type, Tab-commit, then READ THE FIELD BACK
|
|
97
|
+
* and retry if it did not hold — the interval field, when it is the first numeric
|
|
98
|
+
* field after a frequency/type switch, races the dialog's group re-layout and
|
|
99
|
+
* reverts to 1 (UIC7, oddities §8l). Re-focus + re-type after a settle lands it
|
|
100
|
+
* once the re-layout has finished, and the re-focus re-selects the whole value,
|
|
101
|
+
* so a retry starts from a clean field without ⌘A. Fail-closed (an `error`, i.e.
|
|
102
|
+
* a transport failure the pipeline re-verifies) if it never holds — the
|
|
103
|
+
* create/reschedule delta's rule assertion is the final DB-level authority.
|
|
104
|
+
*
|
|
105
|
+
* READ-BACK FIRST (issue #620 item 7): a field that ALREADY holds the requested
|
|
106
|
+
* value is left alone and the script returns {@link OK_ALREADY} — the whole
|
|
107
|
+
* keystroke class disappears for the defaults, which is most drives (the field
|
|
108
|
+
* incident died typing interval `1` into a field already showing `1`). The skip
|
|
109
|
+
* is proven by TWO reads a settle apart, because the one way a matching value
|
|
110
|
+
* can go stale is the UIC7 re-layout revert, which lands within that window; and
|
|
111
|
+
* whatever this decides, the pre-commit audit ({@link axAuditDialogScript})
|
|
112
|
+
* re-reads every control through its own address before the OK press, so a
|
|
113
|
+
* wrongly-skipped field cannot commit.
|
|
54
114
|
*/
|
|
55
115
|
export declare function axSetValueScript(path: string, value: string, attempts?: number): string;
|
|
116
|
+
/**
|
|
117
|
+
* What a typing primitive returns when it typed NOTHING because the field
|
|
118
|
+
* already held the requested value (issue #620 item 7). Distinct from "OK" so
|
|
119
|
+
* the drive can disclose the skip — and so a lab cell can assert that no
|
|
120
|
+
* keystroke hop fired.
|
|
121
|
+
*/
|
|
122
|
+
export declare const OK_ALREADY = "OK-ALREADY";
|
|
123
|
+
/**
|
|
124
|
+
* set-group-number: drive ONE of the Repeat dialog's two numeric fields —
|
|
125
|
+
* the cadence INTERVAL or the ENDS-AFTER COUNT — addressed by the LABEL ROW it
|
|
126
|
+
* sits on (HXPC1, docs/lab/hxpc1-picker-assert.md §A; hardened by CGRD1,
|
|
127
|
+
* docs/lab/cgrd1-precommit-audit.md §A).
|
|
128
|
+
*
|
|
129
|
+
* Both fields used to be spelled `text field 1 of group 1`, which is the same
|
|
130
|
+
* control at different moments. Measured on Things 3.23 (build 32300036):
|
|
131
|
+
*
|
|
132
|
+
* Ends: never → group text fields = 1 · tf1 = interval @[311,283]
|
|
133
|
+
* Ends: after N → group text fields = 2 · tf1 = COUNT @[402,372]
|
|
134
|
+
* tf2 = interval @[311,283]
|
|
135
|
+
*
|
|
136
|
+
* i.e. selecting the "after" bound INSERTS the count ahead of the interval. The
|
|
137
|
+
* create path got away with it by driving the interval while it was still the
|
|
138
|
+
* sole field, but a RESCHEDULE opens the dialog pre-populated: a rule that
|
|
139
|
+
* already ends after N presents both fields from the first step, so the interval
|
|
140
|
+
* drive wrote the requested interval into the count field, the count drive then
|
|
141
|
+
* overwrote it, and the interval silently never changed.
|
|
142
|
+
*
|
|
143
|
+
* The addresses are the {@link AX_CADENCE_HANDLERS} laws — POSITIVE label-row
|
|
144
|
+
* matches wherever the app offers a label to match, a uniqueness check where it
|
|
145
|
+
* does not, and a fail-closed refusal naming the whole numeric-field inventory
|
|
146
|
+
* otherwise. The write itself is the {@link axSetValueScript} closed loop —
|
|
147
|
+
* focus, type, Tab-commit, read back, bounded retries — and, like it, sends NO
|
|
148
|
+
* select-all keystroke: the ⌘A that used to open it is the macOS alert beep
|
|
149
|
+
* (BEEP1, docs/lab/beep1-numeric-field-beep.md), and focusing the field already
|
|
150
|
+
* selects its whole content.
|
|
151
|
+
*
|
|
152
|
+
* The read-back this loop performs is SELF-REFERENTIAL by construction — it
|
|
153
|
+
* re-reads the field it addressed, so it can only prove the keystrokes landed
|
|
154
|
+
* where they were aimed, never that they were aimed at the right control. The
|
|
155
|
+
* PRE-COMMIT AUDIT ({@link axAuditDialogScript}) is what closes that: it re-reads
|
|
156
|
+
* every control through these same handlers just before the OK press.
|
|
157
|
+
*/
|
|
158
|
+
export declare function axSetGroupNumberScript(groupPath: string, target: "interval" | "ends-count", value: string, attempts?: number, rowTolerance?: number): string;
|
|
159
|
+
/**
|
|
160
|
+
* set-row-field: drive a Repeat-dialog text field addressed by the pinned English
|
|
161
|
+
* LABEL sharing its row — the same discrimination law as
|
|
162
|
+
* {@link axSetGroupNumberScript}, applied to a field that lives on the dialog
|
|
163
|
+
* SHELL rather than in the cadence group.
|
|
164
|
+
*
|
|
165
|
+
* Its one caller is the "and start [N] days earlier" offset the "Add deadlines"
|
|
166
|
+
* checkbox reveals, which shipped as `text field 1` of the shell. That address
|
|
167
|
+
* was the HXPC1 error class exactly: a value-bearing text field picked by index
|
|
168
|
+
* out of a STATE-DEPENDENT tree (the field does not exist at all until the
|
|
169
|
+
* checkbox is ticked), verified only by re-reading the same index it wrote. It
|
|
170
|
+
* happened to be right on 3.23 — measured, the shell carries 0 direct text fields
|
|
171
|
+
* with deadlines off and exactly 1 with them on, whether or not reminders are also
|
|
172
|
+
* on (CGRD1 §B census) — but nothing in the address said so, and an AX tree is an
|
|
173
|
+
* undocumented private surface that may add a second field in any release.
|
|
174
|
+
*
|
|
175
|
+
* The label anchor is `days earlier` (y=413 against the field's y=409, CGRD1 §B).
|
|
176
|
+
* A missing label, or anything other than exactly one field on its row, FAILS
|
|
177
|
+
* CLOSED naming the shell's whole text-field inventory. The write is the
|
|
178
|
+
* {@link axSetValueScript} closed loop.
|
|
179
|
+
*/
|
|
180
|
+
export declare function axSetRowFieldScript(containerPath: string, rowLabel: string, value: string, attempts?: number, rowTolerance?: number): string;
|
|
181
|
+
/**
|
|
182
|
+
* ONE control the PRE-COMMIT DIALOG AUDIT re-reads, in the form the generator
|
|
183
|
+
* needs: a resolved element path (the shell / dialog-shape disjunctions are
|
|
184
|
+
* settled by the driver before the script is built) plus the value(s) the drive
|
|
185
|
+
* intended for it.
|
|
186
|
+
*/
|
|
187
|
+
export interface AuditScriptControl {
|
|
188
|
+
/** Human name of the control, as the mismatch report should say it. */
|
|
189
|
+
label: string;
|
|
190
|
+
kind: "popup" | "checkbox" | "group-number" | "row-field" | "weekdays" | "occurrence-popup";
|
|
191
|
+
/** popup / checkbox / occurrence-popup: the resolved element path. */
|
|
192
|
+
path?: string;
|
|
193
|
+
/** group-number: which of the cadence group's numeric fields. */
|
|
194
|
+
numberTarget?: "interval" | "ends-count";
|
|
195
|
+
/** row-field: the pinned English label sharing the field's row. */
|
|
196
|
+
rowLabel?: string;
|
|
197
|
+
/** weekdays: the group pop-up index of the first weekday row (shape-selected). */
|
|
198
|
+
weekdayBase?: number;
|
|
199
|
+
/** The accepted observed values — ANY one satisfies (the singular/plural pair). */
|
|
200
|
+
expected: string[];
|
|
201
|
+
/** How the intended value should READ in the report ("checked", not "1"). */
|
|
202
|
+
expectedLabel?: string;
|
|
203
|
+
}
|
|
204
|
+
/** The resolved audit the {@link axAuditDialogScript} generator compiles. */
|
|
205
|
+
export interface AuditScriptSpec {
|
|
206
|
+
/** The resolved dialog shell (attached sheet or detached editor window). */
|
|
207
|
+
shell: string;
|
|
208
|
+
/** The resolved cadence group inside that shell. */
|
|
209
|
+
group: string;
|
|
210
|
+
controls: AuditScriptControl[];
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* audit-dialog: RE-READ EVERY CONTROL THIS DRIVE SET, through each control's own
|
|
214
|
+
* discriminated address, and refuse to commit if any one of them does not hold
|
|
215
|
+
* the value the drive intended.
|
|
216
|
+
*
|
|
217
|
+
* This exists because a per-step read-back is SELF-REFERENTIAL. Every setter here
|
|
218
|
+
* confirms its write by re-reading the element it addressed, so it proves the
|
|
219
|
+
* keystrokes landed where they were aimed — and nothing else. The #589 wrong-field
|
|
220
|
+
* write reported OK for exactly that reason: the interval drive typed into the
|
|
221
|
+
* ends-count field, then read the ends-count field back and found its own number
|
|
222
|
+
* sitting there. The address was wrong; a read-back through the same address
|
|
223
|
+
* cannot see that.
|
|
224
|
+
*
|
|
225
|
+
* The audit is the outside view. It is assembled from the recipe's OWN step list
|
|
226
|
+
* (so no control the recipe drives can be left out of the audit by omission) and
|
|
227
|
+
* runs as the last step before the OK press, comparing the dialog's complete
|
|
228
|
+
* intended state against what the dialog actually shows: frequency, the
|
|
229
|
+
* after-completion cadence unit, interval, ends bound and its count, the
|
|
230
|
+
* deadline/reminder checkboxes, the start-days-earlier offset, the weekday set,
|
|
231
|
+
* the monthly/yearly anchor pop-ups and the 3.23 first-occurrence pop-up. A
|
|
232
|
+
* mismatch is an `error` naming EVERY differing control with both values, which
|
|
233
|
+
* aborts the drive fail-closed BEFORE the commit and runs the standard clean-abort
|
|
234
|
+
* path — nothing reaches the database.
|
|
235
|
+
*
|
|
236
|
+
* Deterministic throughout: the cadence group is settled on its own shape
|
|
237
|
+
* signature (the BEEP1 two-agreeing-reads gate), never on a sleep, and every field
|
|
238
|
+
* is found by its label row rather than by index ({@link AX_CADENCE_HANDLERS}).
|
|
239
|
+
*
|
|
240
|
+
* The dialog's three `AXDateTimeArea` controls are audited separately — their
|
|
241
|
+
* values are NSDates no System Events read can reach, so they ride
|
|
242
|
+
* {@link axAuditDateAreasScript} through the same ObjC bridge that writes them.
|
|
243
|
+
*/
|
|
244
|
+
export declare function axAuditDialogScript(spec: AuditScriptSpec, rowTolerance?: number): string;
|
|
245
|
+
/** ONE date/time area the pre-commit audit re-reads through the ObjC bridge. */
|
|
246
|
+
export interface AuditDateArea {
|
|
247
|
+
/** Human name of the control, as the mismatch report should say it. */
|
|
248
|
+
label: string;
|
|
249
|
+
target: "next" | "ends" | "reminder";
|
|
250
|
+
/** The spec the drive wrote: `date:YYYY-MM-DD` or `time:HH:mm`. */
|
|
251
|
+
spec: string;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* The pre-commit audit's DATE-AREA leg. The dialog's first-occurrence, ends-on and
|
|
255
|
+
* reminder controls are `AXDateTimeArea`s whose value is an NSDate — unreachable
|
|
256
|
+
* from System Events — so they are re-read through the SAME ObjC bridge, the same
|
|
257
|
+
* shell-scoped walk and the SAME deterministic `pick` discriminator that
|
|
258
|
+
* {@link axSetDateTimeScript} writes them through. A control the audit cannot find,
|
|
259
|
+
* or one holding a different date/time, throws naming every area the dialog does
|
|
260
|
+
* present (y-position + time-of-day), so the drive aborts before the OK press.
|
|
261
|
+
*/
|
|
262
|
+
export declare function axAuditDateAreasScript(areas: AuditDateArea[]): string;
|
|
56
263
|
/**
|
|
57
264
|
* ensure-checkbox: converge a dialog checkbox to a target state through a
|
|
58
265
|
* DETERMINISTIC CLOSED LOOP (RRD1, determinism doctrine) — never a blind toggle.
|
|
@@ -140,6 +347,32 @@ export declare function axProbeDialogShapeScript(groupPath: string, rowTolerance
|
|
|
140
347
|
* (the fail-closed read-back the ANCH2/YANCH1 date drives established).
|
|
141
348
|
*/
|
|
142
349
|
export declare function axSelectNextOccurrenceScript(popupPath: string, isoDate: string, maxLevels?: number): string;
|
|
350
|
+
/**
|
|
351
|
+
* settle-occurrences: let the 3.23 `Next:` pop-up ABSORB the rule change the
|
|
352
|
+
* preceding steps made, before the drive touches the dialog again (NEXTPOP1).
|
|
353
|
+
*
|
|
354
|
+
* MEASURED (golden-v4 / Things 3.23, `research-nextpop1.sh` DIAG3/DIAG4): the
|
|
355
|
+
* dialog recomputes the first-occurrence pop-up — its displayed value AND the
|
|
356
|
+
* menu of occurrences behind it — ASYNCHRONOUSLY. After the yearly anchor was
|
|
357
|
+
* moved from Aug 6 to Aug 20 the control flipped at **t+0.4s** with nothing else
|
|
358
|
+
* driven; when the very next step (the "Add deadlines" checkbox) was pressed
|
|
359
|
+
* inside that window instead, the control NEVER caught up — it still read
|
|
360
|
+
* `Thu, Aug 6, 2026`, and its menu still enumerated the Aug-6 series, six
|
|
361
|
+
* seconds later. A cancelled recompute does not retry.
|
|
362
|
+
*
|
|
363
|
+
* That is what made every deadlined monthly/yearly promote fail closed on 3.23:
|
|
364
|
+
* the anchor drive is followed immediately by the deadline controls, so by the
|
|
365
|
+
* time `select-next-occurrence` opened the menu it was the SEED's series, and
|
|
366
|
+
* the requested date — the rule's own first due date — was genuinely not in it
|
|
367
|
+
* (VMRES1 §4.3, reproduced and explained in NEXTPOP1).
|
|
368
|
+
*
|
|
369
|
+
* The wait is closed-loop in the direction that matters: it exits the moment the
|
|
370
|
+
* control MOVES, which is the case that needs waiting for. When the rule change
|
|
371
|
+
* did not move the first occurrence there is nothing to observe, so the budget
|
|
372
|
+
* bounds it — deliberately over-cautious, since the cost is a fraction of a
|
|
373
|
+
* second and the alternative is a series that starts on the wrong date.
|
|
374
|
+
*/
|
|
375
|
+
export declare function axSettleOccurrencesScript(popupPath: string, budgetMs?: number, pollMs?: number): string;
|
|
143
376
|
/**
|
|
144
377
|
* converge-weekdays: drive the weekly dialog's weekday ROWS onto an exact target
|
|
145
378
|
* set through a deterministic closed loop (RDLG2 — the RRD1 fix).
|
|
@@ -228,10 +461,103 @@ export declare function axSelectHeadingRowScript(tablePath: string, ordinal: num
|
|
|
228
461
|
export declare function axAssertEligibleScript(targetUuid: string, menuItemPath: string): string;
|
|
229
462
|
/** activate: foreground Things (the fallback preamble step). */
|
|
230
463
|
export declare function axActivateScript(): string;
|
|
231
|
-
/**
|
|
464
|
+
/**
|
|
465
|
+
* key: a space-separated keystroke spec (e.g. "down down return").
|
|
466
|
+
*
|
|
467
|
+
* Frontmost-guarded in-script (issue #620): `key code`/`keystroke` reach
|
|
468
|
+
* whatever application owns the screen, so the script refuses — naming that
|
|
469
|
+
* application — rather than firing keys into someone else's window.
|
|
470
|
+
*/
|
|
232
471
|
export declare function axKeyScript(keys: string): string;
|
|
233
|
-
/**
|
|
472
|
+
/**
|
|
473
|
+
* type-text: send literal text to whatever control holds focus (HXPC1). The
|
|
474
|
+
* Move… picker focuses its own filter field the instant it opens, and that field
|
|
475
|
+
* is NOT addressable as a direct child of the picker window — so there is no
|
|
476
|
+
* element to hand `set-value`, whose select-all + Tab commit would be wrong for a
|
|
477
|
+
* search field regardless (a popover filter has no next key view for Tab to move
|
|
478
|
+
* to). Unlike {@link axKeyScript}, which splits its spec on whitespace and would
|
|
479
|
+
* drop the spaces out of a multi-word project title, this sends the string as
|
|
480
|
+
* ONE keystroke. It is deliberately not self-verifying: the `click-picker-row`
|
|
481
|
+
* step that follows resolves the destination row by name and fails closed when
|
|
482
|
+
* the filter did not produce it, so a keystroke that landed elsewhere can never
|
|
483
|
+
* be committed. One stable command shape.
|
|
484
|
+
*/
|
|
485
|
+
export declare function axTypeTextScript(text: string): string;
|
|
486
|
+
/**
|
|
487
|
+
* resolve-frame for a control nested inside a CONTENT-TABLE ROW: walk the
|
|
488
|
+
* table's rows → cells → cell children and return the frame of the one whose
|
|
489
|
+
* `AXDescription` equals `description` (HXPC1, docs/lab/hxpc1-picker-assert.md
|
|
490
|
+
* §B0). Same "x y w h" contract as {@link axFrameScript}.
|
|
491
|
+
*
|
|
492
|
+
* This exists because the heading row's `…` button — the only content-row
|
|
493
|
+
* control that carries its own title (`"More. <heading title>"`, the HEADXPROJ
|
|
494
|
+
* enabler) — sits at `UI element N of cell 1 of row M of the table`, and
|
|
495
|
+
* `first UI element of <table> whose description is …` searches the table's
|
|
496
|
+
* DIRECT children only. Those are the rows, which carry no description, so the
|
|
497
|
+
* shipped one-level spelling matched nothing and the ellipsis drives
|
|
498
|
+
* (`project.move-heading-to-project`, `project.dissolve-heading`) died at their
|
|
499
|
+
* own frame resolution before any click — measured on Things 3.23 against a
|
|
500
|
+
* heading whose button the raw Accessibility API resolves at the same instant.
|
|
501
|
+
* The row/cell indices are never guessed: every row is walked and the match is
|
|
502
|
+
* exact, so a heading whose title changed under us fails closed by name.
|
|
503
|
+
*/
|
|
504
|
+
export declare function axRowCellFrameScript(tablePath: string, description: string): string;
|
|
505
|
+
/**
|
|
506
|
+
* resolve-frame for the Move… picker ROW carrying an exact project title — the
|
|
507
|
+
* step that replaced the recipe's blind Return (HXPC1,
|
|
508
|
+
* docs/lab/hxpc1-picker-assert.md §B).
|
|
509
|
+
*
|
|
510
|
+
* The picker exposes no `AXSelected` / `AXFocused` / `AXHighlighted` on any row
|
|
511
|
+
* (measured — only its filter field is focused), so there is nothing to read
|
|
512
|
+
* back from a keyboard commit and no way to assert what Return would take. What
|
|
513
|
+
* it does expose is one `AXUnknown` per row whose `AXDescription` IS the project
|
|
514
|
+
* title, and — whenever the filter holds text — a trailing
|
|
515
|
+
* `New Project "<typed text>"` row that CREATES a project when committed. That
|
|
516
|
+
* row is what the blind Return took whenever the destination was missing from
|
|
517
|
+
* the picker, which an ordinary database-resolved destination reaches: a
|
|
518
|
+
* COMPLETED or CANCELED project appears nowhere in the picker, so the drive
|
|
519
|
+
* minted a second project of the same title and moved the heading into it
|
|
520
|
+
* (measured 3.23: projects 14 → 15, heading re-parented to the new row).
|
|
521
|
+
*
|
|
522
|
+
* So the commit is addressed instead of guessed. The script requires:
|
|
523
|
+
* - the picker to be the window it claims (its `AXIdentifier` begins
|
|
524
|
+
* `MovePopUpDialog-`) — a positive identity check, so a different detached
|
|
525
|
+
* window can never be clicked into;
|
|
526
|
+
* - EXACTLY ONE row whose description equals the destination title (the
|
|
527
|
+
* New-Project row's description is the quoted form, so an exact match cannot
|
|
528
|
+
* hit it);
|
|
529
|
+
* - that row's centre to lie inside the picker's own scroll area — the CNCAC1
|
|
530
|
+
* off-screen hazard, where a row scrolled past the fold still resolves a
|
|
531
|
+
* frame and a click at it lands on the desktop.
|
|
532
|
+
* Any miss FAILS CLOSED naming the destination and listing every row the picker
|
|
533
|
+
* actually offered, so the caller learns what the app was willing to move to.
|
|
534
|
+
*/
|
|
535
|
+
export declare function axPickerRowFrameScript(pickerPath: string, title: string): string;
|
|
536
|
+
/**
|
|
537
|
+
* The abort keystroke, sent ONLY from the audited cleanup ladder (issue #620)
|
|
538
|
+
* and only once that ladder has proven Things owns the screen. It is scoped to
|
|
539
|
+
* the Things process for readability, but scoping is not what makes it safe —
|
|
540
|
+
* a synthetic key goes to whatever is frontmost, which is why the script
|
|
541
|
+
* carries the same in-script frontmost assertion every other keystroke does.
|
|
542
|
+
*/
|
|
234
543
|
export declare function axAbortScript(): string;
|
|
544
|
+
/**
|
|
545
|
+
* Dismiss the open dialog by PRESSING ITS OWN CANCEL BUTTON (issue #620).
|
|
546
|
+
*
|
|
547
|
+
* Preferred over Escape wherever it works, for two independent reasons: an
|
|
548
|
+
* AXPress is addressed at an ELEMENT, so it cannot leak into another
|
|
549
|
+
* application the way a keystroke can, and it works while Things is in the
|
|
550
|
+
* BACKGROUND — the cleanup never has to steal the user's focus to undo its own
|
|
551
|
+
* half-finished dialog. The button is addressed by its pinned English title,
|
|
552
|
+
* exactly like every other selector in this vector, and the dialog shell is
|
|
553
|
+
* resolved the same two ways the census resolves it (attached sheet, or the
|
|
554
|
+
* detached editor window Things presents when it is not frontmost).
|
|
555
|
+
*
|
|
556
|
+
* Returns "OK" after pressing, or a diagnostic ("NO-DIALOG" / "NO-CANCEL") the
|
|
557
|
+
* ladder falls through on — it never claims a dismissal; the caller re-reads
|
|
558
|
+
* the census to decide that.
|
|
559
|
+
*/
|
|
560
|
+
export declare function axCancelDialogScript(): string;
|
|
235
561
|
/**
|
|
236
562
|
* The PROVEN app-level clearance / relocation maneuver (SESSGATE, #480, live-host
|
|
237
563
|
* recovery): close the front Things window — which takes an attached modal sheet
|
|
@@ -257,6 +583,30 @@ export declare function axCloseReopenActivateScript(): string;
|
|
|
257
583
|
* command shape.
|
|
258
584
|
*/
|
|
259
585
|
export declare function axSheetOpenScript(): string;
|
|
586
|
+
/**
|
|
587
|
+
* The outcome of clearing a half-open dialog after a failed drive:
|
|
588
|
+
* - "none" — the census found no dialog open (nothing to clear);
|
|
589
|
+
* - "dismissed" — it is gone, and a fresh census CONFIRMED that;
|
|
590
|
+
* - "cleared-blind" — the session was AX-blind (locked / off-Space), so no
|
|
591
|
+
* census and no keystroke can be trusted; the PROVEN
|
|
592
|
+
* app-level close+reopen ran to clear it (cannot be
|
|
593
|
+
* AX-confirmed, but the maneuver works blind — SESSGATE);
|
|
594
|
+
* - "foreign" — a dialog is open that this drive did not open, so it was
|
|
595
|
+
* LEFT ALONE (a cleanup must never dismiss the dialog the
|
|
596
|
+
* person at the keyboard opened after our failure);
|
|
597
|
+
* - "may-remain" — ours, and nothing in the ladder would close it
|
|
598
|
+
* (fail-closed: report it precisely, with the sync gate).
|
|
599
|
+
*/
|
|
600
|
+
export type ClearOutcome = UiClearOutcome;
|
|
601
|
+
export interface ClearResult {
|
|
602
|
+
state: ClearOutcome;
|
|
603
|
+
/** How it was closed, for the trace and the disclosure. */
|
|
604
|
+
how?: "cancel-button" | "escape" | "window-close";
|
|
605
|
+
/** What the census identified as open at cleanup time. */
|
|
606
|
+
sheetKind?: UiSheetKind;
|
|
607
|
+
/** Who owned the screen when the cleanup started, when it was not Things. */
|
|
608
|
+
focusOwner?: string;
|
|
609
|
+
}
|
|
260
610
|
/**
|
|
261
611
|
* resolve-frame: read the element's on-screen frame (top-left origin, points)
|
|
262
612
|
* from the live AX tree and print "x y w h". Used by `click-element` to target
|
|
@@ -320,6 +670,29 @@ export declare function parseFrameCenter(stdout: string): {
|
|
|
320
670
|
x: number;
|
|
321
671
|
y: number;
|
|
322
672
|
} | null;
|
|
673
|
+
/**
|
|
674
|
+
* Read the live window/focus census through the shipped dispatch seam — the
|
|
675
|
+
* transport behind the `ui-state` diagnostic (src/ui-state.ts) and, in tests,
|
|
676
|
+
* behind any injected runner. READ-ONLY: the census clicks nothing, types
|
|
677
|
+
* nothing, and changes no state; see src/write/vectors/ui-state.ts.
|
|
678
|
+
*/
|
|
679
|
+
export declare function readLiveUiState(run?: UiRunner): Promise<UiState | null>;
|
|
680
|
+
/**
|
|
681
|
+
* Judge one guard reading. Returns null to proceed, or the refusal sentence —
|
|
682
|
+
* which always names who owns the screen, because that is the one fact the
|
|
683
|
+
* person reading it cannot recover after the fact.
|
|
684
|
+
*
|
|
685
|
+
* Exported for the unit matrix: every branch here is a fail-closed decision
|
|
686
|
+
* about synthetic input, and each one is worth a test.
|
|
687
|
+
*/
|
|
688
|
+
export declare function judgeFocusGuard(state: UiState | null, expectedSheet: UiSheetKind | null, label: string): string | null;
|
|
689
|
+
/**
|
|
690
|
+
* The cleanup disclosure (issue #620). Every branch states what is TRUE of the
|
|
691
|
+
* app right now, and — whenever a dialog may still be open — that Things Cloud
|
|
692
|
+
* sync is held until someone dismisses it, which is the consequence a caller
|
|
693
|
+
* cannot see and would otherwise discover hours later on another device.
|
|
694
|
+
*/
|
|
695
|
+
export declare function describeCleanup(clear: ClearResult): string;
|
|
323
696
|
/** Compile one recipe step into its primitive command (no dispatch). */
|
|
324
697
|
export declare function commandForStep(step: UiStep, targetUuid: string): UiCommand;
|
|
325
698
|
/**
|