things-api 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/cli/commands/area.d.ts +5 -5
- package/dist/cli/commands/area.js +13 -8
- package/dist/cli/commands/area.js.map +1 -1
- package/dist/cli/commands/project.js +2 -2
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/cli/commands/reads.js +94 -16
- package/dist/cli/commands/reads.js.map +1 -1
- package/dist/cli/commands/show.js +6 -2
- package/dist/cli/commands/show.js.map +1 -1
- package/dist/cli/commands/todo.js +2 -2
- package/dist/cli/commands/todo.js.map +1 -1
- package/dist/cli/commands/writes.js +136 -70
- package/dist/cli/commands/writes.js.map +1 -1
- package/dist/cli/glyphs.d.ts +9 -2
- package/dist/cli/glyphs.js +20 -15
- package/dist/cli/glyphs.js.map +1 -1
- package/dist/cli/help.js +4 -2
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/read-driver.d.ts +45 -9
- package/dist/cli/read-driver.js +39 -16
- package/dist/cli/read-driver.js.map +1 -1
- package/dist/cli/render.d.ts +57 -27
- package/dist/cli/render.js +117 -65
- package/dist/cli/render.js.map +1 -1
- package/dist/client.d.ts +127 -38
- package/dist/client.js +86 -22
- package/dist/client.js.map +1 -1
- package/dist/contracts.d.ts +35 -34
- package/dist/contracts.js +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/index.d.ts +9 -7
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +212 -148
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/dates.d.ts +26 -0
- package/dist/model/dates.js +46 -4
- package/dist/model/dates.js.map +1 -1
- package/dist/model/entities.d.ts +61 -39
- package/dist/model/entities.js +0 -4
- package/dist/model/entities.js.map +1 -1
- package/dist/model/mappers.js +24 -43
- package/dist/model/mappers.js.map +1 -1
- package/dist/read/area-filter.d.ts +1 -1
- package/dist/read/area-filter.js +5 -4
- package/dist/read/area-filter.js.map +1 -1
- package/dist/read/area-view.d.ts +0 -11
- package/dist/read/area-view.js +7 -11
- package/dist/read/area-view.js.map +1 -1
- package/dist/read/detail.js +2 -2
- package/dist/read/detail.js.map +1 -1
- package/dist/read/filter-contract.d.ts +1 -1
- package/dist/read/filter-contract.js +3 -0
- package/dist/read/filter-contract.js.map +1 -1
- package/dist/read/log-boundary.d.ts +46 -2
- package/dist/read/log-boundary.js +43 -2
- package/dist/read/log-boundary.js.map +1 -1
- package/dist/read/predicates.d.ts +13 -0
- package/dist/read/predicates.js +13 -0
- package/dist/read/predicates.js.map +1 -1
- package/dist/read/project-view.d.ts +34 -19
- package/dist/read/project-view.js +31 -25
- package/dist/read/project-view.js.map +1 -1
- package/dist/read/scope.d.ts +1 -1
- package/dist/read/scope.js +5 -4
- package/dist/read/scope.js.map +1 -1
- package/dist/read/search-rank.js +2 -2
- package/dist/read/search-rank.js.map +1 -1
- package/dist/read/shape.d.ts +78 -18
- package/dist/read/shape.js +496 -185
- package/dist/read/shape.js.map +1 -1
- package/dist/read/snapshot.js +2 -2
- package/dist/read/snapshot.js.map +1 -1
- package/dist/read/stage.d.ts +31 -13
- package/dist/read/stage.js +28 -11
- package/dist/read/stage.js.map +1 -1
- package/dist/read/truncation.d.ts +75 -2
- package/dist/read/truncation.js +69 -46
- package/dist/read/truncation.js.map +1 -1
- package/dist/read/views.d.ts +81 -18
- package/dist/read/views.js +136 -17
- package/dist/read/views.js.map +1 -1
- package/dist/surface-copy.d.ts +6 -0
- package/dist/surface-copy.js +6 -0
- package/dist/surface-copy.js.map +1 -1
- package/dist/write/batch.js +0 -1
- package/dist/write/batch.js.map +1 -1
- package/dist/write/clear-reminder.js +3 -1
- package/dist/write/clear-reminder.js.map +1 -1
- package/dist/write/commands.d.ts +25 -1
- package/dist/write/commands.js +350 -134
- package/dist/write/commands.js.map +1 -1
- package/dist/write/guards.js +36 -16
- package/dist/write/guards.js.map +1 -1
- package/dist/write/move.d.ts +26 -5
- package/dist/write/move.js +358 -2
- package/dist/write/move.js.map +1 -1
- package/dist/write/operations.d.ts +50 -19
- package/dist/write/operations.js +3 -2
- package/dist/write/operations.js.map +1 -1
- package/dist/write/pipeline.js +28 -7
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/pre-state.d.ts +89 -28
- package/dist/write/pre-state.js +197 -19
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/reorder.js +52 -5
- package/dist/write/reorder.js.map +1 -1
- package/dist/write/resolution-timestamps.d.ts +34 -0
- package/dist/write/resolution-timestamps.js +286 -0
- package/dist/write/resolution-timestamps.js.map +1 -0
- package/dist/write/reversibility.js +10 -5
- package/dist/write/reversibility.js.map +1 -1
- package/dist/write/scope-guard.js +2 -5
- package/dist/write/scope-guard.js.map +1 -1
- package/dist/write/undo.d.ts +1 -7
- package/dist/write/undo.js +63 -16
- package/dist/write/undo.js.map +1 -1
- package/dist/write/vectors/applescript.js +23 -4
- package/dist/write/vectors/applescript.js.map +1 -1
- package/dist/write/vectors/simulator.js +210 -9
- package/dist/write/vectors/simulator.js.map +1 -1
- package/dist/write/vectors/url-scheme.js +0 -8
- package/dist/write/vectors/url-scheme.js.map +1 -1
- package/dist/write/verify/delta.d.ts +71 -12
- package/dist/write/verify/delta.js +94 -12
- package/dist/write/verify/delta.js.map +1 -1
- package/package.json +1 -1
- package/schema/envelope.schema.json +19 -70
- package/skills/things-cli/SKILL.md +13 -9
- package/skills/things-cli/references/contracts.md +3 -4
- package/skills/things-cli/references/{model.md → data-model.md} +10 -8
- package/skills/things-cli/references/ordering.md +18 -7
package/dist/read/shape.js
CHANGED
|
@@ -32,10 +32,11 @@
|
|
|
32
32
|
* non-redundant information — plus `today` (mixes upcoming + anytime-deadline
|
|
33
33
|
* rows), search, changes, the projects/areas listings, the card NODE, and
|
|
34
34
|
* detail.
|
|
35
|
-
* - the former `todaySection` field is
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
35
|
+
* - the former `todaySection` field is DELETED from the entity model entirely:
|
|
36
|
+
* it merely duplicated the presence-keyed `today`/`evening` markers
|
|
37
|
+
* (`todaySection: "evening"` ⇔ `evening: true`; `"today"` ⇔ `today && !evening`),
|
|
38
|
+
* which every consumer now reads directly (the human render, the write-verify
|
|
39
|
+
* delta, the today-view evening split).
|
|
39
40
|
*
|
|
40
41
|
* ## R12 — `when`, the derived TIME-AXIS position (replaces startDate + markers)
|
|
41
42
|
* Today/evening membership and the scheduled/projected date collapse onto ONE
|
|
@@ -54,16 +55,17 @@
|
|
|
54
55
|
* substrate) and KEPT in FULL/DETAIL beside `when` — different facts: `startDate`
|
|
55
56
|
* = what is stored, `when` = where it sits.
|
|
56
57
|
* - 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
|
-
* search
|
|
65
|
-
*
|
|
66
|
-
*
|
|
58
|
+
* it (same fact, one word); the resting-templates `{date: null}` group is
|
|
59
|
+
* unchanged (an unprojected template has no `when`).
|
|
60
|
+
* - `when` is DROPPED inside the `today` view's two `children` bucket records (the
|
|
61
|
+
* bucket key `today`/`evening` states it) and inside any card/heading `upcoming`
|
|
62
|
+
* DATE-GROUP for a member whose `when` equals the group's date (the group states
|
|
63
|
+
* it). KEPT everywhere
|
|
64
|
+
* else it is present — including the flat `upcoming`/`anytime`/`inbox`/`someday`
|
|
65
|
+
* catalogues, search, changes (a deadline-pulled row reads `when: "today"` in the
|
|
66
|
+
* mixed search/changes surfaces, informatively; note R13 re-files it to stage
|
|
67
|
+
* `anytime` and the flat inbox/someday views now EXCLUDE it — it appears in the
|
|
68
|
+
* `anytime` catalogue instead, `when: "today"` kept, stage dropped as pure).
|
|
67
69
|
*
|
|
68
70
|
* ## R13 — provisional Today members + GUI-faithful pulled-row membership
|
|
69
71
|
* BANNER1 / BANNER1b (docs/lab/banner1-research.md). Two coupled facts:
|
|
@@ -77,10 +79,10 @@
|
|
|
77
79
|
* side effect our read cannot perform (watchers beware).
|
|
78
80
|
* - **stage `anytime` for a deadline pull** — a due-deadline pull re-files an undated
|
|
79
81
|
* Inbox/Someday row into Anytime (deriveStage step 2½, L-A). So EVERY Today member
|
|
80
|
-
* derives stage `anytime`, and the today view's
|
|
81
|
-
* `stage` is DROPPED there (
|
|
82
|
-
* someday/inbox views EXCLUDE pulled rows and the anytime view
|
|
83
|
-
* (src/read/views.ts + predicates.ts DEADLINE_PULLED) — GUI fidelity.
|
|
82
|
+
* derives stage `anytime`, and the `today` view's two `children` buckets become
|
|
83
|
+
* stage-PURE → `stage` is DROPPED there (TODAY_SECTION_DROP), alongside the
|
|
84
|
+
* key-implied `when`. The flat someday/inbox views EXCLUDE pulled rows and the anytime view
|
|
85
|
+
* INCLUDES them (src/read/views.ts + predicates.ts DEADLINE_PULLED) — GUI fidelity.
|
|
84
86
|
*
|
|
85
87
|
* ## Universal item-DTO reshapes (R9 — EVERY tier, EVERY read kind incl. detail)
|
|
86
88
|
* - **checklist nesting** — flat counts → presence-keyed `checklist: {open,total}`.
|
|
@@ -256,22 +258,23 @@ function reshapeRepeatingWire(o) {
|
|
|
256
258
|
o["instanceOf"] = r["templateUuid"];
|
|
257
259
|
}
|
|
258
260
|
}
|
|
259
|
-
/** The R10 stage input read straight off a materialized task entity. */
|
|
261
|
+
/** The R10 stage input read straight off a materialized task entity's `derived` bag. */
|
|
260
262
|
function stageOf(s) {
|
|
261
263
|
const repeating = s["repeating"];
|
|
262
264
|
const isTemplate = repeating !== null &&
|
|
263
265
|
typeof repeating === "object" &&
|
|
264
266
|
repeating["isTemplate"] === true;
|
|
267
|
+
const d = (s["derived"] ?? {});
|
|
265
268
|
return deriveStage({
|
|
266
|
-
trashed:
|
|
267
|
-
logged:
|
|
268
|
-
start:
|
|
269
|
+
trashed: d["trashed"] === true,
|
|
270
|
+
logged: d["logged"] === true,
|
|
271
|
+
start: d["start"],
|
|
269
272
|
startDate: s["startDate"] ?? null,
|
|
270
273
|
repeating: { isTemplate },
|
|
271
274
|
// The presence-keyed Today marker (stamped at materialize with the response
|
|
272
275
|
// clock) discriminates an ARRIVED dated row (→ anytime) from a strictly-
|
|
273
|
-
// future one (→ upcoming). Read
|
|
274
|
-
today:
|
|
276
|
+
// future one (→ upcoming). Read from the `derived` substrate bag.
|
|
277
|
+
today: d["today"] === true,
|
|
275
278
|
});
|
|
276
279
|
}
|
|
277
280
|
/** The R12 `when` input read straight off a materialized task entity (given its stage). */
|
|
@@ -283,12 +286,13 @@ function whenOf(s, stage) {
|
|
|
283
286
|
const nextOccurrence = isTemplate && typeof repeating["nextOccurrence"] === "string"
|
|
284
287
|
? repeating["nextOccurrence"]
|
|
285
288
|
: null;
|
|
289
|
+
const d = (s["derived"] ?? {});
|
|
286
290
|
return deriveWhen({
|
|
287
291
|
stage,
|
|
288
292
|
// The SAME presence-keyed markers stageOf reads — never re-derived, so a
|
|
289
293
|
// `when` of today/evening can never disagree with Today-view membership.
|
|
290
|
-
today:
|
|
291
|
-
evening:
|
|
294
|
+
today: d["today"] === true,
|
|
295
|
+
evening: d["evening"] === true,
|
|
292
296
|
startDate: s["startDate"] ?? null,
|
|
293
297
|
repeating: { isTemplate, nextOccurrence },
|
|
294
298
|
});
|
|
@@ -320,7 +324,7 @@ function shapeItem(src, drop, compact, promoter) {
|
|
|
320
324
|
// section, so no enclosing node implies it. The app rewrites start/startDate
|
|
321
325
|
// when the user acknowledges the banner; that is a GUI-only side effect our
|
|
322
326
|
// read cannot clear (watchers beware — see contract.md `provisional`).
|
|
323
|
-
const provisional = whenIsProvisional(when, s["start"], s["startDate"] ?? null);
|
|
327
|
+
const provisional = whenIsProvisional(when, (s["derived"] ?? {})["start"], s["startDate"] ?? null);
|
|
324
328
|
const o = { ...s };
|
|
325
329
|
// R9 universal reshapes (every tier, every kind incl. detail).
|
|
326
330
|
reshapeChecklist(o);
|
|
@@ -330,29 +334,20 @@ function shapeItem(src, drop, compact, promoter) {
|
|
|
330
334
|
if (o["project"] == null && o["headingProject"] != null)
|
|
331
335
|
o["project"] = o["headingProject"];
|
|
332
336
|
delete o["headingProject"];
|
|
333
|
-
//
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
|
|
337
|
+
// The ENTIRE internal derivation substrate leaves the wire in ONE structural
|
|
338
|
+
// drop (one-vocabulary Batch 2, Option B): `start`/`logged`/`trashed` (R10 —
|
|
339
|
+
// replaced by `stage`), `today`/`evening` (R12 — replaced by `when`), and the
|
|
340
|
+
// raw `reminder` byte all live in the nested `o.derived` bag. §9n: the
|
|
341
|
+
// top-level consumer `reminder` is ALREADY the live-gated value (null once the
|
|
342
|
+
// byte is presentation-dead — its `startDate` gone strictly past), gated at the
|
|
343
|
+
// mapper; a null key is pruned by omit-empty, so a stale reminder never reaches
|
|
344
|
+
// the wire without any drop here. `stage`/`when`/`provisional` are then stamped
|
|
345
|
+
// from the derivations above.
|
|
346
|
+
delete o["derived"];
|
|
340
347
|
if (drop.stage !== true)
|
|
341
348
|
o["stage"] = stage;
|
|
342
|
-
// R12 —
|
|
343
|
-
//
|
|
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).
|
|
346
|
-
delete o["today"];
|
|
347
|
-
delete o["evening"];
|
|
348
|
-
// §9n — a reminder byte is presentation-dead once its `startDate` goes strictly
|
|
349
|
-
// past: the GUI hides the bell but never clears the byte. The materialize-time
|
|
350
|
-
// `reminderLive` marker (mappers, via `reminderIsLive` under the response clock)
|
|
351
|
-
// says whether the stored reminder still renders; drop the `reminder` key when
|
|
352
|
-
// it does not, mirroring the GUI. The marker itself never rides the wire.
|
|
353
|
-
if (o["reminderLive"] !== true)
|
|
354
|
-
delete o["reminder"];
|
|
355
|
-
delete o["reminderLive"];
|
|
349
|
+
// R12 — `when` is emitted unless the enclosing context provably states the
|
|
350
|
+
// position (the today view's sections; a card date-group in rebucketChildren).
|
|
356
351
|
if (drop.when !== true && when !== undefined)
|
|
357
352
|
o["when"] = when;
|
|
358
353
|
// R13 — the provisional banner marker (never dropped; presence-keyed).
|
|
@@ -429,40 +424,109 @@ function withShapedItems(base, drop, compact, promoter) {
|
|
|
429
424
|
out["items"] = shapeList(base["items"], drop, compact, promoter);
|
|
430
425
|
return out;
|
|
431
426
|
}
|
|
432
|
-
/** Coerce an unknown value to an array (empty when absent). */
|
|
433
|
-
const asArray = (v) => (Array.isArray(v) ? v : []);
|
|
434
427
|
/**
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
440
|
-
*
|
|
428
|
+
* Re-bucket a project's / area's / heading's live (non-logbook/trash) children
|
|
429
|
+
* into the R10 card shape by their derived {@link deriveStage} — so the bucket an
|
|
430
|
+
* item lands in ALWAYS equals its `stage`:
|
|
431
|
+
* - `anytime` — stage anytime, in encounter order;
|
|
432
|
+
* - `upcoming` — stage upcoming, day-grouped `[{when, items}]` (a dated row under
|
|
433
|
+
* its `startDate`, a template under its `nextOccurrence`), date ASC; date-LESS
|
|
434
|
+
* templates (after-completion / paused) form a trailing `{when: null, items}`
|
|
435
|
+
* group (explicit null per the `area: null` section precedent);
|
|
436
|
+
* - `someday` — stage someday.
|
|
437
|
+
* Items are already in view order (index / date+todayIndex) from the read layer,
|
|
438
|
+
* so encounter order within a date group preserves that ordering. Each item is
|
|
439
|
+
* then run through {@link shapeItem} with the section drop (ancestry + `stage`,
|
|
440
|
+
* since the bucket states it).
|
|
441
441
|
*/
|
|
442
|
-
|
|
442
|
+
function rebucketChildren(children, drop, compact, promoter) {
|
|
443
|
+
const anytime = [];
|
|
444
|
+
const someday = [];
|
|
445
|
+
const datedByKey = new Map();
|
|
446
|
+
const datedOrder = [];
|
|
447
|
+
const restingTemplates = [];
|
|
448
|
+
const shape = (c) => shapeItem(c, drop, compact, promoter);
|
|
449
|
+
for (const raw of children) {
|
|
450
|
+
if (raw === null || typeof raw !== "object")
|
|
451
|
+
continue;
|
|
452
|
+
const c = raw;
|
|
453
|
+
const stage = stageOf(c);
|
|
454
|
+
if (stage === "anytime") {
|
|
455
|
+
anytime.push(shape(c));
|
|
456
|
+
}
|
|
457
|
+
else if (stage === "someday") {
|
|
458
|
+
someday.push(shape(c));
|
|
459
|
+
}
|
|
460
|
+
else if (stage === "upcoming") {
|
|
461
|
+
const repeating = c["repeating"];
|
|
462
|
+
const nextOcc = repeating != null && typeof repeating === "object"
|
|
463
|
+
? (repeating["nextOccurrence"] ?? null)
|
|
464
|
+
: null;
|
|
465
|
+
const date = (c.startDate ?? null) !== null ? c.startDate : nextOcc;
|
|
466
|
+
if (date === null) {
|
|
467
|
+
restingTemplates.push(shape(c));
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
if (!datedByKey.has(date)) {
|
|
471
|
+
datedByKey.set(date, []);
|
|
472
|
+
datedOrder.push(date);
|
|
473
|
+
}
|
|
474
|
+
// R12 — inside a date-group the group states the date, so a member whose
|
|
475
|
+
// `when` equals it drops it (every scheduled row and every projected
|
|
476
|
+
// template does — that IS the group key).
|
|
477
|
+
const shaped = shape(c);
|
|
478
|
+
if (shaped !== null && typeof shaped === "object" && shaped["when"] === date) {
|
|
479
|
+
delete shaped["when"];
|
|
480
|
+
}
|
|
481
|
+
datedByKey.get(date).push(shaped);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
// inbox / logbook / trash should not appear among a card's live children;
|
|
486
|
+
// route defensively to anytime rather than drop the row.
|
|
487
|
+
anytime.push(shape(c));
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const upcoming = datedOrder
|
|
491
|
+
.toSorted((a, b) => a.localeCompare(b))
|
|
492
|
+
.map((date) => ({ when: date, items: datedByKey.get(date) }));
|
|
493
|
+
if (restingTemplates.length > 0)
|
|
494
|
+
upcoming.push({ when: null, items: restingTemplates });
|
|
495
|
+
return { anytime, upcoming, someday };
|
|
496
|
+
}
|
|
497
|
+
/** Flatten an internal IsoDateGroup[] (`[{when, items}]`) to its items, in order. */
|
|
498
|
+
function flattenGroups(groups) {
|
|
499
|
+
if (!Array.isArray(groups))
|
|
500
|
+
return [];
|
|
501
|
+
const out = [];
|
|
502
|
+
for (const g of groups) {
|
|
503
|
+
if (g !== null && typeof g === "object" && Array.isArray(g["items"])) {
|
|
504
|
+
out.push(...g["items"]);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return out;
|
|
508
|
+
}
|
|
509
|
+
/** Coerce an unknown value to an array (empty when absent). */
|
|
510
|
+
const asArray = (v) => (Array.isArray(v) ? v : []);
|
|
443
511
|
/**
|
|
444
|
-
*
|
|
445
|
-
* project/area (the card states them)
|
|
446
|
-
* (
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
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`.
|
|
512
|
+
* The R6 ref drop for the un-headed BODY's four `children` bucket records (v2):
|
|
513
|
+
* every body child drops project/area (the card states them), the bucket-implied
|
|
514
|
+
* stage (each of `anytime`/`upcoming`/`someday`/`logbook` is stage-pure), and the
|
|
515
|
+
* heading ref — a body child is by construction un-headed (its `heading` is null),
|
|
516
|
+
* and a project view surfaces no bare `heading: null` (drop it explicitly rather
|
|
517
|
+
* than leaning on omit-empty).
|
|
453
518
|
*/
|
|
454
|
-
const
|
|
455
|
-
project: true,
|
|
456
|
-
area: true,
|
|
457
|
-
keepHeading: true,
|
|
458
|
-
};
|
|
519
|
+
const PROJECT_CHILD_DROP = { project: true, area: true, heading: true, stage: true };
|
|
459
520
|
/**
|
|
460
|
-
* The
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
521
|
+
* The R6 ref drop for a HEADING's four `children` bucket records (v2): a headed
|
|
522
|
+
* child drops project/area (the card states them), the heading ref (its position
|
|
523
|
+
* UNDER `headings[].children` states membership — structural, #362 / task item 6),
|
|
524
|
+
* and the bucket-implied stage. Applied uniformly to the heading's live buckets
|
|
525
|
+
* AND its `logbook` — the logbook is stage-pure (all logged), so stage drops too.
|
|
464
526
|
*/
|
|
465
|
-
const
|
|
527
|
+
const HEADING_MEMBER_DROP = { project: true, area: true, heading: true, stage: true };
|
|
528
|
+
/** Area-view child-item buckets drop their area (the card states it) + the bucket-implied stage. */
|
|
529
|
+
const AREA_CHILD_DROP = { area: true, stage: true };
|
|
466
530
|
/** Area-view PROJECTS list: a mixed listing of the area's project rows — keep `stage`, drop area. */
|
|
467
531
|
const AREA_PROJECTS_DROP = { area: true };
|
|
468
532
|
/**
|
|
@@ -481,31 +545,28 @@ const SOMEDAY_SECTION_DROP = { area: true, stage: true };
|
|
|
481
545
|
/** The card NODE / detail / mixed lists: keep every ref, `stage`, and `when`. */
|
|
482
546
|
const NO_DROP = {};
|
|
483
547
|
/**
|
|
484
|
-
* The today view's
|
|
485
|
-
*
|
|
486
|
-
* `
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
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.
|
|
548
|
+
* The today view's two `children` bucket records: drop the key-implied `when`
|
|
549
|
+
* (R12 — the `today`/`evening` bucket key states it) AND the bucket-implied
|
|
550
|
+
* `stage` (R13). Every Today member now derives stage `anytime` by construction —
|
|
551
|
+
* an ARRIVED `startDate` (step 5) or a DEADLINE PULL (step 2½) both derive
|
|
552
|
+
* `anytime`, and there are no future-dated or undated-someday Today members — so
|
|
553
|
+
* both buckets are provably stage-PURE `anytime` and the field is redundant there
|
|
554
|
+
* (verified strict by the today purity property test in test/unit/stage.test.ts).
|
|
555
|
+
* `provisional` is NOT a drop — the banner is not a bucket, so nothing implies it.
|
|
494
556
|
*/
|
|
495
|
-
const
|
|
557
|
+
const TODAY_SECTION_DROP = { when: true, stage: true };
|
|
496
558
|
/**
|
|
497
|
-
* Shape a heading
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
* needs — so:
|
|
559
|
+
* Shape a heading GROUP node (the `headings[].heading` / `loggedHeadings[].heading`
|
|
560
|
+
* keyed sub-object). The type is triply implied by position, and a heading has no
|
|
561
|
+
* open/canceled/completed vocabulary the reader needs — so:
|
|
501
562
|
* - DROP `type` (positional: this slot is always a heading; the "absent type =
|
|
502
|
-
* to-do" convention is scoped to ROWS/candidates, never this
|
|
563
|
+
* to-do" convention is scoped to ROWS/candidates, never this keyed sub-object);
|
|
503
564
|
* - DROP `project` (the card states it);
|
|
504
565
|
* - REPLACE `status` with the presence-keyed `archived` (the stopDate, an ISO
|
|
505
566
|
* date-time following the `stopped`/logged-row convention) — emitted ONLY when
|
|
506
|
-
* the heading is archived (status "completed"), OMITTED when open.
|
|
507
|
-
*
|
|
508
|
-
*
|
|
567
|
+
* the heading is archived (status "completed"), OMITTED when open. Region
|
|
568
|
+
* membership (live `headings` vs the logged region) expresses sweep state; the
|
|
569
|
+
* node carries only whether-and-when it was archived.
|
|
509
570
|
*/
|
|
510
571
|
function shapeHeadingNode(src) {
|
|
511
572
|
if (src === null || typeof src !== "object")
|
|
@@ -523,92 +584,294 @@ function shapeHeadingNode(src) {
|
|
|
523
584
|
h["archived"] = stopped;
|
|
524
585
|
return h;
|
|
525
586
|
}
|
|
526
|
-
/**
|
|
527
|
-
function
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
587
|
+
/** A bucket record `{items, total?}` (v2 R1): `total` present IFF the bucket was capped. */
|
|
588
|
+
export function bucketRecord(items, total) {
|
|
589
|
+
return total !== undefined && items.length < total ? { items, total } : { items };
|
|
590
|
+
}
|
|
591
|
+
/** The `stopped` epoch of an internal entity (a Date pre-shaping), or 0 — for logbook DESC ordering. */
|
|
592
|
+
function stoppedMs(o) {
|
|
593
|
+
const s = o["stopped"];
|
|
594
|
+
return s instanceof Date ? s.getTime() : 0;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Build ONE container's four v2 `children` bucket records (PR 2) from its flat
|
|
598
|
+
* child set (live AND logged alike). Every child is routed by DERIVED STAGE, so
|
|
599
|
+
* one entity lands in exactly one place (R5/#V12):
|
|
600
|
+
* - `logbook: {items, total?}` — the swept/resolved children (`logged` flag),
|
|
601
|
+
* most-recently-completed first (`stopped` DESC — the certified HEADARC3/logbook
|
|
602
|
+
* ordering), stage-pure so `stage` drops;
|
|
603
|
+
* - `anytime` / `someday: {items, total?}` — stage-pure records;
|
|
604
|
+
* - `upcoming: [{when, items, total?} …]` — the day-block ARRAY (R3): dated blocks
|
|
605
|
+
* chronological, then a single trailing `{when: null, items}` resting block for
|
|
606
|
+
* date-less recurring templates (#V8). An open child stranded under an archived
|
|
607
|
+
* heading (HEADARC2-C anomaly) is NOT logged, so it rides `anytime` here — its
|
|
608
|
+
* presence in a live bucket under an `archived` heading node is self-evident.
|
|
609
|
+
* `drop` carries the container's ancestry drops (body vs heading); the day-block
|
|
610
|
+
* key is `when` end-to-end (`rebucketChildren` builds it — no rename) — `null` for
|
|
611
|
+
* the resting block. No bucket is capped in the project view today, so every
|
|
612
|
+
* `total` is absent (R1: an untruncated bucket never restates its length); the
|
|
613
|
+
* `total?` argument keeps the record + day-block shape ready for PR 5's sweep and
|
|
614
|
+
* is exercised by the unit tests.
|
|
615
|
+
*/
|
|
616
|
+
function shapeContainerChildren(children, drop, compact, promoter) {
|
|
617
|
+
const live = [];
|
|
618
|
+
const logged = [];
|
|
619
|
+
for (const c of asArray(children)) {
|
|
620
|
+
// The logbook-boundary flag lives on the internal `derived` substrate bag.
|
|
621
|
+
const isLogged = c !== null &&
|
|
622
|
+
typeof c === "object" &&
|
|
623
|
+
c["derived"]?.["logged"] === true;
|
|
624
|
+
if (isLogged)
|
|
625
|
+
logged.push(c);
|
|
626
|
+
else
|
|
627
|
+
live.push(c);
|
|
628
|
+
}
|
|
629
|
+
const { anytime, upcoming, someday } = rebucketChildren(live, drop, compact, promoter);
|
|
630
|
+
const loggedSorted = logged.toSorted((a, b) => stoppedMs(b) - stoppedMs(a));
|
|
631
|
+
const logbook = shapeList(loggedSorted, drop, compact, promoter);
|
|
632
|
+
return {
|
|
633
|
+
anytime: bucketRecord(anytime),
|
|
634
|
+
// The day-block ARRAY (`{when, items}`, `when: null` the resting block #V8);
|
|
635
|
+
// `rebucketChildren` already keys each block by `when`.
|
|
636
|
+
upcoming,
|
|
637
|
+
someday: bucketRecord(someday),
|
|
638
|
+
logbook: bucketRecord(logbook),
|
|
531
639
|
};
|
|
532
|
-
return t(b) - t(a);
|
|
533
640
|
}
|
|
534
641
|
/**
|
|
535
|
-
* Shape a project view
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
* `
|
|
540
|
-
*
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
*
|
|
642
|
+
* Shape a project view into the read-shape v2 wire (PR 2):
|
|
643
|
+
* `{ project, children, headings[] }` — NOTHING else at this level. `children` is
|
|
644
|
+
* the un-headed BODY's four stage-keyed bucket records; `headings[]` is EVERY
|
|
645
|
+
* heading (index order, all lifecycle classes — R5) as `{uuid, title, archived?,
|
|
646
|
+
* children}` with the SAME recursive `children` shape. The per-container `logbook`
|
|
647
|
+
* lives inside each `children` (R6, no root logbook); the v1-era `logbookHeadings`
|
|
648
|
+
* and BOTH advisory keys (`openChildrenWhileResolved` /
|
|
649
|
+
* `openChildrenUnderArchivedHeading`) are DELETED (#V12) — anomalous open children
|
|
650
|
+
* seat in the normal recursive buckets, the heading's `archived` mark making the
|
|
651
|
+
* anomaly self-evident. The card node keeps everything (children derive their
|
|
652
|
+
* container from it). `out` is built fresh, so no render-only field leaks.
|
|
545
653
|
*/
|
|
546
654
|
function shapeProjectView(view, compact, promoter) {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
655
|
+
const headingContainers = asArray(view["headingContainers"]).map((c) => {
|
|
656
|
+
const grp = (c ?? {});
|
|
657
|
+
// The heading NODE (`{uuid, title, archived?}`) gains the recursive `children`
|
|
658
|
+
// (last key, so it reads after the identity). Object.assign mutates the fresh
|
|
659
|
+
// node copy shapeHeadingNode already returns — no spread-in-map.
|
|
660
|
+
const node = shapeHeadingNode(grp["heading"]);
|
|
661
|
+
return Object.assign(node, {
|
|
662
|
+
children: shapeContainerChildren(grp["children"], HEADING_MEMBER_DROP, compact, promoter),
|
|
663
|
+
});
|
|
664
|
+
});
|
|
665
|
+
return {
|
|
666
|
+
// The card NODE keeps everything but is still an item DTO (universal + R10 reshapes).
|
|
667
|
+
project: shapeItem(view["project"], NO_DROP, false, promoter),
|
|
668
|
+
children: shapeContainerChildren(view["bodyChildren"], PROJECT_CHILD_DROP, compact, promoter),
|
|
669
|
+
headings: headingContainers,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Build an area's THREE v2 `children` bucket records (PR 3) from its flat direct
|
|
674
|
+
* to-do set (live only — an area has NO logged-children region, so no `logbook`
|
|
675
|
+
* key; the area logbook is the bounded query `things logbook --area <ref>`, #346).
|
|
676
|
+
* The same stage-derived bucketing as {@link shapeContainerChildren} minus the
|
|
677
|
+
* logbook split: `anytime`/`someday` are `{items, total?}` records, `upcoming` is
|
|
678
|
+
* the day-block ARRAY (R3, keyed by `when`) with the trailing `{when: null, items}`
|
|
679
|
+
* resting block for date-less recurring templates (#V8). Inline `total` is stamped
|
|
680
|
+
* downstream by {@link withAreaBucketTotals} (only `anytime` can be capped — the
|
|
681
|
+
* `--area-limit` scope; the scheduled/someday direct to-dos always survive).
|
|
682
|
+
*/
|
|
683
|
+
function shapeAreaChildren(members, drop, compact, promoter) {
|
|
684
|
+
const { anytime, upcoming, someday } = rebucketChildren(members, drop, compact, promoter);
|
|
685
|
+
return {
|
|
686
|
+
anytime: bucketRecord(anytime),
|
|
687
|
+
// The day-block ARRAY (`{when, items}`, `when: null` the resting block #V8);
|
|
688
|
+
// `rebucketChildren` already keys each block by `when`.
|
|
689
|
+
upcoming,
|
|
690
|
+
someday: bucketRecord(someday),
|
|
691
|
+
};
|
|
584
692
|
}
|
|
585
693
|
/**
|
|
586
|
-
* Shape an area view
|
|
587
|
-
*
|
|
588
|
-
*
|
|
589
|
-
* someday
|
|
590
|
-
*
|
|
591
|
-
*
|
|
592
|
-
* The area node keeps its identity (tags
|
|
694
|
+
* Shape an area view into the read-shape v2 wire (PR 3):
|
|
695
|
+
* `{ area | null, children, projects }` — NOTHING else at this level. `children`
|
|
696
|
+
* is the area's direct to-dos as three stage-keyed bucket records (`anytime`,
|
|
697
|
+
* `upcoming[]`, `someday` — NO `logbook`, #346); `projects` is the child-project
|
|
698
|
+
* sidebar-rank scope as a bucket record `{items, total?}` — a mixed-stage listing
|
|
699
|
+
* that KEEPS `stage`/`when` (the someday-projects / active split is TTY-only). The
|
|
700
|
+
* loose pseudo-area keeps `area: null`. The area node keeps its identity (tags
|
|
701
|
+
* folded to names); each direct-to-do row drops `area` (the node states it) + the
|
|
702
|
+
* bucket-implied `stage`. Inline `total` (present iff a scope was capped, R1) is
|
|
703
|
+
* injected downstream by {@link withAreaBucketTotals}, where the pre-cap sizes are
|
|
704
|
+
* known. `out` is built fresh, so no render-only field leaks.
|
|
593
705
|
*/
|
|
594
706
|
function shapeAreaView(view, compact, promoter) {
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
707
|
+
const looseMembers = [
|
|
708
|
+
...asArray(view["active"]),
|
|
709
|
+
...flattenGroups(view["scheduled"]),
|
|
710
|
+
...asArray(view["someday"]),
|
|
711
|
+
...asArray(view["repeating"]),
|
|
712
|
+
];
|
|
713
|
+
return {
|
|
714
|
+
// The area NODE, or `null` for the loose pseudo-area (shapeArea passes null).
|
|
715
|
+
area: shapeArea(view["area"]),
|
|
716
|
+
children: shapeAreaChildren(looseMembers, AREA_CHILD_DROP, compact, promoter),
|
|
717
|
+
// The projects list is a mixed listing of the area's project rows — keep stage.
|
|
718
|
+
projects: bucketRecord(shapeList(view["projects"], AREA_PROJECTS_DROP, compact, promoter)),
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Inject the area view's inline scope `total`s (read-shape v2 R1, PR 3): present
|
|
723
|
+
* iff the scope was capped (`items.length < total`), absent otherwise — no
|
|
724
|
+
* `meta.truncation.blocks[]` sidecar. `children.anytime` carries the direct-to-dos
|
|
725
|
+
* (`--area-limit`) total; `projects` carries the project-rows (`--project-limit`)
|
|
726
|
+
* total. The scheduled/someday direct-to-do blocks and the scheduled/someday
|
|
727
|
+
* project rows are never capped, so they never gain a `total`. Both the CLI `view`
|
|
728
|
+
* wrapper and the MCP data block run the shaped view through this so completeness
|
|
729
|
+
* is answerable locally. Returns the view unchanged when it is not the expected
|
|
730
|
+
* shape.
|
|
731
|
+
*/
|
|
732
|
+
export function withAreaBucketTotals(view, totals) {
|
|
733
|
+
if (view === null || typeof view !== "object")
|
|
734
|
+
return view;
|
|
735
|
+
const v = view;
|
|
736
|
+
const children = v["children"];
|
|
737
|
+
const withChildTotals = children !== null && typeof children === "object"
|
|
738
|
+
? {
|
|
739
|
+
...children,
|
|
740
|
+
anytime: withBucketTotal(children["anytime"], totals.anytime),
|
|
741
|
+
}
|
|
742
|
+
: children;
|
|
743
|
+
// Spread-then-override keeps the `area` / `children` / `projects` key order.
|
|
744
|
+
return {
|
|
745
|
+
...v,
|
|
746
|
+
children: withChildTotals,
|
|
747
|
+
projects: withBucketTotal(v["projects"], totals.projects),
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* The global `upcoming` view's DAY-BLOCK key for one raw item (read-shape v2 PR 4):
|
|
752
|
+
* its `startDate` when scheduled; else, for a NON-template, its `deadline` (a
|
|
753
|
+
* deadline-forecast row appears at its due day — cohort 2, UPC1); else `null` —
|
|
754
|
+
* a date-LESS recurring template rides the trailing resting block (#V8). This is
|
|
755
|
+
* the emit-boundary twin of the renderer's `groupDate` (src/cli/render.ts) and of
|
|
756
|
+
* {@link upcomingBlockTotals} (the pre-cap sizer), so the wire's day blocks match
|
|
757
|
+
* the TTY grouping row-for-row and each block's inline `total` lines up with its
|
|
758
|
+
* scope. The library keeps its own day grouping; only the wire reshapes here.
|
|
759
|
+
*/
|
|
760
|
+
function upcomingBlockKey(o) {
|
|
761
|
+
const startDate = o["startDate"] ?? null;
|
|
762
|
+
if (startDate !== null)
|
|
763
|
+
return startDate;
|
|
764
|
+
const repeating = o["repeating"];
|
|
765
|
+
const isTemplate = repeating !== null &&
|
|
766
|
+
typeof repeating === "object" &&
|
|
767
|
+
repeating["isTemplate"] === true;
|
|
768
|
+
if (isTemplate)
|
|
769
|
+
return null; // a date-less template → the resting block (#V8)
|
|
770
|
+
return o["deadline"] ?? null; // a forecast row appears at its deadline
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Reshape the global `upcoming` view into the read-shape v2 day-block sections
|
|
774
|
+
* (PR 4): `[{ when, items, total? } …]` — chronological dated blocks keyed by
|
|
775
|
+
* {@link upcomingBlockKey} (each the COMPLETE global day scope, its `when` doubling
|
|
776
|
+
* as the `--in <when>` reorder token), then ONE trailing `{ when: null, items }`
|
|
777
|
+
* block holding the date-less resting recurring templates (#V8) when any exist.
|
|
778
|
+
* The incoming stream is already day-ordered (COALESCE(startDate, deadline) ASC,
|
|
779
|
+
* then the UI's within-day drag order), so encounter order preserves both the
|
|
780
|
+
* block chronology and the within-block order — no re-sort, matching the renderer.
|
|
781
|
+
* Rows KEEP `stage` (the view is projection-side stage-MIXED, R7: future-dated
|
|
782
|
+
* `upcoming` rows beside deadline-forecast `anytime`/`someday` ones) and drop
|
|
783
|
+
* `when` only when it equals the block's date (the block states it — the same rule
|
|
784
|
+
* {@link rebucketChildren} applies to a container day block); a forecast row's
|
|
785
|
+
* `when` is absent already, and a divergent projected `when` (horizon > 1) is kept.
|
|
786
|
+
* Every row keeps its container refs (a global mixed view — NO_DROP). Inline
|
|
787
|
+
* `total` is stamped downstream by {@link withUpcomingBlockTotals}.
|
|
788
|
+
*/
|
|
789
|
+
function shapeUpcomingView(items, compact, promoter) {
|
|
790
|
+
const datedByKey = new Map();
|
|
791
|
+
const datedOrder = [];
|
|
792
|
+
const resting = [];
|
|
793
|
+
for (const raw of items) {
|
|
794
|
+
if (raw === null || typeof raw !== "object")
|
|
795
|
+
continue;
|
|
796
|
+
const key = upcomingBlockKey(raw);
|
|
797
|
+
const shaped = shapeItem(raw, NO_DROP, compact, promoter);
|
|
798
|
+
if (key === null) {
|
|
799
|
+
resting.push(shaped);
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
if (!datedByKey.has(key)) {
|
|
803
|
+
datedByKey.set(key, []);
|
|
804
|
+
datedOrder.push(key);
|
|
805
|
+
}
|
|
806
|
+
// R12 — inside a dated block the block states the date, so a member whose
|
|
807
|
+
// `when` equals it drops it (a scheduled row's when IS the key). A forecast
|
|
808
|
+
// row has no `when`; a horizon-projected row whose `when` diverges keeps it.
|
|
809
|
+
if (shaped !== null && typeof shaped === "object" && shaped["when"] === key) {
|
|
810
|
+
delete shaped["when"];
|
|
811
|
+
}
|
|
812
|
+
datedByKey.get(key).push(shaped);
|
|
813
|
+
}
|
|
814
|
+
const sections = datedOrder.map((when) => ({ when, items: datedByKey.get(when) }));
|
|
815
|
+
if (resting.length > 0)
|
|
816
|
+
sections.push({ when: null, items: resting });
|
|
817
|
+
return sections;
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Inject each global-`upcoming` day block's inline `total` (read-shape v2 R1,
|
|
821
|
+
* PR 4): present iff that day's scope was capped by the flat row limit
|
|
822
|
+
* (`items.length < total`), absent otherwise — no `meta.truncation.blocks[]`
|
|
823
|
+
* sidecar (the whole-view `{shown,total,limit,truncated}` rollup still rides
|
|
824
|
+
* `meta.truncation` for the row hint). The flat cut across the day-ordered stream
|
|
825
|
+
* leaves at most ONE straddling block partial (its pre-cap size looked up by
|
|
826
|
+
* `when` from `totals`); blocks fully before the cut are complete (no `total`),
|
|
827
|
+
* and blocks fully past it never appear. The resting block keys on `null`. Both
|
|
828
|
+
* the CLI `sections` wrapper and the MCP data block run the shaped sections
|
|
829
|
+
* through this so completeness is answerable locally. Returns the input unchanged
|
|
830
|
+
* when it is not the expected sections array.
|
|
831
|
+
*/
|
|
832
|
+
export function withUpcomingBlockTotals(sections, totals) {
|
|
833
|
+
if (!Array.isArray(sections))
|
|
834
|
+
return sections;
|
|
835
|
+
return sections.map((s) => {
|
|
836
|
+
if (s === null || typeof s !== "object")
|
|
837
|
+
return s;
|
|
838
|
+
const sec = s;
|
|
839
|
+
const when = (sec["when"] ?? null);
|
|
840
|
+
const total = totals.get(when);
|
|
841
|
+
const items = sec["items"];
|
|
842
|
+
const shown = Array.isArray(items) ? items.length : 0;
|
|
843
|
+
// Spread-then-add keeps the `when` / `items` / `total` key order.
|
|
844
|
+
return total !== undefined && shown < total ? { ...sec, total } : sec;
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Inject each global anytime/someday section's inline `total` (read-shape v2 R1,
|
|
849
|
+
* PR 5): present iff that section's `items` were capped (`items.length < total`),
|
|
850
|
+
* absent otherwise — an untruncated section never restates its own length, and
|
|
851
|
+
* the pre-v2 `meta.truncation.blocks[]` descriptor-join sidecar is RETIRED. The
|
|
852
|
+
* pre-cap section sizes come from {@link previewSections}/{@link
|
|
853
|
+
* previewSomedaySections} keyed by area uuid (`null` for the loose section); the
|
|
854
|
+
* per-block "… N more" render detail is carried separately (internal
|
|
855
|
+
* {@link GroupBlock}[]), never on the wire. Both the CLI `sections` wrapper and
|
|
856
|
+
* the MCP data block run the shaped sections through this so completeness is
|
|
857
|
+
* answerable locally. Returns the input unchanged when it is not the expected
|
|
858
|
+
* sections array.
|
|
859
|
+
*/
|
|
860
|
+
export function withSectionTotals(sections, totals) {
|
|
861
|
+
if (!Array.isArray(sections))
|
|
862
|
+
return sections;
|
|
863
|
+
return sections.map((s) => {
|
|
864
|
+
if (s === null || typeof s !== "object")
|
|
865
|
+
return s;
|
|
866
|
+
const sec = s;
|
|
867
|
+
const area = sec["area"];
|
|
868
|
+
const key = area !== null && typeof area === "object" ? area["uuid"] : null;
|
|
869
|
+
const total = totals.get(key);
|
|
870
|
+
const items = sec["items"];
|
|
871
|
+
const shown = Array.isArray(items) ? items.length : 0;
|
|
872
|
+
// Spread-then-add keeps the `area` / `items` / `total` key order.
|
|
873
|
+
return total !== undefined && shown < total ? { ...sec, total } : sec;
|
|
874
|
+
});
|
|
612
875
|
}
|
|
613
876
|
/** Fold an area entity's tags to string names in place (returns a shallow copy). */
|
|
614
877
|
function shapeArea(src) {
|
|
@@ -618,6 +881,53 @@ function shapeArea(src) {
|
|
|
618
881
|
flattenTags(o);
|
|
619
882
|
return o;
|
|
620
883
|
}
|
|
884
|
+
/**
|
|
885
|
+
* Shape the today view into its two `children` bucket records (read-shape v2 R1):
|
|
886
|
+
* `{ today: { items }, evening: { items } }`, each a stage/`when`-pure list (the
|
|
887
|
+
* bucket key states both — TODAY_SECTION_DROP). The whole-view `counts` aggregate
|
|
888
|
+
* is NOT here — it rides `meta.counts` (runRead / the MCP metadata block). Inline
|
|
889
|
+
* per-bucket `total` (present iff capped) is injected downstream by
|
|
890
|
+
* {@link withTodayBucketTotals}, where the pre-cap sizes are known.
|
|
891
|
+
*/
|
|
892
|
+
function shapeTodayView(view, compact, promoter) {
|
|
893
|
+
return {
|
|
894
|
+
today: { items: shapeList(view["today"], TODAY_SECTION_DROP, compact, promoter) },
|
|
895
|
+
evening: { items: shapeList(view["evening"], TODAY_SECTION_DROP, compact, promoter) },
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Inject each today bucket's inline `total` (read-shape v2 R1): present iff the
|
|
900
|
+
* bucket was capped (`items.length < total`), absent otherwise — an untruncated
|
|
901
|
+
* bucket never restates its own length. `totals` are the pre-cap bucket sizes
|
|
902
|
+
* from `truncateToday`. Both the CLI `data.children` wrapper and the MCP data
|
|
903
|
+
* block run the shaped children through this so completeness is answerable
|
|
904
|
+
* locally, with no truncation sidecar. Returns the children object unchanged when
|
|
905
|
+
* it is not the expected shape.
|
|
906
|
+
*/
|
|
907
|
+
export function withTodayBucketTotals(children, totals) {
|
|
908
|
+
if (children === null || typeof children !== "object")
|
|
909
|
+
return children;
|
|
910
|
+
const c = children;
|
|
911
|
+
return {
|
|
912
|
+
today: withBucketTotal(c["today"], totals.today),
|
|
913
|
+
evening: withBucketTotal(c["evening"], totals.evening),
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Stamp a bucket record's inline `total` (read-shape v2 R1) iff it was capped
|
|
918
|
+
* (`items.length < total`) — an untruncated bucket never restates its own length.
|
|
919
|
+
* The pre-cap `total` comes from the bounding layer; returns the bucket unchanged
|
|
920
|
+
* when it is not a `{items}` record. Shared by the today and area inline-total
|
|
921
|
+
* injectors.
|
|
922
|
+
*/
|
|
923
|
+
function withBucketTotal(bucket, total) {
|
|
924
|
+
if (bucket === null || typeof bucket !== "object")
|
|
925
|
+
return bucket;
|
|
926
|
+
const b = bucket;
|
|
927
|
+
const items = b["items"];
|
|
928
|
+
const shown = Array.isArray(items) ? items.length : 0;
|
|
929
|
+
return shown < total ? { ...b, total } : b;
|
|
930
|
+
}
|
|
621
931
|
/** Shape sidebar sections (anytime/someday catalogues) with the section's drop spec. */
|
|
622
932
|
function shapeSections(sections, drop, compact, promoter) {
|
|
623
933
|
if (!Array.isArray(sections))
|
|
@@ -628,18 +938,19 @@ function shapeSections(sections, drop, compact, promoter) {
|
|
|
628
938
|
* The flat, mixed-provenance list kinds mapped to their drop spec. Only the
|
|
629
939
|
* stage-PURE catalogues (inbox/someday/logbook/trash; the section-based `anytime`
|
|
630
940
|
* is pure too, handled via shapeSections below) drop the bucket-implied `stage`.
|
|
631
|
-
*
|
|
632
|
-
*
|
|
633
|
-
* stage
|
|
634
|
-
*
|
|
941
|
+
* The mixed/derived surfaces (search/changes/deadlines/projects) keep it. The global
|
|
942
|
+
* `upcoming` view is NOT here — it reshapes into `data.sections` day blocks
|
|
943
|
+
* ({@link shapeUpcomingView}), keeping `stage` (R10.2: stage-mixed — future-dated
|
|
944
|
+
* `upcoming` rows beside deadline-forecast `anytime`/`someday` ones).
|
|
635
945
|
*/
|
|
636
946
|
const FLAT_LIST_DROP = new Map([
|
|
637
947
|
["inbox", { stage: true }],
|
|
638
|
-
["upcoming", NO_DROP],
|
|
639
948
|
["logbook", { stage: true }],
|
|
640
949
|
["trash", { stage: true }],
|
|
641
950
|
["changes", NO_DROP],
|
|
642
951
|
["search", NO_DROP],
|
|
952
|
+
// deadlines is stage-MIXED (to-dos + projects, deadline-ordered) — keep `stage`.
|
|
953
|
+
["deadlines", NO_DROP],
|
|
643
954
|
["projects", NO_DROP],
|
|
644
955
|
]);
|
|
645
956
|
/**
|
|
@@ -661,12 +972,11 @@ export function shapeReadPayload(kind, data, full, promoter) {
|
|
|
661
972
|
const flatDrop = FLAT_LIST_DROP.get(kind);
|
|
662
973
|
if (flatDrop !== undefined)
|
|
663
974
|
return shapeList(data, flatDrop, compact, p);
|
|
664
|
-
// The
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
// `meta.counts`, not `data`. The wire wrapper is `{ items }` (read-driver).
|
|
975
|
+
// The global `upcoming` view reshapes into `data.sections` day blocks (PR 4).
|
|
976
|
+
if (kind === "upcoming" && Array.isArray(data))
|
|
977
|
+
return shapeUpcomingView(data, compact, p);
|
|
668
978
|
if (kind === "today" && data !== null && typeof data === "object") {
|
|
669
|
-
return
|
|
979
|
+
return shapeTodayView(data, compact, p);
|
|
670
980
|
}
|
|
671
981
|
if (kind === "anytime" && Array.isArray(data)) {
|
|
672
982
|
return shapeSections(data, ANYTIME_SECTION_DROP, compact, p); // stage-pure → drop stage
|
|
@@ -723,9 +1033,10 @@ export function candidateRef(type, src) {
|
|
|
723
1033
|
if (project !== null)
|
|
724
1034
|
out.project = project;
|
|
725
1035
|
// stage/when only for the task kinds, and only when the source carries the
|
|
726
|
-
// materialized lifecycle substrate (`start`) — a thin uuid+title
|
|
727
|
-
// does not, so the keys stay absent (presence-keyed, like the wire).
|
|
728
|
-
if ((type === "to-do" || type === "project") &&
|
|
1036
|
+
// materialized lifecycle substrate (`derived.start`) — a thin uuid+title
|
|
1037
|
+
// resolver row does not, so the keys stay absent (presence-keyed, like the wire).
|
|
1038
|
+
if ((type === "to-do" || type === "project") &&
|
|
1039
|
+
typeof (s["derived"] ?? {})["start"] === "string") {
|
|
729
1040
|
const stage = stageOf(s);
|
|
730
1041
|
out.stage = stage;
|
|
731
1042
|
const when = whenOf(s, stage);
|