things-api 0.13.0 → 0.14.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 +2 -2
- package/dist/cli/commands/area.d.ts +5 -5
- package/dist/cli/commands/area.js +12 -7
- package/dist/cli/commands/area.js.map +1 -1
- package/dist/cli/commands/project.js +1 -1
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/cli/commands/reads.js +21 -12
- 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 +1 -1
- package/dist/cli/commands/todo.js.map +1 -1
- package/dist/cli/commands/writes.js +116 -70
- package/dist/cli/commands/writes.js.map +1 -1
- package/dist/cli/glyphs.d.ts +9 -2
- package/dist/cli/glyphs.js +11 -5
- package/dist/cli/glyphs.js.map +1 -1
- package/dist/cli/read-driver.d.ts +39 -9
- package/dist/cli/read-driver.js +37 -16
- package/dist/cli/read-driver.js.map +1 -1
- package/dist/cli/render.d.ts +25 -26
- package/dist/cli/render.js +57 -56
- package/dist/cli/render.js.map +1 -1
- package/dist/client.d.ts +92 -35
- package/dist/client.js +61 -18
- package/dist/client.js.map +1 -1
- package/dist/contracts.d.ts +23 -33
- package/dist/contracts.js +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +158 -144
- 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/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 +1 -5
- package/dist/read/area-view.js.map +1 -1
- package/dist/read/project-view.d.ts +34 -19
- package/dist/read/project-view.js +26 -20
- 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/shape.d.ts +73 -14
- package/dist/read/shape.js +459 -151
- package/dist/read/shape.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 +19 -18
- package/dist/read/views.js +5 -6
- 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/commands.d.ts +25 -1
- package/dist/write/commands.js +256 -121
- package/dist/write/commands.js.map +1 -1
- package/dist/write/guards.js +27 -11
- package/dist/write/guards.js.map +1 -1
- package/dist/write/move.d.ts +26 -5
- package/dist/write/move.js +313 -2
- package/dist/write/move.js.map +1 -1
- package/dist/write/operations.d.ts +49 -19
- package/dist/write/operations.js +2 -2
- package/dist/write/operations.js.map +1 -1
- package/dist/write/pipeline.js +25 -4
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/pre-state.d.ts +48 -28
- package/dist/write/pre-state.js +134 -13
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/reorder.js +3 -3
- 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 +5 -5
- package/dist/write/reversibility.js.map +1 -1
- package/dist/write/scope-guard.js +0 -5
- package/dist/write/scope-guard.js.map +1 -1
- package/dist/write/undo.d.ts +0 -6
- package/dist/write/undo.js +44 -4
- package/dist/write/undo.js.map +1 -1
- package/dist/write/vectors/applescript.js +14 -4
- package/dist/write/vectors/applescript.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 +16 -2
- package/dist/write/verify/delta.js +21 -2
- package/dist/write/verify/delta.js.map +1 -1
- package/package.json +1 -1
- package/schema/envelope.schema.json +2 -78
- package/skills/things-cli/SKILL.md +7 -5
- package/skills/things-cli/references/contracts.md +3 -4
- package/skills/things-cli/references/model.md +8 -8
- package/skills/things-cli/references/ordering.md +16 -7
package/dist/read/shape.js
CHANGED
|
@@ -54,16 +54,17 @@
|
|
|
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
|
-
* search
|
|
65
|
-
*
|
|
66
|
-
*
|
|
57
|
+
* it (same fact, one word); the resting-templates `{date: null}` group is
|
|
58
|
+
* unchanged (an unprojected template has no `when`).
|
|
59
|
+
* - `when` is DROPPED inside the `today` view's two `children` bucket records (the
|
|
60
|
+
* bucket key `today`/`evening` states it) and inside any card/heading `upcoming`
|
|
61
|
+
* DATE-GROUP for a member whose `when` equals the group's date (the group states
|
|
62
|
+
* it). KEPT everywhere
|
|
63
|
+
* else it is present — including the flat `upcoming`/`anytime`/`inbox`/`someday`
|
|
64
|
+
* catalogues, search, changes (a deadline-pulled row reads `when: "today"` in the
|
|
65
|
+
* mixed search/changes surfaces, informatively; note R13 re-files it to stage
|
|
66
|
+
* `anytime` and the flat inbox/someday views now EXCLUDE it — it appears in the
|
|
67
|
+
* `anytime` catalogue instead, `when: "today"` kept, stage dropped as pure).
|
|
67
68
|
*
|
|
68
69
|
* ## R13 — provisional Today members + GUI-faithful pulled-row membership
|
|
69
70
|
* BANNER1 / BANNER1b (docs/lab/banner1-research.md). Two coupled facts:
|
|
@@ -77,10 +78,10 @@
|
|
|
77
78
|
* side effect our read cannot perform (watchers beware).
|
|
78
79
|
* - **stage `anytime` for a deadline pull** — a due-deadline pull re-files an undated
|
|
79
80
|
* 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.
|
|
81
|
+
* derives stage `anytime`, and the `today` view's two `children` buckets become
|
|
82
|
+
* stage-PURE → `stage` is DROPPED there (TODAY_SECTION_DROP), alongside the
|
|
83
|
+
* key-implied `when`. The flat someday/inbox views EXCLUDE pulled rows and the anytime view
|
|
84
|
+
* INCLUDES them (src/read/views.ts + predicates.ts DEADLINE_PULLED) — GUI fidelity.
|
|
84
85
|
*
|
|
85
86
|
* ## Universal item-DTO reshapes (R9 — EVERY tier, EVERY read kind incl. detail)
|
|
86
87
|
* - **checklist nesting** — flat counts → presence-keyed `checklist: {open,total}`.
|
|
@@ -340,9 +341,9 @@ function shapeItem(src, drop, compact, promoter) {
|
|
|
340
341
|
if (drop.stage !== true)
|
|
341
342
|
o["stage"] = stage;
|
|
342
343
|
// R12 — the today/evening marker KEYS are replaced by the derived `when` on
|
|
343
|
-
// EVERY tier (they never appear on the wire); `when` is emitted
|
|
344
|
-
//
|
|
345
|
-
//
|
|
344
|
+
// EVERY tier (they never appear on the wire); `when` is emitted unless the
|
|
345
|
+
// enclosing context provably states the position (the today view's sections;
|
|
346
|
+
// a card date-group — handled in rebucketChildren).
|
|
346
347
|
delete o["today"];
|
|
347
348
|
delete o["evening"];
|
|
348
349
|
// §9n — a reminder byte is presentation-dead once its `startDate` goes strictly
|
|
@@ -429,40 +430,109 @@ function withShapedItems(base, drop, compact, promoter) {
|
|
|
429
430
|
out["items"] = shapeList(base["items"], drop, compact, promoter);
|
|
430
431
|
return out;
|
|
431
432
|
}
|
|
432
|
-
/** Coerce an unknown value to an array (empty when absent). */
|
|
433
|
-
const asArray = (v) => (Array.isArray(v) ? v : []);
|
|
434
433
|
/**
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
440
|
-
*
|
|
434
|
+
* Re-bucket a project's / area's / heading's live (non-logbook/trash) children
|
|
435
|
+
* into the R10 card shape by their derived {@link deriveStage} — so the bucket an
|
|
436
|
+
* item lands in ALWAYS equals its `stage`:
|
|
437
|
+
* - `anytime` — stage anytime, in encounter order;
|
|
438
|
+
* - `upcoming` — stage upcoming, date-grouped `[{date, items}]` (a dated row under
|
|
439
|
+
* its `startDate`, a template under its `nextOccurrence`), date ASC; date-LESS
|
|
440
|
+
* templates (after-completion / paused) form a trailing `{date: null, items}`
|
|
441
|
+
* group (explicit null per the `area: null` section precedent);
|
|
442
|
+
* - `someday` — stage someday.
|
|
443
|
+
* Items are already in view order (index / date+todayIndex) from the read layer,
|
|
444
|
+
* so encounter order within a date group preserves that ordering. Each item is
|
|
445
|
+
* then run through {@link shapeItem} with the section drop (ancestry + `stage`,
|
|
446
|
+
* since the bucket states it).
|
|
441
447
|
*/
|
|
442
|
-
|
|
448
|
+
function rebucketChildren(children, drop, compact, promoter) {
|
|
449
|
+
const anytime = [];
|
|
450
|
+
const someday = [];
|
|
451
|
+
const datedByKey = new Map();
|
|
452
|
+
const datedOrder = [];
|
|
453
|
+
const restingTemplates = [];
|
|
454
|
+
const shape = (c) => shapeItem(c, drop, compact, promoter);
|
|
455
|
+
for (const raw of children) {
|
|
456
|
+
if (raw === null || typeof raw !== "object")
|
|
457
|
+
continue;
|
|
458
|
+
const c = raw;
|
|
459
|
+
const stage = stageOf(c);
|
|
460
|
+
if (stage === "anytime") {
|
|
461
|
+
anytime.push(shape(c));
|
|
462
|
+
}
|
|
463
|
+
else if (stage === "someday") {
|
|
464
|
+
someday.push(shape(c));
|
|
465
|
+
}
|
|
466
|
+
else if (stage === "upcoming") {
|
|
467
|
+
const repeating = c["repeating"];
|
|
468
|
+
const nextOcc = repeating != null && typeof repeating === "object"
|
|
469
|
+
? (repeating["nextOccurrence"] ?? null)
|
|
470
|
+
: null;
|
|
471
|
+
const date = (c.startDate ?? null) !== null ? c.startDate : nextOcc;
|
|
472
|
+
if (date === null) {
|
|
473
|
+
restingTemplates.push(shape(c));
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
if (!datedByKey.has(date)) {
|
|
477
|
+
datedByKey.set(date, []);
|
|
478
|
+
datedOrder.push(date);
|
|
479
|
+
}
|
|
480
|
+
// R12 — inside a date-group the group states the date, so a member whose
|
|
481
|
+
// `when` equals it drops it (every scheduled row and every projected
|
|
482
|
+
// template does — that IS the group key).
|
|
483
|
+
const shaped = shape(c);
|
|
484
|
+
if (shaped !== null && typeof shaped === "object" && shaped["when"] === date) {
|
|
485
|
+
delete shaped["when"];
|
|
486
|
+
}
|
|
487
|
+
datedByKey.get(date).push(shaped);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
// inbox / logbook / trash should not appear among a card's live children;
|
|
492
|
+
// route defensively to anytime rather than drop the row.
|
|
493
|
+
anytime.push(shape(c));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
const upcoming = datedOrder
|
|
497
|
+
.toSorted((a, b) => a.localeCompare(b))
|
|
498
|
+
.map((date) => ({ date, items: datedByKey.get(date) }));
|
|
499
|
+
if (restingTemplates.length > 0)
|
|
500
|
+
upcoming.push({ date: null, items: restingTemplates });
|
|
501
|
+
return { anytime, upcoming, someday };
|
|
502
|
+
}
|
|
503
|
+
/** Flatten an internal IsoDateGroup[] (`[{date, items}]`) to its items, in order. */
|
|
504
|
+
function flattenGroups(groups) {
|
|
505
|
+
if (!Array.isArray(groups))
|
|
506
|
+
return [];
|
|
507
|
+
const out = [];
|
|
508
|
+
for (const g of groups) {
|
|
509
|
+
if (g !== null && typeof g === "object" && Array.isArray(g["items"])) {
|
|
510
|
+
out.push(...g["items"]);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return out;
|
|
514
|
+
}
|
|
515
|
+
/** Coerce an unknown value to an array (empty when absent). */
|
|
516
|
+
const asArray = (v) => (Array.isArray(v) ? v : []);
|
|
443
517
|
/**
|
|
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`.
|
|
518
|
+
* The R6 ref drop for the un-headed BODY's four `children` bucket records (v2):
|
|
519
|
+
* every body child drops project/area (the card states them), the bucket-implied
|
|
520
|
+
* stage (each of `anytime`/`upcoming`/`someday`/`logbook` is stage-pure), and the
|
|
521
|
+
* heading ref — a body child is by construction un-headed (its `heading` is null),
|
|
522
|
+
* and a project view surfaces no bare `heading: null` (drop it explicitly rather
|
|
523
|
+
* than leaning on omit-empty).
|
|
453
524
|
*/
|
|
454
|
-
const
|
|
455
|
-
project: true,
|
|
456
|
-
area: true,
|
|
457
|
-
keepHeading: true,
|
|
458
|
-
};
|
|
525
|
+
const PROJECT_CHILD_DROP = { project: true, area: true, heading: true, stage: true };
|
|
459
526
|
/**
|
|
460
|
-
* The
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
527
|
+
* The R6 ref drop for a HEADING's four `children` bucket records (v2): a headed
|
|
528
|
+
* child drops project/area (the card states them), the heading ref (its position
|
|
529
|
+
* UNDER `headings[].children` states membership — structural, #362 / task item 6),
|
|
530
|
+
* and the bucket-implied stage. Applied uniformly to the heading's live buckets
|
|
531
|
+
* AND its `logbook` — the logbook is stage-pure (all logged), so stage drops too.
|
|
464
532
|
*/
|
|
465
|
-
const
|
|
533
|
+
const HEADING_MEMBER_DROP = { project: true, area: true, heading: true, stage: true };
|
|
534
|
+
/** Area-view child-item buckets drop their area (the card states it) + the bucket-implied stage. */
|
|
535
|
+
const AREA_CHILD_DROP = { area: true, stage: true };
|
|
466
536
|
/** Area-view PROJECTS list: a mixed listing of the area's project rows — keep `stage`, drop area. */
|
|
467
537
|
const AREA_PROJECTS_DROP = { area: true };
|
|
468
538
|
/**
|
|
@@ -481,31 +551,28 @@ const SOMEDAY_SECTION_DROP = { area: true, stage: true };
|
|
|
481
551
|
/** The card NODE / detail / mixed lists: keep every ref, `stage`, and `when`. */
|
|
482
552
|
const NO_DROP = {};
|
|
483
553
|
/**
|
|
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.
|
|
554
|
+
* The today view's two `children` bucket records: drop the key-implied `when`
|
|
555
|
+
* (R12 — the `today`/`evening` bucket key states it) AND the bucket-implied
|
|
556
|
+
* `stage` (R13). Every Today member now derives stage `anytime` by construction —
|
|
557
|
+
* an ARRIVED `startDate` (step 5) or a DEADLINE PULL (step 2½) both derive
|
|
558
|
+
* `anytime`, and there are no future-dated or undated-someday Today members — so
|
|
559
|
+
* both buckets are provably stage-PURE `anytime` and the field is redundant there
|
|
560
|
+
* (verified strict by the today purity property test in test/unit/stage.test.ts).
|
|
561
|
+
* `provisional` is NOT a drop — the banner is not a bucket, so nothing implies it.
|
|
494
562
|
*/
|
|
495
|
-
const
|
|
563
|
+
const TODAY_SECTION_DROP = { when: true, stage: true };
|
|
496
564
|
/**
|
|
497
|
-
* Shape a heading
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
* needs — so:
|
|
565
|
+
* Shape a heading GROUP node (the `headings[].heading` / `loggedHeadings[].heading`
|
|
566
|
+
* keyed sub-object). The type is triply implied by position, and a heading has no
|
|
567
|
+
* open/canceled/completed vocabulary the reader needs — so:
|
|
501
568
|
* - DROP `type` (positional: this slot is always a heading; the "absent type =
|
|
502
|
-
* to-do" convention is scoped to ROWS/candidates, never this
|
|
569
|
+
* to-do" convention is scoped to ROWS/candidates, never this keyed sub-object);
|
|
503
570
|
* - DROP `project` (the card states it);
|
|
504
571
|
* - REPLACE `status` with the presence-keyed `archived` (the stopDate, an ISO
|
|
505
572
|
* date-time following the `stopped`/logged-row convention) — emitted ONLY when
|
|
506
|
-
* the heading is archived (status "completed"), OMITTED when open.
|
|
507
|
-
*
|
|
508
|
-
*
|
|
573
|
+
* the heading is archived (status "completed"), OMITTED when open. Region
|
|
574
|
+
* membership (live `headings` vs the logged region) expresses sweep state; the
|
|
575
|
+
* node carries only whether-and-when it was archived.
|
|
509
576
|
*/
|
|
510
577
|
function shapeHeadingNode(src) {
|
|
511
578
|
if (src === null || typeof src !== "object")
|
|
@@ -523,92 +590,288 @@ function shapeHeadingNode(src) {
|
|
|
523
590
|
h["archived"] = stopped;
|
|
524
591
|
return h;
|
|
525
592
|
}
|
|
526
|
-
/**
|
|
527
|
-
function
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
593
|
+
/** A bucket record `{items, total?}` (v2 R1): `total` present IFF the bucket was capped. */
|
|
594
|
+
export function bucketRecord(items, total) {
|
|
595
|
+
return total !== undefined && items.length < total ? { items, total } : { items };
|
|
596
|
+
}
|
|
597
|
+
/** The `stopped` epoch of an internal entity (a Date pre-shaping), or 0 — for logbook DESC ordering. */
|
|
598
|
+
function stoppedMs(o) {
|
|
599
|
+
const s = o["stopped"];
|
|
600
|
+
return s instanceof Date ? s.getTime() : 0;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Build ONE container's four v2 `children` bucket records (PR 2) from its flat
|
|
604
|
+
* child set (live AND logged alike). Every child is routed by DERIVED STAGE, so
|
|
605
|
+
* one entity lands in exactly one place (R5/#V12):
|
|
606
|
+
* - `logbook: {items, total?}` — the swept/resolved children (`logged` flag),
|
|
607
|
+
* most-recently-completed first (`stopped` DESC — the certified HEADARC3/logbook
|
|
608
|
+
* ordering), stage-pure so `stage` drops;
|
|
609
|
+
* - `anytime` / `someday: {items, total?}` — stage-pure records;
|
|
610
|
+
* - `upcoming: [{when, items, total?} …]` — the day-block ARRAY (R3): dated blocks
|
|
611
|
+
* chronological, then a single trailing `{when: null, items}` resting block for
|
|
612
|
+
* date-less recurring templates (#V8). An open child stranded under an archived
|
|
613
|
+
* heading (HEADARC2-C anomaly) is NOT logged, so it rides `anytime` here — its
|
|
614
|
+
* presence in a live bucket under an `archived` heading node is self-evident.
|
|
615
|
+
* `drop` carries the container's ancestry drops (body vs heading); the day-block
|
|
616
|
+
* `when` renames the internal date group's `date` (rebucketChildren) — `null` for
|
|
617
|
+
* the resting block. No bucket is capped in the project view today, so every
|
|
618
|
+
* `total` is absent (R1: an untruncated bucket never restates its length); the
|
|
619
|
+
* `total?` argument keeps the record + day-block shape ready for PR 5's sweep and
|
|
620
|
+
* is exercised by the unit tests.
|
|
621
|
+
*/
|
|
622
|
+
function shapeContainerChildren(children, drop, compact, promoter) {
|
|
623
|
+
const live = [];
|
|
624
|
+
const logged = [];
|
|
625
|
+
for (const c of asArray(children)) {
|
|
626
|
+
if (c !== null && typeof c === "object" && c["logged"] === true)
|
|
627
|
+
logged.push(c);
|
|
628
|
+
else
|
|
629
|
+
live.push(c);
|
|
630
|
+
}
|
|
631
|
+
const { anytime, upcoming, someday } = rebucketChildren(live, drop, compact, promoter);
|
|
632
|
+
const loggedSorted = logged.toSorted((a, b) => stoppedMs(b) - stoppedMs(a));
|
|
633
|
+
const logbook = shapeList(loggedSorted, drop, compact, promoter);
|
|
634
|
+
return {
|
|
635
|
+
anytime: bucketRecord(anytime),
|
|
636
|
+
// The day-block ARRAY: `date` → `when` (R3); `null` is the resting block (#V8).
|
|
637
|
+
upcoming: upcoming.map((g) => ({ when: g.date, items: g.items })),
|
|
638
|
+
someday: bucketRecord(someday),
|
|
639
|
+
logbook: bucketRecord(logbook),
|
|
531
640
|
};
|
|
532
|
-
return t(b) - t(a);
|
|
533
641
|
}
|
|
534
642
|
/**
|
|
535
|
-
* Shape a project view
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
* `
|
|
540
|
-
*
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
*
|
|
643
|
+
* Shape a project view into the read-shape v2 wire (PR 2):
|
|
644
|
+
* `{ project, children, headings[] }` — NOTHING else at this level. `children` is
|
|
645
|
+
* the un-headed BODY's four stage-keyed bucket records; `headings[]` is EVERY
|
|
646
|
+
* heading (index order, all lifecycle classes — R5) as `{uuid, title, archived?,
|
|
647
|
+
* children}` with the SAME recursive `children` shape. The per-container `logbook`
|
|
648
|
+
* lives inside each `children` (R6, no root logbook); the v1-era `logbookHeadings`
|
|
649
|
+
* and BOTH advisory keys (`openChildrenWhileResolved` /
|
|
650
|
+
* `openChildrenUnderArchivedHeading`) are DELETED (#V12) — anomalous open children
|
|
651
|
+
* seat in the normal recursive buckets, the heading's `archived` mark making the
|
|
652
|
+
* anomaly self-evident. The card node keeps everything (children derive their
|
|
653
|
+
* container from it). `out` is built fresh, so no render-only field leaks.
|
|
545
654
|
*/
|
|
546
655
|
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
|
-
return out;
|
|
656
|
+
const headingContainers = asArray(view["headingContainers"]).map((c) => {
|
|
657
|
+
const grp = (c ?? {});
|
|
658
|
+
// The heading NODE (`{uuid, title, archived?}`) gains the recursive `children`
|
|
659
|
+
// (last key, so it reads after the identity). Object.assign mutates the fresh
|
|
660
|
+
// node copy shapeHeadingNode already returns — no spread-in-map.
|
|
661
|
+
const node = shapeHeadingNode(grp["heading"]);
|
|
662
|
+
return Object.assign(node, {
|
|
663
|
+
children: shapeContainerChildren(grp["children"], HEADING_MEMBER_DROP, compact, promoter),
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
return {
|
|
667
|
+
// The card NODE keeps everything but is still an item DTO (universal + R10 reshapes).
|
|
668
|
+
project: shapeItem(view["project"], NO_DROP, false, promoter),
|
|
669
|
+
children: shapeContainerChildren(view["bodyChildren"], PROJECT_CHILD_DROP, compact, promoter),
|
|
670
|
+
headings: headingContainers,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Build an area's THREE v2 `children` bucket records (PR 3) from its flat direct
|
|
675
|
+
* to-do set (live only — an area has NO logged-children region, so no `logbook`
|
|
676
|
+
* key; the area logbook is the bounded query `things logbook --area <ref>`, #346).
|
|
677
|
+
* The same stage-derived bucketing as {@link shapeContainerChildren} minus the
|
|
678
|
+
* logbook split: `anytime`/`someday` are `{items, total?}` records, `upcoming` is
|
|
679
|
+
* the day-block ARRAY (R3, `date` → `when`) with the trailing `{when: null, items}`
|
|
680
|
+
* resting block for date-less recurring templates (#V8). Inline `total` is stamped
|
|
681
|
+
* downstream by {@link withAreaBucketTotals} (only `anytime` can be capped — the
|
|
682
|
+
* `--area-limit` scope; the scheduled/someday direct to-dos always survive).
|
|
683
|
+
*/
|
|
684
|
+
function shapeAreaChildren(members, drop, compact, promoter) {
|
|
685
|
+
const { anytime, upcoming, someday } = rebucketChildren(members, drop, compact, promoter);
|
|
686
|
+
return {
|
|
687
|
+
anytime: bucketRecord(anytime),
|
|
688
|
+
// The day-block ARRAY: `date` → `when` (R3); `null` is the resting block (#V8).
|
|
689
|
+
upcoming: upcoming.map((g) => ({ when: g.date, items: g.items })),
|
|
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,14 +938,13 @@ 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/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],
|
|
@@ -661,12 +970,11 @@ export function shapeReadPayload(kind, data, full, promoter) {
|
|
|
661
970
|
const flatDrop = FLAT_LIST_DROP.get(kind);
|
|
662
971
|
if (flatDrop !== undefined)
|
|
663
972
|
return shapeList(data, flatDrop, compact, p);
|
|
664
|
-
// The
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
// `meta.counts`, not `data`. The wire wrapper is `{ items }` (read-driver).
|
|
973
|
+
// The global `upcoming` view reshapes into `data.sections` day blocks (PR 4).
|
|
974
|
+
if (kind === "upcoming" && Array.isArray(data))
|
|
975
|
+
return shapeUpcomingView(data, compact, p);
|
|
668
976
|
if (kind === "today" && data !== null && typeof data === "object") {
|
|
669
|
-
return
|
|
977
|
+
return shapeTodayView(data, compact, p);
|
|
670
978
|
}
|
|
671
979
|
if (kind === "anytime" && Array.isArray(data)) {
|
|
672
980
|
return shapeSections(data, ANYTIME_SECTION_DROP, compact, p); // stage-pure → drop stage
|