things-api 0.12.0 → 0.13.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 +1 -1
- package/dist/cli/canonical-ref.d.ts +21 -0
- package/dist/cli/canonical-ref.js +23 -0
- package/dist/cli/canonical-ref.js.map +1 -0
- package/dist/cli/commands/area.js +27 -11
- package/dist/cli/commands/area.js.map +1 -1
- package/dist/cli/commands/install-skill.d.ts +2 -2
- package/dist/cli/commands/install-skill.js +29 -0
- package/dist/cli/commands/install-skill.js.map +1 -1
- package/dist/cli/commands/mcp.js +12 -1
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/project.js +73 -21
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/cli/commands/reads.d.ts +5 -1
- package/dist/cli/commands/reads.js +10 -3
- package/dist/cli/commands/reads.js.map +1 -1
- package/dist/cli/commands/setup.js +11 -4
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/show.js +45 -30
- package/dist/cli/commands/show.js.map +1 -1
- package/dist/cli/commands/todo.js +1 -1
- package/dist/cli/commands/todo.js.map +1 -1
- package/dist/cli/commands/writes.js +31 -6
- package/dist/cli/commands/writes.js.map +1 -1
- package/dist/cli/did-you-mean.js +19 -5
- package/dist/cli/did-you-mean.js.map +1 -1
- package/dist/cli/dry-run.d.ts +55 -0
- package/dist/cli/dry-run.js +92 -0
- package/dist/cli/dry-run.js.map +1 -0
- package/dist/cli/help.js +7 -6
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.js +7 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/read-driver.d.ts +16 -6
- package/dist/cli/read-driver.js +19 -18
- package/dist/cli/read-driver.js.map +1 -1
- package/dist/cli/ref-render.d.ts +14 -0
- package/dist/cli/ref-render.js +9 -0
- package/dist/cli/ref-render.js.map +1 -0
- package/dist/cli/render.d.ts +21 -6
- package/dist/cli/render.js +73 -34
- package/dist/cli/render.js.map +1 -1
- package/dist/cli/resolve-invocation.d.ts +5 -3
- package/dist/cli/resolve-invocation.js +13 -4
- package/dist/cli/resolve-invocation.js.map +1 -1
- package/dist/client.d.ts +18 -7
- package/dist/client.js +2 -1
- package/dist/client.js.map +1 -1
- package/dist/contracts.d.ts +12 -1
- package/dist/contracts.js +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/db/baselines/db-v26.js +4 -1
- package/dist/db/baselines/db-v26.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +81 -42
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/entities.d.ts +11 -1
- package/dist/model/entities.js.map +1 -1
- package/dist/model/mappers.js +3 -0
- package/dist/model/mappers.js.map +1 -1
- package/dist/model/serialize.js +15 -3
- package/dist/model/serialize.js.map +1 -1
- package/dist/read/area-filter.d.ts +2 -2
- package/dist/read/area-filter.js +5 -6
- package/dist/read/area-filter.js.map +1 -1
- package/dist/read/area-view.d.ts +11 -0
- package/dist/read/area-view.js +5 -1
- package/dist/read/area-view.js.map +1 -1
- package/dist/read/project-view.d.ts +61 -1
- package/dist/read/project-view.js +74 -9
- package/dist/read/project-view.js.map +1 -1
- package/dist/read/queries.d.ts +71 -2
- package/dist/read/queries.js +199 -10
- package/dist/read/queries.js.map +1 -1
- package/dist/read/scope.d.ts +2 -2
- package/dist/read/scope.js +5 -6
- package/dist/read/scope.js.map +1 -1
- package/dist/read/shape.d.ts +36 -17
- package/dist/read/shape.js +251 -200
- package/dist/read/shape.js.map +1 -1
- package/dist/read/show-target.js +52 -33
- package/dist/read/show-target.js.map +1 -1
- package/dist/read/truncation.d.ts +0 -7
- package/dist/read/truncation.js +24 -14
- package/dist/read/truncation.js.map +1 -1
- package/dist/read/views.d.ts +19 -7
- package/dist/read/views.js +13 -12
- package/dist/read/views.js.map +1 -1
- package/dist/surface-copy.d.ts +7 -0
- package/dist/surface-copy.js +7 -0
- package/dist/surface-copy.js.map +1 -1
- package/dist/write/commands.js +3 -1
- package/dist/write/commands.js.map +1 -1
- package/dist/write/move.d.ts +10 -6
- package/dist/write/move.js +417 -76
- package/dist/write/move.js.map +1 -1
- package/dist/write/operations.d.ts +1 -1
- package/dist/write/operations.js.map +1 -1
- package/dist/write/pre-state.d.ts +10 -0
- package/dist/write/pre-state.js +118 -41
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/reorder.js +263 -29
- package/dist/write/reorder.js.map +1 -1
- package/package.json +1 -1
- package/schema/envelope.schema.json +14 -0
- package/skills/things-cli/SKILL.md +6 -4
- package/skills/things-cli/references/contracts.md +3 -1
- package/skills/things-cli/references/errors.md +9 -1
- package/skills/things-cli/references/model.md +6 -4
- package/skills/things-cli/references/ordering.md +21 -4
|
@@ -31,11 +31,48 @@ export interface ProjectHeadingGroup {
|
|
|
31
31
|
/** This heading's repeating template rows (invisible in list views). */
|
|
32
32
|
repeating: Todo[];
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* An ARCHIVED heading rendered inside the logged region (HEADARC2-A). Unlike a
|
|
36
|
+
* live {@link ProjectHeadingGroup}, an archived heading is itself a logged item,
|
|
37
|
+
* and its children — ALL of them, whatever their own state — nest beneath it as
|
|
38
|
+
* a grouped section (most-recently-completed first). That includes any odd OPEN
|
|
39
|
+
* child a GUI "Put Back" left stranded under the archived heading (HEADARC2-C):
|
|
40
|
+
* the group renders what is there, it does NOT filter to status=3.
|
|
41
|
+
*/
|
|
42
|
+
export interface LoggedHeadingGroup {
|
|
43
|
+
/** The archived heading (status "completed"); the wire emits its `archived`. */
|
|
44
|
+
heading: Heading;
|
|
45
|
+
/** Every child of the archived heading, most-recently-completed first. */
|
|
46
|
+
items: Todo[];
|
|
47
|
+
}
|
|
34
48
|
export interface ProjectView {
|
|
35
49
|
project: Project;
|
|
50
|
+
/**
|
|
51
|
+
* ALL live (non-logbook, non-trash) children of the project — unheaded AND
|
|
52
|
+
* headed — in project `index` order, each headed row carrying its `heading`
|
|
53
|
+
* ref (and `headingProject`). This is the flat wire representation (the read
|
|
54
|
+
* doctrine's project-view `items[]`): membership in a heading is a per-row
|
|
55
|
+
* attribute, not a bucket. The structured buckets below (`active`, `headings`,
|
|
56
|
+
* `scheduled`, …) are the SAME children re-grouped into the GUI's render layout
|
|
57
|
+
* for the TTY projection — the library owns the clock + `todayIndex`, so the
|
|
58
|
+
* renderer reconstructs the exact GUI placement from them without re-deriving
|
|
59
|
+
* dates. The two are always consistent (built from one child fetch).
|
|
60
|
+
*/
|
|
61
|
+
items: Todo[];
|
|
62
|
+
/**
|
|
63
|
+
* The full heading catalog — EVERY heading (live AND swept archived) in project
|
|
64
|
+
* `index` order (read-shape doctrine §3.12 / #C3). This is the flat wire
|
|
65
|
+
* representation of `headings[]` (`[{uuid,title,archived?}]`): which headings
|
|
66
|
+
* exist, in what order, and whether each is archived — the ORDER axis that
|
|
67
|
+
* earns the bucket its place. A live-group's row membership rides the flat
|
|
68
|
+
* `items[]` refs; a swept archived heading's children ride the flat `logbook`
|
|
69
|
+
* refs. The structured `headings` / `loggedHeadings` below re-group the SAME
|
|
70
|
+
* headings + children into the GUI render layout for the byte-stable TTY.
|
|
71
|
+
*/
|
|
72
|
+
headingCatalog: Heading[];
|
|
36
73
|
/** Open, unscheduled/current UNHEADED children, by index. */
|
|
37
74
|
active: Todo[];
|
|
38
|
-
/**
|
|
75
|
+
/** OPEN headings in project order, each with its own sub-buckets. An archived heading never renders here. */
|
|
39
76
|
headings: ProjectHeadingGroup[];
|
|
40
77
|
/** UNHEADED future-dated children grouped by date ascending. */
|
|
41
78
|
scheduled: IsoDateGroup<Todo>[];
|
|
@@ -43,7 +80,20 @@ export interface ProjectView {
|
|
|
43
80
|
someday: Todo[];
|
|
44
81
|
/** UNHEADED repeating template rows owned by this project (invisible in list views). */
|
|
45
82
|
repeating: Todo[];
|
|
83
|
+
/**
|
|
84
|
+
* The flat logged region: swept children of OPEN headings (each carrying its
|
|
85
|
+
* heading ref) plus swept un-headed children, most-recently-completed first.
|
|
86
|
+
* Children of ARCHIVED headings are NOT here — they group under their heading
|
|
87
|
+
* in {@link ProjectView.loggedHeadings}.
|
|
88
|
+
*/
|
|
46
89
|
logged: Todo[];
|
|
90
|
+
/**
|
|
91
|
+
* The archived-heading GROUPS of the logged region (HEADARC2-A), in project
|
|
92
|
+
* (heading `index`) order — each an archived heading with its children nested.
|
|
93
|
+
* Rendered only under `--show-logged`, after the flat {@link ProjectView.logged}
|
|
94
|
+
* rows (the flat-then-grouped ordering is a defensible choice, not GUI-probed).
|
|
95
|
+
*/
|
|
96
|
+
loggedHeadings: LoggedHeadingGroup[];
|
|
47
97
|
/**
|
|
48
98
|
* PLOG1 (additive): count of untrashed OPEN (status=0) children this project
|
|
49
99
|
* still holds while it is ITSELF completed or canceled — including once swept
|
|
@@ -54,6 +104,16 @@ export interface ProjectView {
|
|
|
54
104
|
* child.
|
|
55
105
|
*/
|
|
56
106
|
openChildrenWhileResolved: number;
|
|
107
|
+
/**
|
|
108
|
+
* HEADARC2-C (additive): count of untrashed OPEN (status=0) children buried
|
|
109
|
+
* under an ARCHIVED heading of this project — the heading analog of the §6¾
|
|
110
|
+
* odd state. The GUI's "Put Back" of a trashed open child restores it in place
|
|
111
|
+
* under the archived heading WITHOUT reopening the heading, leaving an open,
|
|
112
|
+
* actionable to-do invisible to every live view (reachable only by expanding
|
|
113
|
+
* this card's logged region). Counted regardless of the PROJECT's own status.
|
|
114
|
+
* 0 when no archived heading holds an open child.
|
|
115
|
+
*/
|
|
116
|
+
openChildrenUnderArchivedHeading: number;
|
|
57
117
|
}
|
|
58
118
|
export declare class ProjectNotFoundError extends Error {
|
|
59
119
|
constructor(uuid: string);
|
|
@@ -72,30 +72,63 @@ export function projectView(db, uuid, now, filter = {}, zone) {
|
|
|
72
72
|
todo: mapTodo(r, refs, childTags.get(r.uuid) ?? [], packedToday),
|
|
73
73
|
}));
|
|
74
74
|
markLogged([project, ...todos.map((t) => t.todo)], boundary);
|
|
75
|
+
// TRINARY heading split (the completion≠logged law, applied to headings): an
|
|
76
|
+
// OPEN heading — or an archived one NOT yet past the log-move boundary — stays
|
|
77
|
+
// a LIVE group IN PLACE (an archived-unswept heading carries `archived` yet
|
|
78
|
+
// renders among the live groups, exactly as a completed-unswept to-do row
|
|
79
|
+
// stays checked in place, §above / log-boundary.ts). Only a SWEPT archived
|
|
80
|
+
// heading moves into the logged region as a grouped section (HEADARC2-A).
|
|
81
|
+
const headingSwept = (h) => h.status !== "open" && h.stopped !== null && h.stopped <= boundary;
|
|
82
|
+
const liveHeadings = headings.filter((h) => !headingSwept(h));
|
|
83
|
+
const sweptHeadings = headings.filter((h) => headingSwept(h));
|
|
84
|
+
const liveHeadingSet = new Set(liveHeadings.map((h) => h.uuid));
|
|
85
|
+
const sweptHeadingSet = new Set(sweptHeadings.map((h) => h.uuid));
|
|
86
|
+
// The owning-project ref stamped on every headed child — so the heading ref a
|
|
87
|
+
// flat logged row carries promotes its `headingUuid` in the project's scope
|
|
88
|
+
// (the round-trip predicate is project-scoped), mirroring the list views.
|
|
89
|
+
const projectRef = { uuid: project.uuid, title: project.title };
|
|
75
90
|
// Project-level (UNHEADED) buckets.
|
|
76
91
|
const active = [];
|
|
77
92
|
const scheduledRows = [];
|
|
78
93
|
const repeating = [];
|
|
79
94
|
const someday = [];
|
|
80
95
|
const logged = [];
|
|
81
|
-
//
|
|
82
|
-
//
|
|
96
|
+
// Children of a SWEPT archived heading — grouped under their heading in the
|
|
97
|
+
// logged region (keyed by heading uuid), ALL of them regardless of state.
|
|
98
|
+
const sweptHeadingChildren = new Map();
|
|
99
|
+
// Per-heading LIVE sub-buckets, keyed by heading uuid (§9 fidelity fix): a
|
|
100
|
+
// headed scheduled/someday/repeating child nests under its heading, NOT at the
|
|
83
101
|
// project level.
|
|
84
102
|
const headingItems = new Map();
|
|
85
103
|
const headingScheduled = new Map();
|
|
86
104
|
const headingSomeday = new Map();
|
|
87
105
|
const headingRepeating = new Map();
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
// vanishing (mirrors the render's
|
|
91
|
-
const
|
|
92
|
-
const headingUuidOf = (row) => row.heading !== null && headingSet.has(row.heading) ? row.heading : null;
|
|
106
|
+
// A child whose heading FK is not a LIVE heading (an unknown/trashed heading,
|
|
107
|
+
// OR a swept archived heading handled above) falls back to the project-level
|
|
108
|
+
// (unheaded) buckets rather than vanishing (mirrors the render's loose fallback).
|
|
109
|
+
const liveHeadingUuidOf = (row) => row.heading !== null && liveHeadingSet.has(row.heading) ? row.heading : null;
|
|
93
110
|
// Every untrashed, non-template OPEN child, regardless of which bucket it
|
|
94
111
|
// lands in below — surfaced (only when the project is itself resolved) as
|
|
95
112
|
// the PLOG1 stranded-open-child count.
|
|
96
113
|
let openChildren = 0;
|
|
114
|
+
// Open children buried under a SWEPT archived heading — the §6¾ odd state for
|
|
115
|
+
// headings (HEADARC2-C), surfaced regardless of the project's own status.
|
|
116
|
+
let openChildrenUnderArchivedHeading = 0;
|
|
97
117
|
for (const { row, todo } of todos) {
|
|
98
|
-
|
|
118
|
+
if (todo.heading !== null)
|
|
119
|
+
todo.headingProject = projectRef;
|
|
120
|
+
// A child of a SWEPT archived heading groups under it in the logged region,
|
|
121
|
+
// WHATEVER its own state — including the odd OPEN child a GUI Put-Back
|
|
122
|
+
// stranded (HEADARC2-C): render what is there, do NOT filter to status=3.
|
|
123
|
+
if (row.heading !== null && sweptHeadingSet.has(row.heading)) {
|
|
124
|
+
pushInto(sweptHeadingChildren, row.heading, todo);
|
|
125
|
+
if (row.status === 0 && !todo.repeating.isTemplate) {
|
|
126
|
+
openChildren += 1; // also stranded when the project itself is resolved
|
|
127
|
+
openChildrenUnderArchivedHeading += 1;
|
|
128
|
+
}
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const h = liveHeadingUuidOf(row);
|
|
99
132
|
if (todo.repeating.isTemplate) {
|
|
100
133
|
if (h !== null)
|
|
101
134
|
pushInto(headingRepeating, h, todo);
|
|
@@ -151,7 +184,7 @@ export function projectView(db, uuid, now, filter = {}, zone) {
|
|
|
151
184
|
// section; a heading with any surviving child (in ANY sub-bucket) is kept.
|
|
152
185
|
// With no scope active every heading renders (its own empty state).
|
|
153
186
|
const contentScoped = overdue || tf.sql !== "";
|
|
154
|
-
const headingGroups =
|
|
187
|
+
const headingGroups = liveHeadings
|
|
155
188
|
.map((heading) => ({
|
|
156
189
|
heading,
|
|
157
190
|
items: headingItems.get(heading.uuid) ?? [],
|
|
@@ -164,15 +197,47 @@ export function projectView(db, uuid, now, filter = {}, zone) {
|
|
|
164
197
|
g.scheduled.length > 0 ||
|
|
165
198
|
g.someday.length > 0 ||
|
|
166
199
|
g.repeating.length > 0);
|
|
200
|
+
// The logged-region archived-heading groups, in project (heading index) order;
|
|
201
|
+
// each group's children most-recently-completed first (open odd children, with
|
|
202
|
+
// no stopDate, sort last — the same null-last convention the flat logged list
|
|
203
|
+
// uses). Under a content scope (--overdue / --tag) an empty group collapses.
|
|
204
|
+
const loggedHeadings = sweptHeadings
|
|
205
|
+
.map((heading) => ({
|
|
206
|
+
heading,
|
|
207
|
+
items: (sweptHeadingChildren.get(heading.uuid) ?? []).toSorted((a, b) => (b.stopped?.getTime() ?? 0) - (a.stopped?.getTime() ?? 0)),
|
|
208
|
+
}))
|
|
209
|
+
.filter((g) => !contentScoped || g.items.length > 0);
|
|
210
|
+
// The flat wire `items[]`: every LIVE child in project index order (the
|
|
211
|
+
// `todos` fetch is already `ORDER BY index ASC`). Excludes swept logged rows
|
|
212
|
+
// (→ `logged`) and children of swept archived headings (→ `loggedHeadings`) —
|
|
213
|
+
// exactly the rows that leave the live region. Each headed row already carries
|
|
214
|
+
// its `heading` ref + `headingProject` (stamped in the loop above).
|
|
215
|
+
const items = todos
|
|
216
|
+
.map((t) => t.todo)
|
|
217
|
+
.filter((td) => !td.logged && !(td.heading !== null && sweptHeadingSet.has(td.heading.uuid)));
|
|
218
|
+
// The catalog: every heading (live + swept archived) in index order — but under
|
|
219
|
+
// a content scope (`--overdue` / `--tag`) a heading whose children were all
|
|
220
|
+
// filtered out COLLAPSES, exactly as it does for the live groups + logged groups
|
|
221
|
+
// and the TTY. With no scope every heading survives (empty ones included), so
|
|
222
|
+
// this is a no-op there. Built from the SAME survivors the render layout uses.
|
|
223
|
+
const survivingHeadings = new Set([
|
|
224
|
+
...headingGroups.map((g) => g.heading.uuid),
|
|
225
|
+
...loggedHeadings.map((g) => g.heading.uuid),
|
|
226
|
+
]);
|
|
227
|
+
const headingCatalog = headings.filter((h) => survivingHeadings.has(h.uuid));
|
|
167
228
|
return {
|
|
168
229
|
project,
|
|
230
|
+
items,
|
|
231
|
+
headingCatalog,
|
|
169
232
|
active,
|
|
170
233
|
headings: headingGroups,
|
|
171
234
|
scheduled,
|
|
172
235
|
someday,
|
|
173
236
|
repeating,
|
|
174
237
|
logged,
|
|
238
|
+
loggedHeadings,
|
|
175
239
|
openChildrenWhileResolved: project.status === "open" ? 0 : openChildren,
|
|
240
|
+
openChildrenUnderArchivedHeading,
|
|
176
241
|
};
|
|
177
242
|
}
|
|
178
243
|
//# sourceMappingURL=project-view.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project-view.js","sourceRoot":"","sources":["../../src/read/project-view.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAEnF,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,OAAO,EAAgB,MAAM,qBAAqB,CAAC;AACpF,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAClG,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC1C,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAmB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"project-view.js","sourceRoot":"","sources":["../../src/read/project-view.ts"],"names":[],"mappings":"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"}
|
package/dist/read/queries.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import type { DatabaseSync } from "node:sqlite";
|
|
6
6
|
import { type Ref } from "../model/entities.ts";
|
|
7
7
|
import type { ChecklistRow, TaskRow } from "../model/mappers.ts";
|
|
8
|
-
import { type CandidateRef } from "./shape.ts";
|
|
8
|
+
import { type CandidateRef, type RefKind, type RefPromoter } from "./shape.ts";
|
|
9
9
|
/** Rows that repeat via a template are normal; template rows are invisible in list views. */
|
|
10
10
|
export declare const NOT_TEMPLATE = "(t.rt1_recurrenceRule IS NULL AND t.repeater IS NULL)";
|
|
11
11
|
/**
|
|
@@ -129,6 +129,9 @@ export declare function resolveTaskUuidPrefix(db: DatabaseSync, refRaw: string,
|
|
|
129
129
|
* docs/design/reference-resolution.md.
|
|
130
130
|
*/
|
|
131
131
|
export declare function normalizeNameKey(s: string): string;
|
|
132
|
+
/** The fused TTY ref form `Title [8charPrefix]` — the round-trippable decorated ref every candidate renders. */
|
|
133
|
+
export declare const REF_PREFIX_LEN = 8;
|
|
134
|
+
export declare function fusedRef(title: string, uuid: string): string;
|
|
132
135
|
export interface NamedResolution {
|
|
133
136
|
resolved: {
|
|
134
137
|
uuid: string;
|
|
@@ -206,10 +209,49 @@ export declare function resolveProjectWriteTarget(db: DatabaseSync, refRaw: stri
|
|
|
206
209
|
};
|
|
207
210
|
}, includeTrashed?: boolean, includeNonOpen?: boolean): string;
|
|
208
211
|
export declare function resolveTagUuid(db: DatabaseSync, ref: string): string;
|
|
212
|
+
/** Options shared by the read-side project resolvers (a name-tier narrowing subset). */
|
|
213
|
+
interface ReadProjectOptions {
|
|
214
|
+
prefixTier?: boolean;
|
|
215
|
+
scopeWhere?: string;
|
|
216
|
+
scopeBinds?: (string | number)[];
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* A read-side PROJECT NAME resolution verdict under the liveness law (never
|
|
220
|
+
* throws) — the shared core behind the throwing {@link resolveProjectUuid}
|
|
221
|
+
* (`project show`) and the cross-kind {@link classifyShowTarget} router.
|
|
222
|
+
*
|
|
223
|
+
* The uuid / partial-uuid tiers reach EVERY project row (explicit intent stays
|
|
224
|
+
* able to view a trashed project by id); the NAME tiers resolve against LIVE
|
|
225
|
+
* (untrashed) rows only, so a dead twin never shadows a live one nor inflates
|
|
226
|
+
* an ambiguity count. When the live pool is empty, the TRASHED pool is consulted
|
|
227
|
+
* for the reads-only ergonomic fallback: a UNIQUELY-named trashed project still
|
|
228
|
+
* resolves by name (the render discloses it — the card's own `(trashed)` marker
|
|
229
|
+
* / `stage: "trash"`), while several dead twins report honestly instead.
|
|
230
|
+
*/
|
|
231
|
+
export interface ReadProjectVerdict {
|
|
232
|
+
/** The resolved winner — a live unique, a uuid/partial-uuid, or the unique-dead fallback; else null. */
|
|
233
|
+
resolved: {
|
|
234
|
+
uuid: string;
|
|
235
|
+
title: string;
|
|
236
|
+
} | null;
|
|
237
|
+
/** ALL live name-matching rows at the deciding tier (0, 1, or many). */
|
|
238
|
+
liveRows: {
|
|
239
|
+
uuid: string;
|
|
240
|
+
title: string;
|
|
241
|
+
}[];
|
|
242
|
+
/** 0 / 1 / >1 — the LIVE match count (the ambiguity count when > 1). */
|
|
243
|
+
liveMatches: number;
|
|
244
|
+
/** Count of TRASHED-name twins — feeds the disclosure tail / dead-hint. */
|
|
245
|
+
trashedMatches: number;
|
|
246
|
+
}
|
|
247
|
+
export declare function readProjectNameVerdict(db: DatabaseSync, ref: string, options?: ReadProjectOptions): ReadProjectVerdict;
|
|
209
248
|
/**
|
|
210
249
|
* Write destinations stay strict (a trashed project is not a valid target);
|
|
211
250
|
* READ surfaces pass `trashed: true` so a project in the Trash can still be
|
|
212
|
-
* viewed
|
|
251
|
+
* viewed. Under `trashed: true` the read-side liveness law applies: an explicit
|
|
252
|
+
* uuid / partial-uuid reaches any project, but a NAME resolves against LIVE rows
|
|
253
|
+
* only (a dead same-name twin never shadows a live one), with the reads-only
|
|
254
|
+
* unique-dead fallback + trash disclosure ({@link readProjectNameVerdict}).
|
|
213
255
|
*/
|
|
214
256
|
export declare function resolveProjectUuid(db: DatabaseSync, ref: string, options?: {
|
|
215
257
|
trashed?: boolean;
|
|
@@ -217,6 +259,14 @@ export declare function resolveProjectUuid(db: DatabaseSync, ref: string, option
|
|
|
217
259
|
scopeWhere?: string;
|
|
218
260
|
scopeBinds?: (string | number)[];
|
|
219
261
|
}): string;
|
|
262
|
+
/**
|
|
263
|
+
* The disclosure tail appended to a read-side project ambiguity when the
|
|
264
|
+
* uuid-reachable pool holds additional TRASHED twins of the ambiguous name. The
|
|
265
|
+
* ambiguity COUNT stays over live rows (coherent with the rendered candidate
|
|
266
|
+
* list); the dead ones are disclosed separately rather than inflating it. Empty
|
|
267
|
+
* when none are trashed.
|
|
268
|
+
*/
|
|
269
|
+
export declare function trashDisclosureTail(deadCount: number): string;
|
|
220
270
|
export declare function resolveAreaUuid(db: DatabaseSync, ref: string, options?: {
|
|
221
271
|
prefixTier?: boolean;
|
|
222
272
|
scopeWhere?: string;
|
|
@@ -245,6 +295,24 @@ export declare function resolveHeadingUuid(db: DatabaseSync, projectUuid: string
|
|
|
245
295
|
uuid: string;
|
|
246
296
|
title: string;
|
|
247
297
|
};
|
|
298
|
+
/**
|
|
299
|
+
* Does a container ref's bare TITLE round-trip through its own resolver, in its
|
|
300
|
+
* own scope, back to THIS entity's uuid? The single predicate the emit boundary
|
|
301
|
+
* consults for the JSON round-trip law — true only when the sole resolution is
|
|
302
|
+
* `entityUuid` (not-found / ambiguous / a different entity all → false, i.e. the
|
|
303
|
+
* title must promote its uuid sibling). See {@link resolveTitleForRoundTrip}.
|
|
304
|
+
*/
|
|
305
|
+
export declare function titleRoundTrips(db: DatabaseSync, kind: RefKind, title: string, entityUuid: string, projectUuid?: string): boolean;
|
|
306
|
+
/**
|
|
307
|
+
* Build the {@link RefPromoter} the read-shaping transform ({@link shapeReadPayload})
|
|
308
|
+
* consults to decide flat-ref uuid promotion, memoized per (kind, title, scope)
|
|
309
|
+
* for ONE response emission so a large view never re-runs a resolution it has
|
|
310
|
+
* already made. A fresh promoter (fresh memo) per response — the consumer
|
|
311
|
+
* surfaces build one via the client. The memo caches the RESOLVED uuid (or null),
|
|
312
|
+
* independent of the comparison target, so twin entities sharing a title share
|
|
313
|
+
* the one resolution.
|
|
314
|
+
*/
|
|
315
|
+
export declare function makeRefPromoter(db: DatabaseSync): RefPromoter;
|
|
248
316
|
/**
|
|
249
317
|
* A row's EFFECTIVE area: its own `area` link, else the area of its project,
|
|
250
318
|
* else the area of its heading's project. To-dos nested in a project (or under a
|
|
@@ -323,3 +391,4 @@ export declare function fetchTagsForTasks(db: DatabaseSync, taskUuids: string[])
|
|
|
323
391
|
export declare function makeHeadingProjectResolver(db: DatabaseSync): (headingUuid: string) => Ref | null;
|
|
324
392
|
/** Lazy uuid -> Ref resolver over TMTask + TMArea titles, cached per instance. */
|
|
325
393
|
export declare function makeRefResolver(db: DatabaseSync): (uuid: string | null) => Ref | null;
|
|
394
|
+
export {};
|
package/dist/read/queries.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { q, selectList } from "../db/schema.js";
|
|
2
2
|
import { TASK_TYPE_FROM_DB } from "../model/entities.js";
|
|
3
|
-
import { candidateRef, CANDIDATE_CAP } from "./shape.js";
|
|
3
|
+
import { candidateRef, CANDIDATE_CAP, } from "./shape.js";
|
|
4
4
|
/** Rows that repeat via a template are normal; template rows are invisible in list views. */
|
|
5
5
|
export const NOT_TEMPLATE = "(t.rt1_recurrenceRule IS NULL AND t.repeater IS NULL)";
|
|
6
6
|
/** ` AND col IN (…)` for the restricted form; empty for the tag-agnostic form. */
|
|
@@ -234,6 +234,27 @@ export function normalizeNameKey(s) {
|
|
|
234
234
|
.replace(/[\s‐-―−-]+/gu, "");
|
|
235
235
|
}
|
|
236
236
|
const BASE62 = /^[0-9A-Za-z]+$/;
|
|
237
|
+
/**
|
|
238
|
+
* The DECORATED-REF form `Title [ref]` — a bare title with the machine-stable
|
|
239
|
+
* uuid/partial-uuid pinned in trailing brackets (the fused form every TTY
|
|
240
|
+
* candidate renders, `Title [8charPrefix]`). The title half is a COMMENT
|
|
241
|
+
* (ignored), so a stale copy still resolves after a rename; the bracket is the
|
|
242
|
+
* ref, resolved through the uuid/partial-uuid tier. Greedy `.*` so the LAST
|
|
243
|
+
* bracket wins (`"Family [Sub] [TC9yozLk]"` → `TC9yozLk`). The empty-title form
|
|
244
|
+
* `" [J2kPq9Ws]"` (a titleless heading) is legal — `(.*)` matches empty. Because
|
|
245
|
+
* `$`-anchored so the LAST bracket always wins. The space before `[` is OPTIONAL
|
|
246
|
+
* because {@link stripThingsUri} trims the ref first, so the empty-title form
|
|
247
|
+
* `" [J2kPq9Ws]"` (a titleless heading) arrives as the bare `[J2kPq9Ws]` — both
|
|
248
|
+
* resolve. The bracket is 4–22 base62 chars; a segment shorter than the 6-char
|
|
249
|
+
* partial-uuid floor simply fails to resolve (the FORM is recognized, the
|
|
250
|
+
* resolution is the real uuid/partial-uuid tier's).
|
|
251
|
+
*/
|
|
252
|
+
const DECORATED_REF = /^(.*?) ?\[([0-9A-Za-z]{4,22})\]$/;
|
|
253
|
+
/** The fused TTY ref form `Title [8charPrefix]` — the round-trippable decorated ref every candidate renders. */
|
|
254
|
+
export const REF_PREFIX_LEN = 8;
|
|
255
|
+
export function fusedRef(title, uuid) {
|
|
256
|
+
return `${title} [${uuid.slice(0, REF_PREFIX_LEN)}]`;
|
|
257
|
+
}
|
|
237
258
|
/**
|
|
238
259
|
* Tiered reference resolution (docs/design/reference-resolution.md): exact
|
|
239
260
|
* uuid → exact title → case-insensitive title → normalized title → uuid
|
|
@@ -287,12 +308,42 @@ export function resolveNamedRef(db, table, extraWhere, extraBinds, refRaw, optio
|
|
|
287
308
|
if (rows.length > 1)
|
|
288
309
|
return { resolved: null, matches: rows.length, candidates: rows };
|
|
289
310
|
}
|
|
311
|
+
// The DECORATED-REF tier — LAST, after exact/case/normalized title and the
|
|
312
|
+
// uuid/partial-uuid tiers all miss. `Title [ref]`: the title is ignored and the
|
|
313
|
+
// bracketed segment resolves through the uuid/partial-uuid tier (uuid-exact,
|
|
314
|
+
// then a >=6-char partial-uuid prefix). Runs regardless of `prefixTier` — the
|
|
315
|
+
// bracket is EXPLICIT ref intent (a rendered fused ref), like a bare uuid. A
|
|
316
|
+
// literal title `Title [ref]` outranks this by construction (exact-title is an
|
|
317
|
+
// earlier tier). Scope/extraWhere apply (uuid-tier `sel`, not the name tiers).
|
|
318
|
+
const decorated = DECORATED_REF.exec(ref);
|
|
319
|
+
if (decorated !== null) {
|
|
320
|
+
const inner = decorated[2] ?? "";
|
|
321
|
+
const exactRows = sel("uuid = ?", [inner]);
|
|
322
|
+
if (exactRows.length === 1)
|
|
323
|
+
return { resolved: exactRows[0] ?? null, matches: 1 };
|
|
324
|
+
if (inner.length >= 6) {
|
|
325
|
+
const upper = inner.slice(0, -1) + String.fromCharCode(inner.charCodeAt(inner.length - 1) + 1);
|
|
326
|
+
const rows = sel("uuid >= ? AND uuid < ?", [inner, upper]);
|
|
327
|
+
if (rows.length === 1)
|
|
328
|
+
return { resolved: rows[0] ?? null, matches: 1 };
|
|
329
|
+
if (rows.length > 1)
|
|
330
|
+
return { resolved: null, matches: rows.length, candidates: rows };
|
|
331
|
+
}
|
|
332
|
+
}
|
|
290
333
|
return { resolved: null, matches: 0 };
|
|
291
334
|
}
|
|
292
335
|
/** The accepted-forms clause for a name-accepting resolver's not-found copy. */
|
|
293
336
|
function acceptedForms(prefixTier) {
|
|
294
337
|
return prefixTier ? "tried uuid, partial-uuid, and name" : "tried uuid and name";
|
|
295
338
|
}
|
|
339
|
+
/**
|
|
340
|
+
* The single-kind ambiguous-name refusal copy shared by the read resolvers
|
|
341
|
+
* (`"X" matches N projects — use the exact name or a uuid`). `kind` is the
|
|
342
|
+
* singular noun; the plural `s` is appended here so the count agrees.
|
|
343
|
+
*/
|
|
344
|
+
function ambiguousNameMessage(ref, count, kind, capped) {
|
|
345
|
+
return `"${ref}" matches ${count} ${kind}s${capped ? `; first ${CANDIDATE_CAP} shown` : ""} — use the exact name or a uuid`;
|
|
346
|
+
}
|
|
296
347
|
function resolveUuidOrThrow(db, table, extraWhere, ref, kind, listCmd, options) {
|
|
297
348
|
const r = resolveNamedRef(db, table, extraWhere, [], ref, options);
|
|
298
349
|
if (r.resolved !== null)
|
|
@@ -301,8 +352,7 @@ function resolveUuidOrThrow(db, table, extraWhere, ref, kind, listCmd, options)
|
|
|
301
352
|
throw new ReferenceResolutionError(`no ${kind} matching "${ref}" — ${acceptedForms(options?.prefixTier !== false)} (list ${kind}s with \`${listCmd}\`)`, { code: "not-found", ref });
|
|
302
353
|
}
|
|
303
354
|
const all = r.candidates ?? [];
|
|
304
|
-
|
|
305
|
-
throw new ReferenceResolutionError(`"${ref}" matches ${r.matches} ${kind}s${capped ? `; first ${CANDIDATE_CAP} shown` : ""} — use the exact name or a uuid`, {
|
|
355
|
+
throw new ReferenceResolutionError(ambiguousNameMessage(ref, r.matches, kind, all.length > CANDIDATE_CAP), {
|
|
306
356
|
code: "ambiguous",
|
|
307
357
|
ref,
|
|
308
358
|
candidates: all.slice(0, CANDIDATE_CAP).map((c) => candidateRef(kind, c)),
|
|
@@ -402,11 +452,13 @@ export function resolveProjectWriteTarget(db, refRaw, scope, includeTrashed = fa
|
|
|
402
452
|
}
|
|
403
453
|
const all = describeProjectCandidates(db, r.candidates ?? []);
|
|
404
454
|
const shown = all.slice(0, CANDIDATE_CAP);
|
|
455
|
+
// The fused ref form `Title [8charPrefix]` — the bracketed prefix pastes back
|
|
456
|
+
// as a decorated ref that resolves straight to this project.
|
|
405
457
|
const lines = shown
|
|
406
|
-
.map((c) => ` ${c.
|
|
458
|
+
.map((c) => ` ${fusedRef(c.title, c.uuid)}${c.area !== undefined ? ` (in ${c.area})` : ""}`)
|
|
407
459
|
.join("\n");
|
|
408
460
|
const more = all.length > CANDIDATE_CAP ? `\n … ${all.length - CANDIDATE_CAP} more` : "";
|
|
409
|
-
throw new ReferenceResolutionError(`"${ref}" matches ${r.matches} projects — disambiguate with a
|
|
461
|
+
throw new ReferenceResolutionError(`"${ref}" matches ${r.matches} projects — disambiguate with a ref below:\n${lines}${more}`, { code: "ambiguous", ref, candidates: shown });
|
|
410
462
|
}
|
|
411
463
|
/** Fixed-shape candidates for an ambiguous project name, each with its area container hint. */
|
|
412
464
|
function describeProjectCandidates(db, candidates) {
|
|
@@ -419,13 +471,82 @@ function describeProjectCandidates(db, candidates) {
|
|
|
419
471
|
export function resolveTagUuid(db, ref) {
|
|
420
472
|
return resolveUuidOrThrow(db, "TMTag", "1=1", ref, "tag", "things tags");
|
|
421
473
|
}
|
|
474
|
+
/** Count (and, when unique, resolve) a NAME against the TRASHED project pool only. */
|
|
475
|
+
function trashedProjectNameMatches(db, ref, options) {
|
|
476
|
+
return resolveNamedRef(db, "TMTask", "type = 1 AND trashed = 1", [], ref, {
|
|
477
|
+
prefixTier: false,
|
|
478
|
+
...(options?.scopeWhere !== undefined && {
|
|
479
|
+
scopeWhere: options.scopeWhere,
|
|
480
|
+
scopeBinds: options.scopeBinds,
|
|
481
|
+
}),
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
export function readProjectNameVerdict(db, ref, options) {
|
|
485
|
+
// uuid/partial-uuid tiers span every project (`type = 1`); name tiers narrow
|
|
486
|
+
// to LIVE rows via `nameExtraWhere` — the same split the write side uses.
|
|
487
|
+
const live = resolveNamedRef(db, "TMTask", "type = 1", [], ref, {
|
|
488
|
+
...options,
|
|
489
|
+
nameExtraWhere: "trashed = 0",
|
|
490
|
+
});
|
|
491
|
+
if (live.resolved !== null)
|
|
492
|
+
return {
|
|
493
|
+
resolved: live.resolved,
|
|
494
|
+
liveRows: [live.resolved],
|
|
495
|
+
liveMatches: 1,
|
|
496
|
+
trashedMatches: 0,
|
|
497
|
+
};
|
|
498
|
+
if (live.matches > 1)
|
|
499
|
+
return {
|
|
500
|
+
resolved: null,
|
|
501
|
+
liveRows: live.candidates ?? [],
|
|
502
|
+
liveMatches: live.matches,
|
|
503
|
+
// Disclose extra trashed twins WITHOUT folding them into the count.
|
|
504
|
+
trashedMatches: trashedProjectNameMatches(db, ref, options).matches,
|
|
505
|
+
};
|
|
506
|
+
// Zero live name/uuid matches: the trashed pool decides the ergonomic fallback.
|
|
507
|
+
const dead = trashedProjectNameMatches(db, ref, options);
|
|
508
|
+
if (dead.resolved !== null)
|
|
509
|
+
return { resolved: dead.resolved, liveRows: [], liveMatches: 0, trashedMatches: dead.matches };
|
|
510
|
+
return { resolved: null, liveRows: [], liveMatches: 0, trashedMatches: dead.matches };
|
|
511
|
+
}
|
|
422
512
|
/**
|
|
423
513
|
* Write destinations stay strict (a trashed project is not a valid target);
|
|
424
514
|
* READ surfaces pass `trashed: true` so a project in the Trash can still be
|
|
425
|
-
* viewed
|
|
515
|
+
* viewed. Under `trashed: true` the read-side liveness law applies: an explicit
|
|
516
|
+
* uuid / partial-uuid reaches any project, but a NAME resolves against LIVE rows
|
|
517
|
+
* only (a dead same-name twin never shadows a live one), with the reads-only
|
|
518
|
+
* unique-dead fallback + trash disclosure ({@link readProjectNameVerdict}).
|
|
426
519
|
*/
|
|
427
520
|
export function resolveProjectUuid(db, ref, options) {
|
|
428
|
-
|
|
521
|
+
if (options?.trashed !== true) {
|
|
522
|
+
// The non-widening read pool: name AND uuid tiers are LIVE-only (a trashed
|
|
523
|
+
// project is invisible even by uuid here) — the filter/scope/write-target
|
|
524
|
+
// callers, unchanged.
|
|
525
|
+
return resolveUuidOrThrow(db, "TMTask", "type = 1 AND trashed = 0", ref, "project", "things projects", options);
|
|
526
|
+
}
|
|
527
|
+
const v = readProjectNameVerdict(db, ref, options);
|
|
528
|
+
if (v.resolved !== null)
|
|
529
|
+
return v.resolved.uuid;
|
|
530
|
+
if (v.liveMatches > 1) {
|
|
531
|
+
const shown = v.liveRows.slice(0, CANDIDATE_CAP);
|
|
532
|
+
throw new ReferenceResolutionError(ambiguousNameMessage(ref, v.liveMatches, "project", v.liveRows.length > CANDIDATE_CAP) +
|
|
533
|
+
trashDisclosureTail(v.trashedMatches), { code: "ambiguous", ref, candidates: shown.map((c) => candidateRef("project", c)) });
|
|
534
|
+
}
|
|
535
|
+
// Zero live matches (a unique dead row would have resolved above), so a
|
|
536
|
+
// non-empty trash count here is always the several-dead-twins case.
|
|
537
|
+
throw new ReferenceResolutionError(`no project matching "${ref}" — ${acceptedForms(options.prefixTier !== false)} (list projects with \`things projects\`)${deadNameMatchHint({ trashed: v.trashedMatches })}`, { code: "not-found", ref });
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* The disclosure tail appended to a read-side project ambiguity when the
|
|
541
|
+
* uuid-reachable pool holds additional TRASHED twins of the ambiguous name. The
|
|
542
|
+
* ambiguity COUNT stays over live rows (coherent with the rendered candidate
|
|
543
|
+
* list); the dead ones are disclosed separately rather than inflating it. Empty
|
|
544
|
+
* when none are trashed.
|
|
545
|
+
*/
|
|
546
|
+
export function trashDisclosureTail(deadCount) {
|
|
547
|
+
return deadCount === 0
|
|
548
|
+
? ""
|
|
549
|
+
: `; also matched: ${deadCount} in the trash — \`things trash\` lists them, a uuid reaches one directly`;
|
|
429
550
|
}
|
|
430
551
|
export function resolveAreaUuid(db, ref, options) {
|
|
431
552
|
return resolveUuidOrThrow(db, "TMArea", "1=1", ref, "area", "things areas", options);
|
|
@@ -464,13 +585,81 @@ export function resolveHeadingUuid(db, projectUuid, refRaw) {
|
|
|
464
585
|
throw new ReferenceResolutionError(`no heading matching ${label} in this project — a heading selector is an exact title or a uuid (list them with \`things project-view <project>\`)`, { code: "not-found", ref: refRaw });
|
|
465
586
|
}
|
|
466
587
|
const all = r.candidates ?? [];
|
|
467
|
-
const
|
|
468
|
-
|
|
588
|
+
const shown = all.slice(0, CANDIDATE_CAP);
|
|
589
|
+
// The fused ref form `Title [8charPrefix]` per candidate — a titleless heading
|
|
590
|
+
// reads ` [prefix]`, which pastes back as its decorated ref.
|
|
591
|
+
const lines = shown.map((c) => ` ${fusedRef(c.title, c.uuid)}`).join("\n");
|
|
592
|
+
const more = all.length > CANDIDATE_CAP ? `\n … ${all.length - CANDIDATE_CAP} more` : "";
|
|
593
|
+
throw new ReferenceResolutionError(`${label} matches ${r.matches} headings in this project — disambiguate with a ref below:\n${lines}${more}`, {
|
|
469
594
|
code: "ambiguous",
|
|
470
595
|
ref: refRaw,
|
|
471
|
-
candidates:
|
|
596
|
+
candidates: shown.map((c) => candidateRef("heading", c)),
|
|
472
597
|
});
|
|
473
598
|
}
|
|
599
|
+
/**
|
|
600
|
+
* Resolve a container ref's bare TITLE through the REAL resolver for its kind,
|
|
601
|
+
* returning the sole resolved uuid or null (not-found OR ambiguous → null). This
|
|
602
|
+
* is the emit-side promotion judgment behind the JSON round-trip law (the flat
|
|
603
|
+
* `area`/`project`/`heading` refs promote a `*Uuid` sibling exactly when their
|
|
604
|
+
* bare title would NOT round-trip): it is the RESOLVER'S OWN judgment, not a
|
|
605
|
+
* re-derived uniqueness query, so it inherits every quirk of the write path —
|
|
606
|
+
* notably projects resolve through {@link resolveProjectWriteTarget} (the
|
|
607
|
+
* uuid-prefix tier runs FIRST, over the live+open write-target pool, so a title
|
|
608
|
+
* that is a valid unique uuid-prefix of ANOTHER task resolves to that task, not
|
|
609
|
+
* this project → does not round-trip → promotes; logged/trashed/completed
|
|
610
|
+
* same-titled twins never resolve by name, so they never trigger promotion).
|
|
611
|
+
* Areas resolve over all areas ({@link resolveAreaUuid}); headings within their
|
|
612
|
+
* project ({@link resolveHeadingRef}, `type = 2 AND trashed = 0 AND project = ?`).
|
|
613
|
+
* A thrown {@link ReferenceResolutionError} (not-found / ambiguous) is caught and
|
|
614
|
+
* read as "did not resolve" → null.
|
|
615
|
+
*/
|
|
616
|
+
function resolveTitleForRoundTrip(db, kind, title, projectUuid) {
|
|
617
|
+
try {
|
|
618
|
+
if (kind === "area")
|
|
619
|
+
return resolveAreaUuid(db, title);
|
|
620
|
+
if (kind === "project")
|
|
621
|
+
return resolveProjectWriteTarget(db, title);
|
|
622
|
+
if (projectUuid === undefined)
|
|
623
|
+
return null; // heading round-trip needs its project scope
|
|
624
|
+
return resolveHeadingRef(db, projectUuid, title).resolved?.uuid ?? null;
|
|
625
|
+
}
|
|
626
|
+
catch {
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Does a container ref's bare TITLE round-trip through its own resolver, in its
|
|
632
|
+
* own scope, back to THIS entity's uuid? The single predicate the emit boundary
|
|
633
|
+
* consults for the JSON round-trip law — true only when the sole resolution is
|
|
634
|
+
* `entityUuid` (not-found / ambiguous / a different entity all → false, i.e. the
|
|
635
|
+
* title must promote its uuid sibling). See {@link resolveTitleForRoundTrip}.
|
|
636
|
+
*/
|
|
637
|
+
export function titleRoundTrips(db, kind, title, entityUuid, projectUuid) {
|
|
638
|
+
return resolveTitleForRoundTrip(db, kind, title, projectUuid) === entityUuid;
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Build the {@link RefPromoter} the read-shaping transform ({@link shapeReadPayload})
|
|
642
|
+
* consults to decide flat-ref uuid promotion, memoized per (kind, title, scope)
|
|
643
|
+
* for ONE response emission so a large view never re-runs a resolution it has
|
|
644
|
+
* already made. A fresh promoter (fresh memo) per response — the consumer
|
|
645
|
+
* surfaces build one via the client. The memo caches the RESOLVED uuid (or null),
|
|
646
|
+
* independent of the comparison target, so twin entities sharing a title share
|
|
647
|
+
* the one resolution.
|
|
648
|
+
*/
|
|
649
|
+
export function makeRefPromoter(db) {
|
|
650
|
+
const memo = new Map();
|
|
651
|
+
const resolve = (kind, title, projectUuid) => {
|
|
652
|
+
const key = `${kind}\x00${projectUuid ?? ""}\x00${title}`;
|
|
653
|
+
if (memo.has(key))
|
|
654
|
+
return memo.get(key) ?? null;
|
|
655
|
+
const uuid = resolveTitleForRoundTrip(db, kind, title, projectUuid);
|
|
656
|
+
memo.set(key, uuid);
|
|
657
|
+
return uuid;
|
|
658
|
+
};
|
|
659
|
+
return {
|
|
660
|
+
roundTrips: (kind, title, entityUuid, projectUuid) => resolve(kind, title, projectUuid) === entityUuid,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
474
663
|
/**
|
|
475
664
|
* A row's EFFECTIVE area: its own `area` link, else the area of its project,
|
|
476
665
|
* else the area of its heading's project. To-dos nested in a project (or under a
|