things-api 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +3 -3
  2. package/dist/cli/commands/area.d.ts +5 -5
  3. package/dist/cli/commands/area.js +13 -8
  4. package/dist/cli/commands/area.js.map +1 -1
  5. package/dist/cli/commands/project.js +2 -2
  6. package/dist/cli/commands/project.js.map +1 -1
  7. package/dist/cli/commands/reads.js +94 -16
  8. package/dist/cli/commands/reads.js.map +1 -1
  9. package/dist/cli/commands/show.js +6 -2
  10. package/dist/cli/commands/show.js.map +1 -1
  11. package/dist/cli/commands/todo.js +2 -2
  12. package/dist/cli/commands/todo.js.map +1 -1
  13. package/dist/cli/commands/writes.js +136 -70
  14. package/dist/cli/commands/writes.js.map +1 -1
  15. package/dist/cli/glyphs.d.ts +9 -2
  16. package/dist/cli/glyphs.js +20 -15
  17. package/dist/cli/glyphs.js.map +1 -1
  18. package/dist/cli/help.js +4 -2
  19. package/dist/cli/help.js.map +1 -1
  20. package/dist/cli/read-driver.d.ts +45 -9
  21. package/dist/cli/read-driver.js +39 -16
  22. package/dist/cli/read-driver.js.map +1 -1
  23. package/dist/cli/render.d.ts +57 -27
  24. package/dist/cli/render.js +117 -65
  25. package/dist/cli/render.js.map +1 -1
  26. package/dist/client.d.ts +127 -38
  27. package/dist/client.js +86 -22
  28. package/dist/client.js.map +1 -1
  29. package/dist/contracts.d.ts +35 -34
  30. package/dist/contracts.js +1 -1
  31. package/dist/contracts.js.map +1 -1
  32. package/dist/index.d.ts +9 -7
  33. package/dist/index.js +3 -3
  34. package/dist/index.js.map +1 -1
  35. package/dist/mcp/server.js +212 -148
  36. package/dist/mcp/server.js.map +1 -1
  37. package/dist/model/dates.d.ts +26 -0
  38. package/dist/model/dates.js +46 -4
  39. package/dist/model/dates.js.map +1 -1
  40. package/dist/model/entities.d.ts +61 -39
  41. package/dist/model/entities.js +0 -4
  42. package/dist/model/entities.js.map +1 -1
  43. package/dist/model/mappers.js +24 -43
  44. package/dist/model/mappers.js.map +1 -1
  45. package/dist/read/area-filter.d.ts +1 -1
  46. package/dist/read/area-filter.js +5 -4
  47. package/dist/read/area-filter.js.map +1 -1
  48. package/dist/read/area-view.d.ts +0 -11
  49. package/dist/read/area-view.js +7 -11
  50. package/dist/read/area-view.js.map +1 -1
  51. package/dist/read/detail.js +2 -2
  52. package/dist/read/detail.js.map +1 -1
  53. package/dist/read/filter-contract.d.ts +1 -1
  54. package/dist/read/filter-contract.js +3 -0
  55. package/dist/read/filter-contract.js.map +1 -1
  56. package/dist/read/log-boundary.d.ts +46 -2
  57. package/dist/read/log-boundary.js +43 -2
  58. package/dist/read/log-boundary.js.map +1 -1
  59. package/dist/read/predicates.d.ts +13 -0
  60. package/dist/read/predicates.js +13 -0
  61. package/dist/read/predicates.js.map +1 -1
  62. package/dist/read/project-view.d.ts +34 -19
  63. package/dist/read/project-view.js +31 -25
  64. package/dist/read/project-view.js.map +1 -1
  65. package/dist/read/scope.d.ts +1 -1
  66. package/dist/read/scope.js +5 -4
  67. package/dist/read/scope.js.map +1 -1
  68. package/dist/read/search-rank.js +2 -2
  69. package/dist/read/search-rank.js.map +1 -1
  70. package/dist/read/shape.d.ts +78 -18
  71. package/dist/read/shape.js +496 -185
  72. package/dist/read/shape.js.map +1 -1
  73. package/dist/read/snapshot.js +2 -2
  74. package/dist/read/snapshot.js.map +1 -1
  75. package/dist/read/stage.d.ts +31 -13
  76. package/dist/read/stage.js +28 -11
  77. package/dist/read/stage.js.map +1 -1
  78. package/dist/read/truncation.d.ts +75 -2
  79. package/dist/read/truncation.js +69 -46
  80. package/dist/read/truncation.js.map +1 -1
  81. package/dist/read/views.d.ts +81 -18
  82. package/dist/read/views.js +136 -17
  83. package/dist/read/views.js.map +1 -1
  84. package/dist/surface-copy.d.ts +6 -0
  85. package/dist/surface-copy.js +6 -0
  86. package/dist/surface-copy.js.map +1 -1
  87. package/dist/write/batch.js +0 -1
  88. package/dist/write/batch.js.map +1 -1
  89. package/dist/write/clear-reminder.js +3 -1
  90. package/dist/write/clear-reminder.js.map +1 -1
  91. package/dist/write/commands.d.ts +25 -1
  92. package/dist/write/commands.js +350 -134
  93. package/dist/write/commands.js.map +1 -1
  94. package/dist/write/guards.js +36 -16
  95. package/dist/write/guards.js.map +1 -1
  96. package/dist/write/move.d.ts +26 -5
  97. package/dist/write/move.js +358 -2
  98. package/dist/write/move.js.map +1 -1
  99. package/dist/write/operations.d.ts +50 -19
  100. package/dist/write/operations.js +3 -2
  101. package/dist/write/operations.js.map +1 -1
  102. package/dist/write/pipeline.js +28 -7
  103. package/dist/write/pipeline.js.map +1 -1
  104. package/dist/write/pre-state.d.ts +89 -28
  105. package/dist/write/pre-state.js +197 -19
  106. package/dist/write/pre-state.js.map +1 -1
  107. package/dist/write/reorder.js +52 -5
  108. package/dist/write/reorder.js.map +1 -1
  109. package/dist/write/resolution-timestamps.d.ts +34 -0
  110. package/dist/write/resolution-timestamps.js +286 -0
  111. package/dist/write/resolution-timestamps.js.map +1 -0
  112. package/dist/write/reversibility.js +10 -5
  113. package/dist/write/reversibility.js.map +1 -1
  114. package/dist/write/scope-guard.js +2 -5
  115. package/dist/write/scope-guard.js.map +1 -1
  116. package/dist/write/undo.d.ts +1 -7
  117. package/dist/write/undo.js +63 -16
  118. package/dist/write/undo.js.map +1 -1
  119. package/dist/write/vectors/applescript.js +23 -4
  120. package/dist/write/vectors/applescript.js.map +1 -1
  121. package/dist/write/vectors/simulator.js +210 -9
  122. package/dist/write/vectors/simulator.js.map +1 -1
  123. package/dist/write/vectors/url-scheme.js +0 -8
  124. package/dist/write/vectors/url-scheme.js.map +1 -1
  125. package/dist/write/verify/delta.d.ts +71 -12
  126. package/dist/write/verify/delta.js +94 -12
  127. package/dist/write/verify/delta.js.map +1 -1
  128. package/package.json +1 -1
  129. package/schema/envelope.schema.json +19 -70
  130. package/skills/things-cli/SKILL.md +13 -9
  131. package/skills/things-cli/references/contracts.md +3 -4
  132. package/skills/things-cli/references/{model.md → data-model.md} +10 -8
  133. package/skills/things-cli/references/ordering.md +18 -7
package/dist/client.d.ts CHANGED
@@ -5,18 +5,20 @@
5
5
  import type { AuditWriter } from "./audit/log.ts";
6
6
  import { type ThingsApiConfig } from "./config.ts";
7
7
  import { type ClockMeta } from "./model/clock.ts";
8
- import { type Truncation } from "./contracts.ts";
8
+ import { type GroupBlock, type Truncation } from "./contracts.ts";
9
9
  import { type FingerprintStatus, type SchemaStatus } from "./db/fingerprint.ts";
10
10
  import type { AnyTask, Area, Project, Tag } from "./model/entities.ts";
11
11
  import type { RefPromoter } from "./read/shape.ts";
12
12
  import { type AreaView } from "./read/area-view.ts";
13
+ import { type LogState } from "./read/log-boundary.ts";
13
14
  import { type ProjectView } from "./read/project-view.ts";
14
15
  import { type Snapshot } from "./read/snapshot.ts";
15
16
  import { type ShowTarget } from "./read/show-target.ts";
16
- import { type ChangedItem, type InboxFilter, type LiteSearchResult, type ListItem, type LogbookFilter, type SearchOptions, type SearchResultItem, type SidebarSection, type SomedayFilter, type TodayFilter, type TodayView, type UpcomingFilter, type ViewFilter } from "./read/views.ts";
17
+ import { type ChangedItem, type DeadlinesFilter, type InboxFilter, type LiteSearchResult, type ListItem, type LogbookFilter, type SearchOptions, type SearchResultItem, type SidebarSection, type SomedayFilter, type TodayFilter, type TodayView, type UpcomingFilter, type ViewFilter } from "./read/views.ts";
18
+ import { type TodayBucketTotals, type AreaBucketTotals, type SectionTotals, type UpcomingBlockTotals } from "./read/truncation.ts";
17
19
  import { type AreaScopedRead, type ViewFilterMeta } from "./read/area-filter.ts";
18
20
  import { type ScopeMeta } from "./read/scope.ts";
19
- import type { AreaAddParams, AreaUpdateParams, ContainerRef, ProjectCancelParams, OperationKind, OperationParamsMap, ProjectAddParams, ProjectCompleteParams, ProjectAddRepeatingParams, ProjectUpdateParams, RepeatRuleParams, ReorderParams, TagAddParams, TagUpdateParams, HeadingArchiveParams, HeadingUnarchiveParams, HeadingPlacement, TodoAddLoggedParams, TodoAddParams, TodoBackdateParams, TodoMoveParams, TodoUpdateParams } from "./write/operations.ts";
21
+ import type { AreaAddParams, AreaUpdateParams, ContainerRef, ProjectCancelParams, OperationKind, OperationParamsMap, ProjectAddParams, ProjectCompleteParams, ProjectAddRepeatingParams, ProjectUpdateParams, RepeatRuleParams, ReorderParams, TagAddParams, TagUpdateParams, HeadingArchiveParams, HeadingUnarchiveParams, HeadingPlacement, TodoAddParams, TodoMoveParams, TodoUpdateParams } from "./write/operations.ts";
20
22
  import { type MutationResult, type WriteOptions } from "./write/pipeline.ts";
21
23
  import { type BatchItemResult, type BatchOp, type BatchOptions, type BatchResult } from "./write/batch.ts";
22
24
  import { type EnvironmentTracker } from "./write/environment.ts";
@@ -99,33 +101,74 @@ export interface BoundedList<T> {
99
101
  filter?: ViewFilterMeta;
100
102
  }
101
103
  /**
102
- * A bounded today view: `view` is the shown flat `items[]` (capped in comparator
103
- * order) plus the whole-view `counts`, and `truncation` the exact counts,
104
- * including the per-render-section (`today`/`evening`) breakdown derived per row
105
- * from `when` — a renderer needs to stay honest.
104
+ * A bounded Logbook list: the shown logged rows plus the view-level log-move
105
+ * cadence fact ({@link LogState}) surfaced as `meta.logging`, rendered as the
106
+ * card header on a TTY. A sibling of `filter`, never part of the JSON `data`.
107
+ */
108
+ export interface BoundedLogbook extends BoundedList<ListItem> {
109
+ logging: LogState;
110
+ }
111
+ /**
112
+ * A bounded global `upcoming` list: the flat {@link truncateList} slice (rendered
113
+ * as-is, grouped by day in the renderer) plus `upcomingTotals` — the PRE-cap
114
+ * per-day-block sizes keyed by `when`, from which each `data.sections` day block's
115
+ * inline `total` derives (present iff the flat row cap straddled that day, R1).
116
+ */
117
+ export interface BoundedUpcomingList extends BoundedList<ListItem> {
118
+ /** Pre-cap day-block sizes for the wire's inline `total` (see {@link UpcomingBlockTotals}). */
119
+ upcomingTotals: UpcomingBlockTotals;
120
+ }
121
+ /**
122
+ * A bounded Today view: `view` is the shown split (capped in render order —
123
+ * Today, then This Evening), `truncation` the exact global counts, and `totals`
124
+ * the PRE-cap Today/This-Evening bucket sizes — the renderer keeps This Evening
125
+ * honest under the single global cap, and each `children` bucket's inline `total`
126
+ * (present iff capped, read-shape v2 R1) derives from them.
106
127
  */
107
128
  export interface BoundedTodayView {
108
129
  view: TodayView;
109
130
  truncation: Truncation;
131
+ /** Pre-cap Today / This-Evening bucket sizes (see {@link TodayBucketTotals}). */
132
+ totals: TodayBucketTotals;
110
133
  /** The active `area` scope, when one was applied (surfaced as `meta.filter`). */
111
134
  filter?: ViewFilterMeta;
112
135
  }
113
136
  /**
114
137
  * A bounded sidebar catalogue (anytime/someday): `view` is the
115
- * per-block-capped sections and `truncation` the unified completeness metadata
116
- * whose `blocks` carry the identity-bearing per-block counts (project blocks
117
- * nested under their area/loose block).
138
+ * per-block-capped sections and `truncation` the WHOLE-VIEW completeness rollup
139
+ * (`{shown,total,limit,truncated}`). Per-section completeness rides its inline
140
+ * `total` on the wire (stamped from `sectionTotals`, R1); `blocks` carries the
141
+ * identity-bearing per-block counts (project blocks nested under their area/loose
142
+ * block) as INTERNAL render plumbing only — the grouped renderers' "… N more"
143
+ * drill-downs — never on the wire (doctrine v2 PR 5: the sidecar retired).
118
144
  */
119
145
  export interface BoundedSectionsView {
120
146
  view: SidebarSection[];
121
147
  truncation: Truncation;
148
+ /** Per-block render detail (TTY drill-downs); never serialized. */
149
+ blocks: GroupBlock[];
150
+ /**
151
+ * Pre-cap per-section sizes keyed by area uuid (`null` = loose) — the inline
152
+ * `total` a consumer stamps on each capped section ({@link withSectionTotals},
153
+ * R1), so completeness is answerable locally with no `blocks[]` sidecar.
154
+ */
155
+ sectionTotals: SectionTotals;
122
156
  /** The active `area` scope, when one was applied (surfaced as `meta.filter`). */
123
157
  filter?: ViewFilterMeta;
124
158
  }
125
- /** A bounded composite area card: the per-section-capped view and the per-block truncation. */
159
+ /** A bounded composite area card: the per-section-capped view and the whole-view rollup. */
126
160
  export interface BoundedAreaView {
127
161
  view: AreaView;
128
162
  truncation: Truncation;
163
+ /** Per-block render detail (the `area show` "… N more" footers); never serialized. */
164
+ blocks: GroupBlock[];
165
+ /**
166
+ * Pre-cap scope sizes (read-shape v2 R1, PR 3) — the direct-to-dos and
167
+ * project-rows counts a consumer stamps as each capped scope's inline `total`
168
+ * ({@link withAreaBucketTotals}), so completeness is answerable locally with no
169
+ * `meta.truncation.blocks[]` sidecar.
170
+ */
171
+ totals: AreaBucketTotals;
129
172
  /**
130
173
  * A resolution disclosure — present ONLY on the `loose` pseudo-area read when
131
174
  * a real area shadows the reserved word (names it, by uuid, for targeting).
@@ -141,6 +184,16 @@ export interface BoundedAreaView {
141
184
  */
142
185
  loggedCount?: number;
143
186
  }
187
+ /**
188
+ * Resolution-timestamp flags (plan §2): an ISO date (`2025-01-15`) or datetime
189
+ * (`2025-01-15T09:30`). `completedAt` sets the completion timestamp (also the
190
+ * "Completed on" stamp for canceled items); `createdAt` backdates creation. A
191
+ * date-only value normalizes to noon in the effective zone (§5).
192
+ */
193
+ export interface ResolutionDates {
194
+ createdAt?: string;
195
+ completedAt?: string;
196
+ }
144
197
  export interface ThingsClient {
145
198
  dbPath: string;
146
199
  config: ThingsApiConfig;
@@ -192,11 +245,11 @@ export interface ThingsClient {
192
245
  refPromoter(): RefPromoter;
193
246
  read: {
194
247
  /**
195
- * The Today list one flat `items[]` of Today members (Today-proper and
196
- * This-Evening interleaved in comparator order, each row's `when` carrying
197
- * which) plus the whole-view `counts` aggregate, bounded to `limit` rows
198
- * (default 50). `all`/`limit: null` returns every row; the `truncation`
199
- * metadata carries the per-render-section (`today`/`evening`) counts.
248
+ * The Today list (Today + This Evening split) plus the whole-view `counts`
249
+ * aggregate (the app's sidebar count), bounded to `limit` rows (default 50)
250
+ * counted in render order Today first, then This Evening. `all`/`limit:
251
+ * null` returns every row; `totals` carries the pre-cap Today/This-Evening
252
+ * bucket sizes (the two `children` bucket records project from the split).
200
253
  */
201
254
  today(options?: TodayFilter & ListBound & ClockScopedRead & AreaScopedRead): BoundedTodayView;
202
255
  /** Inbox captures, bounded (default 50). */
@@ -208,8 +261,8 @@ export interface ThingsClient {
208
261
  * catalogue to one area (its rows survive; the rest drop).
209
262
  */
210
263
  anytime(options?: ViewFilter & GroupedBound & ClockScopedRead & AreaScopedRead): BoundedSectionsView;
211
- /** Future-scheduled items in date order, bounded (default 50). */
212
- upcoming(options?: UpcomingFilter & ListBound & ClockScopedRead & AreaScopedRead): BoundedList<ListItem>;
264
+ /** Future-scheduled items in date order, bounded (default 50); day-block totals for R1. */
265
+ upcoming(options?: UpcomingFilter & ListBound & ClockScopedRead & AreaScopedRead): BoundedUpcomingList;
213
266
  /**
214
267
  * Someday catalogue: `areaLimit` (default 30) caps each group; with
215
268
  * `activeProjectItems`, `projectLimit` (default: every item) caps each
@@ -217,8 +270,13 @@ export interface ThingsClient {
217
270
  * the catalogue to one area.
218
271
  */
219
272
  someday(options?: SomedayFilter & GroupedBound & ClockScopedRead & AreaScopedRead): BoundedSectionsView;
220
- /** Logbook entries (most recent first), bounded (default 50). */
221
- logbook(options?: Omit<LogbookFilter, "limit"> & ListBound & ClockScopedRead): BoundedList<ListItem>;
273
+ /**
274
+ * Logbook entries (most recent first), bounded (default 50), plus the
275
+ * view-level log-move cadence fact ({@link LogState}) surfaced as
276
+ * `meta.logging` — the "Move completed items to Logbook" setting in CC's own
277
+ * words, and the last-logged instant under Manually.
278
+ */
279
+ logbook(options?: Omit<LogbookFilter, "limit"> & ListBound & ClockScopedRead): BoundedLogbook;
222
280
  /** Trashed items (most recently modified first), bounded (default 50). */
223
281
  trash(options?: ListBound & ClockScopedRead): BoundedList<ListItem>;
224
282
  /**
@@ -268,6 +326,16 @@ export interface ThingsClient {
268
326
  changes(options: {
269
327
  since: Date;
270
328
  } & ListBound & ClockScopedRead): BoundedList<ChangedItem>;
329
+ /**
330
+ * The deadline-horizon view: every live to-do AND project carrying a
331
+ * deadline, ordered deadline ASC (most-overdue first), then todayIndex, then
332
+ * uuid; deadline-bearing repeating templates project at their next
333
+ * occurrence's deadline. Bounded (default 50). `todayOnly` restricts to
334
+ * current Today members (evening-inclusive); `overdue` keeps only open,
335
+ * past-deadline rows (and excludes projections); `project`/`area` scope to a
336
+ * container; the tag filters compose.
337
+ */
338
+ deadlines(options?: DeadlinesFilter & ListBound & ClockScopedRead): BoundedList<ListItem>;
271
339
  byUuid(uuid: string): AnyTask | null;
272
340
  /**
273
341
  * Classify a loose reference (uuid, >=6-char prefix, share link, or
@@ -281,9 +349,20 @@ export interface ThingsClient {
281
349
  /** Generic entry: run any cataloged operation. */
282
350
  run<K extends OperationKind>(op: K, params: OperationParamsMap[K], options?: WriteOptions): Promise<MutationResult>;
283
351
  addTodo(params: TodoAddParams, options?: WriteOptions): Promise<MutationResult>;
284
- updateTodo(uuid: string, patch: Omit<TodoUpdateParams, "uuid">, options?: WriteOptions): Promise<MutationResult>;
285
- completeTodo(uuid: string, options?: WriteOptions): Promise<MutationResult>;
286
- cancelTodo(uuid: string, options?: WriteOptions): Promise<MutationResult>;
352
+ updateTodo(uuid: string, patch: Omit<TodoUpdateParams, "uuid"> & ResolutionDates, options?: WriteOptions): Promise<MutationResult>;
353
+ /**
354
+ * Complete a to-do. With `resolution.completedAt` (ISO date or datetime) the
355
+ * to-do lands completed and BACKDATED — resolving it first if needed, then
356
+ * an AppleScript completion-date write; a date-only value normalizes to noon
357
+ * in the effective zone (§5). Multi-leg, disclosed in the result / dry-run.
358
+ */
359
+ completeTodo(uuid: string, resolution?: ResolutionDates, options?: WriteOptions): Promise<MutationResult>;
360
+ /**
361
+ * Cancel a to-do. With `resolution.completedAt` it lands canceled and
362
+ * BACKDATED via the certified flip-dance (→completed · AS backdate · →canceled)
363
+ * — the only headless path that keeps a canceled item canceled.
364
+ */
365
+ cancelTodo(uuid: string, resolution?: ResolutionDates, options?: WriteOptions): Promise<MutationResult>;
287
366
  reopenTodo(uuid: string, options?: WriteOptions): Promise<MutationResult>;
288
367
  moveTodo(uuid: string, dest: Omit<TodoMoveParams, "uuid">, options?: WriteOptions): Promise<MutationResult>;
289
368
  /**
@@ -303,6 +382,18 @@ export interface ThingsClient {
303
382
  * fail closed.
304
383
  */
305
384
  reorderTodos(request: ReorderRequest, options?: WriteOptions): Promise<MoveResult>;
385
+ /**
386
+ * The ONE reorder verb (`things reorder`). Rearranges a single-KIND set IN
387
+ * PLACE — to-dos, projects, headings, OR sidebar areas — dispatching the
388
+ * protocol by kind: the index/day/view engine for to-dos and projects (a
389
+ * to-do+project set intermixes only on the shared Today/Evening/day axes), the
390
+ * certified heading-block wire for a project's headings (archived headings
391
+ * reorderable, reopens disclosed — #V11), and the sidebar-drag driver for
392
+ * areas. A mixed-kind set, a cross-container set, and a non-member anchor each
393
+ * get one precise refusal. Bare (no position) assembles the block at the
394
+ * earliest movee's slot; --start/--end/--before/--after position it.
395
+ */
396
+ reorderAny(request: ReorderRequest, options?: WriteOptions): Promise<MoveResult>;
306
397
  /** Replace the full tag set (an empty list clears all tags). */
307
398
  setTags(uuid: string, tags: string[], options?: WriteOptions): Promise<MutationResult>;
308
399
  /** Merge: current direct tags + new ones, then replace. */
@@ -321,17 +412,6 @@ export interface ThingsClient {
321
412
  * Reversible with `undo`. Force a path with `vector: "shortcuts" | "url-scheme"`.
322
413
  */
323
414
  clearReminder(uuid: string, options?: WriteOptions): Promise<MutationResult>;
324
- /**
325
- * Rewrite a to-do's completion and/or creation timestamp to noon (local)
326
- * on the given date. Completion requires the to-do to be completed or
327
- * canceled already.
328
- */
329
- backdateTodo(uuid: string, dates: Omit<TodoBackdateParams, "uuid">, options?: WriteOptions): Promise<MutationResult>;
330
- /**
331
- * Create a to-do directly in the Logbook, completed, with backdated
332
- * completion (and optionally creation) timestamps.
333
- */
334
- addLoggedTodo(params: TodoAddLoggedParams, options?: WriteOptions): Promise<MutationResult>;
335
415
  /**
336
416
  * Create a heading inside an EXISTING project; the new heading's uuid is
337
417
  * on the result. Delivered through the Things proxy shortcuts (run
@@ -391,8 +471,9 @@ export interface ThingsClient {
391
471
  */
392
472
  editChecklist(uuid: string, edit: ChecklistEdit, options?: WriteOptions): Promise<MutationResult>;
393
473
  addProject(params: ProjectAddParams, options?: WriteOptions): Promise<MutationResult>;
394
- updateProject(uuid: string, patch: Omit<ProjectUpdateParams, "uuid">, options?: WriteOptions): Promise<MutationResult>;
395
- completeProject(uuid: string, policy: Pick<ProjectCompleteParams, "children">, options?: WriteOptions): Promise<MutationResult>;
474
+ updateProject(uuid: string, patch: Omit<ProjectUpdateParams, "uuid"> & ResolutionDates, options?: WriteOptions): Promise<MutationResult>;
475
+ /** Complete a project; `resolution.completedAt` backdates it (see completeTodo). */
476
+ completeProject(uuid: string, policy: Pick<ProjectCompleteParams, "children"> & ResolutionDates, options?: WriteOptions): Promise<MutationResult>;
396
477
  /** Move a project to another area. */
397
478
  moveProject(uuid: string, area: ContainerRef, options?: WriteOptions): Promise<MutationResult>;
398
479
  /** Detach a project from its current area. */
@@ -405,8 +486,8 @@ export interface ThingsClient {
405
486
  moveProjects(request: ProjectMoveRequest, options?: WriteOptions): Promise<MoveResult>;
406
487
  /** Reorder projects IN PLACE among their siblings (spec §4). */
407
488
  reorderProjects(request: ReorderRequest, options?: WriteOptions): Promise<MoveResult>;
408
- /** Cancel a project — open children are canceled with it, so the children policy is mandatory. */
409
- cancelProject(uuid: string, policy: Pick<ProjectCancelParams, "children">, options?: WriteOptions): Promise<MutationResult>;
489
+ /** Cancel a project — open children are canceled with it, so the children policy is mandatory. `resolution.completedAt` backdates it (flip-dance). */
490
+ cancelProject(uuid: string, policy: Pick<ProjectCancelParams, "children"> & ResolutionDates, options?: WriteOptions): Promise<MutationResult>;
410
491
  /**
411
492
  * Reopen a completed/canceled project. Children stay resolved unless
412
493
  * restoreChildren reopens the ones resolved together with the project.
@@ -446,6 +527,14 @@ export interface ThingsClient {
446
527
  updateTag(target: string, patch: Omit<TagUpdateParams, "target">, options?: WriteOptions): Promise<MutationResult>;
447
528
  deleteTag(target: string, options?: WriteOptions): Promise<MutationResult>;
448
529
  emptyTrash(options?: WriteOptions): Promise<MutationResult>;
530
+ /**
531
+ * Move completed items into the Logbook now (`log completed now`). The result
532
+ * discloses how many items were logged (`observed.logged`); when nothing is
533
+ * pending it is a clean no-op (`logged: 0`, not an error). IRREVERSIBLE — the
534
+ * log-move boundary cannot be rewound by any official surface, so no undo token
535
+ * is emitted.
536
+ */
537
+ logNow(options?: WriteOptions): Promise<MutationResult>;
449
538
  /**
450
539
  * Reorder within Today / This Evening / a project / an area. Partial
451
540
  * uuid lists are placed on top; the rest keep their current order.
package/dist/client.js CHANGED
@@ -11,12 +11,13 @@ import { byUuid } from "./read/detail.js";
11
11
  import { makeRefPromoter, resolveAreaUuid, resolveHeadingUuid, resolveProjectUuid, resolveTaskUuidPrefix, } from "./read/queries.js";
12
12
  import { areaView } from "./read/area-view.js";
13
13
  import { isLooseRef, looseShadowNotice, shadowingLooseArea } from "./read/pseudo-area.js";
14
+ import { logState } from "./read/log-boundary.js";
14
15
  import { projectView } from "./read/project-view.js";
15
16
  import { snapshotView } from "./read/snapshot.js";
16
17
  import { classifyShowTarget } from "./read/show-target.js";
17
18
  import { areasView, tagsView } from "./read/tags.js";
18
- import { anytimeView, areaLoggedCount, changesView, inboxView, liteTitleSearch, logbookView, projectsView, searchView, somedayView, todayView, trashView, upcomingView, } from "./read/views.js";
19
- import { capAreaSections, previewSections, previewSomedaySections, truncateList, truncateToday, } from "./read/truncation.js";
19
+ import { anytimeView, areaLoggedCount, changesView, deadlinesView, inboxView, liteTitleSearch, logbookView, projectsView, searchView, somedayView, todayView, trashView, upcomingView, } from "./read/views.js";
20
+ import { capAreaSections, previewSections, previewSomedaySections, truncateList, truncateToday, upcomingBlockTotals, } from "./read/truncation.js";
20
21
  import { filterListByArea, filterSectionsByArea, filterTodayByArea, resolveAreaFilter, } from "./read/area-filter.js";
21
22
  import { filterListByScope, filterSectionsByScope, filterTodayByScope, inScopeItem, namedAreaClause, namedProjectClause, resolveScope, scopeMeta, taskMembershipClause, } from "./read/scope.js";
22
23
  import { localToday } from "./model/dates.js";
@@ -31,8 +32,9 @@ import { runAddHeading, runHeadingArchive, runHeadingUnarchive, } from "./write/
31
32
  import { runClearReminder } from "./write/clear-reminder.js";
32
33
  import { runEditChecklist } from "./write/edit-checklist.js";
33
34
  import { runAddRepeatingProject, runMakeRepeatingProject } from "./write/make-repeating-project.js";
35
+ import { runCancelWithDate, runCompleteWithDate, runUpdateDates, } from "./write/resolution-timestamps.js";
34
36
  import { runReorder } from "./write/reorder.js";
35
- import { runInPlaceReorder, runProjectMove, runTodoMove, } from "./write/move.js";
37
+ import { runInPlaceReorder, runProjectMove, runTodoMove, runUniversalReorder, } from "./write/move.js";
36
38
  import { readAuditRecords, runUndo } from "./write/undo.js";
37
39
  import { runProjectReopen, } from "./write/reopen.js";
38
40
  import { defaultVectors } from "./write/vectors/registry.js";
@@ -182,6 +184,26 @@ export function openThings(options = {}) {
182
184
  // to the effective zone before dispatch (a no-op without a zone).
183
185
  return runMutation(writeDeps, op, params, { ...writeOptions, normalizeWhen: true });
184
186
  };
187
+ // update dispatcher: the resolution-timestamp flags route through the
188
+ // multi-leg orchestrator; plain attribute edits stay on `todo/project.update`.
189
+ // When BOTH are present the attribute edit lands first (its own undo), then
190
+ // the timestamp legs — two independently-undoable changes.
191
+ const runUpdate = async (kind, uuid, patch, o) => {
192
+ const { createdAt, completedAt, ...attrs } = patch;
193
+ const op = kind === "project" ? "project.update" : "todo.update";
194
+ if (createdAt === undefined && completedAt === undefined) {
195
+ return run(op, { uuid, ...attrs }, o);
196
+ }
197
+ if (Object.keys(attrs).length > 0) {
198
+ const attrResult = await run(op, { uuid, ...attrs }, o);
199
+ if (attrResult.kind !== "ok" && attrResult.kind !== "dry-run")
200
+ return attrResult;
201
+ }
202
+ return runUpdateDates(writeDeps, kind, uuid, {
203
+ ...(createdAt !== undefined && { createdAt }),
204
+ ...(completedAt !== undefined && { completedAt }),
205
+ }, o ?? {});
206
+ };
185
207
  return {
186
208
  dbPath: located.path,
187
209
  config,
@@ -227,8 +249,8 @@ export function openThings(options = {}) {
227
249
  view = filterTodayByArea(view, target.uuid, localToday(now(), zoneOf(o)));
228
250
  filter = { area: target };
229
251
  }
230
- const { data, truncation } = truncateToday(view, listCap(o));
231
- return { view: data, truncation, ...(filter !== undefined && { filter }) };
252
+ const { data, truncation, totals } = truncateToday(view, listCap(o));
253
+ return { view: data, truncation, totals, ...(filter !== undefined && { filter }) };
232
254
  },
233
255
  inbox: (o) => {
234
256
  let items = inboxView(conn.db, now(), o, zoneOf(o));
@@ -249,8 +271,14 @@ export function openThings(options = {}) {
249
271
  sections = filterSectionsByArea(sections, target.uuid);
250
272
  filter = { area: target };
251
273
  }
252
- const { data, truncation } = previewSections(sections, groupedCaps(o, AREA_PREVIEW_LIMIT, PROJECT_PREVIEW_LIMIT));
253
- return { view: data, truncation, ...(filter !== undefined && { filter }) };
274
+ const { data, truncation, blocks, totals } = previewSections(sections, groupedCaps(o, AREA_PREVIEW_LIMIT, PROJECT_PREVIEW_LIMIT));
275
+ return {
276
+ view: data,
277
+ truncation,
278
+ blocks,
279
+ sectionTotals: totals,
280
+ ...(filter !== undefined && { filter }),
281
+ };
254
282
  },
255
283
  upcoming: (o) => {
256
284
  let items = upcomingView(conn.db, now(), o, zoneOf(o));
@@ -262,8 +290,11 @@ export function openThings(options = {}) {
262
290
  items = filterListByArea(items, target.uuid);
263
291
  filter = { area: target };
264
292
  }
293
+ // Pre-cap per-day-block sizes for the wire's inline `total` (R1): counted
294
+ // over the in-scope, area-filtered stream BEFORE the flat row cap slices it.
295
+ const upcomingTotals = upcomingBlockTotals(items);
265
296
  const { data, truncation } = truncateList(items, listCap(o));
266
- return { items: data, truncation, ...(filter !== undefined && { filter }) };
297
+ return { items: data, truncation, upcomingTotals, ...(filter !== undefined && { filter }) };
267
298
  },
268
299
  someday: (o) => {
269
300
  let sections = somedayView(conn.db, now(), o, zoneOf(o));
@@ -275,8 +306,14 @@ export function openThings(options = {}) {
275
306
  sections = filterSectionsByArea(sections, target.uuid);
276
307
  filter = { area: target };
277
308
  }
278
- const { data, truncation } = previewSomedaySections(sections, groupedCaps(o, AREA_PREVIEW_LIMIT, null));
279
- return { view: data, truncation, ...(filter !== undefined && { filter }) };
309
+ const { data, truncation, blocks, totals } = previewSomedaySections(sections, groupedCaps(o, AREA_PREVIEW_LIMIT, null));
310
+ return {
311
+ view: data,
312
+ truncation,
313
+ blocks,
314
+ sectionTotals: totals,
315
+ ...(filter !== undefined && { filter }),
316
+ };
280
317
  },
281
318
  logbook: (o) => {
282
319
  // The bound is the truncation cap; the underlying query stays unbounded
@@ -293,7 +330,12 @@ export function openThings(options = {}) {
293
330
  // only resolves the target for the additive `filter` annotation so the
294
331
  // meta shape matches the post-filtered views.
295
332
  const areaFilter = o?.area !== undefined ? { area: areaFilterTarget(o.area) } : undefined;
296
- return { items: data, truncation, ...(areaFilter !== undefined && { filter: areaFilter }) };
333
+ return {
334
+ items: data,
335
+ truncation,
336
+ logging: logState(conn.db),
337
+ ...(areaFilter !== undefined && { filter: areaFilter }),
338
+ };
297
339
  },
298
340
  trash: (o) => {
299
341
  let items = trashView(conn.db, now(), { limit: null }, zoneOf(o));
@@ -340,7 +382,7 @@ export function openThings(options = {}) {
340
382
  : { where: "0", binds: [] };
341
383
  resolveAreaUuid(conn.db, ref, { scopeWhere: clause.where, scopeBinds: clause.binds });
342
384
  }
343
- const { data, truncation } = capAreaSections(areaView(conn.db, ref, now(), o ?? {}, zoneOf(o)), groupedCaps(o, AREA_PREVIEW_LIMIT, AREA_PREVIEW_LIMIT), now(), zoneOf(o));
385
+ const { data, truncation, blocks, totals } = capAreaSections(areaView(conn.db, ref, now(), o ?? {}, zoneOf(o)), groupedCaps(o, AREA_PREVIEW_LIMIT, AREA_PREVIEW_LIMIT), now(), zoneOf(o));
344
386
  // Reserved-word disclosure: `loose` ALWAYS wins over a real area named
345
387
  // "Loose"; when one shadows, name it (by uuid) so it stays targetable.
346
388
  const shadow = isLooseRef(ref) ? shadowingLooseArea(conn.db) : undefined;
@@ -353,6 +395,8 @@ export function openThings(options = {}) {
353
395
  return {
354
396
  view: data,
355
397
  truncation,
398
+ blocks,
399
+ totals,
356
400
  ...(shadow !== undefined && { notice: looseShadowNotice(shadow) }),
357
401
  ...(loggedCount !== undefined && { loggedCount }),
358
402
  };
@@ -396,11 +440,25 @@ export function openThings(options = {}) {
396
440
  const { data, truncation } = truncateList(items, listCap(o));
397
441
  return { items: data, truncation };
398
442
  },
443
+ deadlines: (o) => {
444
+ const { limit: _limit, ...filter } = o ?? {};
445
+ // The view is computed UNBOUNDED (project/area/tag/overdue/today scoping
446
+ // happens inside it); the whole-view truncation is applied here so the
447
+ // total behind the cut is honest.
448
+ let items = deadlinesView(conn.db, now(), filter, zoneOf(o));
449
+ // A container jail is an additive post-filter (parity with search): keep
450
+ // only in-scope rows so an out-of-scope deadline never leaks.
451
+ if (scope !== undefined)
452
+ items = items.filter((i) => inScopeItem(i, scope));
453
+ const { data, truncation } = truncateList(items, listCap(o));
454
+ return { items: data, truncation };
455
+ },
399
456
  showTarget: (ref) => classifyShowTarget(conn.db, ref, scope),
400
457
  byUuid: (uuid) => {
401
458
  // Prefix-friendly: unknown refs keep the null contract; ambiguity throws.
402
- // The injected clock gates `todaySection` to Today members under the
403
- // consumer's own today (a pinned-clock/lab run reads honestly). Under a
459
+ // The injected clock gates the today/evening markers (and reminder
460
+ // liveness) under the consumer's own today (a pinned-clock/lab run reads
461
+ // honestly). Under a
404
462
  // scope, an out-of-scope uuid resolves to not-found → null (parity).
405
463
  try {
406
464
  return byUuid(conn.db, resolveTaskUuidPrefix(conn.db, uuid, "to-do", scopeClauses?.task), now(), defaultZone);
@@ -423,13 +481,14 @@ export function openThings(options = {}) {
423
481
  write: {
424
482
  run,
425
483
  addTodo: (params, o) => run("todo.add", params, o),
426
- updateTodo: (uuid, patch, o) => run("todo.update", { uuid, ...patch }, o),
427
- completeTodo: (uuid, o) => run("todo.complete", { uuid }, o),
428
- cancelTodo: (uuid, o) => run("todo.cancel", { uuid }, o),
484
+ updateTodo: (uuid, patch, o) => runUpdate("todo", uuid, patch, o),
485
+ completeTodo: (uuid, resolution, o) => runCompleteWithDate(writeDeps, "todo", uuid, resolution ?? {}, o ?? {}),
486
+ cancelTodo: (uuid, resolution, o) => runCancelWithDate(writeDeps, "todo", uuid, resolution ?? {}, o ?? {}),
429
487
  reopenTodo: (uuid, o) => run("todo.reopen", { uuid }, o),
430
488
  moveTodo: (uuid, dest, o) => run("todo.move", { uuid, ...dest }, o),
431
489
  moveTodos: (request, o) => runTodoMove(writeDeps, request, o ?? {}),
432
490
  reorderTodos: (request, o) => runInPlaceReorder(writeDeps, "todo.move", request, o ?? {}),
491
+ reorderAny: (request, o) => runUniversalReorder(writeDeps, request, o ?? {}),
433
492
  setTags: (uuid, tags, o) => run("todo.set-tags", { uuid, tags }, o),
434
493
  addTags(uuid, tags, o) {
435
494
  const current = byUuid(conn.db, uuid);
@@ -441,8 +500,6 @@ export function openThings(options = {}) {
441
500
  deleteTodo: (uuid, o) => run("todo.delete", { uuid }, o),
442
501
  duplicateTodo: (uuid, o) => run("todo.duplicate", { uuid }, o),
443
502
  restoreTodo: (uuid, o) => run("todo.restore", { uuid }, o),
444
- backdateTodo: (uuid, dates, o) => run("todo.backdate", { uuid, ...dates }, o),
445
- addLoggedTodo: (params, o) => run("todo.add-logged", params, o),
446
503
  addHeading: (project, title, placement, o) => runAddHeading(writeDeps, project, title, placement, o ?? {}),
447
504
  renameHeading: (uuid, title, o) => run("project.rename-heading", { uuid, title }, o),
448
505
  moveHeading: (project, headings, placement, o) => run("project.move-heading", { project, headings, placement }, o),
@@ -454,13 +511,19 @@ export function openThings(options = {}) {
454
511
  detachTodo: (uuid, o) => run("todo.move", { uuid, loose: true }, o),
455
512
  editChecklist: (uuid, edit, o) => runEditChecklist(writeDeps, uuid, edit, o ?? {}),
456
513
  addProject: (params, o) => run("project.add", params, o),
457
- updateProject: (uuid, patch, o) => run("project.update", { uuid, ...patch }, o),
458
- completeProject: (uuid, policy, o) => run("project.complete", { uuid, children: policy.children }, o),
514
+ updateProject: (uuid, patch, o) => runUpdate("project", uuid, patch, o),
515
+ completeProject: (uuid, policy, o) => runCompleteWithDate(writeDeps, "project", uuid, {
516
+ children: policy.children,
517
+ ...(policy.completedAt !== undefined && { completedAt: policy.completedAt }),
518
+ }, o ?? {}),
459
519
  moveProject: (uuid, area, o) => run("project.move", { uuid, area }, o),
460
520
  detachProject: (uuid, o) => run("project.move", { uuid, noArea: true }, o),
461
521
  moveProjects: (request, o) => runProjectMove(writeDeps, request, o ?? {}),
462
522
  reorderProjects: (request, o) => runInPlaceReorder(writeDeps, "project.move", request, o ?? {}),
463
- cancelProject: (uuid, policy, o) => run("project.cancel", { uuid, children: policy.children }, o),
523
+ cancelProject: (uuid, policy, o) => runCancelWithDate(writeDeps, "project", uuid, {
524
+ children: policy.children,
525
+ ...(policy.completedAt !== undefined && { completedAt: policy.completedAt }),
526
+ }, o ?? {}),
464
527
  reopenProject: (uuid, o) => runProjectReopen(writeDeps, uuid, o ?? {}),
465
528
  restoreProject: (uuid, o) => run("project.restore", { uuid }, o),
466
529
  duplicateProject: (uuid, o) => run("project.duplicate", { uuid }, o),
@@ -481,6 +544,7 @@ export function openThings(options = {}) {
481
544
  updateTag: (target, patch, o) => run("tag.update", { target, ...patch }, o),
482
545
  deleteTag: (target, o) => run("tag.delete", { target }, o),
483
546
  emptyTrash: (o) => run("trash.empty", {}, o),
547
+ logNow: (o) => run("log-now", {}, o),
484
548
  reorder: (params, o) => runReorder(writeDeps, params, o ?? {}),
485
549
  batch: (ops, o, onResult) => runBatch(writeDeps, ops, o ?? {}, onResult),
486
550
  undo: (o, onItem) => runUndo(writeDeps, auditDir(env), o ?? {}, onItem),