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
package/dist/read/shape.js
CHANGED
|
@@ -54,16 +54,16 @@
|
|
|
54
54
|
* substrate) and KEPT in FULL/DETAIL beside `when` — different facts: `startDate`
|
|
55
55
|
* = what is stored, `when` = where it sits.
|
|
56
56
|
* - a template's `repeating.nextOccurrence` is GONE from the wire — `when` replaces
|
|
57
|
-
* it (same fact, one word);
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
57
|
+
* it (same fact, one word); an unprojected template has no `when`.
|
|
58
|
+
* - `when` is KEPT on every row it is present on. (The read-shape doctrine
|
|
59
|
+
* flattened the card `upcoming` date-groups into the project-/area-view flat
|
|
60
|
+
* `items[]`, so no enclosing node states the position anymore.) It rides the
|
|
61
|
+
* today view's flat `items[]` (Today-proper vs This-Evening), the project-/area-
|
|
62
|
+
* view `items[]`, and the flat `upcoming`/`anytime`/`inbox`/`someday` catalogues,
|
|
63
|
+
* search, changes (a deadline-pulled row reads `when: "today"` in the mixed
|
|
64
|
+
* search/changes surfaces, informatively; note R13 re-files it to stage `anytime`
|
|
65
|
+
* and the flat inbox/someday views now EXCLUDE it — it appears in the `anytime`
|
|
66
|
+
* catalogue instead, `when: "today"` kept, stage dropped as pure).
|
|
67
67
|
*
|
|
68
68
|
* ## R13 — provisional Today members + GUI-faithful pulled-row membership
|
|
69
69
|
* BANNER1 / BANNER1b (docs/lab/banner1-research.md). Two coupled facts:
|
|
@@ -77,10 +77,10 @@
|
|
|
77
77
|
* side effect our read cannot perform (watchers beware).
|
|
78
78
|
* - **stage `anytime` for a deadline pull** — a due-deadline pull re-files an undated
|
|
79
79
|
* Inbox/Someday row into Anytime (deriveStage step 2½, L-A). So EVERY Today member
|
|
80
|
-
* derives stage `anytime`, and the
|
|
81
|
-
* `stage` is DROPPED there (
|
|
82
|
-
*
|
|
83
|
-
*
|
|
80
|
+
* derives stage `anytime`, and the today view's flat `items[]` is stage-PURE →
|
|
81
|
+
* `stage` is DROPPED there (TODAY_ITEM_DROP) while `when` is KEPT. The flat
|
|
82
|
+
* someday/inbox views EXCLUDE pulled rows and the anytime view INCLUDES them
|
|
83
|
+
* (src/read/views.ts + predicates.ts DEADLINE_PULLED) — GUI fidelity.
|
|
84
84
|
*
|
|
85
85
|
* ## Universal item-DTO reshapes (R9 — EVERY tier, EVERY read kind incl. detail)
|
|
86
86
|
* - **checklist nesting** — flat counts → presence-keyed `checklist: {open,total}`.
|
|
@@ -113,6 +113,46 @@
|
|
|
113
113
|
* universal reshapes, and R10.
|
|
114
114
|
*/
|
|
115
115
|
import { deriveStage, deriveWhen, whenIsProvisional } from "./stage.js";
|
|
116
|
+
/** The DB-less default: assume every title round-trips (bare title, no uuid sibling). */
|
|
117
|
+
const ALWAYS_ROUND_TRIPS = { roundTrips: () => true };
|
|
118
|
+
/** The uuid of a `{uuid,title}` container Ref, or undefined for a non-object / string. */
|
|
119
|
+
function refUuid(v) {
|
|
120
|
+
if (v !== null && typeof v === "object" && typeof v["uuid"] === "string") {
|
|
121
|
+
return v["uuid"];
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Whether a still-unflattened container Ref carries the repeating-TEMPLATE mark.
|
|
127
|
+
* Only project/container refs ever set it (area/heading refs never do — see
|
|
128
|
+
* entities.Ref), so this reads true only for a template PROJECT container.
|
|
129
|
+
*/
|
|
130
|
+
function refIsTemplate(v) {
|
|
131
|
+
return v !== null && typeof v === "object" && v["isRepeatingTemplate"] === true;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Flatten ONE container ref `o[key]` from a `{uuid,title}` object to its bare
|
|
135
|
+
* TITLE string, adding a flat sibling `o[uuidKey]` = the full uuid ONLY when the
|
|
136
|
+
* round-trip law demands it: `forceUuid` (the FULL/detail tier — uuid siblings
|
|
137
|
+
* unconditional) OR the bare title does not resolve back to this exact entity
|
|
138
|
+
* (`!promoter.roundTrips`). A null/absent ref, or one already flattened to a
|
|
139
|
+
* string, is left untouched. The container's `isRepeatingTemplate` marker (a
|
|
140
|
+
* TTY-render disambiguator on the internal entity) does not survive the flatten
|
|
141
|
+
* — the human render reads the unshaped entity. {@link shapeItem} re-emits that
|
|
142
|
+
* fact for the JSON container PROJECT as the flat presence-keyed sibling
|
|
143
|
+
* `projectIsTemplate: true` BEFORE this flatten runs.
|
|
144
|
+
*/
|
|
145
|
+
function flattenRef(o, key, uuidKey, kind, forceUuid, promoter, projectUuid) {
|
|
146
|
+
const ref = o[key];
|
|
147
|
+
if (ref === null || typeof ref !== "object")
|
|
148
|
+
return; // absent, or already a bare string
|
|
149
|
+
const r = ref;
|
|
150
|
+
const uuid = typeof r["uuid"] === "string" ? r["uuid"] : "";
|
|
151
|
+
const title = typeof r["title"] === "string" ? r["title"] : "";
|
|
152
|
+
o[key] = title;
|
|
153
|
+
if (forceUuid || !promoter.roundTrips(kind, title, uuid, projectUuid))
|
|
154
|
+
o[uuidKey] = uuid;
|
|
155
|
+
}
|
|
116
156
|
/**
|
|
117
157
|
* Fold the flat `tags` / `inheritedTags` arrays of `{title}` objects into plain
|
|
118
158
|
* arrays of tag NAMES (universal across tiers and kinds). Tag uuids were never
|
|
@@ -260,7 +300,7 @@ function whenOf(s, stage) {
|
|
|
260
300
|
* (`changeKind` on a changes row, `match` on a search hit) pass through
|
|
261
301
|
* untouched. Non-task values (areas, tags, refs, headings) are returned as-is.
|
|
262
302
|
*/
|
|
263
|
-
function shapeItem(src, drop, compact) {
|
|
303
|
+
function shapeItem(src, drop, compact, promoter) {
|
|
264
304
|
if (src === null || typeof src !== "object")
|
|
265
305
|
return src;
|
|
266
306
|
const s = src;
|
|
@@ -300,9 +340,9 @@ function shapeItem(src, drop, compact) {
|
|
|
300
340
|
if (drop.stage !== true)
|
|
301
341
|
o["stage"] = stage;
|
|
302
342
|
// R12 — the today/evening marker KEYS are replaced by the derived `when` on
|
|
303
|
-
// EVERY tier (they never appear on the wire); `when` is emitted
|
|
304
|
-
//
|
|
305
|
-
//
|
|
343
|
+
// EVERY tier (they never appear on the wire); `when` is emitted on every row it
|
|
344
|
+
// is present on (since the read-shape doctrine flattened the card date-groups,
|
|
345
|
+
// no enclosing node states the position anymore — the flat lists carry it).
|
|
306
346
|
delete o["today"];
|
|
307
347
|
delete o["evening"];
|
|
308
348
|
// §9n — a reminder byte is presentation-dead once its `startDate` goes strictly
|
|
@@ -318,6 +358,12 @@ function shapeItem(src, drop, compact) {
|
|
|
318
358
|
// R13 — the provisional banner marker (never dropped; presence-keyed).
|
|
319
359
|
if (provisional)
|
|
320
360
|
o["provisional"] = true;
|
|
361
|
+
// The owning project's uuid scopes the heading round-trip (headings resolve
|
|
362
|
+
// within their project). Captured BEFORE the R6 project-drop so a project-view
|
|
363
|
+
// LOGBOOK row — whose `project` is dropped as redundant, yet which KEEPS its
|
|
364
|
+
// heading ref (drop.keepHeading) — can still promote its `headingUuid` in the
|
|
365
|
+
// project's scope.
|
|
366
|
+
const projectUuid = refUuid(o["project"]);
|
|
321
367
|
// R6 — drop redundant ancestry (both tiers).
|
|
322
368
|
if (drop.project === true)
|
|
323
369
|
delete o["project"];
|
|
@@ -325,6 +371,30 @@ function shapeItem(src, drop, compact) {
|
|
|
325
371
|
delete o["area"];
|
|
326
372
|
if (drop.heading === true)
|
|
327
373
|
delete o["heading"];
|
|
374
|
+
// Absent `type` = to-do — omit it on to-do rows (project/heading keep theirs).
|
|
375
|
+
if (o["type"] === "to-do")
|
|
376
|
+
delete o["type"];
|
|
377
|
+
// Flatten the surviving container refs to bare TITLE strings, adding a flat
|
|
378
|
+
// `*Uuid` sibling per the round-trip law (FULL tier: unconditional; compact:
|
|
379
|
+
// only when the title would not resolve back to this entity). The `heading`
|
|
380
|
+
// ref is compact-dropped below (except drop.keepHeading), so it is flattened
|
|
381
|
+
// on the FULL tier OR when a logbook row explicitly keeps it.
|
|
382
|
+
const forceUuid = !compact;
|
|
383
|
+
// The container PROJECT's repeating-TEMPLATE fact — the JSON twin of the TTY ↻
|
|
384
|
+
// glyph (src/cli/render.ts). flattenRef discards the internal ref's
|
|
385
|
+
// `isRepeatingTemplate` marker, so re-emit it here as a flat presence-keyed
|
|
386
|
+
// sibling of the `project` ref (never `false`), riding wherever `project`
|
|
387
|
+
// rides. A heading-nested row already merged its owning project into `project`
|
|
388
|
+
// above, so direct AND headed template children mark; the R6 project-drop
|
|
389
|
+
// above already removed `project` where the view implies it, so a project-view
|
|
390
|
+
// child carries no orphaned marker. Both tiers — it is a correctness signal,
|
|
391
|
+
// not detail. Only project refs ever carry the flag (area/heading never do).
|
|
392
|
+
if (refIsTemplate(o["project"]))
|
|
393
|
+
o["projectIsTemplate"] = true;
|
|
394
|
+
flattenRef(o, "project", "projectUuid", "project", forceUuid, promoter);
|
|
395
|
+
flattenRef(o, "area", "areaUuid", "area", forceUuid, promoter);
|
|
396
|
+
if (!compact || drop.keepHeading === true)
|
|
397
|
+
flattenRef(o, "heading", "headingUuid", "heading", forceUuid, promoter, projectUuid);
|
|
328
398
|
// R12 — FULL/DETAIL keep the raw `startDate` beside `when` as the SUBSTRATE
|
|
329
399
|
// (`startDate` = what is stored, `when` = where it sits). COMPACT drops it below
|
|
330
400
|
// (the position `when` carries is what a list needs).
|
|
@@ -341,112 +411,58 @@ function shapeItem(src, drop, compact) {
|
|
|
341
411
|
if (notes !== "")
|
|
342
412
|
o["hasNotes"] = true;
|
|
343
413
|
// The heading ref is compact-dropped everywhere (the GUI shows the project,
|
|
344
|
-
// never the heading, outside a project view). Full tier / detail keep it
|
|
345
|
-
|
|
414
|
+
// never the heading, outside a project view). Full tier / detail keep it; a
|
|
415
|
+
// project-view logbook row (drop.keepHeading) keeps it too — flattened above.
|
|
416
|
+
if (drop.keepHeading !== true)
|
|
417
|
+
delete o["heading"];
|
|
346
418
|
return o;
|
|
347
419
|
}
|
|
348
420
|
/** Map a plain array of items with the item shaper. */
|
|
349
|
-
function shapeList(items, drop, compact) {
|
|
421
|
+
function shapeList(items, drop, compact, promoter) {
|
|
350
422
|
if (!Array.isArray(items))
|
|
351
423
|
return items;
|
|
352
|
-
return items.map((i) => shapeItem(i, drop, compact));
|
|
424
|
+
return items.map((i) => shapeItem(i, drop, compact, promoter));
|
|
353
425
|
}
|
|
354
426
|
/** Copy `base` and overwrite `items` with the shaped list (avoids spread-in-map). */
|
|
355
|
-
function withShapedItems(base, drop, compact) {
|
|
427
|
+
function withShapedItems(base, drop, compact, promoter) {
|
|
356
428
|
const out = { ...base };
|
|
357
|
-
out["items"] = shapeList(base["items"], drop, compact);
|
|
358
|
-
return out;
|
|
359
|
-
}
|
|
360
|
-
/**
|
|
361
|
-
* Re-bucket a project's / area's / heading's live (non-logbook/trash) children
|
|
362
|
-
* into the R10 card shape by their derived {@link deriveStage} — so the bucket an
|
|
363
|
-
* item lands in ALWAYS equals its `stage`:
|
|
364
|
-
* - `anytime` — stage anytime, in encounter order;
|
|
365
|
-
* - `upcoming` — stage upcoming, date-grouped `[{date, items}]` (a dated row under
|
|
366
|
-
* its `startDate`, a template under its `nextOccurrence`), date ASC; date-LESS
|
|
367
|
-
* templates (after-completion / paused) form a trailing `{date: null, items}`
|
|
368
|
-
* group (explicit null per the `area: null` section precedent);
|
|
369
|
-
* - `someday` — stage someday.
|
|
370
|
-
* Items are already in view order (index / date+todayIndex) from the read layer,
|
|
371
|
-
* so encounter order within a date group preserves that ordering. Each item is
|
|
372
|
-
* then run through {@link shapeItem} with the section drop (ancestry + `stage`,
|
|
373
|
-
* since the bucket states it).
|
|
374
|
-
*/
|
|
375
|
-
function rebucketChildren(children, drop, compact) {
|
|
376
|
-
const anytime = [];
|
|
377
|
-
const someday = [];
|
|
378
|
-
const datedByKey = new Map();
|
|
379
|
-
const datedOrder = [];
|
|
380
|
-
const restingTemplates = [];
|
|
381
|
-
const shape = (c) => shapeItem(c, drop, compact);
|
|
382
|
-
for (const raw of children) {
|
|
383
|
-
if (raw === null || typeof raw !== "object")
|
|
384
|
-
continue;
|
|
385
|
-
const c = raw;
|
|
386
|
-
const stage = stageOf(c);
|
|
387
|
-
if (stage === "anytime") {
|
|
388
|
-
anytime.push(shape(c));
|
|
389
|
-
}
|
|
390
|
-
else if (stage === "someday") {
|
|
391
|
-
someday.push(shape(c));
|
|
392
|
-
}
|
|
393
|
-
else if (stage === "upcoming") {
|
|
394
|
-
const repeating = c["repeating"];
|
|
395
|
-
const nextOcc = repeating != null && typeof repeating === "object"
|
|
396
|
-
? (repeating["nextOccurrence"] ?? null)
|
|
397
|
-
: null;
|
|
398
|
-
const date = (c.startDate ?? null) !== null ? c.startDate : nextOcc;
|
|
399
|
-
if (date === null) {
|
|
400
|
-
restingTemplates.push(shape(c));
|
|
401
|
-
}
|
|
402
|
-
else {
|
|
403
|
-
if (!datedByKey.has(date)) {
|
|
404
|
-
datedByKey.set(date, []);
|
|
405
|
-
datedOrder.push(date);
|
|
406
|
-
}
|
|
407
|
-
// R12 — inside a date-group the group states the date, so a member whose
|
|
408
|
-
// `when` equals it drops it (every scheduled row and every projected
|
|
409
|
-
// template does — that IS the group key).
|
|
410
|
-
const shaped = shape(c);
|
|
411
|
-
if (shaped !== null && typeof shaped === "object" && shaped["when"] === date) {
|
|
412
|
-
delete shaped["when"];
|
|
413
|
-
}
|
|
414
|
-
datedByKey.get(date).push(shaped);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
else {
|
|
418
|
-
// inbox / logbook / trash should not appear among a card's live children;
|
|
419
|
-
// route defensively to anytime rather than drop the row.
|
|
420
|
-
anytime.push(shape(c));
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
const upcoming = datedOrder
|
|
424
|
-
.toSorted((a, b) => a.localeCompare(b))
|
|
425
|
-
.map((date) => ({ date, items: datedByKey.get(date) }));
|
|
426
|
-
if (restingTemplates.length > 0)
|
|
427
|
-
upcoming.push({ date: null, items: restingTemplates });
|
|
428
|
-
return { anytime, upcoming, someday };
|
|
429
|
-
}
|
|
430
|
-
/** Flatten an internal IsoDateGroup[] (`[{date, items}]`) to its items, in order. */
|
|
431
|
-
function flattenGroups(groups) {
|
|
432
|
-
if (!Array.isArray(groups))
|
|
433
|
-
return [];
|
|
434
|
-
const out = [];
|
|
435
|
-
for (const g of groups) {
|
|
436
|
-
if (g !== null && typeof g === "object" && Array.isArray(g["items"])) {
|
|
437
|
-
out.push(...g["items"]);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
429
|
+
out["items"] = shapeList(base["items"], drop, compact, promoter);
|
|
440
430
|
return out;
|
|
441
431
|
}
|
|
442
432
|
/** Coerce an unknown value to an array (empty when absent). */
|
|
443
433
|
const asArray = (v) => (Array.isArray(v) ? v : []);
|
|
444
|
-
/**
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
434
|
+
/**
|
|
435
|
+
* The flat project-view `items[]` rows (read-shape doctrine §3.12): drop
|
|
436
|
+
* project/area (the card states them) but KEEP `stage` (the list is stage-MIXED:
|
|
437
|
+
* anytime + upcoming + someday + closed-unswept), `when`, and the `heading` ref.
|
|
438
|
+
* The heading ref is kept + flattened even in compact (keepHeading), scoped to
|
|
439
|
+
* the owning project — flat title + `headingUuid` when the title would not
|
|
440
|
+
* round-trip — so a consumer reconstructs a heading's members from the rows.
|
|
441
|
+
*/
|
|
442
|
+
const PROJECT_ITEM_DROP = { project: true, area: true, keepHeading: true };
|
|
443
|
+
/**
|
|
444
|
+
* Project-view LOGBOOK (flat logged) rows (read-shape doctrine §3.12 / #C4): drop
|
|
445
|
+
* project/area (the card states them) but KEEP `stage` and the `heading` ref
|
|
446
|
+
* (drop.keepHeading). The bucket is stage-MIXED since it absorbs the children of
|
|
447
|
+
* SWEPT ARCHIVED headings too — mostly `logbook`, but it can hold the odd OPEN
|
|
448
|
+
* child a Put-Back stranded under an archived heading (HEADARC2-C, stage
|
|
449
|
+
* `anytime`) — so `stage` is not provably implied and is kept. The heading ref is
|
|
450
|
+
* the GUI hint (the in-project logged toggle labels the HEADING; the global
|
|
451
|
+
* Logbook labels the PROJECT — the two-view asymmetry, HEADARC2-B), flat title +
|
|
452
|
+
* project-scoped `headingUuid`.
|
|
453
|
+
*/
|
|
454
|
+
const PROJECT_LOGBOOK_DROP = {
|
|
455
|
+
project: true,
|
|
456
|
+
area: true,
|
|
457
|
+
keepHeading: true,
|
|
458
|
+
};
|
|
459
|
+
/**
|
|
460
|
+
* The flat area-view `items[]` rows (read-shape doctrine §3.13): drop `area` (the
|
|
461
|
+
* card states it) but KEEP `stage` (the list is stage-MIXED — anytime + upcoming
|
|
462
|
+
* + someday + closed-unswept) and `when`. Area direct to-dos are never headed and
|
|
463
|
+
* never project-nested, so nothing else to drop.
|
|
464
|
+
*/
|
|
465
|
+
const AREA_ITEM_DROP = { area: true };
|
|
450
466
|
/** Area-view PROJECTS list: a mixed listing of the area's project rows — keep `stage`, drop area. */
|
|
451
467
|
const AREA_PROJECTS_DROP = { area: true };
|
|
452
468
|
/**
|
|
@@ -465,100 +481,133 @@ const SOMEDAY_SECTION_DROP = { area: true, stage: true };
|
|
|
465
481
|
/** The card NODE / detail / mixed lists: keep every ref, `stage`, and `when`. */
|
|
466
482
|
const NO_DROP = {};
|
|
467
483
|
/**
|
|
468
|
-
* The today view's
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
484
|
+
* The today view's flat `items[]`: drop the view-implied `stage` (R13) but KEEP
|
|
485
|
+
* `when`. Every Today member derives stage `anytime` by construction — an ARRIVED
|
|
486
|
+
* `startDate` (step 5) or a DEADLINE PULL (step 2½) both derive `anytime`, and
|
|
487
|
+
* there are no future-dated or undated-someday Today members — so the today view
|
|
488
|
+
* is provably stage-PURE `anytime` and the field is redundant (verified strict by
|
|
489
|
+
* the today purity property test in test/unit/stage.test.ts). `when` is KEPT: the
|
|
490
|
+
* flat list interleaves Today-proper (`when: "today"`) and This-Evening
|
|
491
|
+
* (`when: "evening"`) members, so each row must carry which render section it
|
|
492
|
+
* belongs to — the split is derived from `when`, not a wire bucket. `provisional`
|
|
493
|
+
* is NOT a drop — the banner pip is per-row, nothing implies it.
|
|
476
494
|
*/
|
|
477
|
-
const
|
|
478
|
-
/**
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
495
|
+
const TODAY_ITEM_DROP = { stage: true };
|
|
496
|
+
/**
|
|
497
|
+
* Shape a heading catalog entry (a `headings[]` node — the flat `{uuid,title,
|
|
498
|
+
* archived?}` catalog, read-shape doctrine §3.12 / #C3). The type is implied by
|
|
499
|
+
* position, and a heading has no open/canceled/completed vocabulary the reader
|
|
500
|
+
* needs — so:
|
|
501
|
+
* - DROP `type` (positional: this slot is always a heading; the "absent type =
|
|
502
|
+
* to-do" convention is scoped to ROWS/candidates, never this catalog entry);
|
|
503
|
+
* - DROP `project` (the card states it);
|
|
504
|
+
* - REPLACE `status` with the presence-keyed `archived` (the stopDate, an ISO
|
|
505
|
+
* date-time following the `stopped`/logged-row convention) — emitted ONLY when
|
|
506
|
+
* the heading is archived (status "completed"), OMITTED when open. The entry
|
|
507
|
+
* carries only whether-and-when it was archived; sweptness (past the logbook
|
|
508
|
+
* boundary) is not on the wire — the TTY derives it (#C3a).
|
|
509
|
+
*/
|
|
510
|
+
function shapeHeadingNode(src) {
|
|
511
|
+
if (src === null || typeof src !== "object")
|
|
512
|
+
return src;
|
|
513
|
+
const h = { ...src };
|
|
514
|
+
delete h["project"];
|
|
515
|
+
delete h["type"];
|
|
516
|
+
const isArchived = h["status"] !== undefined && h["status"] !== "open";
|
|
517
|
+
const stopped = h["stopped"];
|
|
518
|
+
delete h["status"];
|
|
519
|
+
delete h["stopped"];
|
|
520
|
+
// Presence-keyed `archived` — the ISO archive timestamp (past-participle twin of
|
|
521
|
+
// `stopped`/`created`/`modified`), full-datetime serialization like `stopped`.
|
|
522
|
+
if (isArchived && stopped != null)
|
|
523
|
+
h["archived"] = stopped;
|
|
524
|
+
return h;
|
|
525
|
+
}
|
|
526
|
+
/** Stopped-DESC comparator for the flat logbook (open odd children — no stopDate — sort last). */
|
|
527
|
+
function byStoppedDesc(a, b) {
|
|
528
|
+
const t = (x) => {
|
|
529
|
+
const s = x !== null && typeof x === "object" ? x["stopped"] : null;
|
|
530
|
+
return s instanceof Date ? s.getTime() : -Infinity;
|
|
507
531
|
};
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
532
|
+
return t(b) - t(a);
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Shape a project view (read-shape doctrine §3.12 / #C3 / #C4). The live children
|
|
536
|
+
* are ONE flat `items[]` in project index order — each row carrying `stage`,
|
|
537
|
+
* `when`, and its `heading` ref (flat title + project-scoped `headingUuid`) — so a
|
|
538
|
+
* consumer reconstructs a heading's members by filtering `items` on `heading`.
|
|
539
|
+
* `headings[]` is the flat catalog `[{uuid,title,archived?}]` of EVERY heading
|
|
540
|
+
* (live + swept archived) in index order — the ORDER axis. `logbook` is one flat
|
|
541
|
+
* `stopDate DESC` list of ALL swept children — of open headings, un-headed, AND
|
|
542
|
+
* archived headings — each carrying its `heading` ref; `logbookHeadings` is gone
|
|
543
|
+
* (its rows folded in, the archived heading itself now a catalog entry with
|
|
544
|
+
* `archived`). The card node is left full + ancestry-intact.
|
|
545
|
+
*/
|
|
546
|
+
function shapeProjectView(view, compact, promoter) {
|
|
547
|
+
// The heading catalog: every heading node (live + swept archived), index order,
|
|
548
|
+
// flattened to `{uuid,title,archived?}`. Membership rides the flat item/logbook
|
|
549
|
+
// row `heading` refs; archived-ness reads off `archived` here.
|
|
550
|
+
const headings = Array.isArray(view["headingCatalog"])
|
|
551
|
+
? view["headingCatalog"].map(shapeHeadingNode)
|
|
552
|
+
: [];
|
|
553
|
+
// The flat logbook: swept children of open/un-headed headings (`logged`) PLUS
|
|
554
|
+
// the children of swept archived headings (`loggedHeadings`), merged into ONE
|
|
555
|
+
// stopDate-DESC list (open odd children last), each carrying its heading ref.
|
|
556
|
+
const loggedRows = [
|
|
557
|
+
...asArray(view["logged"]),
|
|
558
|
+
...asArray(view["loggedHeadings"]).flatMap((g) => g !== null && typeof g === "object" ? asArray(g["items"]) : []),
|
|
559
|
+
].toSorted(byStoppedDesc);
|
|
518
560
|
const out = { ...view };
|
|
519
561
|
delete out["active"];
|
|
520
562
|
delete out["scheduled"];
|
|
521
563
|
delete out["repeating"];
|
|
564
|
+
delete out["someday"];
|
|
522
565
|
delete out["logged"];
|
|
566
|
+
delete out["loggedHeadings"];
|
|
567
|
+
delete out["headingCatalog"];
|
|
523
568
|
// Trashed children live only in `things trash` — never a project-view bucket.
|
|
524
569
|
// Delete defensively in case an untyped source carries the old key.
|
|
525
570
|
delete out["trashed"];
|
|
526
571
|
// The project card NODE keeps everything (children derive their container from
|
|
527
572
|
// it), but is still an item DTO, so the universal + R10 reshapes apply.
|
|
528
|
-
out["project"] = shapeItem(view["project"], NO_DROP, false);
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
573
|
+
out["project"] = shapeItem(view["project"], NO_DROP, false, promoter);
|
|
574
|
+
// The flat live children — stage/when/heading kept, project/area dropped (the
|
|
575
|
+
// card states them). The heading ref is kept + flattened even in compact
|
|
576
|
+
// (keepHeading), project-scoped like the logbook rows.
|
|
577
|
+
out["items"] = shapeList(view["items"], PROJECT_ITEM_DROP, compact, promoter);
|
|
532
578
|
out["headings"] = headings;
|
|
533
|
-
// A project keeps its in-context `logbook` (a project is a bounded object with
|
|
534
|
-
//
|
|
535
|
-
|
|
579
|
+
// A project keeps its in-context `logbook` (a project is a bounded object with a
|
|
580
|
+
// real done-state); trashed children live only in `things trash`. The flat
|
|
581
|
+
// rows KEEP stage (mixed) + their heading ref (PROJECT_LOGBOOK_DROP).
|
|
582
|
+
out["logbook"] = shapeList(loggedRows, PROJECT_LOGBOOK_DROP, compact, promoter);
|
|
536
583
|
return out;
|
|
537
584
|
}
|
|
538
|
-
/**
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
585
|
+
/**
|
|
586
|
+
* Shape an area view (read-shape doctrine §3.13). The direct to-dos dissolve into
|
|
587
|
+
* ONE flat `items[]` in index order — each row carrying `stage` + `when`, `area`
|
|
588
|
+
* dropped (the card states it). The stage/date sub-buckets (anytime/upcoming/
|
|
589
|
+
* someday) are gone. `projects[]` is KEPT — the area's child-project sidebar rank
|
|
590
|
+
* is a DISTINCT order axis from the direct-to-do index, so it earns its own list
|
|
591
|
+
* (mixed-stage, someday/scheduled projects included; the render split is TTY-only).
|
|
592
|
+
* The area node keeps its identity (tags folded).
|
|
593
|
+
*/
|
|
594
|
+
function shapeAreaView(view, compact, promoter) {
|
|
547
595
|
const out = { ...view };
|
|
548
596
|
delete out["active"];
|
|
549
597
|
delete out["scheduled"];
|
|
550
598
|
delete out["repeating"];
|
|
599
|
+
delete out["someday"];
|
|
551
600
|
// No `logbook` or `trash` bucket: an area's logbook is the bounded query
|
|
552
601
|
// `things logbook --area <ref>`, and trashed rows live only in `things trash`.
|
|
553
602
|
// Delete defensively in case an untyped source carries the old keys.
|
|
554
603
|
delete out["logged"];
|
|
555
604
|
delete out["trashed"];
|
|
556
605
|
out["area"] = shapeArea(view["area"]);
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
out["
|
|
606
|
+
// The flat direct to-dos — stage/when kept, area dropped.
|
|
607
|
+
out["items"] = shapeList(view["items"], AREA_ITEM_DROP, compact, promoter);
|
|
608
|
+
// The projects list is a mixed listing of the area's project rows — keep stage,
|
|
609
|
+
// sidebar order (the distinct order axis).
|
|
610
|
+
out["projects"] = shapeList(view["projects"], AREA_PROJECTS_DROP, compact, promoter);
|
|
562
611
|
return out;
|
|
563
612
|
}
|
|
564
613
|
/** Fold an area entity's tags to string names in place (returns a shallow copy). */
|
|
@@ -569,19 +618,11 @@ function shapeArea(src) {
|
|
|
569
618
|
flattenTags(o);
|
|
570
619
|
return o;
|
|
571
620
|
}
|
|
572
|
-
/** Shape the today/evening split (mixed list — keep refs + stage; drop the section-implied `when`). */
|
|
573
|
-
function shapeTodayView(view, compact) {
|
|
574
|
-
return {
|
|
575
|
-
...view,
|
|
576
|
-
today: shapeList(view["today"], TODAY_SECTION_DROP, compact),
|
|
577
|
-
evening: shapeList(view["evening"], TODAY_SECTION_DROP, compact),
|
|
578
|
-
};
|
|
579
|
-
}
|
|
580
621
|
/** Shape sidebar sections (anytime/someday catalogues) with the section's drop spec. */
|
|
581
|
-
function shapeSections(sections, drop, compact) {
|
|
622
|
+
function shapeSections(sections, drop, compact, promoter) {
|
|
582
623
|
if (!Array.isArray(sections))
|
|
583
624
|
return sections;
|
|
584
|
-
return sections.map((s) => s === null || typeof s !== "object" ? s : withShapedItems(s, drop, compact));
|
|
625
|
+
return sections.map((s) => s === null || typeof s !== "object" ? s : withShapedItems(s, drop, compact, promoter));
|
|
585
626
|
}
|
|
586
627
|
/**
|
|
587
628
|
* The flat, mixed-provenance list kinds mapped to their drop spec. Only the
|
|
@@ -608,28 +649,36 @@ const FLAT_LIST_DROP = new Map([
|
|
|
608
649
|
* mutated (shallow copies throughout), so the human-render path keeps the full
|
|
609
650
|
* entities.
|
|
610
651
|
*/
|
|
611
|
-
export function shapeReadPayload(kind, data, full) {
|
|
652
|
+
export function shapeReadPayload(kind, data, full, promoter) {
|
|
653
|
+
// The ref-promotion oracle drives the round-trip law for flat container refs.
|
|
654
|
+
// Absent (a DB-less unit shaping): assume every title round-trips — bare
|
|
655
|
+
// titles, no uuid siblings. Production always passes the client's promoter.
|
|
656
|
+
const p = promoter ?? ALWAYS_ROUND_TRIPS;
|
|
612
657
|
// `detail` is the FULL record and drops no ancestry / stage / `when`.
|
|
613
658
|
if (kind === "detail")
|
|
614
|
-
return shapeItem(data, NO_DROP, false);
|
|
659
|
+
return shapeItem(data, NO_DROP, false, p);
|
|
615
660
|
const compact = !full;
|
|
616
661
|
const flatDrop = FLAT_LIST_DROP.get(kind);
|
|
617
662
|
if (flatDrop !== undefined)
|
|
618
|
-
return shapeList(data, flatDrop, compact);
|
|
663
|
+
return shapeList(data, flatDrop, compact, p);
|
|
664
|
+
// The today view: one flat `items[]` of Today members (Today-proper + This
|
|
665
|
+
// Evening interleaved in comparator order); drop the view-implied `stage`,
|
|
666
|
+
// KEEP `when` (it carries the render section). The `counts` aggregate rides
|
|
667
|
+
// `meta.counts`, not `data`. The wire wrapper is `{ items }` (read-driver).
|
|
619
668
|
if (kind === "today" && data !== null && typeof data === "object") {
|
|
620
|
-
return
|
|
669
|
+
return shapeList(data["items"], TODAY_ITEM_DROP, compact, p);
|
|
621
670
|
}
|
|
622
671
|
if (kind === "anytime" && Array.isArray(data)) {
|
|
623
|
-
return shapeSections(data, ANYTIME_SECTION_DROP, compact); // stage-pure → drop stage
|
|
672
|
+
return shapeSections(data, ANYTIME_SECTION_DROP, compact, p); // stage-pure → drop stage
|
|
624
673
|
}
|
|
625
674
|
if (kind === "someday" && Array.isArray(data)) {
|
|
626
|
-
return shapeSections(data, SOMEDAY_SECTION_DROP, compact); // stage-pure → drop stage
|
|
675
|
+
return shapeSections(data, SOMEDAY_SECTION_DROP, compact, p); // stage-pure → drop stage
|
|
627
676
|
}
|
|
628
677
|
if (kind === "area-view" && data !== null && typeof data === "object") {
|
|
629
|
-
return shapeAreaView(data, compact);
|
|
678
|
+
return shapeAreaView(data, compact, p);
|
|
630
679
|
}
|
|
631
680
|
if (kind === "project-view" && data !== null && typeof data === "object") {
|
|
632
|
-
return shapeProjectView(data, compact);
|
|
681
|
+
return shapeProjectView(data, compact, p);
|
|
633
682
|
}
|
|
634
683
|
// The `areas` listing carries Area entities whose tags fold to names.
|
|
635
684
|
if (kind === "areas" && Array.isArray(data))
|
|
@@ -663,8 +712,10 @@ export function candidateRef(type, src) {
|
|
|
663
712
|
const out = {
|
|
664
713
|
uuid: typeof s["uuid"] === "string" ? s["uuid"] : "",
|
|
665
714
|
title: typeof s["title"] === "string" ? s["title"] : "",
|
|
666
|
-
type,
|
|
667
715
|
};
|
|
716
|
+
// Absent `type` = to-do — emit it only for the other kinds.
|
|
717
|
+
if (type !== "to-do")
|
|
718
|
+
out.type = type;
|
|
668
719
|
const area = candidateContainerTitle(s["area"]);
|
|
669
720
|
if (area !== null)
|
|
670
721
|
out.area = area;
|