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.
Files changed (154) hide show
  1. package/README.md +3 -1
  2. package/deputy/prebuilt/Things API Helper.app/Contents/CodeResources +0 -0
  3. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/MacOS/things-reader +0 -0
  4. package/deputy/prebuilt/Things API Helper.app/Contents/MacOS/things-deputy +0 -0
  5. package/dist/audit/schema.d.ts +19 -2
  6. package/dist/audit/schema.js.map +1 -1
  7. package/dist/capability.d.ts +166 -4
  8. package/dist/capability.js +303 -13
  9. package/dist/capability.js.map +1 -1
  10. package/dist/cli/commands/doctor.d.ts +13 -1
  11. package/dist/cli/commands/doctor.js +52 -22
  12. package/dist/cli/commands/doctor.js.map +1 -1
  13. package/dist/cli/commands/helpers.js +14 -3
  14. package/dist/cli/commands/helpers.js.map +1 -1
  15. package/dist/cli/commands/mcp.js +8 -0
  16. package/dist/cli/commands/mcp.js.map +1 -1
  17. package/dist/cli/commands/repeat-flags.js +34 -2
  18. package/dist/cli/commands/repeat-flags.js.map +1 -1
  19. package/dist/cli/commands/setup.js +16 -3
  20. package/dist/cli/commands/setup.js.map +1 -1
  21. package/dist/cli/commands/ui-state.d.ts +11 -0
  22. package/dist/cli/commands/ui-state.js +27 -0
  23. package/dist/cli/commands/ui-state.js.map +1 -0
  24. package/dist/cli/commands/writes.js +102 -67
  25. package/dist/cli/commands/writes.js.map +1 -1
  26. package/dist/cli/help.js +2 -0
  27. package/dist/cli/help.js.map +1 -1
  28. package/dist/cli/interrupt.d.ts +54 -6
  29. package/dist/cli/interrupt.js +78 -12
  30. package/dist/cli/interrupt.js.map +1 -1
  31. package/dist/cli/main.js +10 -5
  32. package/dist/cli/main.js.map +1 -1
  33. package/dist/client.d.ts +1 -1
  34. package/dist/contracts.d.ts +4 -4
  35. package/dist/contracts.js +1 -1
  36. package/dist/contracts.js.map +1 -1
  37. package/dist/deputy/install.d.ts +15 -1
  38. package/dist/deputy/install.js +46 -6
  39. package/dist/deputy/install.js.map +1 -1
  40. package/dist/deputy/osa.d.ts +42 -2
  41. package/dist/deputy/osa.js +79 -5
  42. package/dist/deputy/osa.js.map +1 -1
  43. package/dist/deputy/routing.d.ts +34 -0
  44. package/dist/deputy/routing.js +95 -0
  45. package/dist/deputy/routing.js.map +1 -1
  46. package/dist/deputy/wake.d.ts +61 -0
  47. package/dist/deputy/wake.js +149 -0
  48. package/dist/deputy/wake.js.map +1 -0
  49. package/dist/diagnose.d.ts +6 -4
  50. package/dist/diagnose.js +20 -4
  51. package/dist/diagnose.js.map +1 -1
  52. package/dist/direct-setup.d.ts +31 -5
  53. package/dist/direct-setup.js +200 -26
  54. package/dist/direct-setup.js.map +1 -1
  55. package/dist/host-access.d.ts +12 -0
  56. package/dist/host-access.js +12 -0
  57. package/dist/host-access.js.map +1 -1
  58. package/dist/index.d.ts +5 -3
  59. package/dist/index.js +3 -2
  60. package/dist/index.js.map +1 -1
  61. package/dist/mcp/server.js +23 -9
  62. package/dist/mcp/server.js.map +1 -1
  63. package/dist/op-result.js +8 -1
  64. package/dist/op-result.js.map +1 -1
  65. package/dist/session-grant.js +16 -1
  66. package/dist/session-grant.js.map +1 -1
  67. package/dist/surface-copy.d.ts +11 -3
  68. package/dist/surface-copy.js +11 -3
  69. package/dist/surface-copy.js.map +1 -1
  70. package/dist/ui-state.d.ts +46 -0
  71. package/dist/ui-state.js +82 -0
  72. package/dist/ui-state.js.map +1 -0
  73. package/dist/wizard.js +8 -1
  74. package/dist/wizard.js.map +1 -1
  75. package/dist/write/accessibility-probe.js +4 -1
  76. package/dist/write/accessibility-probe.js.map +1 -1
  77. package/dist/write/automation-probe.js +7 -1
  78. package/dist/write/automation-probe.js.map +1 -1
  79. package/dist/write/availability.d.ts +0 -15
  80. package/dist/write/availability.js +7 -62
  81. package/dist/write/availability.js.map +1 -1
  82. package/dist/write/batch.js +17 -6
  83. package/dist/write/batch.js.map +1 -1
  84. package/dist/write/commands.d.ts +10 -6
  85. package/dist/write/commands.js +120 -31
  86. package/dist/write/commands.js.map +1 -1
  87. package/dist/write/failure-hints.d.ts +36 -7
  88. package/dist/write/failure-hints.js +33 -14
  89. package/dist/write/failure-hints.js.map +1 -1
  90. package/dist/write/field-limits.d.ts +100 -0
  91. package/dist/write/field-limits.js +144 -0
  92. package/dist/write/field-limits.js.map +1 -0
  93. package/dist/write/guards.js +3 -2
  94. package/dist/write/guards.js.map +1 -1
  95. package/dist/write/move.js +9 -12
  96. package/dist/write/move.js.map +1 -1
  97. package/dist/write/operations.d.ts +15 -8
  98. package/dist/write/operations.js +9 -0
  99. package/dist/write/operations.js.map +1 -1
  100. package/dist/write/opid.d.ts +67 -23
  101. package/dist/write/opid.js +141 -19
  102. package/dist/write/opid.js.map +1 -1
  103. package/dist/write/param-schema.d.ts +17 -44
  104. package/dist/write/param-schema.js +74 -7
  105. package/dist/write/param-schema.js.map +1 -1
  106. package/dist/write/pipeline.d.ts +38 -7
  107. package/dist/write/pipeline.js +134 -18
  108. package/dist/write/pipeline.js.map +1 -1
  109. package/dist/write/pre-state.d.ts +30 -16
  110. package/dist/write/pre-state.js +88 -20
  111. package/dist/write/pre-state.js.map +1 -1
  112. package/dist/write/promote-clone.d.ts +26 -0
  113. package/dist/write/promote-clone.js +333 -41
  114. package/dist/write/promote-clone.js.map +1 -1
  115. package/dist/write/repeat-collateral.d.ts +31 -0
  116. package/dist/write/repeat-collateral.js +153 -0
  117. package/dist/write/repeat-collateral.js.map +1 -0
  118. package/dist/write/resolution-timestamps.js +25 -2
  119. package/dist/write/resolution-timestamps.js.map +1 -1
  120. package/dist/write/template-mutation.js +163 -16
  121. package/dist/write/template-mutation.js.map +1 -1
  122. package/dist/write/vectors/applescript.js +3 -10
  123. package/dist/write/vectors/applescript.js.map +1 -1
  124. package/dist/write/vectors/simulator.js +8 -2
  125. package/dist/write/vectors/simulator.js.map +1 -1
  126. package/dist/write/vectors/types.d.ts +236 -4
  127. package/dist/write/vectors/ui-certification.js +63 -13
  128. package/dist/write/vectors/ui-certification.js.map +1 -1
  129. package/dist/write/vectors/ui-chord.d.ts +144 -0
  130. package/dist/write/vectors/ui-chord.js +428 -0
  131. package/dist/write/vectors/ui-chord.js.map +1 -0
  132. package/dist/write/vectors/ui-drag.d.ts +8 -4
  133. package/dist/write/vectors/ui-drag.js +2 -0
  134. package/dist/write/vectors/ui-drag.js.map +1 -1
  135. package/dist/write/vectors/ui-recipes.d.ts +51 -3
  136. package/dist/write/vectors/ui-recipes.js +452 -44
  137. package/dist/write/vectors/ui-recipes.js.map +1 -1
  138. package/dist/write/vectors/ui-state.d.ts +157 -0
  139. package/dist/write/vectors/ui-state.js +237 -0
  140. package/dist/write/vectors/ui-state.js.map +1 -0
  141. package/dist/write/vectors/ui.d.ts +386 -13
  142. package/dist/write/vectors/ui.js +1543 -124
  143. package/dist/write/vectors/ui.js.map +1 -1
  144. package/dist/write/vectors/url-scheme.js +3 -0
  145. package/dist/write/vectors/url-scheme.js.map +1 -1
  146. package/dist/write/verify/delta.d.ts +41 -0
  147. package/dist/write/verify/delta.js +36 -2
  148. package/dist/write/verify/delta.js.map +1 -1
  149. package/dist/write/verify/poller.d.ts +4 -1
  150. package/dist/write/verify/poller.js +14 -0
  151. package/dist/write/verify/poller.js.map +1 -1
  152. package/package.json +1 -1
  153. package/schema/envelope.schema.json +2 -1
  154. package/skills/things-cli/SKILL.md +2 -2
@@ -60,7 +60,72 @@ export interface UiRecipe {
60
60
  */
61
61
  needsWindowReachability?: boolean;
62
62
  }
63
- export type UiPrimitive = "reveal" | "activate" | "resolve" | "press" | "set-value" | "select-popup" | "wait" | "key"
63
+ export type UiPrimitive = "reveal" | "activate" | "resolve" | "press" | "set-value"
64
+ /**
65
+ * Set ONE of the Repeat dialog's two numeric fields — the cadence INTERVAL
66
+ * ("Every [n] days") or the ENDS-AFTER COUNT ("Ends: after [n] times") —
67
+ * addressed by the ROW it sits on rather than by its index among the group's
68
+ * text fields (HXPC1). Both used to be spelled `text field 1 of group 1`,
69
+ * which is only correct in the order the create path happens to drive them:
70
+ * the interval is the group's sole text field until an "Ends: after" bound is
71
+ * selected, and the count then takes index 1 with the interval displaced to 2
72
+ * (measured, Things 3.23 — docs/lab/hxpc1-picker-assert.md §A). A RESCHEDULE
73
+ * opens the dialog PRE-POPULATED, so a rule that already ends after N
74
+ * presents both fields from the first step and the index spelling wrote the
75
+ * requested interval into the count field. The row anchor is the group's
76
+ * `Ends:` static text: the count is the field sharing its row, the interval
77
+ * is the field that does not (and after-completion rules, which offer no ends
78
+ * bound at all, have only the interval). {@link UiStep.numberTarget} picks
79
+ * which. Drives with the same focus → select-all → type → Tab → read-back
80
+ * closed loop as set-value, and fails closed when the row anchor resolves
81
+ * anything other than exactly one field.
82
+ */
83
+ | "set-group-number"
84
+ /**
85
+ * Set a Repeat-dialog text field addressed by the pinned English LABEL sharing
86
+ * its row — the same discrimination law as `set-group-number`, for a field that
87
+ * lives on the dialog SHELL rather than in the cadence group. Its one caller is
88
+ * the "and start [n] days earlier" offset the "Add deadlines" checkbox reveals,
89
+ * which shipped as `text field 1` of the shell: a value-bearing field picked by
90
+ * index out of a tree whose shape depends on that checkbox, and verified only by
91
+ * re-reading the same index it wrote — the HXPC1 error class exactly. It was
92
+ * right on Things 3.23 (measured: the shell carries 0 direct text fields with
93
+ * deadlines off and exactly 1 with them on, CGRD1 §B), but nothing in the address
94
+ * said so. {@link UiStep.rowLabel} names the anchor (`days earlier`); a missing
95
+ * label, or anything but exactly one field on its row, fails closed naming the
96
+ * shell's whole text-field inventory.
97
+ */
98
+ | "set-row-field"
99
+ /**
100
+ * PRE-COMMIT FULL-DIALOG AUDIT — re-read every control this drive set, through
101
+ * each control's own discriminated address, and refuse to press OK if any of
102
+ * them does not hold the intended value (CGRD1).
103
+ *
104
+ * Every setter in this vocabulary confirms its own write by re-reading the
105
+ * element it addressed, which proves the keystrokes landed where they were
106
+ * aimed and nothing more; a wrong ADDRESS is invisible to it (#589: the interval
107
+ * drive typed into the ends-count field, then read that field back and found its
108
+ * own number). This step is the outside view. The plan is derived from the
109
+ * recipe's own step list ({@link UiStep.audit}), so a control the recipe drives
110
+ * cannot be left out of the audit by omission, and it compares the dialog's
111
+ * complete intended state — frequency, cadence unit, interval, ends bound and
112
+ * count, deadline/reminder checkboxes, start-days-earlier, the weekday set, the
113
+ * monthly/yearly anchors, the first occurrence and the ends date. A mismatch
114
+ * names every differing control with both values and aborts the drive
115
+ * fail-closed BEFORE the commit, through the usual clean-abort path.
116
+ */
117
+ | "audit-dialog" | "select-popup" | "wait" | "key"
118
+ /**
119
+ * Type literal text into whatever control currently holds focus (HXPC1). Used
120
+ * for the Move… picker's filter field, which the picker focuses for itself the
121
+ * moment it opens: the field is not addressable as a direct child of the
122
+ * picker window, and the set-value primitive's select-all + Tab commit is
123
+ * wrong for a search field anyway (Tab has no next key view in a popover). The
124
+ * keystroke is not the verification — the `click-picker-row` step that follows
125
+ * resolves the intended destination row by name and fails closed if the filter
126
+ * did not produce it, so a keystroke that went astray can never be committed.
127
+ */
128
+ | "type-text"
64
129
  /**
65
130
  * Synthesize a MOUSE click at an AX-resolved element's frame center (the
66
131
  * NATIVE1 JXA ObjC-bridge / HID-tap primitive). Used ONLY for Things' custom
@@ -71,6 +136,28 @@ export type UiPrimitive = "reveal" | "activate" | "resolve" | "press" | "set-val
71
136
  * foreground surface, NATIVE1-e), so a recipe using it must activate first.
72
137
  */
73
138
  | "click-element"
139
+ /**
140
+ * Commit the Move… project picker by CLICKING the row that carries the
141
+ * destination project's exact title — never by pressing Return on whatever the
142
+ * filter happened to highlight (HXPC1). The picker exposes no selection,
143
+ * focus, or highlight attribute on any row, so there is nothing to read back
144
+ * from a keyboard commit; what it does expose is one `AXUnknown` per row whose
145
+ * `AXDescription` IS the project title, plus — whenever text has been typed —
146
+ * a `New Project "<typed text>"` row that CREATES a project when committed.
147
+ * The blind Return committed that row whenever the destination was absent from
148
+ * the picker, which is reachable from an ordinary database-resolved
149
+ * destination: a COMPLETED or CANCELED project is offered nowhere in the
150
+ * picker, so the drive silently created a second project of the same name and
151
+ * moved the heading into it (measured on Things 3.23,
152
+ * docs/lab/hxpc1-picker-assert.md §B4). Addressing the row by exact title
153
+ * cannot match the New-Project row (its description is the quoted form) and
154
+ * fails closed — naming every row the picker DID offer — when the intended one
155
+ * is absent, ambiguous, or scrolled out of its own scroll area (the CNCAC1
156
+ * off-screen-frame hazard). `path` is the picker WINDOW — the resolver reads its
157
+ * `AXIdentifier` to confirm identity before it looks at any row; `value` is the
158
+ * destination title.
159
+ */
160
+ | "click-picker-row"
74
161
  /**
75
162
  * Synthesize a MOUSE DRAG that reorders a sidebar AREA row (the AXDRAG1
76
163
  * primitive). The driver resolves the source row and the destination slot
@@ -80,6 +167,22 @@ export type UiPrimitive = "reveal" | "activate" | "resolve" | "press" | "set-val
80
167
  * bound like click-element.
81
168
  */
82
169
  | "drag-reorder"
170
+ /**
171
+ * Reorder a project's HEADINGS with the arrow chords (CHORDMH1). Things has no
172
+ * menu item, context-menu item or AX action for heading order — the affordance
173
+ * is four bare keybindings on a selected heading row: ⌘↑/⌘↓ move it one slot,
174
+ * ⌘⌥↑/⌘⌥↓ move it to the top/bottom (HEADORD1). The driver selects the row
175
+ * with the pure-AX positional primitive and posts one chord at a time straight
176
+ * to the Things process, reading the heading order back out of the DATABASE
177
+ * after each — the only oracle a bare keybinding offers. Every chord is
178
+ * computed from that read (never fired speculatively: a chord the app declines
179
+ * costs the user an alert beep), and every chord must move exactly the intended
180
+ * heading exactly one slot with no other row's `index` and no child row
181
+ * touched, or the drive stops. Background-capable with no focus steal — the
182
+ * whole gesture runs with Things behind the user's frontmost app. See
183
+ * src/write/vectors/ui-chord.ts.
184
+ */
185
+ | "chord-reorder"
83
186
  /**
84
187
  * Select a PROJECT as a content-table ROW by matching its title, purely via
85
188
  * AX (UIC4-a): the content table's `AXSelectedRows` is settable, so the
@@ -174,6 +277,18 @@ export type UiPrimitive = "reveal" | "activate" | "resolve" | "press" | "set-val
174
277
  * the ≤3.22 `set-datetime next` drive wrote.
175
278
  */
176
279
  | "select-next-occurrence"
280
+ /**
281
+ * Let the Things 3.23 `Next:` occurrence pop-up ABSORB the rule change the
282
+ * preceding steps made, before any further input reaches the dialog
283
+ * (NEXTPOP1). The dialog recomputes that control — its displayed first
284
+ * occurrence AND the menu of occurrences behind it — ASYNCHRONOUSLY, ~0.4s
285
+ * after a calendar-anchor change, and an input that lands inside that window
286
+ * CANCELS the recompute for good: the pop-up goes on describing the PREVIOUS
287
+ * rule indefinitely. The step polls the control until it moves (the common
288
+ * case, ~0.4s) or a bounded budget expires (nothing to absorb), so the next
289
+ * input never races it.
290
+ */
291
+ | "settle-occurrences"
177
292
  /**
178
293
  * Converge the weekly dialog's weekday ROWS onto an exact target set through a
179
294
  * deterministic closed loop (RDLG2, the RRD1 fix): read the live row count,
@@ -224,6 +339,42 @@ export interface UiStep {
224
339
  keys?: string;
225
340
  /** wait: how long to poll for the element before aborting. */
226
341
  timeoutMs?: number;
342
+ /**
343
+ * set-group-number only: WHICH of the Repeat dialog's two numeric fields to
344
+ * drive. `interval` = the cadence field ("Every [n] …"); `ends-count` = the
345
+ * "Ends: after [n] times" field. Each is resolved by the ROW it sits on, so
346
+ * the drive no longer depends on the order the dialog's controls were touched
347
+ * or on whether a reschedule opened it pre-populated. Required for every
348
+ * set-group-number step.
349
+ */
350
+ numberTarget?: "interval" | "ends-count";
351
+ /**
352
+ * set-row-field only: the pinned English static text whose ROW the target field
353
+ * shares (`days earlier` for the start-offset field). Locale fail-closed, like
354
+ * every other title-pinned selector here.
355
+ */
356
+ rowLabel?: string;
357
+ /**
358
+ * audit-dialog only: the complete set of controls to re-read before the commit,
359
+ * with the value the drive intended for each. Built by the recipe from its own
360
+ * emitted steps, so the audit and the drive can never disagree about which
361
+ * controls were touched.
362
+ */
363
+ audit?: DialogAuditPlan;
364
+ /**
365
+ * click-element only: resolve the click target by walking the addressed
366
+ * content TABLE's rows → cells → cell children for the element whose
367
+ * `AXDescription` equals this, instead of resolving `path` itself. The heading
368
+ * row's `…` button is the one control that needs it: it carries its heading's
369
+ * title in `AXDescription` ("More. <title>"), but it sits three levels below
370
+ * the table, and a `whose` clause on `UI elements of <table>` searches only
371
+ * the table's DIRECT children — the rows — so the shipped one-level spelling
372
+ * matched nothing and every ellipsis drive died at its own frame resolution
373
+ * (measured on Things 3.23, docs/lab/hxpc1-picker-assert.md §B0 — the same
374
+ * frame-resolution miss the timestamp cells recorded against the golden-v2
375
+ * rig). The walk is an exact match and fails closed naming what it sought.
376
+ */
377
+ rowCellDescription?: string;
227
378
  /**
228
379
  * click-element only: the element expected to appear right AFTER the click
229
380
  * (a popover opening, a sheet appearing). The driver polls for it and, on
@@ -253,6 +404,8 @@ export interface UiStep {
253
404
  activateFallback?: boolean;
254
405
  /** drag-reorder only: the sidebar move the drag driver performs. */
255
406
  drag?: import("./ui-drag.ts").SidebarDragSpec;
407
+ /** chord-reorder only: the heading order the chord driver steps the project into. */
408
+ chord?: import("./ui-chord.ts").HeadingChordSpec;
256
409
  /**
257
410
  * set-datetime only: WHICH of the dialog's date areas to drive (ANCH2). The
258
411
  * driver selects deterministically — `reminder` = the only time-bearing area;
@@ -287,6 +440,47 @@ export interface UiStep {
287
440
  value?: string;
288
441
  }>>;
289
442
  }
443
+ /**
444
+ * ONE control of the pre-commit dialog audit, as the RECIPE declares it — before
445
+ * the driver has resolved which dialog shell is live or which shape the dialog is.
446
+ * Paths are per-shell candidate lists in the same order as {@link DialogAuditPlan.shells}.
447
+ */
448
+ export interface DialogAuditControl {
449
+ /** Human name of the control, as the mismatch report should say it. */
450
+ label: string;
451
+ kind: "popup" | "checkbox" | "group-number" | "row-field" | "weekdays" | "occurrence-popup" | "date-area";
452
+ /** popup / checkbox / occurrence-popup: per-shell candidate paths. */
453
+ pathCandidates?: string[];
454
+ /** group-number: which of the cadence group's numeric fields. */
455
+ numberTarget?: "interval" | "ends-count";
456
+ /** row-field: the pinned English label sharing the field's row. */
457
+ rowLabel?: string;
458
+ /** weekdays: the group pop-up index of the first weekday row. */
459
+ weekdayBase?: number;
460
+ /** date-area: which of the dialog's `AXDateTimeArea` controls. */
461
+ dtTarget?: "next" | "ends" | "reminder";
462
+ /** date-area: the spec the drive wrote (`date:YYYY-MM-DD` / `time:HH:mm`). */
463
+ dtSpec?: string;
464
+ /** The accepted observed values — ANY one satisfies (the singular/plural pair). */
465
+ expected?: string[];
466
+ /** How the intended value should READ in the report ("checked", not "1"). */
467
+ expectedLabel?: string;
468
+ /** Check this control ONLY under the named dialog shape (the +1-index fork). */
469
+ onlyShape?: RepeatDialogShape;
470
+ /** Per-shape overrides, merged once the dialog shape is measured. */
471
+ shaped?: Partial<Record<RepeatDialogShape, {
472
+ pathCandidates?: string[];
473
+ weekdayBase?: number;
474
+ }>>;
475
+ }
476
+ /** The pre-commit audit an `audit-dialog` step carries. */
477
+ export interface DialogAuditPlan {
478
+ /** The dialog shells, in the SAME priority order the drive resolves them. */
479
+ shells: string[];
480
+ /** The cadence group inside each shell (same order as {@link shells}). */
481
+ groups: string[];
482
+ controls: DialogAuditControl[];
483
+ }
290
484
  /**
291
485
  * The structural SHAPE of the Repeat dialog, MEASURED live (never sniffed from
292
486
  * the app version — a version string says nothing about the tree the driver has
@@ -325,6 +519,16 @@ export interface VectorSupport {
325
519
  notes?: string;
326
520
  }
327
521
  export type VectorMatrix = Partial<Record<OperationKind, VectorSupport>>;
522
+ /**
523
+ * How the ui vector's audited cleanup ladder left an open dialog (issue #620):
524
+ * `none` — nothing was open; `dismissed` — closed, and a fresh read confirmed
525
+ * it closed; `cleared-blind` — cleared by the window close+reopen on an
526
+ * AX-blind session, so it cannot be confirmed; `foreign` — a dialog this drive
527
+ * did not open was found and deliberately LEFT ALONE; `may-remain` — ours, and
528
+ * nothing in the ladder would close it (the stranded case, which also holds
529
+ * Things Cloud sync until someone dismisses it).
530
+ */
531
+ export type UiClearOutcome = "none" | "dismissed" | "cleared-blind" | "foreign" | "may-remain";
328
532
  export interface ExecuteResult {
329
533
  exitCode: number | null;
330
534
  stdout: string;
@@ -358,8 +562,8 @@ export interface ExecuteResult {
358
562
  elapsedMs: number;
359
563
  /** The step the drive was about to run (or running) when the budget blew. */
360
564
  lastStep: string;
361
- /** How the open dialog was cleaned up (SESSGATE clearDialog outcome). */
362
- clear: "dismissed" | "cleared-blind" | "may-remain";
565
+ /** How the open dialog was cleaned up (the audited cleanup ladder's outcome). */
566
+ clear: UiClearOutcome;
363
567
  /** The local trace file reconstructing the timeline, when tracing is on. */
364
568
  tracePath?: string | null;
365
569
  };
@@ -387,7 +591,7 @@ export interface ExecuteResult {
387
591
  */
388
592
  cause: "unreachable" | "unresponsive";
389
593
  /** How a half-open sheet was cleaned up, when the drive had opened one. */
390
- clear?: "dismissed" | "cleared-blind" | "may-remain";
594
+ clear?: UiClearOutcome;
391
595
  /** What the caller does to make a retry work. */
392
596
  remediation: string;
393
597
  };
@@ -406,6 +610,18 @@ export interface WriteVector {
406
610
  * vectors and the simulator, so a caller with no ui vector simply skips the gate.
407
611
  */
408
612
  probeReachability?: () => Promise<import("./session-reachability.ts").ReachabilityVerdict>;
613
+ /**
614
+ * ui vector ONLY: read the window/focus census (read-only — no click, no
615
+ * keystroke, no activation), so a composite ORCHESTRATOR can refuse BEFORE it
616
+ * seeds a row when a dialog is already standing in Things (MODALX1, issue
617
+ * #620). Measured: the seed leg rides the URL scheme, which an open dialog
618
+ * does not touch, so the copy lands and then every AppleScript leg after it
619
+ * fails with `-1728` — the compound mutates for a reason it could have known
620
+ * up front. Null means the state could not be read; the caller proceeds (the
621
+ * drive's own precondition is the backstop). Absent on the real transport
622
+ * vectors and the simulator.
623
+ */
624
+ probeUiState?: () => Promise<import("./ui-state.ts").UiState | null>;
409
625
  /**
410
626
  * The bench-harness SIMULATOR vector (src/write/vectors/simulator.ts). It
411
627
  * presents under a real {@link VectorId} but applies mutations via SQL from
@@ -445,4 +661,20 @@ export interface WriteVector {
445
661
  * imports the developer's own grants into CI).
446
662
  */
447
663
  drivesGui?: boolean;
664
+ /**
665
+ * This vector delivers MUTATIONS through `things:///` URLs, so it is subject
666
+ * to Things' own in-app authorization — Settings ▸ General ▸ "Enable Things
667
+ * URLs" (URLEN1, #611). No macOS consent class is involved, but the two
668
+ * not-enabled states are both silent from the caller's side: an explicitly
669
+ * disabled app discards the command outright, and an app nobody has answered
670
+ * the first-use dialog for PARKS the command behind that dialog. Either way
671
+ * the write reports a silent no-op minutes later, so the pipeline reads the
672
+ * setting prompt-free and refuses before dispatch instead.
673
+ *
674
+ * Set ONLY by {@link import("./url-scheme.ts").createUrlSchemeVector}, for the
675
+ * same reason as its two siblings above: engine tests substitute fakes under
676
+ * the real ids, and a fake that opens no URL must not be gated on the state of
677
+ * the developer's own Things install.
678
+ */
679
+ dispatchesUrls?: boolean;
448
680
  }
@@ -1,7 +1,9 @@
1
1
  import { UI_DRIVE_OPS } from "../operations.js";
2
2
  /** The manifest profile — records the tier + Things build the suite certified. */
3
- export const UI_CERTIFICATION_PROFILE = "UIC1 + UIC3 + AXDRAG2 + UIC5 + UIC6 + UIC7 + UIC7b + HEADCERT1 + HXPC1 + DISS1 in-VM (Things 3.22.11) + " +
4
- "UIC8 promote-via-clone compounds in-VM (golden-v2 / Things 3.22.12) on-device pending";
3
+ export const UI_CERTIFICATION_PROFILE = "UIC1 + UIC3 + AXDRAG2 + UIC5 + UIC6 + UIC7 + UIC7b + HEADCERT1 + HEADXPROJ + DISS1 in-VM (Things 3.22.11) + " +
4
+ "UIC8 promote-via-clone compounds in-VM (golden-v2 / Things 3.22.12) + RDLG2 recipe re-point + " +
5
+ "HXPC1 heading-ellipsis/Move-picker paths in-VM (golden-v4 / Things 3.23) + " +
6
+ "CHORDMH1 heading-order arrow chords in-VM (golden-v4 / Things 3.23) — on-device pending";
5
7
  const CERTIFICATION = {
6
8
  "todo.make-repeating": {
7
9
  status: "lab-certified",
@@ -48,16 +50,59 @@ const CERTIFICATION = {
48
50
  evidence: ["UI2-d", "UIC1-a", "HEADCERT1"],
49
51
  },
50
52
  "project.move-heading-to-project": {
51
- // HXPC1 (2026-07-28, bjhx-lab, Things 3.22.11): the ellipsis Move… recipe ran
52
- // end-to-end in the clone HID-click the "More. <title>" button → Move… →
53
- // type the destination → Return and the exact HEADXPROJ delta was observed:
54
- // heading `HXH` project-FK rewritten HX-PA → HX-PB, both children followed via
55
- // their intact heading FK (project NULL, heading=HXH), single-row change, no
56
- // index churn. The shipped recipe's popover-item AX PATHS (description-based
57
- // click-element) are provisional pending an on-device sitting HXPC1 drove the
58
- // equivalent frame-center synthesis; the DB oracle + recipe flow are certified.
53
+ // HEADXPROJ (2026-07-27, bjhx-lab, Things 3.22.11) established the flow and the
54
+ // DB oracle with a hand-written driver: HID-click the "More. <title>" button →
55
+ // Move… → type the destination → Return, and heading `HXH`'s project FK
56
+ // rewritten HX-PA → HX-PB with both children following via their intact heading
57
+ // FK (project NULL, heading=HXH) — a single-row change, no index churn.
58
+ //
59
+ // HXPC1 (2026-08-25, hxpc1-lab, Things 3.23 / golden-v4) certified the SHIPPED
60
+ // recipe for the first time and corrected three of the four provisional paths:
61
+ // the "More. <title>" button is three levels below the content table (so the
62
+ // shipped one-level `whose` clause matched nothing and the drive never got past
63
+ // its first click), the picker is a detached `MovePopUpDialog-` window rather
64
+ // than a sheet of the main window, and the commit is a CLICK on the row whose
65
+ // title matches exactly — the blind Return could take the picker's
66
+ // `New Project "<typed>"` row whenever the destination was absent from it,
67
+ // which a completed or canceled destination is. 7/7 through the production CLI
68
+ // (clean match, prefix collision, completed-destination refusal with zero
69
+ // mutation). docs/lab/hxpc1-picker-assert.md.
59
70
  status: "lab-certified",
60
- evidence: ["HXPC1"],
71
+ evidence: ["HEADXPROJ", "HXPC1"],
72
+ },
73
+ "project.move-heading": {
74
+ // HEADORD1 (2026-08-25, headord1-lab, Things 3.23 / golden-v4) discovered and
75
+ // characterised the affordance: ⌘↑/⌘↓ = ±1 slot, ⌘⌥↑/⌘⌥↓ = to top/bottom on a
76
+ // selected heading row; a single-row `index` rewrite with no sibling renumber
77
+ // and children untouched behind their FK; a chord with nowhere to go declined
78
+ // with zero delta and one alert beep; System Events modifiers frontmost-only,
79
+ // `CGEventPostToPid` background-capable.
80
+ //
81
+ // CHORDMH1 (2026-08-25, chordmh1-lab, Things 3.23 / golden-v4) certified the
82
+ // SHIPPED op. Its delivery gate re-measured the whole gesture with Things
83
+ // never activated at all — `open -g` reveal, pure-AX row select, pid-posted
84
+ // chord — and Finder frontmost at every stage, so the op ships BACKGROUND
85
+ // delivery with no `activate` step (the least-disruptive tier any ordering op
86
+ // has reached). The certification arms drove the production CLI over
87
+ // 3-heading fixtures: ±1 up, ±1 down, to-top, to-bottom, a multi-hop, the
88
+ // already-in-position no-op, the dry run, and the ungated call — children
89
+ // byte-identical on every arm, sibling indexes byte-identical, zero beeps on
90
+ // the normal paths. docs/lab/chordmh1-move-heading-build.md.
91
+ //
92
+ // CHORDMH2 (2026-08-25, chordmh2-lab, Things 3.23 / golden-v4) lifted CHORDMH1's
93
+ // one deliberate capability cut — the whole-project refusal on any archived
94
+ // heading — after CHORD2 cell 7a′ measured that an archived heading renders no
95
+ // content row, takes no ordinal in the walk, and is skipped by a live heading's
96
+ // ±1. The fence stood in for an ordinal mismatch, so the three reads that feed
97
+ // the plan, the driver and the bare-placement anchor now return the RENDERED
98
+ // (`status = 0`) order. Certified through the production CLI: one chord carried
99
+ // a heading past an archived row's slot, that heading was the only row
100
+ // rewritten, the archived row was byte-untouched on status/index/stopDate/umd,
101
+ // children intact, zero beeps; an archived heading named as a movee or an
102
+ // anchor still refuses with zero mutation.
103
+ // docs/lab/chordmh2-archived-fence-lift.md.
104
+ status: "lab-certified",
105
+ evidence: ["HEADORD1", "CHORDMH1", "CHORDMH2"],
61
106
  },
62
107
  "project.dissolve-heading": {
63
108
  // DISS1 (2026-07-28, bjhx-lab, Things 3.22.11): the ellipsis Delete recipe ran
@@ -66,8 +111,13 @@ const CERTIFICATION = {
66
111
  // TMTask), its 3 children re-homed as DIRECT project children (heading→NULL,
67
112
  // project→parent, index preserved: c1<c2<c3, trashed=0). NO confirm sheet.
68
113
  // Popover items are AX-description-enumerable (the recipe resolves Delete by
69
- // description, scoped to the popover). The shipped recipe's popover AX paths are
70
- // provisional pending an on-device sitting; the DB oracle + flow are certified.
114
+ // description, scoped to the popover) the one provisional path HXPC1 measured
115
+ // CORRECT. Its sibling was not: the "More. <title>" button this drive opens the
116
+ // popover with was addressed one level too shallow, so it resolved nothing and
117
+ // the drive died at its first click on every host. HXPC1 (2026-08-25, Things
118
+ // 3.23) fixed the walk and certified it through the move-heading sibling, but
119
+ // this op was NOT driven end to end there — its next sitting should carry a cell
120
+ // (docs/lab/hxpc1-picker-assert.md §B0/§D). The DB oracle + flow are certified.
71
121
  status: "lab-certified",
72
122
  evidence: ["DISS1"],
73
123
  },
@@ -1 +1 @@
1
- {"version":3,"file":"ui-certification.js","sourceRoot":"","sources":["../../../src/write/vectors/ui-certification.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ui-certification.js","sourceRoot":"","sources":["../../../src/write/vectors/ui-certification.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,144 @@
1
+ import type { DatabaseSync } from "node:sqlite";
2
+ import type { UiCommand, UiRunner } from "./ui.ts";
3
+ /** The move a `chord-reorder` step performs, compiled from the pre-state. */
4
+ export interface HeadingChordSpec {
5
+ /** The project whose headings are being reordered. */
6
+ projectUuid: string;
7
+ /**
8
+ * Every LIVE (`status = 0`) heading of the project, in the order they must end
9
+ * up in — the full end state, not just the moved block. The driver places them
10
+ * left to right, so this doubles as the verification target. Archived headings
11
+ * are absent by construction: they render no row, so they are neither
12
+ * addressable nor displaced (CHORD2 cell 7a′).
13
+ */
14
+ targetOrder: string[];
15
+ /**
16
+ * The headings the caller named. Only these may ever be chorded: if the
17
+ * driver's placement walk would have to move a heading the caller did not
18
+ * name, it refuses rather than touch a bystander.
19
+ */
20
+ movees: string[];
21
+ /** The project view's content table (the recipe supplies the one path). */
22
+ tablePath: string;
23
+ }
24
+ /** The database read the driver asserts against between chords. */
25
+ export interface HeadingOrderState {
26
+ /**
27
+ * The project's LIVE (`status = 0`) headings in `index` order, with their raw
28
+ * index — the rows the project view actually renders, and therefore the rows
29
+ * the positional `select-heading-row` walk enumerates. An ARCHIVED heading is
30
+ * absent: it renders no row, takes no ordinal, and a live heading's ±1 chord
31
+ * skips its slot in one dispatch (CHORD2 cell 7a′). Filtering here is what
32
+ * keeps the driver's ordinals in step with the walk's.
33
+ */
34
+ headings: {
35
+ uuid: string;
36
+ index: number;
37
+ }[];
38
+ /**
39
+ * Digest over every non-trashed child of those headings: `uuid:headingFK:index`.
40
+ * The chord law says children are untouched behind their FK — this is what
41
+ * proves it, and what catches the one measured hazard in the family (a row
42
+ * driven across a heading boundary has its heading FK rewritten, HEADORD1 §2
43
+ * cells 1h4/1i2; it cannot happen to a heading, and this asserts that).
44
+ */
45
+ childDigest: string;
46
+ /** How many child rows the digest covers (reported in the failure text). */
47
+ childCount: number;
48
+ }
49
+ /** The database seam the chord driver reads its ground truth through. */
50
+ export type HeadingOrderReader = (projectUuid: string) => HeadingOrderState;
51
+ /**
52
+ * The client-side default: heading order + child containment, from the open DB.
53
+ *
54
+ * The heading read is filtered to `status = 0` so it returns the RENDERED order
55
+ * (CHORD2 cell 7a′). The child digest is deliberately NOT filtered — it covers
56
+ * every non-trashed child of every heading of the project, archived headings
57
+ * included, because no heading chord may disturb any of them.
58
+ */
59
+ export declare function createHeadingOrderReader(db: DatabaseSync): HeadingOrderReader;
60
+ /**
61
+ * The four chords, as `(key code, CGEventFlags)` pairs. `126`/`125` are the
62
+ * arrow keys; `0x100000` is `kCGEventFlagMaskCommand` and `0x80000`
63
+ * `kCGEventFlagMaskAlternate`. Named rather than inlined so the recipe trace and
64
+ * the failure text can say which chord was posted.
65
+ */
66
+ export type ChordId = "up-one" | "down-one" | "to-top" | "to-bottom";
67
+ export declare function chordGlyph(id: ChordId): string;
68
+ /**
69
+ * Post ONE modifier-bearing key event pair straight at the Things process
70
+ * (`CGEventPostToPid`), through the same JXA ObjC bridge every other synthetic
71
+ * input in this codebase rides.
72
+ *
73
+ * Why not System Events. The shipped `key` primitive emits `key code N` with no
74
+ * modifier support at all, and the System-Events spelling that DOES carry
75
+ * modifiers only lands while Things is FRONTMOST (HEADORD1 cell 1h2b: with
76
+ * Finder frontmost it produced no delta). `CGEventPostToPid` addresses the
77
+ * process rather than the focused surface, so the whole gesture — reveal,
78
+ * select, chord — runs with Things in the background and the user's focus
79
+ * untouched. Measured on Things 3.23 / golden-v4 (HEADORD1 1h2a; re-measured end
80
+ * to end, with Things never activated at all, by CHORDMH1's delivery gate).
81
+ *
82
+ * Note this is the KEYBOARD tap only. NATIVE1-e measured `CGEventPostToPid` as
83
+ * inert for Things' MOUSE hit-testing, which is why click synthesis still uses
84
+ * the HID tap and stays foreground-bound; keyboard events to the pid are a
85
+ * different path and do land.
86
+ */
87
+ export declare function jxaChordScript(id: ChordId): string;
88
+ export declare function chordCommand(id: ChordId): UiCommand;
89
+ /** Select the Nth heading row of the revealed project view (the shipped primitive). */
90
+ export declare function chordSelectCommand(tablePath: string, ordinal: number, script: string): UiCommand;
91
+ export interface ChordDriveResult {
92
+ ok: boolean;
93
+ /** Human-readable outcome (chords posted) or the refusal reason. */
94
+ detail: string;
95
+ /** How many chords were posted before the outcome (partial-state honesty). */
96
+ chords: number;
97
+ }
98
+ /** One scheduled step: which heading to chord, with what, and where it must land. */
99
+ export interface ChordStep {
100
+ /** The heading to select and move — ALWAYS one the caller named. */
101
+ uuid: string;
102
+ chord: ChordId;
103
+ /** The position the heading must occupy afterwards (the progress assertion). */
104
+ landsAt: number;
105
+ }
106
+ /**
107
+ * The next chord that advances `order` towards `target`, or null when they
108
+ * already agree. `movees` is the only set of rows a chord may ever move.
109
+ *
110
+ * THE SCHEDULE, and why it is this one. The obvious walk — "put target[i] into
111
+ * slot i, left to right" — is WRONG here, because target[i] is very often a row
112
+ * the caller never named: asking to move heading A down one slot produces a
113
+ * target whose first element is the BYSTANDER B. Moving B is not an option (it
114
+ * would break the untouched-siblings law the whole vector rests on), and the
115
+ * equally obvious repair — "push whichever movee sits at slot i down" — thrashes
116
+ * forever when two movees sit side by side, each pushing the other back.
117
+ *
118
+ * So the rule fixes the first mismatched slot `i` from the row's own side:
119
+ *
120
+ * - target[i] IS a movee → it is necessarily BELOW slot i (everything above
121
+ * already matches), so step it UP; from slot 0 the one-dispatch ⌘⌥↑ takes it
122
+ * there directly.
123
+ * - target[i] is a bystander `R` → every row between slot i and R's current
124
+ * position is provably a movee (the non-movees keep their relative order, so
125
+ * R is the first non-movee at or below i), and pushing the ONE movee directly
126
+ * above R down past it moves R up by exactly one slot.
127
+ *
128
+ * Both branches strictly reduce a distance that cannot go below zero, so the
129
+ * schedule terminates, and every chord it emits moves a named heading by one
130
+ * slot. Two endpoint shortcuts run first, because they are the common single
131
+ * heading requests and each costs one dispatch instead of a walk: a movee whose
132
+ * target is the very top or the very bottom is sent there with ⌘⌥↑ / ⌘⌥↓.
133
+ */
134
+ export declare function planChordStep(order: readonly string[], target: readonly string[], movees: ReadonlySet<string>): ChordStep | {
135
+ error: string;
136
+ } | null;
137
+ /**
138
+ * Drive a project's headings into `spec.targetOrder`, one verified chord at a
139
+ * time (see {@link planChordStep} for the schedule). Nothing here trusts the
140
+ * plan it made a moment ago: every step is re-derived from a fresh database
141
+ * read, and the step is only counted once the database says it landed where it
142
+ * was aimed with nothing else touched.
143
+ */
144
+ export declare function driveHeadingChordReorder(spec: HeadingChordSpec, run: UiRunner, reader: HeadingOrderReader | undefined, selectScript: (ordinal: number) => string, sleep?: (ms: number) => Promise<void>): Promise<ChordDriveResult>;