things-api 0.18.0 → 0.19.1
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 +18 -9
- package/deputy/AppIcon.icns +0 -0
- package/deputy/VERSION +1 -1
- package/deputy/helpers-Info.plist +4 -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 +3 -1
- 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/Resources/AppIcon.icns +0 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/_CodeSignature/CodeResources +15 -2
- package/deputy/prebuilt/Things API Helper.app/Contents/Info.plist +5 -1
- package/deputy/prebuilt/Things API Helper.app/Contents/MacOS/things-deputy +0 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/Resources/AppIcon.icns +0 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/_CodeSignature/CodeResources +14 -2
- package/deputy/reader/Info.plist +2 -0
- package/deputy/reader/entitlements.plist +30 -0
- package/deputy/reader/main.swift +129 -40
- package/deputy/src/server.swift +25 -2
- package/deputy/src/tcc.swift +58 -0
- package/dist/audit/schema.d.ts +30 -1
- package/dist/audit/schema.js.map +1 -1
- package/dist/capability.d.ts +322 -0
- package/dist/capability.js +706 -0
- package/dist/capability.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +13 -1
- package/dist/cli/commands/doctor.js +141 -5
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/helpers.js +189 -51
- 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/op-result.js +6 -0
- package/dist/cli/commands/op-result.js.map +1 -1
- package/dist/cli/commands/reads.js +47 -1
- package/dist/cli/commands/reads.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.d.ts +9 -0
- package/dist/cli/commands/setup.js +76 -83
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/writes.d.ts +1 -1
- package/dist/cli/commands/writes.js +311 -142
- package/dist/cli/commands/writes.js.map +1 -1
- package/dist/cli/dry-run.d.ts +1 -1
- package/dist/cli/dry-run.js +1 -1
- package/dist/cli/glyphs.d.ts +23 -4
- package/dist/cli/glyphs.js +46 -19
- package/dist/cli/glyphs.js.map +1 -1
- package/dist/cli/help.d.ts +1 -1
- package/dist/cli/help.js +49 -27
- 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 +8 -5
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/period.d.ts +3 -1
- package/dist/cli/period.js +1 -1
- package/dist/cli/period.js.map +1 -1
- package/dist/cli/read-driver.js +26 -1
- package/dist/cli/read-driver.js.map +1 -1
- package/dist/cli/render.d.ts +31 -4
- package/dist/cli/render.js +127 -15
- package/dist/cli/render.js.map +1 -1
- package/dist/client.d.ts +23 -3
- package/dist/client.js +36 -13
- package/dist/client.js.map +1 -1
- package/dist/contracts.d.ts +8 -4
- package/dist/contracts.js +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/deputy/install.d.ts +234 -6
- package/dist/deputy/install.js +843 -31
- package/dist/deputy/install.js.map +1 -1
- package/dist/deputy/notices.js +2 -2
- package/dist/deputy/notices.js.map +1 -1
- package/dist/deputy/protocol.d.ts +59 -5
- package/dist/deputy/protocol.js +48 -11
- package/dist/deputy/protocol.js.map +1 -1
- package/dist/deputy/routing.d.ts +37 -0
- package/dist/deputy/routing.js +181 -27
- 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 +19 -4
- package/dist/diagnose.js +64 -8
- package/dist/diagnose.js.map +1 -1
- package/dist/direct-setup.d.ts +104 -0
- package/dist/direct-setup.js +658 -0
- package/dist/direct-setup.js.map +1 -0
- package/dist/host-access.d.ts +75 -0
- package/dist/host-access.js +177 -0
- package/dist/host-access.js.map +1 -0
- package/dist/index.d.ts +20 -5
- package/dist/index.js +14 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +27 -1
- package/dist/mcp/server.js +308 -109
- package/dist/mcp/server.js.map +1 -1
- package/dist/op-result.d.ts +9 -0
- package/dist/op-result.js +13 -1
- package/dist/op-result.js.map +1 -1
- package/dist/read/filter-contract.d.ts +1 -1
- package/dist/read/filter-contract.js +4 -0
- package/dist/read/filter-contract.js.map +1 -1
- package/dist/read/shape.js +4 -0
- package/dist/read/shape.js.map +1 -1
- package/dist/read/views.d.ts +29 -0
- package/dist/read/views.js +60 -0
- package/dist/read/views.js.map +1 -1
- package/dist/session-grant.d.ts +48 -0
- package/dist/session-grant.js +185 -0
- package/dist/session-grant.js.map +1 -0
- package/dist/surface-copy.d.ts +19 -3
- package/dist/surface-copy.js +19 -3
- package/dist/surface-copy.js.map +1 -1
- package/dist/wizard.d.ts +58 -0
- package/dist/wizard.js +174 -0
- package/dist/wizard.js.map +1 -0
- package/dist/write/availability.d.ts +1 -16
- package/dist/write/availability.js +10 -65
- package/dist/write/availability.js.map +1 -1
- package/dist/write/batch.d.ts +20 -0
- package/dist/write/batch.js +111 -21
- package/dist/write/batch.js.map +1 -1
- package/dist/write/capabilities.d.ts +9 -0
- package/dist/write/capabilities.js +2 -0
- package/dist/write/capabilities.js.map +1 -1
- package/dist/write/clear-reminder.js +1 -1
- package/dist/write/clear-reminder.js.map +1 -1
- package/dist/write/commands.d.ts +22 -7
- package/dist/write/commands.js +231 -254
- package/dist/write/commands.js.map +1 -1
- package/dist/write/failure-hints.d.ts +32 -7
- package/dist/write/failure-hints.js +9 -14
- package/dist/write/failure-hints.js.map +1 -1
- package/dist/write/guards.js +22 -4
- package/dist/write/guards.js.map +1 -1
- package/dist/write/lock.d.ts +16 -13
- package/dist/write/lock.js +55 -0
- package/dist/write/lock.js.map +1 -1
- package/dist/write/make-repeating-project.js +9 -13
- package/dist/write/make-repeating-project.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 +51 -29
- package/dist/write/operations.js +16 -0
- package/dist/write/operations.js.map +1 -1
- package/dist/write/opid.d.ts +72 -14
- package/dist/write/opid.js +152 -8
- package/dist/write/opid.js.map +1 -1
- package/dist/write/param-schema.d.ts +80 -0
- package/dist/write/param-schema.js +754 -0
- package/dist/write/param-schema.js.map +1 -0
- package/dist/write/pipeline.d.ts +110 -5
- package/dist/write/pipeline.js +288 -14
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/pre-state.d.ts +38 -16
- package/dist/write/pre-state.js +117 -22
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/promote-clone.js +658 -444
- package/dist/write/promote-clone.js.map +1 -1
- package/dist/write/reorder.js +25 -8
- package/dist/write/reorder.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/repeat-rule.d.ts +28 -1
- package/dist/write/repeat-rule.js +54 -4
- package/dist/write/repeat-rule.js.map +1 -1
- package/dist/write/resolution-timestamps.d.ts +11 -0
- package/dist/write/resolution-timestamps.js +108 -40
- package/dist/write/resolution-timestamps.js.map +1 -1
- package/dist/write/reversibility.js +4 -0
- package/dist/write/reversibility.js.map +1 -1
- package/dist/write/template-mutation.d.ts +137 -0
- package/dist/write/template-mutation.js +598 -0
- package/dist/write/template-mutation.js.map +1 -0
- package/dist/write/undo.js +10 -20
- package/dist/write/undo.js.map +1 -1
- package/dist/write/update-fields.d.ts +151 -0
- package/dist/write/update-fields.js +427 -0
- package/dist/write/update-fields.js.map +1 -0
- package/dist/write/vectors/applescript.js +6 -10
- package/dist/write/vectors/applescript.js.map +1 -1
- package/dist/write/vectors/registry.d.ts +1 -1
- package/dist/write/vectors/registry.js +10 -2
- package/dist/write/vectors/registry.js.map +1 -1
- package/dist/write/vectors/shortcuts.js +2 -2
- package/dist/write/vectors/shortcuts.js.map +1 -1
- package/dist/write/vectors/simulator.js +15 -4
- package/dist/write/vectors/simulator.js.map +1 -1
- package/dist/write/vectors/types.d.ts +268 -1
- 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 +70 -3
- package/dist/write/vectors/ui-recipes.js +482 -44
- package/dist/write/vectors/ui-recipes.js.map +1 -1
- package/dist/write/vectors/ui.d.ts +280 -10
- package/dist/write/vectors/ui.js +1068 -77
- 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 +62 -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 +2 -1
- package/schema/envelope.schema.json +6 -4
- package/scripts/build-helpers.sh +5 -0
- package/skills/things-cli/SKILL.md +5 -3
- package/skills/things-cli/references/contracts.md +1 -0
- package/skills/things-cli/references/data-model.md +3 -0
- package/skills/things-cli/references/errors.md +1 -1
- package/skills/things-cli/references/gui.md +1 -0
- package/skills/things-cli/references/repeating.md +6 -0
- package/skills/things-cli/references/tag-cleanup.md +64 -0
|
@@ -64,6 +64,36 @@ export function pauseRepeatRecipe(targetUuid) {
|
|
|
64
64
|
],
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* `Items ▸ Repeat ▸ Create Next Copy` — Things 3.23's "spawn the pending
|
|
69
|
+
* occurrence now" command, and the first half of every template mutation we
|
|
70
|
+
* ship (CNC1, docs/lab/cnc1-template-mutations.md).
|
|
71
|
+
*
|
|
72
|
+
* One press materializes the instance the projection cursor points at AND
|
|
73
|
+
* advances the series, and the template delta is FIELD FOR FIELD what the app's
|
|
74
|
+
* own `Make Exception` writes (`rt1_instanceCreationCount +1`,
|
|
75
|
+
* `rt1_instanceCreationStartDate` → consumed slot + 1, `rt1_nextInstanceStartDate`
|
|
76
|
+
* → the next rule date, `todayIndexReferenceDate` → the cursor, `umd` silent,
|
|
77
|
+
* rule blob untouched) — measured on a fixed weekly rule against REPX3 §1.2 and
|
|
78
|
+
* on a daily rule against §2.1. Mutating the minted instance afterwards is
|
|
79
|
+
* therefore the exception the chooser withholds from automation.
|
|
80
|
+
*
|
|
81
|
+
* Template-only, like every verb in this submenu: with an INSTANCE selected the
|
|
82
|
+
* `Items` menu carries no `Repeat` item at all (REPX1 §5.1), which is what the
|
|
83
|
+
* eligibility assert catches before the press.
|
|
84
|
+
*/
|
|
85
|
+
export function createNextCopyRecipe(targetUuid) {
|
|
86
|
+
const item = `menu item "Create Next Copy" of menu 1 of menu item "Repeat" of ${ITEMS_MENU}`;
|
|
87
|
+
return {
|
|
88
|
+
op: "todo.create-next-copy",
|
|
89
|
+
targetUuid,
|
|
90
|
+
steps: [
|
|
91
|
+
...preamble(targetUuid),
|
|
92
|
+
assertEligible(targetUuid, item),
|
|
93
|
+
menuPress("Items ▸ Repeat ▸ Create Next Copy", item, REPEAT_SUBMENU_ANCHOR),
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
67
97
|
export function resumeRepeatRecipe(targetUuid) {
|
|
68
98
|
return {
|
|
69
99
|
op: "todo.resume-repeat",
|
|
@@ -82,12 +112,20 @@ export function convertToProjectRecipe(op, targetUuid) {
|
|
|
82
112
|
steps: [
|
|
83
113
|
...preamble(targetUuid),
|
|
84
114
|
menuPress("Items ▸ Convert to Project…", `menu item "Convert to Project…" of ${ITEMS_MENU}`),
|
|
115
|
+
// positional-ok: a window presents at most ONE attached sheet at a time, so
|
|
116
|
+
// `sheet 1` is a uniqueness statement, not an index among peers. It is also
|
|
117
|
+
// only ever a WAIT target here — the control acted on inside it is
|
|
118
|
+
// AXIdentifier-addressed below.
|
|
85
119
|
waitFor("the confirmation sheet", `sheet 1 of ${MAIN_WINDOW}`),
|
|
86
120
|
{
|
|
87
121
|
// The alert's primary button carries a stable, locale-proof
|
|
88
122
|
// AXIdentifier "action-button-1" (UIC1); prefer it over the English title.
|
|
123
|
+
// positional-ok: the `sheet 1` container is the one-sheet-per-window law
|
|
124
|
+
// above; the BUTTON is discriminated by AXIdentifier, never by position.
|
|
89
125
|
primitive: "press",
|
|
90
126
|
label: 'confirm — press "Convert"',
|
|
127
|
+
// positional-ok: `sheet 1` is the one attached sheet a window can present;
|
|
128
|
+
// the BUTTON inside it is AXIdentifier-discriminated, never positional.
|
|
91
129
|
path: `(first button of sheet 1 of ${MAIN_WINDOW} whose value of attribute "AXIdentifier" is "action-button-1")`,
|
|
92
130
|
dynamic: true,
|
|
93
131
|
addressing: "axidentifier",
|
|
@@ -107,6 +145,60 @@ export function convertToProjectRecipe(op, targetUuid) {
|
|
|
107
145
|
* DB effect (UI2-d / HEADCERT1): the heading uuid dies, a new type=1 project is
|
|
108
146
|
* promoted into the parent project's area, its children reparent (heading→NULL).
|
|
109
147
|
*/
|
|
148
|
+
/**
|
|
149
|
+
* Reorder a project's HEADINGS with the arrow chords (CHORDMH1, on the HEADORD1
|
|
150
|
+
* law). Things exposes heading order through four bare keybindings and nothing
|
|
151
|
+
* else — no menu item, no context-menu item, no AX action anywhere carries the
|
|
152
|
+
* equivalent — so the recipe has no element to press: it reveals the project,
|
|
153
|
+
* confirms the content table is there, and hands the whole move to the chord
|
|
154
|
+
* driver, which selects each heading positionally and posts one verified chord
|
|
155
|
+
* at a time (src/write/vectors/ui-chord.ts).
|
|
156
|
+
*
|
|
157
|
+
* NO `activate` step, deliberately. The reveal is a background `open -g`, the
|
|
158
|
+
* row selection is pure System Events, and the chord is posted to the Things
|
|
159
|
+
* PROCESS rather than to the focused surface — so the entire gesture runs with
|
|
160
|
+
* Things behind whatever the user is looking at. Measured end to end on Things
|
|
161
|
+
* 3.23 / golden-v4 with Finder frontmost at every stage and Things never
|
|
162
|
+
* activated at all (docs/lab/chordmh1-move-heading-build.md §1).
|
|
163
|
+
*
|
|
164
|
+
* `needsWindowReachability` is set even though no sheet opens: the heading rows
|
|
165
|
+
* only exist in a rendered project view, so a locked screen or a full-screen
|
|
166
|
+
* Space must refuse (SESSGATE) rather than post chords at a window nothing can
|
|
167
|
+
* read back.
|
|
168
|
+
*/
|
|
169
|
+
export function moveHeadingChordRecipe(projectUuid, targetOrder, movees) {
|
|
170
|
+
const spec = {
|
|
171
|
+
projectUuid,
|
|
172
|
+
targetOrder,
|
|
173
|
+
movees,
|
|
174
|
+
tablePath: PROJECT_CONTENT_TABLE,
|
|
175
|
+
};
|
|
176
|
+
return {
|
|
177
|
+
op: "project.move-heading",
|
|
178
|
+
targetUuid: spec.projectUuid,
|
|
179
|
+
needsWindowReachability: true,
|
|
180
|
+
steps: [
|
|
181
|
+
{
|
|
182
|
+
primitive: "reveal",
|
|
183
|
+
label: "reveal the project in Things (things:///show?id=<project>)",
|
|
184
|
+
value: spec.projectUuid,
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
// Canaried: if the project view's content table is not there, the drive
|
|
188
|
+
// refuses before a single chord is posted.
|
|
189
|
+
primitive: "resolve",
|
|
190
|
+
label: "confirm the project view's content table is present",
|
|
191
|
+
path: PROJECT_CONTENT_TABLE,
|
|
192
|
+
addressing: "title",
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
primitive: "chord-reorder",
|
|
196
|
+
label: `reorder ${spec.movees.length} heading(s) with the arrow chords`,
|
|
197
|
+
chord: spec,
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
110
202
|
export function headingConvertToProjectRecipe(projectUuid, ordinal) {
|
|
111
203
|
return {
|
|
112
204
|
op: "project.promote-heading",
|
|
@@ -135,12 +227,17 @@ export function headingConvertToProjectRecipe(projectUuid, ordinal) {
|
|
|
135
227
|
// With the heading selected, Convert to Project… is enabled (it exists
|
|
136
228
|
// regardless, so the canary resolves it; the press lands post-selection).
|
|
137
229
|
menuPress("Items ▸ Convert to Project…", `menu item "Convert to Project…" of ${ITEMS_MENU}`),
|
|
230
|
+
// positional-ok: one attached sheet per window (see convertToProjectRecipe).
|
|
138
231
|
waitFor("the confirmation sheet", `sheet 1 of ${MAIN_WINDOW}`),
|
|
139
232
|
{
|
|
140
233
|
// The alert's primary button carries the locale-proof AXIdentifier
|
|
141
234
|
// "action-button-1" (UIC1); prefer it over the English title.
|
|
235
|
+
// positional-ok: one attached sheet per window; the button itself is
|
|
236
|
+
// AXIdentifier-discriminated.
|
|
142
237
|
primitive: "press",
|
|
143
238
|
label: 'confirm — press "Convert"',
|
|
239
|
+
// positional-ok: `sheet 1` is the one attached sheet a window can present;
|
|
240
|
+
// the BUTTON inside it is AXIdentifier-discriminated, never positional.
|
|
144
241
|
path: `(first button of sheet 1 of ${MAIN_WINDOW} whose value of attribute "AXIdentifier" is "action-button-1")`,
|
|
145
242
|
dynamic: true,
|
|
146
243
|
addressing: "axidentifier",
|
|
@@ -168,9 +265,16 @@ export function headingConvertToProjectRecipe(projectUuid, ordinal) {
|
|
|
168
265
|
// The reminder-time control is undrivable (its AXDateTimeArea ignores AX writes),
|
|
169
266
|
// so `--reminder` is refused upstream in assertRepeatRule; its recipe step is
|
|
170
267
|
// retained but unreachable.
|
|
171
|
-
/** The content list's table (row 0 = area/Someday header, then projects/to-dos). Confirmed UIC5.
|
|
268
|
+
/** The content list's table (row 0 = area/Someday header, then projects/to-dos). Confirmed UIC5.
|
|
269
|
+
* positional-ok: a CONTAINER handle only — the main window's list area holds one
|
|
270
|
+
* scroll area holding one table (UIC5), and no value is read or written through
|
|
271
|
+
* this path. Every step that uses it resolves its target WITHIN the table by
|
|
272
|
+
* title readback (select-row), by selection-readback ordinal (select-heading-row)
|
|
273
|
+
* or by exact AXDescription (click-element's rowCellDescription walk, HXPC1 §B0). */
|
|
172
274
|
const PROJECT_CONTENT_TABLE = `table 1 of scroll area 1 of ${MAIN_WINDOW}`;
|
|
173
|
-
/** The Repeat editor when Things is frontmost — an attached sheet (interval nested in group 1, UIC1).
|
|
275
|
+
/** The Repeat editor when Things is frontmost — an attached sheet (interval nested in group 1, UIC1).
|
|
276
|
+
* positional-ok: a window presents at most ONE attached sheet at a time, so
|
|
277
|
+
* `sheet 1` names the only sheet there can be rather than picking among peers. */
|
|
174
278
|
const REPEAT_SHEET = `sheet 1 of ${MAIN_WINDOW}`;
|
|
175
279
|
/** The Repeat editor when Things is backgrounded — a detached AXUnknown window (UIC4-a). Its
|
|
176
280
|
* controls sit at the SAME depth as the sheet's (frequency a direct child, interval in group 1) — UIC5-e. */
|
|
@@ -182,10 +286,25 @@ function dualForm(inner) {
|
|
|
182
286
|
return DIALOG_SHELLS.map((shell) => `${inner} of ${shell}`);
|
|
183
287
|
}
|
|
184
288
|
// --- CERTIFIED controls (UIC1/UIC5) --------------------------------------
|
|
185
|
-
|
|
289
|
+
//
|
|
290
|
+
// WHY THE POP-UPS BELOW MAY STAY POSITIONAL WHILE THE TEXT FIELDS MAY NOT.
|
|
291
|
+
// A pop-up drive is SELF-DISCRIMINATING: `select-popup` opens the control and
|
|
292
|
+
// clicks a menu item matched by its pinned English NAME, so a mis-addressed
|
|
293
|
+
// pop-up has no such item and the step fails closed ("none of the candidate menu
|
|
294
|
+
// items exist"). A text field accepts any string silently — which is precisely
|
|
295
|
+
// how #589 wrote an interval into the ends-count field. The pop-up label sets are
|
|
296
|
+
// also disjoint across the cadence group (`never`/`after`/`on date` for the ends
|
|
297
|
+
// bound; `day(s)`…`year(s)` for the after-completion unit; weekday names, ordinals
|
|
298
|
+
// and month names for the anchors), so a swapped index cannot land a plausible
|
|
299
|
+
// value. Belt and braces: the PRE-COMMIT AUDIT re-reads every one of them by value
|
|
300
|
+
// before the OK press, so a pop-up that took a wrong-but-existing item is caught
|
|
301
|
+
// before anything is committed.
|
|
302
|
+
/** Frequency pop-up — a direct child of the dialog.
|
|
303
|
+
* positional-ok: MEASURED sole direct-child pop-up of the dialog shell in every
|
|
304
|
+
* reachable state — the shell carries `popups=1`, every other pop-up living in the
|
|
305
|
+
* cadence group (CGRD1 §B census, all four frequencies + after-completion). Its
|
|
306
|
+
* menu items are matched by name, and the pre-commit audit re-reads its value. */
|
|
186
307
|
const DIALOG_FREQUENCY = dualForm("pop up button 1");
|
|
187
|
-
/** Interval field — nested in group 1 (UIC5-e). */
|
|
188
|
-
const DIALOG_INTERVAL = dualForm("text field 1 of group 1");
|
|
189
308
|
/** OK button. */
|
|
190
309
|
const DIALOG_OK = dualForm(`button "OK"`);
|
|
191
310
|
// --- UIC6-CERTIFIED controls, RE-INDEXED PER DIALOG SHAPE (RDLG2) ----------
|
|
@@ -199,18 +318,45 @@ const DIALOG_OK = dualForm(`button "OK"`);
|
|
|
199
318
|
// driver measures in the open dialog (`probe-dialog-shape`), never to the app
|
|
200
319
|
// version. Titles/`_NS:` ids are never used (both drift). Evidence:
|
|
201
320
|
// docs/lab/uic6-rule-vocabulary.md (≤3.22) and docs/lab/rdlg2-323-recipe-cert.md.
|
|
202
|
-
/** After-completion cadence unit pop-up — the ONLY group pop-up in that mode (both shapes).
|
|
321
|
+
/** After-completion cadence unit pop-up — the ONLY group pop-up in that mode (both shapes).
|
|
322
|
+
* positional-ok: this and DIALOG_ENDS are the SAME spelling because they are the
|
|
323
|
+
* same slot in two MUTUALLY EXCLUSIVE dialog states, and the recipe emits exactly
|
|
324
|
+
* one of them per drive. Measured (CGRD1 §A census): an after-completion cadence
|
|
325
|
+
* group has `popups=1` and no `Ends:` label at all, while every fixed frequency
|
|
326
|
+
* puts the ends bound at group pop-up 1. Their menu-item sets are disjoint
|
|
327
|
+
* (`day(s)`…`year(s)` vs `never`/`after`/`on date`), so a wrong-state drive fails
|
|
328
|
+
* closed on the item name; the pre-commit audit re-reads whichever one ran. */
|
|
203
329
|
const DIALOG_AC_UNIT = dualForm("pop up button 1 of group 1");
|
|
204
|
-
/** "Ends" bound pop-up (`never` · `after` · `on date`) — always pop up button 1 of the group.
|
|
330
|
+
/** "Ends" bound pop-up (`never` · `after` · `on date`) — always pop up button 1 of the group.
|
|
331
|
+
* positional-ok: see DIALOG_AC_UNIT — one slot, two exclusive states, name-matched
|
|
332
|
+
* items, audited before the commit. */
|
|
205
333
|
const DIALOG_ENDS = dualForm("pop up button 1 of group 1");
|
|
206
|
-
/** The cadence group itself — the handle the shape probe and the weekday converge address.
|
|
334
|
+
/** The cadence group itself — the handle the shape probe and the weekday converge address.
|
|
335
|
+
* positional-ok: MEASURED sole group of the dialog shell (`groups=1` in every
|
|
336
|
+
* state, CGRD1 §B), and a CONTAINER handle only — every primitive that takes it
|
|
337
|
+
* (set-group-number, probe-dialog-shape, converge-weekdays, audit-dialog)
|
|
338
|
+
* discriminates WITHIN it by label row or control class and fails closed on
|
|
339
|
+
* anything but exactly one match. */
|
|
207
340
|
const DIALOG_GROUP = dualForm("group 1");
|
|
208
|
-
/** The 3.23 `Next:` first-occurrence pop-up — group pop-up 2 (this shape only).
|
|
341
|
+
/** The 3.23 `Next:` first-occurrence pop-up — group pop-up 2 (this shape only).
|
|
342
|
+
* positional-ok: reachable ONLY under a MEASURED dialog shape — the recipe's
|
|
343
|
+
* probe-dialog-shape step must have returned `next-popup` (a positive match on the
|
|
344
|
+
* control class occupying the `Next:` row) before this step runs, and an
|
|
345
|
+
* unrecognized shape refuses the drive. The drive then matches a menu item by
|
|
346
|
+
* PARSED DATE and reads the pop-up back; the pre-commit audit re-parses it. */
|
|
209
347
|
const DIALOG_NEXT_POPUP = dualForm("pop up button 2 of group 1");
|
|
210
|
-
/** A group pop-up addressed at DIFFERENT indices in the two dialog shapes (RDLG2).
|
|
348
|
+
/** A group pop-up addressed at DIFFERENT indices in the two dialog shapes (RDLG2).
|
|
349
|
+
* positional-ok: the index is not a guess but the MEASURED consequence of the
|
|
350
|
+
* shape the drive probed — CGRD1 §A counts the cadence group's pop-ups per
|
|
351
|
+
* frequency on 3.23 (daily 2 · weekly 3 · monthly 4 · yearly 5, i.e. Ends + Next +
|
|
352
|
+
* the per-frequency controls), which is the +1 fork these two index sets encode.
|
|
353
|
+
* A step carrying `shaped` with no shape probed fails closed. Every one of these
|
|
354
|
+
* is a name-matched pop-up, and all of them are re-read by the pre-commit audit. */
|
|
211
355
|
function shapedPopup(nextPopupIndex, legacyIndex) {
|
|
212
356
|
return {
|
|
357
|
+
// positional-ok: shape-MEASURED index, per this function's contract above.
|
|
213
358
|
"next-popup": { pathCandidates: dualForm(`pop up button ${nextPopupIndex} of group 1`) },
|
|
359
|
+
// positional-ok: shape-MEASURED index, per this function's contract above.
|
|
214
360
|
legacy: { pathCandidates: dualForm(`pop up button ${legacyIndex} of group 1`) },
|
|
215
361
|
};
|
|
216
362
|
}
|
|
@@ -228,13 +374,23 @@ const DIALOG_YEAR_ORDINAL = shapedPopup(5, 4);
|
|
|
228
374
|
* the add button from live structure).
|
|
229
375
|
*/
|
|
230
376
|
const WEEKDAY_BASE = { "next-popup": 3, legacy: 2 };
|
|
231
|
-
/** "Ends after [n]" count field — becomes text field 1 of the group once shown (interval was set earlier while it was the sole field). */
|
|
232
|
-
const DIALOG_ENDS_COUNT = dualForm("text field 1 of group 1");
|
|
233
377
|
/** "Add reminders" checkbox (sheet-level, title-pinned). The time is an AXDateTimeArea driven by set-datetime. */
|
|
234
378
|
const DIALOG_ADD_REMINDERS = dualForm(`checkbox "Add reminders"`);
|
|
235
|
-
/** "Add deadlines" checkbox + the "start
|
|
379
|
+
/** "Add deadlines" checkbox + the "and start [n] days earlier" offset field it reveals. */
|
|
236
380
|
const DIALOG_ADD_DEADLINES = dualForm(`checkbox "Add deadlines"`);
|
|
237
|
-
|
|
381
|
+
/**
|
|
382
|
+
* The pinned English static text whose ROW the start-offset field sits on. The
|
|
383
|
+
* field shipped as `text field 1` of the dialog shell — a value-bearing numeric
|
|
384
|
+
* field picked by index out of a tree whose shape depends on the "Add deadlines"
|
|
385
|
+
* checkbox, verified only by re-reading the same index it wrote. That is the #589
|
|
386
|
+
* error class, and it happened to be right rather than provably right: measured on
|
|
387
|
+
* 3.23, the shell carries 0 direct text fields with deadlines OFF and exactly 1
|
|
388
|
+
* with them ON — `[and start] [ 0 ] [days earlier]`, the field at y=409 against the
|
|
389
|
+
* label's y=413 — whether or not reminders are also on (CGRD1 §B census). Now the
|
|
390
|
+
* field is found by that label's row, exactly like the cadence numbers, and
|
|
391
|
+
* anything but one match on it fails closed with the shell's field inventory.
|
|
392
|
+
*/
|
|
393
|
+
const DIALOG_START_EARLIER_LABEL = "days earlier";
|
|
238
394
|
/**
|
|
239
395
|
* After-completion cadence-unit pop-up labels. The options are NOT the frequency
|
|
240
396
|
* word (`weekly`) — they are the time unit, and the app PLURALIZES them by the
|
|
@@ -317,11 +473,52 @@ function selectPopupAny(label, pathCandidates, valueCandidates) {
|
|
|
317
473
|
addressing: "title",
|
|
318
474
|
};
|
|
319
475
|
}
|
|
320
|
-
|
|
476
|
+
/**
|
|
477
|
+
* Drive a dialog-SHELL text field addressed by the pinned English label sharing
|
|
478
|
+
* its row — the {@link setGroupNumber} discrimination law, applied outside the
|
|
479
|
+
* cadence group. Its one caller is the start-days-earlier offset; see
|
|
480
|
+
* {@link DIALOG_START_EARLIER_LABEL} for why the old `text field 1` spelling had
|
|
481
|
+
* to go even though it resolved correctly on 3.23.
|
|
482
|
+
*/
|
|
483
|
+
function setRowField(label, rowLabel, value) {
|
|
321
484
|
return {
|
|
322
|
-
primitive: "set-
|
|
485
|
+
primitive: "set-row-field",
|
|
323
486
|
label,
|
|
324
|
-
pathCandidates,
|
|
487
|
+
pathCandidates: DIALOG_SHELLS,
|
|
488
|
+
rowLabel,
|
|
489
|
+
value,
|
|
490
|
+
dynamic: true,
|
|
491
|
+
addressing: "title",
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Drive one of the Repeat dialog's TWO numeric fields — the cadence interval
|
|
496
|
+
* ("Every [n] days") or the "Ends: after [n] times" count. Both are text fields
|
|
497
|
+
* of the cadence group, and WHICH INDEX each holds depends on the dialog's
|
|
498
|
+
* state rather than on which one the caller means. Measured on Things 3.23
|
|
499
|
+
* (HXPC1, docs/lab/hxpc1-picker-assert.md §A):
|
|
500
|
+
*
|
|
501
|
+
* Ends: never → 1 text field · #1 interval @[311,283]
|
|
502
|
+
* Ends: after N → 2 text fields · #1 COUNT @[402,372] (`Ends:` label y=375)
|
|
503
|
+
* #2 interval @[311,283] (`Every` label y=286)
|
|
504
|
+
*
|
|
505
|
+
* Both used to be spelled `text field 1 of group 1`, which the create path
|
|
506
|
+
* survived only because it drives the interval BEFORE selecting the ends bound,
|
|
507
|
+
* while it is still the sole field. A RESCHEDULE opens the dialog PRE-POPULATED:
|
|
508
|
+
* a rule that already ends after N presents both fields from the first step, so
|
|
509
|
+
* the interval drive wrote the requested interval into the count field and the
|
|
510
|
+
* count drive then overwrote it — the interval never changed. The driver now
|
|
511
|
+
* resolves each field by the ROW it sits on (the count shares the `Ends:`
|
|
512
|
+
* label's row, the interval does not), which is order-independent and covers the
|
|
513
|
+
* after-completion dialog too: that one offers no ends bound at all, so the
|
|
514
|
+
* interval is again the only field. See ui.ts `axSetGroupNumberScript`.
|
|
515
|
+
*/
|
|
516
|
+
function setGroupNumber(label, numberTarget, value) {
|
|
517
|
+
return {
|
|
518
|
+
primitive: "set-group-number",
|
|
519
|
+
label,
|
|
520
|
+
pathCandidates: DIALOG_GROUP,
|
|
521
|
+
numberTarget,
|
|
325
522
|
value,
|
|
326
523
|
dynamic: true,
|
|
327
524
|
addressing: "title",
|
|
@@ -423,7 +620,7 @@ function repeatDialogEntry(rule) {
|
|
|
423
620
|
else {
|
|
424
621
|
steps.push(selectPopup(`frequency = ${rule.frequency}`, DIALOG_FREQUENCY, rule.frequency));
|
|
425
622
|
}
|
|
426
|
-
steps.push(
|
|
623
|
+
steps.push(setGroupNumber(`interval = ${rule.interval}`, "interval", String(rule.interval)));
|
|
427
624
|
// MEASURE the dialog before touching any control the 3.23 redesign moved
|
|
428
625
|
// (RDLG2). Emitted only when such a control is actually addressed, so the
|
|
429
626
|
// certified two-control path (frequency + interval + OK) costs no extra hop —
|
|
@@ -471,6 +668,31 @@ function repeatDialogEntry(rule) {
|
|
|
471
668
|
steps.push(selectPopupShaped(`yearly month = ${y.month}`, DIALOG_YEAR_MONTH, MONTH_TITLE[y.month - 1] ?? ""));
|
|
472
669
|
steps.push(...monthlyAnchorSteps(y, DIALOG_YEAR_MODE, DIALOG_YEAR_ORDINAL));
|
|
473
670
|
}
|
|
671
|
+
// LET THE `Next:` POP-UP ABSORB THE RULE (NEXTPOP1) before any further input.
|
|
672
|
+
//
|
|
673
|
+
// The 3.23 dialog recomputes its first-occurrence pop-up — the displayed date
|
|
674
|
+
// AND the menu of occurrences behind it — ASYNCHRONOUSLY, ~0.4s after the
|
|
675
|
+
// calendar anchor moves, and an input that lands inside that window CANCELS
|
|
676
|
+
// the recompute permanently: the control keeps describing the PREVIOUS rule.
|
|
677
|
+
// The very next thing this recipe drives is the deadline checkbox, which is
|
|
678
|
+
// exactly such an input, so every deadlined monthly/yearly promote reached
|
|
679
|
+
// `select-next-occurrence` with the SEED's occurrence series in the menu and
|
|
680
|
+
// failed closed on a date the rule really does produce (VMRES1 §4.3).
|
|
681
|
+
//
|
|
682
|
+
// Emitted for the pop-up shape only — the ≤3.22 `Next:` is a free date area
|
|
683
|
+
// with no menu to recompute — and never for after-completion, which has no
|
|
684
|
+
// first-occurrence control at all. It is a WAIT, not a setter, so it
|
|
685
|
+
// contributes no control to the pre-commit audit.
|
|
686
|
+
if (needsShape && rule.afterCompletion !== true) {
|
|
687
|
+
steps.push({
|
|
688
|
+
primitive: "settle-occurrences",
|
|
689
|
+
label: "let the first-occurrence pop-up absorb the rule",
|
|
690
|
+
pathCandidates: DIALOG_NEXT_POPUP,
|
|
691
|
+
onlyShape: "next-popup",
|
|
692
|
+
dynamic: true,
|
|
693
|
+
addressing: "title",
|
|
694
|
+
});
|
|
695
|
+
}
|
|
474
696
|
// "Add deadlines" / "start N days earlier" — DEADLINE MODE, converged (RRD1)
|
|
475
697
|
// BEFORE the "Next:" field is driven below. In deadline mode the "Next:" field IS
|
|
476
698
|
// the deadline date and the instance start = deadline − startDaysEarlier (YANCH1
|
|
@@ -496,7 +718,7 @@ function repeatDialogEntry(rule) {
|
|
|
496
718
|
// startDaysEarlier is requested-fields-only too: drive the offset field only
|
|
497
719
|
// when it was given (>0), else leave it at its pre-populated value.
|
|
498
720
|
if (deadlineTarget && (rule.startDaysEarlier ?? 0) > 0) {
|
|
499
|
-
steps.push(
|
|
721
|
+
steps.push(setRowField(`start ${rule.startDaysEarlier} days earlier`, DIALOG_START_EARLIER_LABEL, String(rule.startDaysEarlier)));
|
|
500
722
|
}
|
|
501
723
|
}
|
|
502
724
|
// Ends bound + "Next:" first-occurrence field (ANCH2, issue #476). ORDER MATTERS:
|
|
@@ -508,7 +730,7 @@ function repeatDialogEntry(rule) {
|
|
|
508
730
|
const endsOnDate = rule.ends !== undefined && rule.ends.kind === "on-date" ? rule.ends : null;
|
|
509
731
|
if (rule.ends !== undefined && rule.ends.kind === "after") {
|
|
510
732
|
steps.push(selectPopup("ends = after", DIALOG_ENDS, "after"));
|
|
511
|
-
steps.push(
|
|
733
|
+
steps.push(setGroupNumber(`ends after = ${rule.ends.count}`, "ends-count", String(rule.ends.count)));
|
|
512
734
|
}
|
|
513
735
|
else if (endsOnDate !== null) {
|
|
514
736
|
steps.push(selectPopup("ends = on date", DIALOG_ENDS, "on date"));
|
|
@@ -550,9 +772,159 @@ function repeatDialogEntry(rule) {
|
|
|
550
772
|
steps.push(ensureCheckbox("Add reminders", DIALOG_ADD_REMINDERS, true));
|
|
551
773
|
steps.push(setDateTime(`reminder = ${rule.reminder}`, `time:${rule.reminder}`, "reminder"));
|
|
552
774
|
}
|
|
775
|
+
const audit = dialogAuditStep(steps);
|
|
776
|
+
if (audit !== null)
|
|
777
|
+
steps.push(audit);
|
|
553
778
|
steps.push(pressControl('press "OK"', DIALOG_OK));
|
|
554
779
|
return steps;
|
|
555
780
|
}
|
|
781
|
+
/**
|
|
782
|
+
* Build the PRE-COMMIT FULL-DIALOG AUDIT step from the drive's OWN step list
|
|
783
|
+
* (CGRD1) — the last thing that runs before the OK press.
|
|
784
|
+
*
|
|
785
|
+
* Derivation from the steps, rather than a second hand-written list of controls,
|
|
786
|
+
* is the point: every setter the recipe emits contributes exactly one audit
|
|
787
|
+
* control, so a rule field added to `repeatDialogEntry` is audited the moment it
|
|
788
|
+
* is driven, and a control can never be silently left out. It also means the audit
|
|
789
|
+
* addresses each control through the SAME candidate paths / shape overrides / row
|
|
790
|
+
* anchors the drive used, so the two can never disagree about what was touched.
|
|
791
|
+
*
|
|
792
|
+
* Why it is needed at all: each setter's own read-back is SELF-REFERENTIAL — it
|
|
793
|
+
* re-reads the element it addressed, so it proves the keystrokes landed where they
|
|
794
|
+
* were aimed and nothing more. #589 was a wrong ADDRESS, invisible to that check.
|
|
795
|
+
* The audit is the outside view; any mismatch aborts the drive fail-closed before
|
|
796
|
+
* anything is committed.
|
|
797
|
+
*
|
|
798
|
+
* Steps that set nothing (waits, the menu press, the shape probe, the OK press)
|
|
799
|
+
* contribute nothing; a recipe that drove no control at all yields no audit step.
|
|
800
|
+
*/
|
|
801
|
+
function weekdayBaseOfEncoded(value) {
|
|
802
|
+
const base = Number(value.split("|", 1)[0]);
|
|
803
|
+
return Number.isFinite(base) && base > 0 ? Math.trunc(base) : 2;
|
|
804
|
+
}
|
|
805
|
+
/** The per-shape PATH overrides of a step, in the audit control's shape. */
|
|
806
|
+
function shapedPaths(step) {
|
|
807
|
+
if (step.shaped === undefined)
|
|
808
|
+
return undefined;
|
|
809
|
+
return {
|
|
810
|
+
...(step.shaped["next-popup"]?.pathCandidates !== undefined && {
|
|
811
|
+
"next-popup": { pathCandidates: step.shaped["next-popup"].pathCandidates },
|
|
812
|
+
}),
|
|
813
|
+
...(step.shaped.legacy?.pathCandidates !== undefined && {
|
|
814
|
+
legacy: { pathCandidates: step.shaped.legacy.pathCandidates },
|
|
815
|
+
}),
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
function dialogAuditStep(steps) {
|
|
819
|
+
const controls = [];
|
|
820
|
+
for (const step of steps) {
|
|
821
|
+
const base = {
|
|
822
|
+
label: step.label,
|
|
823
|
+
...(step.onlyShape !== undefined && { onlyShape: step.onlyShape }),
|
|
824
|
+
};
|
|
825
|
+
switch (step.primitive) {
|
|
826
|
+
case "select-popup": {
|
|
827
|
+
const shaped = shapedPaths(step);
|
|
828
|
+
controls.push({
|
|
829
|
+
...base,
|
|
830
|
+
kind: "popup",
|
|
831
|
+
...(step.pathCandidates !== undefined && { pathCandidates: step.pathCandidates }),
|
|
832
|
+
...(shaped !== undefined && { shaped }),
|
|
833
|
+
// valueCandidates carries the singular/plural pair the app pluralizes by
|
|
834
|
+
// interval; ANY of them satisfies, exactly as the drive accepted any.
|
|
835
|
+
expected: step.valueCandidates ?? [step.value ?? ""],
|
|
836
|
+
});
|
|
837
|
+
break;
|
|
838
|
+
}
|
|
839
|
+
case "set-group-number":
|
|
840
|
+
controls.push({
|
|
841
|
+
...base,
|
|
842
|
+
kind: "group-number",
|
|
843
|
+
numberTarget: step.numberTarget ?? "interval",
|
|
844
|
+
expected: [step.value ?? ""],
|
|
845
|
+
});
|
|
846
|
+
break;
|
|
847
|
+
case "set-row-field":
|
|
848
|
+
controls.push({
|
|
849
|
+
...base,
|
|
850
|
+
kind: "row-field",
|
|
851
|
+
rowLabel: step.rowLabel ?? "",
|
|
852
|
+
expected: [step.value ?? ""],
|
|
853
|
+
});
|
|
854
|
+
break;
|
|
855
|
+
case "ensure-checkbox":
|
|
856
|
+
controls.push({
|
|
857
|
+
...base,
|
|
858
|
+
kind: "checkbox",
|
|
859
|
+
...(step.pathCandidates !== undefined && { pathCandidates: step.pathCandidates }),
|
|
860
|
+
expected: [step.checkboxTarget === true ? "1" : "0"],
|
|
861
|
+
expectedLabel: step.checkboxTarget === true ? "checked" : "unchecked",
|
|
862
|
+
});
|
|
863
|
+
break;
|
|
864
|
+
case "converge-weekdays": {
|
|
865
|
+
// The weekday step encodes "<base>|<Weekday>,<Weekday>…" per dialog shape
|
|
866
|
+
// (the base is the group pop-up index of the first weekday row, which the
|
|
867
|
+
// +1 fork moves). The audit reads every row pop-up from that base and
|
|
868
|
+
// compares as a SET: the converge law assigns EVERY row from the target set
|
|
869
|
+
// cycling, so a surplus row duplicates a target weekday rather than keeping
|
|
870
|
+
// a stale one, and set equality is the exact property to check.
|
|
871
|
+
const nextValue = step.shaped?.["next-popup"]?.value;
|
|
872
|
+
const legacyValue = step.shaped?.legacy?.value;
|
|
873
|
+
const encoded = nextValue ?? legacyValue;
|
|
874
|
+
if (encoded === undefined)
|
|
875
|
+
break;
|
|
876
|
+
const titles = encoded
|
|
877
|
+
.slice(encoded.indexOf("|") + 1)
|
|
878
|
+
.split(",")
|
|
879
|
+
.filter((t) => t !== "");
|
|
880
|
+
if (titles.length === 0)
|
|
881
|
+
break;
|
|
882
|
+
controls.push({
|
|
883
|
+
...base,
|
|
884
|
+
kind: "weekdays",
|
|
885
|
+
expected: titles,
|
|
886
|
+
expectedLabel: titles.join(" + "),
|
|
887
|
+
shaped: {
|
|
888
|
+
...(nextValue !== undefined && {
|
|
889
|
+
"next-popup": { weekdayBase: weekdayBaseOfEncoded(nextValue) },
|
|
890
|
+
}),
|
|
891
|
+
...(legacyValue !== undefined && {
|
|
892
|
+
legacy: { weekdayBase: weekdayBaseOfEncoded(legacyValue) },
|
|
893
|
+
}),
|
|
894
|
+
},
|
|
895
|
+
});
|
|
896
|
+
break;
|
|
897
|
+
}
|
|
898
|
+
case "select-next-occurrence":
|
|
899
|
+
controls.push({
|
|
900
|
+
...base,
|
|
901
|
+
kind: "occurrence-popup",
|
|
902
|
+
...(step.pathCandidates !== undefined && { pathCandidates: step.pathCandidates }),
|
|
903
|
+
expected: [step.value ?? ""],
|
|
904
|
+
});
|
|
905
|
+
break;
|
|
906
|
+
case "set-datetime":
|
|
907
|
+
controls.push({
|
|
908
|
+
...base,
|
|
909
|
+
kind: "date-area",
|
|
910
|
+
dtTarget: step.dtTarget ?? "next",
|
|
911
|
+
dtSpec: step.value ?? "",
|
|
912
|
+
});
|
|
913
|
+
break;
|
|
914
|
+
default:
|
|
915
|
+
break;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
if (controls.length === 0)
|
|
919
|
+
return null;
|
|
920
|
+
return {
|
|
921
|
+
primitive: "audit-dialog",
|
|
922
|
+
label: "audit the Repeat dialog against the requested rule (before committing)",
|
|
923
|
+
audit: { shells: DIALOG_SHELLS, groups: DIALOG_GROUP, controls },
|
|
924
|
+
dynamic: true,
|
|
925
|
+
addressing: "title",
|
|
926
|
+
};
|
|
927
|
+
}
|
|
556
928
|
export function makeRepeatingRecipe(targetUuid, frequency, interval, extras = {}) {
|
|
557
929
|
return {
|
|
558
930
|
op: "todo.make-repeating",
|
|
@@ -685,9 +1057,17 @@ export function rescheduleRepeatRecipe(targetUuid, frequency, interval, extras =
|
|
|
685
1057
|
// repeat bar, the popover, and the popover items are best-guess structural
|
|
686
1058
|
// paths derived from the UIC2 AX inventory; the certification pass confirms or
|
|
687
1059
|
// corrects them exactly as the to-do recipes were corrected in UIC1.
|
|
688
|
-
/** The header cell of the project view (row 1 of the content table).
|
|
1060
|
+
/** The header cell of the project view (row 1 of the content table).
|
|
1061
|
+
* positional-ok: the project view's header IS its first content-table row by
|
|
1062
|
+
* construction (UIC2/UIC3) — the row the app renders above the item list — and
|
|
1063
|
+
* this path only ever reaches a CLICK target, never a value. */
|
|
689
1064
|
const PROJECT_HEADER_CELL = `UI element 1 of row 1 of table 1 of scroll area 1 of ${MAIN_WINDOW}`;
|
|
690
|
-
/** The always-visible repeat bar of a repeating project (UIC2/UIC3: text area 2).
|
|
1065
|
+
/** The always-visible repeat bar of a repeating project (UIC2/UIC3: text area 2).
|
|
1066
|
+
* positional-ok: the bar carries no title or description to address it by (UIC2),
|
|
1067
|
+
* so its index in the header cell is the only handle the app offers. It is not a
|
|
1068
|
+
* value-bearing field: the step that uses it is a click whose `assertPath` REQUIRES
|
|
1069
|
+
* the repeat popover to appear afterwards, so a click that lands on the wrong text
|
|
1070
|
+
* area fails closed (Escape + abort) instead of cascading into blind presses. */
|
|
691
1071
|
const PROJECT_REPEAT_BAR = `text area 2 of ${PROJECT_HEADER_CELL}`;
|
|
692
1072
|
/**
|
|
693
1073
|
* The popover opened by clicking the repeat bar. Confirmed by UIC3 discovery: it
|
|
@@ -698,6 +1078,9 @@ const PROJECT_REPEAT_BAR = `text area 2 of ${PROJECT_HEADER_CELL}`;
|
|
|
698
1078
|
* utility window; its items live in the window's scroll area.
|
|
699
1079
|
*/
|
|
700
1080
|
const PROJECT_REPEAT_POPOVER = `(first window whose subrole is "AXUnknown" and size is not {40, 40})`;
|
|
1081
|
+
// positional-ok: a CONTAINER handle only — these custom AXUnknown popovers hold
|
|
1082
|
+
// exactly one scroll area (MEASURED on the sibling Move… picker, HXPC1 §B2:
|
|
1083
|
+
// `scroll areas=1`), and every item inside is addressed by exact AXDescription.
|
|
701
1084
|
const PROJECT_REPEAT_POPOVER_ITEMS = `scroll area 1 of ${PROJECT_REPEAT_POPOVER}`;
|
|
702
1085
|
/** A project view + foreground preamble — the mouse segment needs Things frontmost. */
|
|
703
1086
|
function projectPreamble(targetUuid) {
|
|
@@ -775,6 +1158,8 @@ export function projectRescheduleRepeatRecipe(targetUuid, frequency, interval, e
|
|
|
775
1158
|
...projectPreamble(targetUuid),
|
|
776
1159
|
openProjectRepeatPopover(),
|
|
777
1160
|
popoverItemClick("repeat menu ▸ Change…", "Change…", {
|
|
1161
|
+
// positional-ok: a post-click EXISTENCE assert on the one attached sheet a
|
|
1162
|
+
// window can present; nothing is read or written through this path.
|
|
778
1163
|
path: `sheet 1 of ${MAIN_WINDOW}`,
|
|
779
1164
|
label: "the Repeat dialog",
|
|
780
1165
|
}),
|
|
@@ -794,27 +1179,74 @@ export function projectRescheduleRepeatRecipe(targetUuid, frequency, interval, e
|
|
|
794
1179
|
// repeat popover. The popover (Archive / Move… / Convert to Project… / Delete) is
|
|
795
1180
|
// the same custom AXUnknown-window shape as the repeat popover.
|
|
796
1181
|
//
|
|
797
|
-
//
|
|
798
|
-
//
|
|
799
|
-
//
|
|
800
|
-
//
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
1182
|
+
// CERTIFIED by HXPC1 (2026-08-25, Things 3.23 / golden-v4 —
|
|
1183
|
+
// docs/lab/hxpc1-picker-assert.md), which corrected three of the four provisional
|
|
1184
|
+
// paths the way UIC1 corrected the repeat recipes:
|
|
1185
|
+
// * the `…` button is three levels below the content table, so no `whose`
|
|
1186
|
+
// clause can reach it (§B0) — the click resolves it by walking rows/cells;
|
|
1187
|
+
// * the picker is a DETACHED `MovePopUpDialog-` window, not a sheet of the main
|
|
1188
|
+
// window (§B2), and its filter field is not addressable as a child of it;
|
|
1189
|
+
// * the commit is a CLICK on the destination row, never a Return — the picker
|
|
1190
|
+
// publishes no highlight to read back, and its `New Project "<typed>"` row
|
|
1191
|
+
// turns an absent destination into a newly created project (§B4).
|
|
1192
|
+
// The popover-item enumeration (`whose description is "Move…"`) was the one
|
|
1193
|
+
// provisional path that measured correct: those items are direct children of the
|
|
1194
|
+
// popover's scroll area.
|
|
1195
|
+
/**
|
|
1196
|
+
* The heading `…` "More. <title>" button. It carries the heading's title in its
|
|
1197
|
+
* `AXDescription`, but it sits at `UI element N of cell 1 of row M` of the
|
|
1198
|
+
* content table — three levels down — and a `whose` clause searches only DIRECT
|
|
1199
|
+
* children, so the button is NOT addressable as a path. The click step names the
|
|
1200
|
+
* table here and the description separately (`rowCellDescription`), and the
|
|
1201
|
+
* driver walks rows → cells → children for the exact match (HXPC1 §B0; the
|
|
1202
|
+
* one-level spelling this replaces matched nothing on 3.23, which is why every
|
|
1203
|
+
* ellipsis drive died at its own frame resolution).
|
|
1204
|
+
*/
|
|
1205
|
+
function headingMoreDescription(headingTitle) {
|
|
1206
|
+
return `More. ${headingTitle}`;
|
|
804
1207
|
}
|
|
805
1208
|
/** The ellipsis popover — a custom AXUnknown window (same shape as the repeat popover). */
|
|
806
1209
|
const HEADING_ELLIPSIS_POPOVER = `(first window whose subrole is "AXUnknown" and size is not {40, 40})`;
|
|
1210
|
+
// positional-ok: container handle only — one scroll area per popover (HXPC1 §B2),
|
|
1211
|
+
// items addressed by exact AXDescription.
|
|
807
1212
|
const HEADING_POPOVER_ITEMS = `scroll area 1 of ${HEADING_ELLIPSIS_POPOVER}`;
|
|
808
|
-
/**
|
|
809
|
-
|
|
810
|
-
|
|
1213
|
+
/**
|
|
1214
|
+
* The Move… project picker. NOT a sheet of the main window (the shape this
|
|
1215
|
+
* recipe first assumed) — a DETACHED top-level window of the same custom
|
|
1216
|
+
* `AXUnknown` class as the ellipsis popover, distinguished by an `AXIdentifier`
|
|
1217
|
+
* beginning `MovePopUpDialog-` (HXPC1 §B2). The popover that opened it is gone
|
|
1218
|
+
* by the time it appears, so the generic detached-window selector resolves it
|
|
1219
|
+
* unambiguously; the row-commit script re-checks the identifier before clicking
|
|
1220
|
+
* anything, so a different detached window can never be committed into.
|
|
1221
|
+
*/
|
|
1222
|
+
const HEADING_MOVE_PICKER = `(first window whose subrole is "AXUnknown" and size is not {40, 40})`;
|
|
1223
|
+
/**
|
|
1224
|
+
* What proves the detached window that appeared IS the picker and not the
|
|
1225
|
+
* ellipsis popover it replaced: the picker carries a direct child described
|
|
1226
|
+
* `Move` (its header), the popover carries only its item list. Without this the
|
|
1227
|
+
* post-click assert would be satisfied by the popover that was already open —
|
|
1228
|
+
* i.e. by the click having done nothing (HXPC1 §B2).
|
|
1229
|
+
*/
|
|
1230
|
+
const HEADING_MOVE_PICKER_MARKER = `(first UI element of ${HEADING_MOVE_PICKER} whose description is "Move")`;
|
|
811
1231
|
/**
|
|
812
1232
|
* Move a HEADING to a different project via the ellipsis `Move…` menu (HEADXPROJ).
|
|
813
1233
|
* `sourceProjectUuid` is revealed to render the heading row; `headingTitle` is the
|
|
814
|
-
* `"More. <title>"` click target; `destProjectTitle`
|
|
815
|
-
* then
|
|
816
|
-
* `project` FK becomes the destination; its children follow via their intact
|
|
1234
|
+
* `"More. <title>"` click target; `destProjectTitle` narrows the picker's list and
|
|
1235
|
+
* then names the row that is CLICKED to commit. DB effect (HEADXPROJ): the heading
|
|
1236
|
+
* row's `project` FK becomes the destination; its children follow via their intact
|
|
817
1237
|
* heading FK (a single-row change — no child rewrite, no index churn).
|
|
1238
|
+
*
|
|
1239
|
+
* The commit used to be a Return pressed BLIND on whatever the filter had
|
|
1240
|
+
* highlighted, which is how a move could CREATE a project instead of performing
|
|
1241
|
+
* one: the picker offers a trailing `New Project "<typed text>"` row, and it omits
|
|
1242
|
+
* completed and canceled projects entirely — so a destination our pre-state had
|
|
1243
|
+
* resolved from the database could be absent from the picker, leaving that row as
|
|
1244
|
+
* the only offer. Measured on Things 3.23: the heading landed in a brand-new
|
|
1245
|
+
* second project of the same name (HXPC1 §B4). The picker publishes no selection
|
|
1246
|
+
* or highlight attribute on any row, so there is nothing to read back from a
|
|
1247
|
+
* keyboard commit; the recipe therefore addresses the destination row by its exact
|
|
1248
|
+
* title and clicks it, and fails closed — naming every row the picker offered —
|
|
1249
|
+
* when that row is absent, duplicated, or scrolled out of view.
|
|
818
1250
|
*/
|
|
819
1251
|
export function moveHeadingToProjectRecipe(sourceProjectUuid, headingTitle, destProjectTitle) {
|
|
820
1252
|
return {
|
|
@@ -836,7 +1268,8 @@ export function moveHeadingToProjectRecipe(sourceProjectUuid, headingTitle, dest
|
|
|
836
1268
|
{
|
|
837
1269
|
primitive: "click-element",
|
|
838
1270
|
label: `open the heading's ellipsis menu ("More. ${headingTitle}")`,
|
|
839
|
-
path:
|
|
1271
|
+
path: PROJECT_CONTENT_TABLE,
|
|
1272
|
+
rowCellDescription: headingMoreDescription(headingTitle),
|
|
840
1273
|
assertPath: HEADING_ELLIPSIS_POPOVER,
|
|
841
1274
|
assertLabel: "the heading ellipsis menu",
|
|
842
1275
|
assertTimeoutMs: 5000,
|
|
@@ -847,24 +1280,28 @@ export function moveHeadingToProjectRecipe(sourceProjectUuid, headingTitle, dest
|
|
|
847
1280
|
primitive: "click-element",
|
|
848
1281
|
label: "ellipsis menu ▸ Move…",
|
|
849
1282
|
path: `(first UI element of ${HEADING_POPOVER_ITEMS} whose description is "Move…")`,
|
|
850
|
-
assertPath:
|
|
1283
|
+
assertPath: HEADING_MOVE_PICKER_MARKER,
|
|
851
1284
|
assertLabel: "the Move… project picker",
|
|
852
1285
|
assertTimeoutMs: 5000,
|
|
853
1286
|
dynamic: true,
|
|
854
1287
|
addressing: "title",
|
|
855
1288
|
},
|
|
856
1289
|
{
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1290
|
+
// The picker focuses its own filter field as it opens, and that field is
|
|
1291
|
+
// not addressable as a child of the picker window — so the destination is
|
|
1292
|
+
// typed at the focus. Nothing is committed on the strength of the
|
|
1293
|
+
// keystroke: the next step resolves the destination row by name.
|
|
1294
|
+
primitive: "type-text",
|
|
1295
|
+
label: `narrow the Move… picker to "${destProjectTitle}"`,
|
|
860
1296
|
value: destProjectTitle,
|
|
861
1297
|
dynamic: true,
|
|
862
1298
|
addressing: "title",
|
|
863
1299
|
},
|
|
864
1300
|
{
|
|
865
|
-
primitive: "
|
|
866
|
-
label:
|
|
867
|
-
|
|
1301
|
+
primitive: "click-picker-row",
|
|
1302
|
+
label: `commit the Move… picker on the "${destProjectTitle}" row`,
|
|
1303
|
+
path: HEADING_MOVE_PICKER,
|
|
1304
|
+
value: destProjectTitle,
|
|
868
1305
|
dynamic: true,
|
|
869
1306
|
addressing: "title",
|
|
870
1307
|
},
|
|
@@ -900,7 +1337,8 @@ export function dissolveHeadingRecipe(projectReveal, headingTitle) {
|
|
|
900
1337
|
{
|
|
901
1338
|
primitive: "click-element",
|
|
902
1339
|
label: `open the heading's ellipsis menu ("More. ${headingTitle}")`,
|
|
903
|
-
path:
|
|
1340
|
+
path: PROJECT_CONTENT_TABLE,
|
|
1341
|
+
rowCellDescription: headingMoreDescription(headingTitle),
|
|
904
1342
|
assertPath: HEADING_ELLIPSIS_POPOVER,
|
|
905
1343
|
assertLabel: "the heading ellipsis menu",
|
|
906
1344
|
assertTimeoutMs: 5000,
|