things-api 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -12
- package/dist/audit/log.js +56 -2
- package/dist/audit/log.js.map +1 -1
- package/dist/audit/schema.d.ts +26 -8
- package/dist/audit/schema.js +42 -1
- package/dist/audit/schema.js.map +1 -1
- package/dist/cli/clock.d.ts +8 -0
- package/dist/cli/clock.js +24 -0
- package/dist/cli/clock.js.map +1 -0
- package/dist/cli/commands/area.d.ts +26 -3
- package/dist/cli/commands/area.js +129 -75
- package/dist/cli/commands/area.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts +1 -1
- package/dist/cli/commands/doctor.js +44 -5
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/install-skill.d.ts +45 -0
- package/dist/cli/commands/install-skill.js +207 -0
- package/dist/cli/commands/install-skill.js.map +1 -0
- package/dist/cli/commands/mcp.js +23 -4
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/project.d.ts +18 -1
- package/dist/cli/commands/project.js +74 -35
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/cli/commands/reads.d.ts +15 -2
- package/dist/cli/commands/reads.js +459 -165
- package/dist/cli/commands/reads.js.map +1 -1
- package/dist/cli/commands/repeat-flags.d.ts +20 -0
- package/dist/cli/commands/repeat-flags.js +76 -0
- package/dist/cli/commands/repeat-flags.js.map +1 -0
- package/dist/cli/commands/setup.js +8 -4
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/show.js +53 -30
- package/dist/cli/commands/show.js.map +1 -1
- package/dist/cli/commands/todo.d.ts +1 -1
- package/dist/cli/commands/todo.js +20 -13
- package/dist/cli/commands/todo.js.map +1 -1
- package/dist/cli/commands/writes.js +388 -161
- package/dist/cli/commands/writes.js.map +1 -1
- package/dist/cli/did-you-mean.d.ts +1 -1
- package/dist/cli/excess-args.d.ts +15 -0
- package/dist/cli/excess-args.js +51 -0
- package/dist/cli/excess-args.js.map +1 -0
- package/dist/cli/glyphs.d.ts +55 -2
- package/dist/cli/glyphs.js +114 -21
- package/dist/cli/glyphs.js.map +1 -1
- package/dist/cli/help.d.ts +54 -0
- package/dist/cli/help.js +405 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/main.js +64 -26
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/move-hint.d.ts +45 -0
- package/dist/cli/move-hint.js +196 -0
- package/dist/cli/move-hint.js.map +1 -0
- package/dist/cli/period.d.ts +9 -8
- package/dist/cli/period.js +56 -30
- package/dist/cli/period.js.map +1 -1
- package/dist/cli/read-driver.d.ts +26 -19
- package/dist/cli/read-driver.js +77 -33
- package/dist/cli/read-driver.js.map +1 -1
- package/dist/cli/render.d.ts +92 -28
- package/dist/cli/render.js +333 -103
- package/dist/cli/render.js.map +1 -1
- package/dist/cli/resolve-invocation.d.ts +42 -4
- package/dist/cli/resolve-invocation.js +97 -14
- package/dist/cli/resolve-invocation.js.map +1 -1
- package/dist/cli/skill-check.d.ts +21 -0
- package/dist/cli/skill-check.js +82 -0
- package/dist/cli/skill-check.js.map +1 -0
- package/dist/cli/skill.d.ts +62 -0
- package/dist/cli/skill.js +142 -0
- package/dist/cli/skill.js.map +1 -0
- package/dist/cli/tag-filters.d.ts +63 -0
- package/dist/cli/tag-filters.js +59 -0
- package/dist/cli/tag-filters.js.map +1 -0
- package/dist/cli/verb-hint.d.ts +25 -0
- package/dist/cli/verb-hint.js +163 -0
- package/dist/cli/verb-hint.js.map +1 -0
- package/dist/cli/version.d.ts +4 -0
- package/dist/cli/version.js +18 -0
- package/dist/cli/version.js.map +1 -0
- package/dist/cli/width.d.ts +135 -0
- package/dist/cli/width.js +313 -0
- package/dist/cli/width.js.map +1 -0
- package/dist/client.d.ts +160 -24
- package/dist/client.js +132 -19
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +11 -0
- package/dist/config.js +3 -0
- package/dist/config.js.map +1 -1
- package/dist/contracts.d.ts +97 -17
- package/dist/contracts.js +44 -1
- package/dist/contracts.js.map +1 -1
- package/dist/db/fingerprint.d.ts +12 -0
- package/dist/db/fingerprint.js +15 -1
- package/dist/db/fingerprint.js.map +1 -1
- package/dist/db/locate.js +1 -1
- package/dist/db/locate.js.map +1 -1
- package/dist/diagnose.d.ts +63 -0
- package/dist/diagnose.js +38 -1
- package/dist/diagnose.js.map +1 -1
- package/dist/index.d.ts +36 -5
- package/dist/index.js +38 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +9 -1
- package/dist/mcp/server.js +1038 -436
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/clock.d.ts +61 -0
- package/dist/model/clock.js +71 -0
- package/dist/model/clock.js.map +1 -0
- package/dist/model/dates.d.ts +26 -3
- package/dist/model/dates.js +84 -3
- package/dist/model/dates.js.map +1 -1
- package/dist/model/entities.d.ts +48 -10
- package/dist/model/entities.js.map +1 -1
- package/dist/model/mappers.d.ts +9 -2
- package/dist/model/mappers.js +32 -9
- package/dist/model/mappers.js.map +1 -1
- package/dist/model/serialize.d.ts +28 -0
- package/dist/model/serialize.js +80 -0
- package/dist/model/serialize.js.map +1 -0
- package/dist/model/when-sugar.d.ts +47 -0
- package/dist/model/when-sugar.js +45 -0
- package/dist/model/when-sugar.js.map +1 -0
- package/dist/read/area-view.d.ts +8 -1
- package/dist/read/area-view.js +55 -17
- package/dist/read/area-view.js.map +1 -1
- package/dist/read/detail.d.ts +1 -1
- package/dist/read/detail.js +11 -4
- package/dist/read/detail.js.map +1 -1
- package/dist/read/filter-contract.d.ts +117 -0
- package/dist/read/filter-contract.js +78 -0
- package/dist/read/filter-contract.js.map +1 -0
- package/dist/read/log-boundary.d.ts +1 -1
- package/dist/read/log-boundary.js +13 -3
- package/dist/read/log-boundary.js.map +1 -1
- package/dist/read/predicates.d.ts +30 -0
- package/dist/read/predicates.js +30 -0
- package/dist/read/predicates.js.map +1 -1
- package/dist/read/project-view.d.ts +12 -1
- package/dist/read/project-view.js +45 -12
- package/dist/read/project-view.js.map +1 -1
- package/dist/read/queries.d.ts +152 -7
- package/dist/read/queries.js +277 -36
- package/dist/read/queries.js.map +1 -1
- package/dist/read/sections.d.ts +51 -0
- package/dist/read/sections.js +37 -0
- package/dist/read/sections.js.map +1 -0
- package/dist/read/sidebar-order.js +2 -1
- package/dist/read/sidebar-order.js.map +1 -1
- package/dist/read/snapshot.d.ts +1 -1
- package/dist/read/snapshot.js +9 -2
- package/dist/read/snapshot.js.map +1 -1
- package/dist/read/tags.d.ts +27 -3
- package/dist/read/tags.js +83 -12
- package/dist/read/tags.js.map +1 -1
- package/dist/read/truncation.d.ts +71 -0
- package/dist/read/{pagination.js → truncation.js} +103 -94
- package/dist/read/truncation.js.map +1 -0
- package/dist/read/views.d.ts +147 -22
- package/dist/read/views.js +282 -80
- package/dist/read/views.js.map +1 -1
- package/dist/surface-copy.d.ts +19 -0
- package/dist/surface-copy.js +32 -0
- package/dist/surface-copy.js.map +1 -1
- package/dist/sync-health.d.ts +78 -0
- package/dist/sync-health.js +312 -0
- package/dist/sync-health.js.map +1 -0
- package/dist/write/accessibility-probe.d.ts +12 -0
- package/dist/write/accessibility-probe.js +71 -0
- package/dist/write/accessibility-probe.js.map +1 -0
- package/dist/write/automation-probe.d.ts +8 -1
- package/dist/write/automation-probe.js +16 -1
- package/dist/write/automation-probe.js.map +1 -1
- package/dist/write/batch.js +5 -2
- package/dist/write/batch.js.map +1 -1
- package/dist/write/capabilities.d.ts +8 -0
- package/dist/write/capabilities.js +13 -6
- package/dist/write/capabilities.js.map +1 -1
- package/dist/write/commands.d.ts +3 -1
- package/dist/write/commands.js +602 -49
- package/dist/write/commands.js.map +1 -1
- package/dist/write/edit-checklist.js +3 -2
- package/dist/write/edit-checklist.js.map +1 -1
- package/dist/write/guards.d.ts +2 -7
- package/dist/write/guards.js +87 -6
- package/dist/write/guards.js.map +1 -1
- package/dist/write/heading.js +2 -0
- package/dist/write/heading.js.map +1 -1
- package/dist/write/lock.d.ts +40 -2
- package/dist/write/lock.js +91 -14
- package/dist/write/lock.js.map +1 -1
- package/dist/write/make-repeating-project.d.ts +4 -0
- package/dist/write/make-repeating-project.js +253 -0
- package/dist/write/make-repeating-project.js.map +1 -0
- package/dist/write/operations.d.ts +145 -1
- package/dist/write/operations.js +48 -0
- package/dist/write/operations.js.map +1 -1
- package/dist/write/pipeline.d.ts +73 -2
- package/dist/write/pipeline.js +218 -43
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/planner.js +8 -1
- package/dist/write/planner.js.map +1 -1
- package/dist/write/pre-state.d.ts +105 -3
- package/dist/write/pre-state.js +144 -3
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/recurrence-rule-blob.d.ts +65 -0
- package/dist/write/recurrence-rule-blob.js +150 -0
- package/dist/write/recurrence-rule-blob.js.map +1 -0
- package/dist/write/reopen.js +3 -2
- package/dist/write/reopen.js.map +1 -1
- package/dist/write/reorder.js +27 -17
- package/dist/write/reorder.js.map +1 -1
- package/dist/write/repeat-rule.d.ts +22 -0
- package/dist/write/repeat-rule.js +258 -0
- package/dist/write/repeat-rule.js.map +1 -0
- package/dist/write/reversibility.js +49 -0
- package/dist/write/reversibility.js.map +1 -1
- package/dist/write/tag-refs.d.ts +47 -0
- package/dist/write/tag-refs.js +126 -0
- package/dist/write/tag-refs.js.map +1 -0
- package/dist/write/undo.d.ts +74 -5
- package/dist/write/undo.js +493 -80
- package/dist/write/undo.js.map +1 -1
- package/dist/write/vectors/registry.d.ts +15 -1
- package/dist/write/vectors/registry.js +65 -2
- package/dist/write/vectors/registry.js.map +1 -1
- package/dist/write/vectors/simulator.d.ts +44 -0
- package/dist/write/vectors/simulator.js +1136 -0
- package/dist/write/vectors/simulator.js.map +1 -0
- package/dist/write/vectors/types.d.ts +154 -3
- package/dist/write/vectors/ui-certification.d.ts +54 -0
- package/dist/write/vectors/ui-certification.js +43 -0
- package/dist/write/vectors/ui-certification.js.map +1 -0
- package/dist/write/vectors/ui-drag.d.ts +188 -0
- package/dist/write/vectors/ui-drag.js +1095 -0
- package/dist/write/vectors/ui-drag.js.map +1 -0
- package/dist/write/vectors/ui-recipes.d.ts +83 -0
- package/dist/write/vectors/ui-recipes.js +610 -0
- package/dist/write/vectors/ui-recipes.js.map +1 -0
- package/dist/write/vectors/ui.d.ts +152 -0
- package/dist/write/vectors/ui.js +704 -0
- package/dist/write/vectors/ui.js.map +1 -0
- package/dist/write/verify/delta.d.ts +141 -4
- package/dist/write/verify/delta.js +199 -21
- package/dist/write/verify/delta.js.map +1 -1
- package/dist/write/verify/poller.d.ts +7 -1
- package/dist/write/verify/poller.js +21 -1
- package/dist/write/verify/poller.js.map +1 -1
- package/package.json +18 -3
- package/skills/things-cli/SKILL.md +53 -0
- package/skills/things-cli/references/contracts.md +41 -0
- package/skills/things-cli/references/gui.md +11 -0
- package/skills/things-cli/references/model.md +51 -0
- package/dist/cli/exit-codes.d.ts +0 -26
- package/dist/cli/exit-codes.js +0 -26
- package/dist/cli/exit-codes.js.map +0 -1
- package/dist/cli/output.d.ts +0 -42
- package/dist/cli/output.js +0 -16
- package/dist/cli/output.js.map +0 -1
- package/dist/read/pagination.d.ts +0 -104
- package/dist/read/pagination.js.map +0 -1
package/dist/read/views.js
CHANGED
|
@@ -3,26 +3,94 @@ import { logBoundary, markLogged } from "./log-boundary.js";
|
|
|
3
3
|
import { mapProject, mapTodo } from "../model/mappers.js";
|
|
4
4
|
import { projectOccurrences } from "../model/occurrences.js";
|
|
5
5
|
import { decodeRecurrenceRule } from "../model/recurrence.js";
|
|
6
|
-
import { fetchTagsForTasks, fetchTaskRows, makeHeadingProjectResolver, makeRefResolver, NOT_TEMPLATE, resolveAreaUuid, resolveProjectUuid, resolveTagUuid, tagScopeBinds, tagScopeSql, tagWithDescendants, } from "./queries.js";
|
|
7
|
-
import { ANYTIME_SELF, CONTAINER_UNTRASHED, EFF_PROJECT, LIVE, OPEN, PROJECT_ANYTIME_ACTIVE, } from "./predicates.js";
|
|
6
|
+
import { directTagScopeSql, directUntaggedScopeSql, fetchTagsForTasks, fetchTaskRows, makeHeadingProjectResolver, makeRefResolver, NOT_TEMPLATE, resolveAreaUuid, resolveProjectUuid, resolveTagUuid, tagScopeBinds, tagScopeSql, tagWithDescendants, untaggedScopeSql, } from "./queries.js";
|
|
7
|
+
import { ANYTIME_SELF, CONTAINER_UNTRASHED, EFF_PROJECT, LIVE, OPEN, OPEN_OR_UNSWEPT, OVERDUE, PROJECT_ANYTIME_ACTIVE, } from "./predicates.js";
|
|
8
8
|
import { groupBySidebar } from "./sidebar-order.js";
|
|
9
9
|
import { areasView } from "./tags.js";
|
|
10
10
|
import { compareSearchMatches } from "./search-rank.js";
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Escape a user term for a SQL LIKE pattern so `%` and `_` match LITERALLY,
|
|
13
|
+
* not as wildcards (search "my_task" must not match "myXtask"; "50%" must not
|
|
14
|
+
* match "50"-anything). Backslash is the escape character — escape it first,
|
|
15
|
+
* then the two wildcard metacharacters. Every LIKE fed an escaped term must
|
|
16
|
+
* append `ESCAPE '\'` (see {@link LIKE_ESCAPE}).
|
|
17
|
+
*/
|
|
18
|
+
function escapeLike(term) {
|
|
19
|
+
return term.replace(/[\\%_]/g, (c) => `\\${c}`);
|
|
20
|
+
}
|
|
21
|
+
/** SQL clause suffix pairing an escaped LIKE pattern with its escape char. */
|
|
22
|
+
const LIKE_ESCAPE = " ESCAPE '\\'";
|
|
23
|
+
/**
|
|
24
|
+
* Compose the tag scope for a filter into an AND-chained SQL fragment plus its
|
|
25
|
+
* binds. Every clause AND-combines: the `untagged` negation and each `--tag`
|
|
26
|
+
* ref (each ref independently resolved and descendant-expanded per `exactTag`).
|
|
27
|
+
* The mutually-exclusive combination (`untagged` with a tag-presence flag) is
|
|
28
|
+
* refused at the CLI/MCP surfaces; here it would simply AND to an empty result.
|
|
29
|
+
* Splices in before {@link overdueFilter} so the tag binds precede the overdue
|
|
30
|
+
* bind.
|
|
31
|
+
*
|
|
32
|
+
* `options.container` selects the CONTAINER semantics (`project show` /
|
|
33
|
+
* `area show` / the `projects` list): the view's own inheritance hop is dropped,
|
|
34
|
+
* so `--tag` matches a DIRECT assignment (still descendant-expanded) and
|
|
35
|
+
* `--untagged` means "no direct tag". Every child inherits its container's tags,
|
|
36
|
+
* so an inheritance-inclusive filter there is vacuous — direct-on-the-row is the
|
|
37
|
+
* useful, GUI-faithful behavior. FLAT views omit the option and keep the
|
|
38
|
+
* inheritance-inclusive relation.
|
|
39
|
+
*/
|
|
40
|
+
export function tagFilter(db, filter, options) {
|
|
41
|
+
const container = options?.container === true;
|
|
42
|
+
const clauses = [];
|
|
43
|
+
const binds = [];
|
|
44
|
+
if (filter?.untagged === true)
|
|
45
|
+
clauses.push(container ? directUntaggedScopeSql() : untaggedScopeSql());
|
|
46
|
+
const exact = filter?.exactTag === true;
|
|
47
|
+
const tagRefs = [...(filter?.tags ?? []), ...(filter?.tag !== undefined ? [filter.tag] : [])];
|
|
48
|
+
const expand = (ref) => {
|
|
49
|
+
const target = resolveTagUuid(db, ref);
|
|
50
|
+
return exact ? [target] : tagWithDescendants(db, target);
|
|
51
|
+
};
|
|
52
|
+
for (const ref of tagRefs) {
|
|
53
|
+
const uuids = expand(ref);
|
|
54
|
+
if (container) {
|
|
55
|
+
clauses.push(directTagScopeSql(uuids.length));
|
|
56
|
+
binds.push(...uuids);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
clauses.push(tagScopeSql(uuids.length));
|
|
60
|
+
binds.push(...tagScopeBinds(uuids));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (clauses.length === 0)
|
|
64
|
+
return { sql: "", binds: [] };
|
|
65
|
+
return { sql: ` AND ${clauses.join(" AND ")}`, binds };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The `--overdue` content scope as a spliceable SQL fragment (empty when off).
|
|
69
|
+
* Appended AFTER {@link tagFilter} in every host view so it intersects with the
|
|
70
|
+
* tag scope, and its single packed-today bind trails the tag binds in the same
|
|
71
|
+
* order. `packedToday` is encodePackedDate(localToday(now)) — the caller's
|
|
72
|
+
* injected clock — never a hardcoded date.
|
|
73
|
+
*/
|
|
74
|
+
function overdueFilter(filter, packedToday) {
|
|
75
|
+
if (filter?.overdue !== true)
|
|
13
76
|
return { sql: "", binds: [] };
|
|
14
|
-
|
|
15
|
-
const uuids = filter.exactTag === true ? [target] : tagWithDescendants(db, target);
|
|
16
|
-
return { sql: ` AND ${tagScopeSql(uuids.length)}`, binds: tagScopeBinds(uuids) };
|
|
77
|
+
return { sql: ` AND ${OVERDUE}`, binds: [packedToday] };
|
|
17
78
|
}
|
|
18
|
-
|
|
79
|
+
// `boundary` is REQUIRED (no wall-clock default): every caller threads the
|
|
80
|
+
// view's injected `now` via logBoundary(db, now) so the `logged` flag it stamps
|
|
81
|
+
// honors the same clock as the SQL membership filter (a pinned test clock, a
|
|
82
|
+
// lab run) — nothing here silently reaches for real time.
|
|
83
|
+
// `packedToday` (the view's injected clock, encodePackedDate(localToday(now)))
|
|
84
|
+
// is threaded so the mapper can gate `todaySection` to Today members only —
|
|
85
|
+
// the same clock the SQL membership filter uses.
|
|
86
|
+
function materialize(db, rows, boundary, packedToday) {
|
|
19
87
|
const refs = makeRefResolver(db);
|
|
20
88
|
const headingProject = makeHeadingProjectResolver(db);
|
|
21
89
|
const tags = fetchTagsForTasks(db, rows.map((r) => r.uuid));
|
|
22
90
|
const items = rows.map((row) => {
|
|
23
91
|
if (row.type === 1)
|
|
24
|
-
return mapProject(row, refs, tags.get(row.uuid) ?? []);
|
|
25
|
-
const todo = mapTodo(row, refs, tags.get(row.uuid) ?? []);
|
|
92
|
+
return mapProject(row, refs, tags.get(row.uuid) ?? [], packedToday);
|
|
93
|
+
const todo = mapTodo(row, refs, tags.get(row.uuid) ?? [], packedToday);
|
|
26
94
|
if (todo.heading !== null) {
|
|
27
95
|
const p = headingProject(todo.heading.uuid);
|
|
28
96
|
if (p !== null)
|
|
@@ -30,12 +98,18 @@ function materialize(db, rows) {
|
|
|
30
98
|
}
|
|
31
99
|
return todo;
|
|
32
100
|
});
|
|
33
|
-
|
|
101
|
+
// The caller may pass the boundary it already used for the SQL membership
|
|
102
|
+
// filter so the `logged` flag and the row's presence agree (a checked-unswept
|
|
103
|
+
// row must materialize with logged=false).
|
|
104
|
+
return markLogged(items, boundary);
|
|
34
105
|
}
|
|
35
|
-
|
|
36
|
-
|
|
106
|
+
/** Badge = OPEN members only (the GUI badge counts remaining work). */
|
|
107
|
+
const isOpen = (i) => i.status === "open";
|
|
108
|
+
export function todayView(db, now, filter, zone) {
|
|
109
|
+
const todayIso = localToday(now, zone);
|
|
37
110
|
const packedToday = encodePackedDate(todayIso);
|
|
38
111
|
const tf = tagFilter(db, filter);
|
|
112
|
+
const of = overdueFilter(filter, packedToday);
|
|
39
113
|
// Membership + comparator per the UI-oracle research (lab, 2026-07-04;
|
|
40
114
|
// docs/lab/today-order-research.md, two reproducing runs + exact live
|
|
41
115
|
// reconciliation: 405 − 12 suppressed = 393 = the UI's own count):
|
|
@@ -47,59 +121,165 @@ export function todayView(db, now, filter) {
|
|
|
47
121
|
// (the date the item ENTERED Today: its startDate, or its deadline for
|
|
48
122
|
// deadline-driven members), then todayIndex ASC, then uuid (observed
|
|
49
123
|
// stable tiebreak).
|
|
50
|
-
|
|
124
|
+
// Membership is OPEN_OR_UNSWEPT (not OPEN): a checked row the log-move sweep
|
|
125
|
+
// has not yet passed keeps its comparator slot in place (GUI-parity ruling
|
|
126
|
+
// 2026-07-14). The boundary is threaded into both the SQL filter and
|
|
127
|
+
// materialize so the row's presence and its `logged` flag agree.
|
|
128
|
+
const boundary = logBoundary(db, now, zone);
|
|
129
|
+
const rows = fetchTaskRows(db, `${OPEN_OR_UNSWEPT} AND ${CONTAINER_UNTRASHED} AND (
|
|
51
130
|
(t.startDate IS NOT NULL AND t.startDate <= ? AND t.start IN (1, 2))
|
|
52
131
|
OR (t.deadline IS NOT NULL AND t.deadline <= ? AND t.startDate IS NULL
|
|
53
132
|
AND (t.deadlineSuppressionDate IS NULL OR t.deadlineSuppressionDate < t.deadline))
|
|
54
|
-
)${tf.sql}
|
|
133
|
+
)${tf.sql}${of.sql}
|
|
55
134
|
ORDER BY t.startBucket ASC,
|
|
56
135
|
COALESCE(t.todayIndexReferenceDate, t.startDate, t.deadline) DESC,
|
|
57
|
-
t.todayIndex ASC, t.uuid ASC`, [packedToday, packedToday, ...tf.binds]);
|
|
58
|
-
const items = materialize(db, rows);
|
|
136
|
+
t.todayIndex ASC, t.uuid ASC`, [boundary.getTime() / 1000, packedToday, packedToday, ...tf.binds, ...of.binds]);
|
|
137
|
+
const items = materialize(db, rows, boundary, packedToday);
|
|
59
138
|
// Evening membership expires daily: raw startBucket=1 counts only while
|
|
60
139
|
// startDate is exactly today; stale evening items belong to Today proper.
|
|
61
140
|
const isEvening = (i) => i.todaySection === "evening" && i.startDate === todayIso;
|
|
62
|
-
const
|
|
141
|
+
const evening = items.filter(isEvening);
|
|
142
|
+
const dueIn = (list) => list.filter((i) => i.deadline !== null && i.deadline <= todayIso).length;
|
|
143
|
+
// Badge = OPEN members only (the GUI badge counts remaining work). A
|
|
144
|
+
// checked-but-unswept row is present in the list but never moves the badge —
|
|
145
|
+
// this preserves the exact live reconciliation computed under OPEN membership.
|
|
146
|
+
// The evening filter mirrors the tag filter's badge treatment: the badge
|
|
147
|
+
// reflects exactly the members the view now returns (here, evening only).
|
|
148
|
+
if (filter?.eveningOnly === true) {
|
|
149
|
+
const openEvening = evening.filter(isOpen);
|
|
150
|
+
const eveningDue = dueIn(openEvening);
|
|
151
|
+
return {
|
|
152
|
+
today: [],
|
|
153
|
+
evening,
|
|
154
|
+
badge: { dueOrOverdue: eveningDue, other: openEvening.length - eveningDue },
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
const openItems = items.filter(isOpen);
|
|
158
|
+
const dueOrOverdue = dueIn(openItems);
|
|
63
159
|
return {
|
|
64
160
|
today: items.filter((i) => !isEvening(i)),
|
|
65
|
-
evening
|
|
66
|
-
badge: { dueOrOverdue, other:
|
|
161
|
+
evening,
|
|
162
|
+
badge: { dueOrOverdue, other: openItems.length - dueOrOverdue },
|
|
67
163
|
};
|
|
68
164
|
}
|
|
69
|
-
export function inboxView(db, filter) {
|
|
165
|
+
export function inboxView(db, now, filter, zone) {
|
|
70
166
|
const tf = tagFilter(db, filter);
|
|
71
|
-
|
|
72
|
-
|
|
167
|
+
// `now` is threaded only for the `--overdue` boundary (the inbox order and
|
|
168
|
+
// the since/until window key on epoch creationDate, not on today).
|
|
169
|
+
const of = overdueFilter(filter, encodePackedDate(localToday(now, zone)));
|
|
170
|
+
const where = [OPEN, "t.start = 0"];
|
|
171
|
+
const binds = [];
|
|
172
|
+
// creationDate is an epoch REAL (Unix seconds) — mirror the changes/logbook
|
|
173
|
+
// comparison (getTime()/1000 against the raw column), never encodePackedDate.
|
|
174
|
+
if (filter?.since !== undefined) {
|
|
175
|
+
where.push("t.creationDate >= ?");
|
|
176
|
+
binds.push(filter.since.getTime() / 1000);
|
|
177
|
+
}
|
|
178
|
+
if (filter?.until !== undefined) {
|
|
179
|
+
where.push("t.creationDate <= ?");
|
|
180
|
+
binds.push(filter.until.getTime() / 1000);
|
|
181
|
+
}
|
|
182
|
+
const rows = fetchTaskRows(db, `${where.join(" AND ")}${tf.sql}${of.sql} ORDER BY t."index" ASC`, [...binds, ...tf.binds, ...of.binds]);
|
|
183
|
+
return materialize(db, rows, logBoundary(db, now, zone), encodePackedDate(localToday(now, zone)));
|
|
73
184
|
}
|
|
74
|
-
export function anytimeView(db, now, filter) {
|
|
75
|
-
const packedToday = encodePackedDate(localToday(now));
|
|
185
|
+
export function anytimeView(db, now, filter, zone) {
|
|
186
|
+
const packedToday = encodePackedDate(localToday(now, zone));
|
|
76
187
|
const tf = tagFilter(db, filter);
|
|
188
|
+
const of = overdueFilter(filter, packedToday);
|
|
77
189
|
// Mirrors UI membership: every active item, including Today members
|
|
78
190
|
// (starred in the UI) and pending-promotion rows (start=2, past-dated) —
|
|
79
|
-
// MINUS children of non-active containers (the project cascade).
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
191
|
+
// MINUS children of non-active containers (the project cascade). Membership
|
|
192
|
+
// is OPEN_OR_UNSWEPT (GUI-parity ruling 2026-07-14): a checked row the sweep
|
|
193
|
+
// has not passed keeps its index slot. PROJECT_ANYTIME_ACTIVE still requires
|
|
194
|
+
// the parent project be open, so a closed-but-unswept project's children stay
|
|
195
|
+
// cascade-excluded — the checked project row represents them. The boundary is
|
|
196
|
+
// threaded into the SQL filter and materialize so presence and `logged` agree.
|
|
197
|
+
const boundary = logBoundary(db, now, zone);
|
|
198
|
+
const rows = fetchTaskRows(db, `${OPEN_OR_UNSWEPT} AND ${ANYTIME_SELF("t")}
|
|
199
|
+
AND ${PROJECT_ANYTIME_ACTIVE}${tf.sql}${of.sql} ORDER BY t."index" ASC`, [
|
|
200
|
+
boundary.getTime() / 1000,
|
|
201
|
+
packedToday,
|
|
202
|
+
packedToday,
|
|
203
|
+
packedToday,
|
|
204
|
+
packedToday,
|
|
205
|
+
...tf.binds,
|
|
206
|
+
...of.binds,
|
|
207
|
+
]);
|
|
208
|
+
return groupBySidebar(db, materialize(db, rows, boundary, packedToday));
|
|
83
209
|
}
|
|
84
|
-
/**
|
|
85
|
-
|
|
86
|
-
|
|
210
|
+
/**
|
|
211
|
+
* The UI's star marker in Anytime: the item is also a Today member. Mirrors
|
|
212
|
+
* {@link todayView}'s two membership arms so an unscheduled item pulled into
|
|
213
|
+
* Today by a due deadline stars exactly like a scheduled one:
|
|
214
|
+
* - SCHEDULED — a When-date that has arrived (startDate <= today); or
|
|
215
|
+
* - DEADLINE-DRIVEN — no When-date, but a due/overdue deadline (deadline <=
|
|
216
|
+
* today) pulls it into Today (the todayView arm that lets a bare deadline
|
|
217
|
+
* surface an item). Only reached for anytime-visible rows (start=1,
|
|
218
|
+
* startDate NULL), so it never over-stars someday/inbox rows.
|
|
219
|
+
* KNOWN LIMITATION: todayView's deadline arm also drops a DISMISSED-nag
|
|
220
|
+
* deadline (deadlineSuppressionDate >= deadline); that column is not surfaced
|
|
221
|
+
* on the entity, so a suppressed unscheduled overdue row is over-starred here.
|
|
222
|
+
*/
|
|
223
|
+
export function isTodayMember(item, now, zone) {
|
|
224
|
+
const today = localToday(now, zone);
|
|
225
|
+
if (item.startDate !== null)
|
|
226
|
+
return item.startDate <= today;
|
|
227
|
+
return item.deadline !== null && item.deadline <= today;
|
|
87
228
|
}
|
|
88
|
-
|
|
89
|
-
|
|
229
|
+
/**
|
|
230
|
+
* The merged date-ordered stream keys on COALESCE(startDate, deadline) — a
|
|
231
|
+
* scheduled row groups under its when-date, a forecast row under its deadline.
|
|
232
|
+
*/
|
|
233
|
+
const groupKey = (i) => i.startDate ?? i.deadline ?? "";
|
|
234
|
+
export function upcomingView(db, now, filter, zone) {
|
|
235
|
+
const packedToday = encodePackedDate(localToday(now, zone));
|
|
236
|
+
const boundary = logBoundary(db, now, zone);
|
|
90
237
|
const until = filter?.until;
|
|
91
238
|
const since = filter?.since;
|
|
92
|
-
const untilSql = until === undefined ? "" : " AND t.startDate <= ?";
|
|
93
239
|
const untilBinds = until === undefined ? [] : [encodePackedDate(until)];
|
|
94
|
-
const sinceSql = since === undefined ? "" : " AND t.startDate >= ?";
|
|
95
240
|
const sinceBinds = since === undefined ? [] : [encodePackedDate(since)];
|
|
96
241
|
const tf = tagFilter(db, filter);
|
|
242
|
+
// Cohort 1 — SCHEDULED: start=2 with a future startDate. The bounds clip the
|
|
243
|
+
// APPEARANCE date, which for these rows is the startDate (when-date).
|
|
244
|
+
const schedUntilSql = until === undefined ? "" : " AND t.startDate <= ?";
|
|
245
|
+
const schedSinceSql = since === undefined ? "" : " AND t.startDate >= ?";
|
|
97
246
|
const rows = fetchTaskRows(db, `${OPEN} AND ${CONTAINER_UNTRASHED}
|
|
98
|
-
AND t.start = 2 AND t.startDate IS NOT NULL AND t.startDate > ?${
|
|
247
|
+
AND t.start = 2 AND t.startDate IS NOT NULL AND t.startDate > ?${schedUntilSql}${schedSinceSql}${tf.sql}
|
|
99
248
|
ORDER BY t.startDate ASC, t."index" ASC`, [packedToday, ...untilBinds, ...sinceBinds, ...tf.binds]);
|
|
100
|
-
|
|
249
|
+
// Cohort 2 — DEADLINE-FORECAST (UPC1, GUI-verified): anytime/someday to-dos
|
|
250
|
+
// and someday projects carrying a FUTURE deadline and no when-date. Inbox
|
|
251
|
+
// (start=0) is excluded; a dismissed-nag deadline (deadlineSuppressionDate ==
|
|
252
|
+
// deadline) is dropped while a re-armed one (supp < deadline) survives — the
|
|
253
|
+
// todayView suppression clause one step earlier (deadline > today, not <=).
|
|
254
|
+
// These rows keep startDate NULL (JSON honesty) and APPEAR under their
|
|
255
|
+
// deadline, so the bounds clip on deadline, exactly as cohort 1 clips on
|
|
256
|
+
// startDate.
|
|
257
|
+
const fcUntilSql = until === undefined ? "" : " AND t.deadline <= ?";
|
|
258
|
+
const fcSinceSql = since === undefined ? "" : " AND t.deadline >= ?";
|
|
259
|
+
const forecastRows = fetchTaskRows(db, `${OPEN} AND ${CONTAINER_UNTRASHED}
|
|
260
|
+
AND t.startDate IS NULL AND t.start IN (1, 2)
|
|
261
|
+
AND t.deadline IS NOT NULL AND t.deadline > ?
|
|
262
|
+
AND (t.deadlineSuppressionDate IS NULL OR t.deadlineSuppressionDate < t.deadline)${fcUntilSql}${fcSinceSql}${tf.sql}
|
|
263
|
+
ORDER BY t.deadline ASC, t."index" ASC`, [packedToday, ...untilBinds, ...sinceBinds, ...tf.binds]);
|
|
264
|
+
const scheduled = materialize(db, rows, boundary, packedToday);
|
|
265
|
+
const forecast = materialize(db, forecastRows, boundary, packedToday);
|
|
266
|
+
// The merged date-ordered stream keys on COALESCE(startDate, deadline) — a
|
|
267
|
+
// scheduled row groups under its when-date, a forecast row under its deadline
|
|
268
|
+
// — then the UI's within-day drag order (todayIndex ASC; live-verified
|
|
269
|
+
// 2026-07-11 against the GUI — plain `index` disagrees), then a stable
|
|
270
|
+
// seed-order/uuid tiebreak.
|
|
271
|
+
const sortDated = (list, indexRows) => {
|
|
272
|
+
const todayIndexOf = new Map(indexRows.map((r) => [r.uuid, r.todayIndex ?? 0]));
|
|
273
|
+
return list
|
|
274
|
+
.map((item, pos) => ({ item, pos }))
|
|
275
|
+
.toSorted((a, b) => groupKey(a.item).localeCompare(groupKey(b.item)) ||
|
|
276
|
+
(todayIndexOf.get(a.item.uuid) ?? 0) - (todayIndexOf.get(b.item.uuid) ?? 0) ||
|
|
277
|
+
a.pos - b.pos ||
|
|
278
|
+
a.item.uuid.localeCompare(b.item.uuid))
|
|
279
|
+
.map((x) => x.item);
|
|
280
|
+
};
|
|
101
281
|
if (filter?.repeats === false)
|
|
102
|
-
return
|
|
282
|
+
return sortDated([...scheduled, ...forecast], [...rows, ...forecastRows]);
|
|
103
283
|
// UI parity: repeating templates surface at their app-materialized next
|
|
104
284
|
// occurrence (rt1_nextInstanceStartDate). Fixed rules only — after-
|
|
105
285
|
// completion templates carry no next date until the prior instance
|
|
@@ -112,7 +292,7 @@ export function upcomingView(db, now, filter) {
|
|
|
112
292
|
ORDER BY t.rt1_nextInstanceStartDate ASC, t."index" ASC`, [packedToday, ...untilBinds, ...sinceBinds, ...tf.binds]);
|
|
113
293
|
const horizon = Math.max(1, Math.min(10, Math.trunc(filter?.horizon ?? 1)));
|
|
114
294
|
const rawByUuid = new Map(templateRows.map((r) => [r.uuid, r.rt1_recurrenceRule]));
|
|
115
|
-
const occurrences = materialize(db, templateRows).flatMap((template) => {
|
|
295
|
+
const occurrences = materialize(db, templateRows, boundary, packedToday).flatMap((template) => {
|
|
116
296
|
const startDate = template.repeating.nextOccurrence ?? null;
|
|
117
297
|
if (startDate === null)
|
|
118
298
|
return [];
|
|
@@ -153,17 +333,9 @@ export function upcomingView(db, now, filter) {
|
|
|
153
333
|
deadline: o.deadline,
|
|
154
334
|
})));
|
|
155
335
|
});
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
const todayIndexOf = new Map([...rows, ...templateRows].map((r) => [r.uuid, r.todayIndex ?? 0]));
|
|
160
|
-
const dated = [...items, ...occurrences]
|
|
161
|
-
.map((item, pos) => ({ item, pos }))
|
|
162
|
-
.toSorted((a, b) => (a.item.startDate ?? "").localeCompare(b.item.startDate ?? "") ||
|
|
163
|
-
(todayIndexOf.get(a.item.uuid) ?? 0) - (todayIndexOf.get(b.item.uuid) ?? 0) ||
|
|
164
|
-
a.pos - b.pos ||
|
|
165
|
-
a.item.uuid.localeCompare(b.item.uuid))
|
|
166
|
-
.map((x) => x.item);
|
|
336
|
+
// The scheduled + forecast + occurrence rows share one date-ordered stream
|
|
337
|
+
// (COALESCE(startDate, deadline), then the UI's within-day drag order).
|
|
338
|
+
const dated = sortDated([...scheduled, ...forecast, ...occurrences], [...rows, ...forecastRows, ...templateRows]);
|
|
167
339
|
// The UI's trailing "Repeating To-Dos" section: templates with NO set
|
|
168
340
|
// next occurrence (after-completion rules between instances, rules past
|
|
169
341
|
// their end date) plus paused ones — startDate stays null, the decoded
|
|
@@ -173,7 +345,7 @@ export function upcomingView(db, now, filter) {
|
|
|
173
345
|
AND (t.rt1_nextInstanceStartDate IS NULL OR t.rt1_nextInstanceStartDate <= ?
|
|
174
346
|
OR t.rt1_instanceCreationPaused = 1)${tf.sql}
|
|
175
347
|
ORDER BY t.todayIndex ASC, t."index" ASC`, [packedToday, ...tf.binds]);
|
|
176
|
-
const resting = materialize(db, restingRows).map((item) => {
|
|
348
|
+
const resting = materialize(db, restingRows, boundary, packedToday).map((item) => {
|
|
177
349
|
const raw = restingRows.find((r) => r.uuid === item.uuid)?.rt1_recurrenceRule;
|
|
178
350
|
if (raw !== null && raw !== undefined) {
|
|
179
351
|
try {
|
|
@@ -187,9 +359,12 @@ export function upcomingView(db, now, filter) {
|
|
|
187
359
|
});
|
|
188
360
|
return [...dated, ...resting];
|
|
189
361
|
}
|
|
190
|
-
|
|
362
|
+
/** A someday to-do nested under a project (directly or via a heading). */
|
|
363
|
+
const isChild = (i) => i.type === "to-do" && (i.project !== null || i.headingProject !== null);
|
|
364
|
+
export function somedayView(db, now, filter, zone) {
|
|
191
365
|
const tf = tagFilter(db, filter);
|
|
192
|
-
const packedToday = encodePackedDate(localToday(now));
|
|
366
|
+
const packedToday = encodePackedDate(localToday(now, zone));
|
|
367
|
+
const of = overdueFilter(filter, packedToday);
|
|
193
368
|
// Default membership excludes ALL project children (the UI shows only
|
|
194
369
|
// container-less someday to-dos, area members, and someday project rows);
|
|
195
370
|
// the toggle adds someday children of anytime-ACTIVE projects only.
|
|
@@ -199,14 +374,13 @@ export function somedayView(db, now, filter) {
|
|
|
199
374
|
AND p.trashed = 0 AND p.status = 0 AND ${ANYTIME_SELF("p")})`
|
|
200
375
|
: "0";
|
|
201
376
|
const rows = fetchTaskRows(db, `${OPEN} AND t.start = 2 AND t.startDate IS NULL
|
|
202
|
-
AND (${EFF_PROJECT} IS NULL OR ${childArm})${tf.sql} ORDER BY t."index" ASC`, [...(withActiveChildren ? [packedToday, packedToday] : []), ...tf.binds]);
|
|
203
|
-
const sections = groupBySidebar(db, materialize(db, rows));
|
|
377
|
+
AND (${EFF_PROJECT} IS NULL OR ${childArm})${tf.sql}${of.sql} ORDER BY t."index" ASC`, [...(withActiveChildren ? [packedToday, packedToday] : []), ...tf.binds, ...of.binds]);
|
|
378
|
+
const sections = groupBySidebar(db, materialize(db, rows, logBoundary(db, now, zone), encodePackedDate(localToday(now, zone))));
|
|
204
379
|
// GUI order within a Someday group (live side-by-side, 2026-07-12):
|
|
205
380
|
// PROJECT rows first (their sidebar order preserved), then direct to-dos in
|
|
206
381
|
// drag order. Someday children of active projects (the activeProjectItems
|
|
207
382
|
// toggle) trail the group, still clustered by their project — surfaces
|
|
208
383
|
// present them as a separate "From active projects" section.
|
|
209
|
-
const isChild = (i) => i.type === "to-do" && (i.project !== null || i.headingProject !== null);
|
|
210
384
|
return sections.map((s) => ({
|
|
211
385
|
area: s.area,
|
|
212
386
|
items: [
|
|
@@ -216,7 +390,7 @@ export function somedayView(db, now, filter) {
|
|
|
216
390
|
],
|
|
217
391
|
}));
|
|
218
392
|
}
|
|
219
|
-
export function logbookView(db, options) {
|
|
393
|
+
export function logbookView(db, now, options, zone) {
|
|
220
394
|
const cap = options?.limit === null ? null : (options?.limit ?? 100);
|
|
221
395
|
const tf = tagFilter(db, options);
|
|
222
396
|
const where = [];
|
|
@@ -240,21 +414,38 @@ export function logbookView(db, options) {
|
|
|
240
414
|
}
|
|
241
415
|
const extra = where.length > 0 ? ` AND ${where.join(" AND ")}` : "";
|
|
242
416
|
// Completion ≠ logged: closed items past the log-move boundary still sit
|
|
243
|
-
// checked in their original lists, exactly like the GUI's Logbook.
|
|
244
|
-
|
|
245
|
-
|
|
417
|
+
// checked in their original lists, exactly like the GUI's Logbook. The
|
|
418
|
+
// membership boundary rides the injected clock (threaded into both the SQL
|
|
419
|
+
// filter and materialize so presence and the `logged` flag agree).
|
|
420
|
+
const boundary = logBoundary(db, now, zone);
|
|
421
|
+
const rows = fetchTaskRows(db, `${LIVE} AND t.status IN (2, 3) AND t.stopDate <= ?${extra}${tf.sql} ORDER BY t.stopDate DESC${cap === null ? "" : " LIMIT ?"}`, [boundary.getTime() / 1000, ...binds, ...tf.binds, ...(cap === null ? [] : [cap])]);
|
|
422
|
+
return materialize(db, rows, boundary, encodePackedDate(localToday(now, zone)));
|
|
246
423
|
}
|
|
247
|
-
export function trashView(db, options) {
|
|
424
|
+
export function trashView(db, now, options, zone) {
|
|
248
425
|
const cap = options?.limit === null ? null : (options?.limit ?? 200);
|
|
249
426
|
const rows = fetchTaskRows(db, `t.type IN (0, 1) AND t.trashed = 1 AND ${NOT_TEMPLATE}
|
|
250
427
|
ORDER BY t.userModificationDate DESC${cap === null ? "" : " LIMIT ?"}`, cap === null ? [] : [cap]);
|
|
251
|
-
return materialize(db, rows);
|
|
428
|
+
return materialize(db, rows, logBoundary(db, now, zone), encodePackedDate(localToday(now, zone)));
|
|
252
429
|
}
|
|
253
430
|
export function projectsView(db, options) {
|
|
254
431
|
// areaUuid accepts a uuid OR a unique (case-insensitive) title; ambiguous
|
|
255
432
|
// or unknown references throw like every other ref resolver.
|
|
256
433
|
const area = options?.areaUuid === undefined ? null : resolveAreaUuid(db, options.areaUuid);
|
|
257
|
-
const packedToday = encodePackedDate(localToday(options?.now));
|
|
434
|
+
const packedToday = encodePackedDate(localToday(options?.now, options?.zone));
|
|
435
|
+
// Tag scope (§9a): the projects LIST is a FLAT view (like `anytime` restricted
|
|
436
|
+
// to project rows), NOT a single-container view — projects sit in different
|
|
437
|
+
// areas with heterogeneous inheritance, there is no universally-inherited tag
|
|
438
|
+
// set, and inherited tags are opaque here (area headers don't show what they
|
|
439
|
+
// confer), so filtering by an inherited tag is useful. `--tag`/`--untagged`
|
|
440
|
+
// are INHERITANCE-INCLUSIVE (a project inherits its area's tags). Its binds sit
|
|
441
|
+
// in the WHERE, after the overdue bind and before the active-first binds.
|
|
442
|
+
const tf = tagFilter(db, options);
|
|
443
|
+
// OWN-DEADLINE UNIFORM: `--overdue` keeps only projects whose OWN deadline is
|
|
444
|
+
// overdue (open, strictly before today) — projects carry a `deadline` column
|
|
445
|
+
// exactly like to-dos, so the shared OVERDUE predicate applies to project
|
|
446
|
+
// rows verbatim. A content scope: it narrows the list, never lifts a limit.
|
|
447
|
+
const overdueSql = options?.overdue === true ? ` AND ${OVERDUE}` : "";
|
|
448
|
+
const overdueBinds = options?.overdue === true ? [packedToday] : [];
|
|
258
449
|
// Sidebar default: LATER (someday + future-scheduled) projects are hidden —
|
|
259
450
|
// active means ANYTIME_SELF (a scheduled project whose date has arrived
|
|
260
451
|
// counts active, exactly the Anytime membership test). With later=true they
|
|
@@ -270,16 +461,18 @@ export function projectsView(db, options) {
|
|
|
270
461
|
// above the areas — then each area by ITS sidebar rank (TMArea."index"),
|
|
271
462
|
// projects within an area by their drag order.
|
|
272
463
|
const where = area
|
|
273
|
-
? `${OPEN} AND t.type = 1 AND t.area = ?${laterSql}
|
|
464
|
+
? `${OPEN} AND t.type = 1 AND t.area = ?${laterSql}${overdueSql}${tf.sql}
|
|
274
465
|
ORDER BY ${activeFirst}t."index" ASC`
|
|
275
|
-
: `${OPEN} AND t.type = 1${laterSql} ORDER BY (t.area IS NOT NULL) ASC,
|
|
466
|
+
: `${OPEN} AND t.type = 1${laterSql}${overdueSql}${tf.sql} ORDER BY (t.area IS NOT NULL) ASC,
|
|
276
467
|
(SELECT a."index" FROM TMArea a WHERE a.uuid = t.area) ASC, ${activeFirst}t."index" ASC`;
|
|
277
468
|
const rows = fetchTaskRows(db, where, [
|
|
278
469
|
...(area ? [area] : []),
|
|
279
470
|
...laterBinds,
|
|
471
|
+
...overdueBinds,
|
|
472
|
+
...tf.binds,
|
|
280
473
|
...activeFirstBinds,
|
|
281
474
|
]);
|
|
282
|
-
const items = materialize(db, rows);
|
|
475
|
+
const items = materialize(db, rows, logBoundary(db, options?.now, options?.zone), packedToday);
|
|
283
476
|
if (options?.later !== true)
|
|
284
477
|
return items;
|
|
285
478
|
// Each group's later sub-block reads like Upcoming: SCHEDULED projects
|
|
@@ -287,7 +480,7 @@ export function projectsView(db, options) {
|
|
|
287
480
|
// hidden on entities but present on the raw rows) — then someday in drag
|
|
288
481
|
// order. The SQL already made group runs contiguous with actives leading,
|
|
289
482
|
// so only the later runs are re-sorted (stable within someday).
|
|
290
|
-
const todayIso = localToday(options?.now);
|
|
483
|
+
const todayIso = localToday(options?.now, options?.zone);
|
|
291
484
|
const ti = new Map(rows.map((r) => [r.uuid, r.todayIndex ?? 0]));
|
|
292
485
|
const isLater = (p) => p.startDate !== null ? p.startDate > todayIso : p.start === "someday";
|
|
293
486
|
const out = [];
|
|
@@ -333,13 +526,14 @@ export function projectsView(db, options) {
|
|
|
333
526
|
* carry no modification dates, and checklist-item edits do not bump the
|
|
334
527
|
* parent task — those changes are invisible here.
|
|
335
528
|
*/
|
|
336
|
-
export function changesView(db, options) {
|
|
529
|
+
export function changesView(db, now, options, zone) {
|
|
337
530
|
const cap = options.limit === null ? null : (options.limit ?? 200);
|
|
338
531
|
const sinceEpoch = options.since.getTime() / 1000;
|
|
339
532
|
const rows = fetchTaskRows(db, `t.type IN (0, 1) AND t.userModificationDate > ?
|
|
340
533
|
ORDER BY t.userModificationDate DESC${cap === null ? "" : " LIMIT ?"}`, [sinceEpoch, ...(cap === null ? [] : [cap])]);
|
|
534
|
+
const changed = materialize(db, rows, logBoundary(db, now, zone), encodePackedDate(localToday(now, zone)));
|
|
341
535
|
// oxlint-disable-next-line no-map-spread -- building fresh change objects, not mutating
|
|
342
|
-
return
|
|
536
|
+
return changed.map((item, i) => ({
|
|
343
537
|
...item,
|
|
344
538
|
changeKind: (rows[i]?.creationDate ?? 0) > sinceEpoch ? "created" : "modified",
|
|
345
539
|
}));
|
|
@@ -359,7 +553,7 @@ const somedayRank = (i) => (i.start === "someday" ? 1 : 0);
|
|
|
359
553
|
function byStatusThenRecent(a, b) {
|
|
360
554
|
return somedayRank(a) - somedayRank(b) || b.modified.getTime() - a.modified.getTime();
|
|
361
555
|
}
|
|
362
|
-
export function liteTitleSearch(db, query, options) {
|
|
556
|
+
export function liteTitleSearch(db, query, options, now, zone) {
|
|
363
557
|
const cap = options?.limit ?? 10;
|
|
364
558
|
const type = options?.type;
|
|
365
559
|
const needle = query.toLowerCase();
|
|
@@ -371,8 +565,8 @@ export function liteTitleSearch(db, query, options) {
|
|
|
371
565
|
let tasks = [];
|
|
372
566
|
if (type === undefined || type === "to-do" || type === "project") {
|
|
373
567
|
const typeSql = type === "to-do" ? "t.type = 0" : type === "project" ? "t.type = 1" : "t.type IN (0, 1)";
|
|
374
|
-
const rows = fetchTaskRows(db, `${OPEN} AND ${CONTAINER_UNTRASHED} AND ${typeSql} AND t.title LIKE
|
|
375
|
-
tasks = materialize(db, rows);
|
|
568
|
+
const rows = fetchTaskRows(db, `${OPEN} AND ${CONTAINER_UNTRASHED} AND ${typeSql} AND t.title LIKE ?${LIKE_ESCAPE}`, [`%${escapeLike(query)}%`]);
|
|
569
|
+
tasks = materialize(db, rows, logBoundary(db, now, zone), encodePackedDate(localToday(now, zone)));
|
|
376
570
|
}
|
|
377
571
|
const projects = tasks.filter((t) => t.type === "project").toSorted(byStatusThenRecent);
|
|
378
572
|
const todos = tasks.filter((t) => t.type === "to-do").toSorted(byStatusThenRecent);
|
|
@@ -383,9 +577,17 @@ export function liteTitleSearch(db, query, options) {
|
|
|
383
577
|
];
|
|
384
578
|
return { candidates: ordered.slice(0, cap), total: ordered.length };
|
|
385
579
|
}
|
|
386
|
-
export function searchView(db, query, options) {
|
|
580
|
+
export function searchView(db, query, options, now, zone) {
|
|
387
581
|
const cap = options?.limit === null ? null : (options?.limit ?? 50);
|
|
388
|
-
const needle = `%${query}%`;
|
|
582
|
+
const needle = `%${escapeLike(query)}%`;
|
|
583
|
+
// `--overdue` narrows to OPEN, past-deadline matches. Its open-only predicate
|
|
584
|
+
// is contradictory with the status-widening flags (logged/trashed/all); the
|
|
585
|
+
// CLI/MCP surfaces reject that combination, so here it simply intersects.
|
|
586
|
+
const packedToday = encodePackedDate(localToday(now, zone));
|
|
587
|
+
const of = overdueFilter(options, packedToday);
|
|
588
|
+
// Threaded into every materialize so the `logged` flag (--logged widens to
|
|
589
|
+
// closed rows) honors the injected clock, not wall time.
|
|
590
|
+
const boundary = logBoundary(db, now, zone);
|
|
389
591
|
// The scope predicates (everything except the match needle), reused verbatim
|
|
390
592
|
// for the needle query AND the heading-credited-project query so both honor
|
|
391
593
|
// the same type/status/scope constraints.
|
|
@@ -418,10 +620,10 @@ export function searchView(db, query, options) {
|
|
|
418
620
|
}
|
|
419
621
|
const tf = tagFilter(db, options);
|
|
420
622
|
// Needle matches: title OR notes. No SQL LIMIT — ranking runs before the cap.
|
|
421
|
-
const rows = fetchTaskRows(db, `${scope.join(" AND ")} AND (t.title LIKE
|
|
623
|
+
const rows = fetchTaskRows(db, `${scope.join(" AND ")} AND (t.title LIKE ?${LIKE_ESCAPE} OR t.notes LIKE ?${LIKE_ESCAPE})${tf.sql}${of.sql}`, [...scopeBinds, needle, needle, ...tf.binds, ...of.binds]);
|
|
422
624
|
const needleLower = query.toLowerCase();
|
|
423
625
|
const matches = new Map();
|
|
424
|
-
for (const item of materialize(db, rows)) {
|
|
626
|
+
for (const item of materialize(db, rows, boundary, packedToday)) {
|
|
425
627
|
// Field credit: title beats notes when both carry the substring.
|
|
426
628
|
const field = item.title.toLowerCase().includes(needleLower) ? "title" : "notes";
|
|
427
629
|
matches.set(item.uuid, { item, field });
|
|
@@ -433,7 +635,7 @@ export function searchView(db, query, options) {
|
|
|
433
635
|
if (options?.type !== "to-do") {
|
|
434
636
|
const headingRows = db
|
|
435
637
|
.prepare(`SELECT project AS projectUuid, title FROM TMTask
|
|
436
|
-
WHERE type = 2 AND project IS NOT NULL AND trashed = 0 AND title LIKE
|
|
638
|
+
WHERE type = 2 AND project IS NOT NULL AND trashed = 0 AND title LIKE ?${LIKE_ESCAPE}`)
|
|
437
639
|
.all(needle);
|
|
438
640
|
const headingTitleFor = new Map();
|
|
439
641
|
for (const h of headingRows) {
|
|
@@ -446,8 +648,8 @@ export function searchView(db, query, options) {
|
|
|
446
648
|
const wanted = [...headingTitleFor.keys()].filter((uuid) => !matches.has(uuid));
|
|
447
649
|
if (wanted.length > 0) {
|
|
448
650
|
const placeholders = wanted.map(() => "?").join(", ");
|
|
449
|
-
const projectRows = fetchTaskRows(db, `${scope.join(" AND ")} AND t.type = 1 AND t.uuid IN (${placeholders})${tf.sql}`, [...scopeBinds, ...wanted, ...tf.binds]);
|
|
450
|
-
for (const item of materialize(db, projectRows)) {
|
|
651
|
+
const projectRows = fetchTaskRows(db, `${scope.join(" AND ")} AND t.type = 1 AND t.uuid IN (${placeholders})${tf.sql}${of.sql}`, [...scopeBinds, ...wanted, ...tf.binds, ...of.binds]);
|
|
652
|
+
for (const item of materialize(db, projectRows, boundary, packedToday)) {
|
|
451
653
|
const matchedVia = {
|
|
452
654
|
kind: "heading",
|
|
453
655
|
title: headingTitleFor.get(item.uuid) ?? "",
|