things-api 0.11.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 +16 -5
- package/dist/audit/schema.d.ts +7 -0
- package/dist/audit/schema.js.map +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.d.ts +18 -11
- package/dist/cli/commands/area.js +81 -77
- package/dist/cli/commands/area.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts +1 -2
- package/dist/cli/commands/doctor.js +11 -1
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/install-skill.d.ts +19 -4
- package/dist/cli/commands/install-skill.js +130 -52
- package/dist/cli/commands/install-skill.js.map +1 -1
- package/dist/cli/commands/mcp.js +21 -1
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/project.d.ts +1 -0
- package/dist/cli/commands/project.js +92 -48
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/cli/commands/reads.d.ts +5 -1
- package/dist/cli/commands/reads.js +110 -31
- 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 +75 -36
- 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 +795 -144
- package/dist/cli/commands/writes.js.map +1 -1
- package/dist/cli/did-you-mean.d.ts +14 -3
- package/dist/cli/did-you-mean.js +44 -7
- 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/glyphs.d.ts +16 -2
- package/dist/cli/glyphs.js +60 -12
- package/dist/cli/glyphs.js.map +1 -1
- package/dist/cli/help.js +69 -13
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.js +8 -1
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/move-hint.d.ts +3 -2
- package/dist/cli/move-hint.js +2 -2
- package/dist/cli/move-hint.js.map +1 -1
- package/dist/cli/read-driver.d.ts +53 -5
- package/dist/cli/read-driver.js +82 -15
- 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 +40 -14
- package/dist/cli/render.js +128 -55
- package/dist/cli/render.js.map +1 -1
- package/dist/cli/resolve-invocation.d.ts +7 -5
- package/dist/cli/resolve-invocation.js +16 -7
- package/dist/cli/resolve-invocation.js.map +1 -1
- package/dist/cli/skill-check.d.ts +12 -6
- package/dist/cli/skill-check.js +31 -14
- package/dist/cli/skill-check.js.map +1 -1
- package/dist/cli/skill.d.ts +8 -0
- package/dist/cli/skill.js +10 -0
- package/dist/cli/skill.js.map +1 -1
- package/dist/cli/verb-hint.js +1 -1
- package/dist/cli/verb-hint.js.map +1 -1
- package/dist/client.d.ts +158 -26
- package/dist/client.js +254 -38
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +66 -2
- package/dist/config.js +120 -12
- package/dist/config.js.map +1 -1
- package/dist/contracts.d.ts +187 -28
- package/dist/contracts.js +22 -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/diagnose.d.ts +34 -2
- package/dist/diagnose.js +36 -1
- package/dist/diagnose.js.map +1 -1
- package/dist/index.d.ts +21 -8
- package/dist/index.js +26 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +9 -0
- package/dist/mcp/server.js +790 -322
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/entities.d.ts +57 -4
- package/dist/model/entities.js.map +1 -1
- package/dist/model/mappers.d.ts +2 -0
- package/dist/model/mappers.js +44 -2
- package/dist/model/mappers.js.map +1 -1
- package/dist/model/recurrence.d.ts +8 -1
- package/dist/model/recurrence.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 +76 -0
- package/dist/read/area-filter.js +58 -0
- package/dist/read/area-filter.js.map +1 -0
- package/dist/read/area-view.d.ts +22 -11
- package/dist/read/area-view.js +54 -28
- package/dist/read/area-view.js.map +1 -1
- package/dist/read/detail.js +15 -8
- package/dist/read/detail.js.map +1 -1
- package/dist/read/predicates.d.ts +18 -0
- package/dist/read/predicates.js +19 -0
- package/dist/read/predicates.js.map +1 -1
- package/dist/read/project-view.d.ts +91 -15
- package/dist/read/project-view.js +135 -37
- package/dist/read/project-view.js.map +1 -1
- package/dist/read/pseudo-area.d.ts +29 -0
- package/dist/read/pseudo-area.js +27 -0
- package/dist/read/pseudo-area.js.map +1 -0
- package/dist/read/queries.d.ts +175 -19
- package/dist/read/queries.js +376 -32
- package/dist/read/queries.js.map +1 -1
- package/dist/read/scope.d.ts +126 -0
- package/dist/read/scope.js +161 -0
- package/dist/read/scope.js.map +1 -0
- package/dist/read/search-rank.d.ts +2 -5
- package/dist/read/shape.d.ts +181 -0
- package/dist/read/shape.js +737 -0
- package/dist/read/shape.js.map +1 -0
- package/dist/read/show-target.d.ts +9 -1
- package/dist/read/show-target.js +77 -17
- package/dist/read/show-target.js.map +1 -1
- package/dist/read/stage.d.ts +199 -0
- package/dist/read/stage.js +125 -0
- package/dist/read/stage.js.map +1 -0
- package/dist/read/truncation.d.ts +7 -12
- package/dist/read/truncation.js +56 -21
- package/dist/read/truncation.js.map +1 -1
- package/dist/read/views.d.ts +68 -16
- package/dist/read/views.js +206 -52
- package/dist/read/views.js.map +1 -1
- package/dist/surface-copy.d.ts +16 -0
- package/dist/surface-copy.js +16 -0
- package/dist/surface-copy.js.map +1 -1
- package/dist/write/batch.d.ts +49 -10
- package/dist/write/batch.js +423 -71
- package/dist/write/batch.js.map +1 -1
- package/dist/write/commands.js +268 -55
- package/dist/write/commands.js.map +1 -1
- package/dist/write/guards.d.ts +1 -1
- package/dist/write/guards.js +71 -10
- package/dist/write/guards.js.map +1 -1
- package/dist/write/heading.d.ts +10 -1
- package/dist/write/heading.js +35 -5
- package/dist/write/heading.js.map +1 -1
- package/dist/write/make-repeating-project.d.ts +2 -2
- package/dist/write/make-repeating-project.js +9 -9
- package/dist/write/make-repeating-project.js.map +1 -1
- package/dist/write/move.d.ts +134 -0
- package/dist/write/move.js +1874 -0
- package/dist/write/move.js.map +1 -0
- package/dist/write/operations.d.ts +105 -19
- package/dist/write/operations.js +56 -7
- package/dist/write/operations.js.map +1 -1
- package/dist/write/opid.d.ts +31 -0
- package/dist/write/opid.js +30 -0
- package/dist/write/opid.js.map +1 -0
- package/dist/write/pipeline.d.ts +70 -3
- package/dist/write/pipeline.js +198 -33
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/pre-state.d.ts +172 -8
- package/dist/write/pre-state.js +467 -25
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/reorder.d.ts +23 -3
- package/dist/write/reorder.js +2106 -86
- package/dist/write/reorder.js.map +1 -1
- package/dist/write/reversibility.js +19 -7
- package/dist/write/reversibility.js.map +1 -1
- package/dist/write/scope-guard.d.ts +31 -0
- package/dist/write/scope-guard.js +162 -0
- package/dist/write/scope-guard.js.map +1 -0
- package/dist/write/undo.js +120 -13
- package/dist/write/undo.js.map +1 -1
- package/dist/write/vectors/applescript.js +16 -7
- package/dist/write/vectors/applescript.js.map +1 -1
- package/dist/write/vectors/shortcuts.js +1 -1
- package/dist/write/vectors/shortcuts.js.map +1 -1
- package/dist/write/vectors/simulator.js +83 -27
- package/dist/write/vectors/simulator.js.map +1 -1
- package/dist/write/vectors/types.d.ts +11 -0
- package/dist/write/vectors/ui-certification.d.ts +3 -3
- package/dist/write/vectors/ui-certification.js +41 -5
- package/dist/write/vectors/ui-certification.js.map +1 -1
- package/dist/write/vectors/ui-drag.d.ts +21 -1
- package/dist/write/vectors/ui-drag.js +138 -27
- package/dist/write/vectors/ui-drag.js.map +1 -1
- package/dist/write/vectors/ui-recipes.d.ts +21 -1
- package/dist/write/vectors/ui-recipes.js +166 -7
- package/dist/write/vectors/ui-recipes.js.map +1 -1
- package/dist/write/vectors/ui.d.ts +25 -1
- package/dist/write/vectors/ui.js +142 -33
- package/dist/write/vectors/ui.js.map +1 -1
- package/dist/write/verify/delta.d.ts +17 -1
- package/dist/write/verify/delta.js +106 -29
- package/dist/write/verify/delta.js.map +1 -1
- package/package.json +7 -1
- package/schema/envelope.schema.json +397 -0
- package/skills/things-cli/SKILL.md +36 -17
- package/skills/things-cli/references/banner.md +35 -0
- package/skills/things-cli/references/contracts.md +32 -9
- package/skills/things-cli/references/errors.md +57 -0
- package/skills/things-cli/references/gui.md +1 -1
- package/skills/things-cli/references/model.md +18 -7
- package/skills/things-cli/references/ordering.md +88 -0
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The read-payload SHAPING transform: the token-economy rules R6 and R7, the
|
|
3
|
+
* universal item-DTO reshapes (R9), and the R10 lifecycle taxonomy — applied at
|
|
4
|
+
* the JSON emit boundary of the read surfaces (the CLI `--json` read envelope,
|
|
5
|
+
* src/cli/read-driver.ts, and the MCP read tool results, src/mcp/server.ts), the
|
|
6
|
+
* same boundaries omit-empty runs at. Shaping runs BEFORE omit-empty. The
|
|
7
|
+
* human-render path keeps the full, unshaped entities, so this is JSON-only.
|
|
8
|
+
*
|
|
9
|
+
* Both R6/R7 are deterministic BY VIEW KIND / SECTION — the emitter knows whether
|
|
10
|
+
* it is inside a single-container view, a lifecycle bucket, or a mixed list —
|
|
11
|
+
* never a per-item heuristic.
|
|
12
|
+
*
|
|
13
|
+
* ## R10 — the `stage` lifecycle taxonomy (every tier, every kind incl. detail)
|
|
14
|
+
* The three former wire fields `start` / `logged` / `trashed` are DELETED from
|
|
15
|
+
* every item and replaced by ONE derived `stage` ∈ `inbox | upcoming | anytime |
|
|
16
|
+
* someday | logbook | trash` (src/read/stage.ts `deriveStage`, the single pure
|
|
17
|
+
* derivation reused by the card bucketing so `stage` can never disagree with the
|
|
18
|
+
* bucket a view puts an item in). Today/evening membership is a SEPARATE
|
|
19
|
+
* presence-keyed axis — `today: true` / `evening: true` (evening implies today) —
|
|
20
|
+
* derived in the mapper with the Today view's own two-arm predicate.
|
|
21
|
+
* - `stage` is DROPPED only where the enclosing node PROVABLY states it — the R6
|
|
22
|
+
* rule (drop only what the node provably says). That is the stage-PURE flat
|
|
23
|
+
* views (inbox, `anytime`, someday, logbook, trash) and the stage-named card
|
|
24
|
+
* sub-buckets (anytime/upcoming/someday/logbook/trash, which the bucketer splits
|
|
25
|
+
* BY stage). The `anytime` catalogue is stage-PURE (R10.2): every member is an
|
|
26
|
+
* Anytime-view row (ANYTIME_SELF) — undated-active, arrived-active, or arrived
|
|
27
|
+
* someday-scheduled — and an ARRIVED dated row derives `anytime` (Upcoming is
|
|
28
|
+
* STRICTLY FUTURE, UPC1), so the field is redundant there.
|
|
29
|
+
* `stage` is KEPT on the stage-MIXED or derived surfaces: the `upcoming` view
|
|
30
|
+
* carries deadline-forecast stage-`anytime`/`someday` rows alongside its
|
|
31
|
+
* future-dated stage-`upcoming` ones — dropping `stage` there would delete
|
|
32
|
+
* non-redundant information — plus `today` (mixes upcoming + anytime-deadline
|
|
33
|
+
* rows), search, changes, the projects/areas listings, the card NODE, and
|
|
34
|
+
* detail.
|
|
35
|
+
* - the former `todaySection` field is RETIRED from the wire entirely (R10.1):
|
|
36
|
+
* `todaySection: "evening"` merely duplicated the `evening: true` marker. It
|
|
37
|
+
* remains an internal entity field (the human render and the write-verify delta
|
|
38
|
+
* still read it); shaping deletes it from the JSON copy.
|
|
39
|
+
*
|
|
40
|
+
* ## R12 — `when`, the derived TIME-AXIS position (replaces startDate + markers)
|
|
41
|
+
* Today/evening membership and the scheduled/projected date collapse onto ONE
|
|
42
|
+
* derived, presence-keyed field `when` (src/read/stage.ts `deriveWhen`): `"today"`
|
|
43
|
+
* / `"evening"` (Today-view membership, from the SAME `today`/`evening` markers the
|
|
44
|
+
* mapper stamps — never re-derived, so `when` can never disagree with the star), a
|
|
45
|
+
* FUTURE ISO date (a strictly-future scheduled row, or a template's projected next
|
|
46
|
+
* occurrence), or absent (unscheduled + not in Today; also an unprojected template
|
|
47
|
+
* and every logged/trashed row). The doctrine line: **`stage` enumerates the
|
|
48
|
+
* sidebar BUCKETS an item lives in; `when` enumerates its TIME POSITIONS (today |
|
|
49
|
+
* evening | a future date).** Someday is deliberately NOT a `when` value (it is a
|
|
50
|
+
* bucket → stage). Tier/drop rules:
|
|
51
|
+
* - the former `today`/`evening` marker KEYS are DELETED from the wire on EVERY
|
|
52
|
+
* tier — `when` carries the fact; the markers stay internal (they feed `when`).
|
|
53
|
+
* - the raw `startDate` is DELETED in COMPACT (a list needs the position, not the
|
|
54
|
+
* substrate) and KEPT in FULL/DETAIL beside `when` — different facts: `startDate`
|
|
55
|
+
* = what is stored, `when` = where it sits.
|
|
56
|
+
* - a template's `repeating.nextOccurrence` is GONE from the wire — `when` replaces
|
|
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
|
+
*
|
|
68
|
+
* ## R13 — provisional Today members + GUI-faithful pulled-row membership
|
|
69
|
+
* BANNER1 / BANNER1b (docs/lab/banner1-research.md). Two coupled facts:
|
|
70
|
+
* - **`provisional: true`** — a presence-keyed marker on every Today member the GUI
|
|
71
|
+
* pips / counts in the "You have N new to-dos" banner: a Today member NOT yet
|
|
72
|
+
* materialized (`start != active OR startDate IS NULL`, BANNER1 L1). Derived from
|
|
73
|
+
* the SAME today/evening markers + fields the stage/`when` axes use (never
|
|
74
|
+
* re-derived). Emitted on EVERY tier, NEVER dropped (the banner is not a section,
|
|
75
|
+
* so no node implies it). Absent on non-Today rows and on materialized ones.
|
|
76
|
+
* Read-only: the app clears it by materializing the row on banner-OK, a GUI-only
|
|
77
|
+
* side effect our read cannot perform (watchers beware).
|
|
78
|
+
* - **stage `anytime` for a deadline pull** — a due-deadline pull re-files an undated
|
|
79
|
+
* Inbox/Someday row into Anytime (deriveStage step 2½, L-A). So EVERY Today member
|
|
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
|
+
*
|
|
85
|
+
* ## Universal item-DTO reshapes (R9 — EVERY tier, EVERY read kind incl. detail)
|
|
86
|
+
* - **checklist nesting** — flat counts → presence-keyed `checklist: {open,total}`.
|
|
87
|
+
* - **todos counts** — a project's flat leaf-action counts → presence-keyed
|
|
88
|
+
* `todos: {open, total}` (omit when total 0).
|
|
89
|
+
* - **repeating template/instance split (R11)** — the wire drops the
|
|
90
|
+
* `isTemplate`/`isInstance` discriminators; key presence carries the fact. A
|
|
91
|
+
* TEMPLATE keeps a nested `repeating: {paused?, deadlined?, rule?,
|
|
92
|
+
* latestInstance?}` — the series object (rule config + backward pointer +
|
|
93
|
+
* state flags); presence MEANS template. The forward pointer `nextOccurrence`
|
|
94
|
+
* moved to the top-level `when` (R12 — a template's projected date IS its time
|
|
95
|
+
* position); `latestInstance` is detail-only (SL1). An INSTANCE keeps a flat
|
|
96
|
+
* `instanceOf: <templateUuid>` and no `repeating`. A plain row keeps neither.
|
|
97
|
+
* See {@link reshapeRepeatingWire}.
|
|
98
|
+
* - **string tags** — `tags`/`inheritedTags` become plain arrays of names.
|
|
99
|
+
* - **one project key** — a headed item's owning project (formerly
|
|
100
|
+
* `headingProject`) is merged into `project`; `headingProject` never appears.
|
|
101
|
+
*
|
|
102
|
+
* ## R6 — no-redundant-ancestry (both tiers)
|
|
103
|
+
* project-view children drop `project`+`area` (heading-group members also drop
|
|
104
|
+
* `heading`); area-view children/project-cards drop `area`; anytime/someday
|
|
105
|
+
* section items drop `area`. Mixed lists keep every ref. (In the COMPACT tier the
|
|
106
|
+
* `heading` ref is additionally dropped everywhere — R7.)
|
|
107
|
+
*
|
|
108
|
+
* ## R7 — named detail tiers (compact | full)
|
|
109
|
+
* List contexts default to COMPACT; `detail`/`show` and `--full` / `full:true`
|
|
110
|
+
* use FULL. Compact drops `created`/`modified`, the full `notes` string (a
|
|
111
|
+
* presence-keyed `hasNotes: true` marks a row with notes), and the `heading` ref;
|
|
112
|
+
* `status` is omitted when `open`. FULL keeps them but still applies R6, the
|
|
113
|
+
* universal reshapes, and R10.
|
|
114
|
+
*/
|
|
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
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Fold the flat `tags` / `inheritedTags` arrays of `{title}` objects into plain
|
|
158
|
+
* arrays of tag NAMES (universal across tiers and kinds). Tag uuids were never
|
|
159
|
+
* on the wire; the title is the identity. A missing key is left missing; an
|
|
160
|
+
* empty array stays `[]` (omit-empty prunes it later).
|
|
161
|
+
*/
|
|
162
|
+
function flattenTags(o) {
|
|
163
|
+
for (const key of ["tags", "inheritedTags"]) {
|
|
164
|
+
const v = o[key];
|
|
165
|
+
if (!Array.isArray(v))
|
|
166
|
+
continue;
|
|
167
|
+
o[key] = v.map((t) => t !== null && typeof t === "object" && "title" in t ? t["title"] : t);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Reshape the flat checklist counts (and, on a `detail` read, the items array)
|
|
172
|
+
* into ONE presence-keyed `checklist` object — universal across tiers and kinds.
|
|
173
|
+
* No checklist → the `checklist` key is absent entirely; otherwise
|
|
174
|
+
* `{open, total}`, plus `items` when the source carried them (detail reads).
|
|
175
|
+
*/
|
|
176
|
+
function reshapeChecklist(o) {
|
|
177
|
+
const total = typeof o["checklistItemsCount"] === "number" ? o["checklistItemsCount"] : 0;
|
|
178
|
+
const open = typeof o["openChecklistItemsCount"] === "number" ? o["openChecklistItemsCount"] : 0;
|
|
179
|
+
const items = o["checklist"]; // ChecklistItem[] on a detail read; absent on list rows
|
|
180
|
+
delete o["checklistItemsCount"];
|
|
181
|
+
delete o["openChecklistItemsCount"];
|
|
182
|
+
delete o["checklist"];
|
|
183
|
+
const hasItems = Array.isArray(items) && items.length > 0;
|
|
184
|
+
if (total === 0 && !hasItems)
|
|
185
|
+
return; // no checklist → omit the key
|
|
186
|
+
const cl = { open, total };
|
|
187
|
+
if (Array.isArray(items))
|
|
188
|
+
cl["items"] = items;
|
|
189
|
+
o["checklist"] = cl;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Reshape a project's flat leaf-action counts into ONE presence-keyed `todos`
|
|
193
|
+
* object — universal across tiers and kinds, mirroring {@link reshapeChecklist}.
|
|
194
|
+
* The source columns are the app-maintained materialized child counts
|
|
195
|
+
* (`untrashedLeafActionsCount` / `openUntrashedLeafActionsCount`), which count
|
|
196
|
+
* to-do children only (headings and checklist items excluded by construction).
|
|
197
|
+
* No to-do children (total 0) → the `todos` key is absent entirely; otherwise
|
|
198
|
+
* `{open, total}`. A no-op on to-dos (they carry no such columns).
|
|
199
|
+
*/
|
|
200
|
+
function reshapeTodos(o) {
|
|
201
|
+
const total = typeof o["untrashedLeafActionsCount"] === "number"
|
|
202
|
+
? o["untrashedLeafActionsCount"]
|
|
203
|
+
: 0;
|
|
204
|
+
const open = typeof o["openUntrashedLeafActionsCount"] === "number"
|
|
205
|
+
? o["openUntrashedLeafActionsCount"]
|
|
206
|
+
: 0;
|
|
207
|
+
delete o["untrashedLeafActionsCount"];
|
|
208
|
+
delete o["openUntrashedLeafActionsCount"];
|
|
209
|
+
if (total === 0)
|
|
210
|
+
return; // presence-keyed: no to-do children → omit the key
|
|
211
|
+
o["todos"] = { open, total };
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* R11 — rewrite the internal `repeating` block into the wire's template/instance
|
|
215
|
+
* split, mutating `o` in place. The internal entity carries the full
|
|
216
|
+
* RepeatingInfo (`isTemplate`/`isInstance`/`templateUuid`/…); the wire loses the
|
|
217
|
+
* `isTemplate`/`isInstance` discriminators entirely and instead lets KEY
|
|
218
|
+
* PRESENCE carry the fact:
|
|
219
|
+
*
|
|
220
|
+
* - **Template** (`isTemplate`) → a nested `repeating` object — the series state
|
|
221
|
+
* `{paused?, deadlined?, rule?, latestInstance?}`. Presence of `repeating` MEANS
|
|
222
|
+
* template (an unadorned template emits `repeating: {}` — a bare `{}` is NOT
|
|
223
|
+
* pruned by omit-empty, so the presence signal survives). The inner false
|
|
224
|
+
* booleans are default-pruned (presence-keyed). The forward pointer
|
|
225
|
+
* `nextOccurrence` moved OUT to the top-level `when` (R12 — a template's
|
|
226
|
+
* projected date IS its time position); `rule` and `latestInstance` stay
|
|
227
|
+
* detail-only (populated by src/read/detail.ts on `entity.repeating`);
|
|
228
|
+
* `latestInstance` is the backward pointer symmetric to `when`.
|
|
229
|
+
* - **Instance** (`isInstance`) → a flat presence-keyed `instanceOf:
|
|
230
|
+
* <templateUuid>` and NO `repeating` object. Presence of `instanceOf` MEANS
|
|
231
|
+
* instance.
|
|
232
|
+
* - **Plain** (neither) → neither key.
|
|
233
|
+
*/
|
|
234
|
+
function reshapeRepeatingWire(o) {
|
|
235
|
+
const rep = o["repeating"];
|
|
236
|
+
delete o["repeating"];
|
|
237
|
+
if (rep === null || typeof rep !== "object")
|
|
238
|
+
return;
|
|
239
|
+
const r = rep;
|
|
240
|
+
if (r["isTemplate"] === true) {
|
|
241
|
+
const out = {}; // presence MEANS template — a bare {} survives omit-empty
|
|
242
|
+
if (r["paused"] === true)
|
|
243
|
+
out["paused"] = true;
|
|
244
|
+
if (r["deadlined"] === true)
|
|
245
|
+
out["deadlined"] = true;
|
|
246
|
+
if (r["rule"] != null)
|
|
247
|
+
out["rule"] = r["rule"]; // detail read only
|
|
248
|
+
// The SL1 "Show Latest" pick — detail-only; the backward pointer symmetric to
|
|
249
|
+
// the top-level `when` (the forward pointer, R12).
|
|
250
|
+
if (typeof r["latestInstance"] === "string")
|
|
251
|
+
out["latestInstance"] = r["latestInstance"];
|
|
252
|
+
o["repeating"] = out;
|
|
253
|
+
}
|
|
254
|
+
else if (r["isInstance"] === true) {
|
|
255
|
+
if (typeof r["templateUuid"] === "string")
|
|
256
|
+
o["instanceOf"] = r["templateUuid"];
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/** The R10 stage input read straight off a materialized task entity. */
|
|
260
|
+
function stageOf(s) {
|
|
261
|
+
const repeating = s["repeating"];
|
|
262
|
+
const isTemplate = repeating !== null &&
|
|
263
|
+
typeof repeating === "object" &&
|
|
264
|
+
repeating["isTemplate"] === true;
|
|
265
|
+
return deriveStage({
|
|
266
|
+
trashed: s["trashed"] === true,
|
|
267
|
+
logged: s["logged"] === true,
|
|
268
|
+
start: s["start"],
|
|
269
|
+
startDate: s["startDate"] ?? null,
|
|
270
|
+
repeating: { isTemplate },
|
|
271
|
+
// The presence-keyed Today marker (stamped at materialize with the response
|
|
272
|
+
// clock) discriminates an ARRIVED dated row (→ anytime) from a strictly-
|
|
273
|
+
// future one (→ upcoming). Read BEFORE the marker is stripped downstream.
|
|
274
|
+
today: s["today"] === true,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
/** The R12 `when` input read straight off a materialized task entity (given its stage). */
|
|
278
|
+
function whenOf(s, stage) {
|
|
279
|
+
const repeating = s["repeating"];
|
|
280
|
+
const isTemplate = repeating !== null &&
|
|
281
|
+
typeof repeating === "object" &&
|
|
282
|
+
repeating["isTemplate"] === true;
|
|
283
|
+
const nextOccurrence = isTemplate && typeof repeating["nextOccurrence"] === "string"
|
|
284
|
+
? repeating["nextOccurrence"]
|
|
285
|
+
: null;
|
|
286
|
+
return deriveWhen({
|
|
287
|
+
stage,
|
|
288
|
+
// The SAME presence-keyed markers stageOf reads — never re-derived, so a
|
|
289
|
+
// `when` of today/evening can never disagree with Today-view membership.
|
|
290
|
+
today: s["today"] === true,
|
|
291
|
+
evening: s["evening"] === true,
|
|
292
|
+
startDate: s["startDate"] ?? null,
|
|
293
|
+
repeating: { isTemplate, nextOccurrence },
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Shape ONE task entity (to-do or project): the universal reshapes, the R10/R12
|
|
298
|
+
* stage/`when` rewrite, then the R6 ancestry drops, then — when `compact` — the
|
|
299
|
+
* R7 default-pruning. A shallow copy is taken so unknown sibling keys
|
|
300
|
+
* (`changeKind` on a changes row, `match` on a search hit) pass through
|
|
301
|
+
* untouched. Non-task values (areas, tags, refs, headings) are returned as-is.
|
|
302
|
+
*/
|
|
303
|
+
function shapeItem(src, drop, compact, promoter) {
|
|
304
|
+
if (src === null || typeof src !== "object")
|
|
305
|
+
return src;
|
|
306
|
+
const s = src;
|
|
307
|
+
const type = s["type"];
|
|
308
|
+
if (type !== "to-do" && type !== "project")
|
|
309
|
+
return src; // not a shaped entity
|
|
310
|
+
const stage = stageOf(s); // from the ORIGINAL fields, before any reshape
|
|
311
|
+
const when = whenOf(s, stage); // R12 — derived from the same fields + markers
|
|
312
|
+
// R13 (BANNER1 law L-B): a Today member is PROVISIONAL — the GUI pips it and
|
|
313
|
+
// counts it in the "You have N new to-dos" banner — until the app MATERIALIZES
|
|
314
|
+
// it (start:=1, startDate:=today). Presence-keyed marker, derived from the SAME
|
|
315
|
+
// inputs as the stage/`when` axes (never an independent re-derivation): the row
|
|
316
|
+
// is a Today member (via the today/evening markers `whenOf` reuses — so it can
|
|
317
|
+
// never disagree with the star) AND not yet materialized (BANNER1 L1:
|
|
318
|
+
// `start != active OR startDate IS NULL`). Templates are never Today members,
|
|
319
|
+
// so they never mark. NEVER dropped by any view/section — the banner is not a
|
|
320
|
+
// section, so no enclosing node implies it. The app rewrites start/startDate
|
|
321
|
+
// when the user acknowledges the banner; that is a GUI-only side effect our
|
|
322
|
+
// read cannot clear (watchers beware — see contract.md `provisional`).
|
|
323
|
+
const provisional = whenIsProvisional(when, s["start"], s["startDate"] ?? null);
|
|
324
|
+
const o = { ...s };
|
|
325
|
+
// R9 universal reshapes (every tier, every kind incl. detail).
|
|
326
|
+
reshapeChecklist(o);
|
|
327
|
+
reshapeTodos(o);
|
|
328
|
+
flattenTags(o);
|
|
329
|
+
reshapeRepeatingWire(o);
|
|
330
|
+
if (o["project"] == null && o["headingProject"] != null)
|
|
331
|
+
o["project"] = o["headingProject"];
|
|
332
|
+
delete o["headingProject"];
|
|
333
|
+
// R10 — the three lifecycle fields are replaced by the one derived `stage`.
|
|
334
|
+
delete o["start"];
|
|
335
|
+
delete o["logged"];
|
|
336
|
+
delete o["trashed"];
|
|
337
|
+
// R10.1 — `todaySection` is retired from the wire (it duplicated `evening`);
|
|
338
|
+
// the internal entity keeps it for the render / write-verify paths.
|
|
339
|
+
delete o["todaySection"];
|
|
340
|
+
if (drop.stage !== true)
|
|
341
|
+
o["stage"] = stage;
|
|
342
|
+
// 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 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).
|
|
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"];
|
|
356
|
+
if (drop.when !== true && when !== undefined)
|
|
357
|
+
o["when"] = when;
|
|
358
|
+
// R13 — the provisional banner marker (never dropped; presence-keyed).
|
|
359
|
+
if (provisional)
|
|
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"]);
|
|
367
|
+
// R6 — drop redundant ancestry (both tiers).
|
|
368
|
+
if (drop.project === true)
|
|
369
|
+
delete o["project"];
|
|
370
|
+
if (drop.area === true)
|
|
371
|
+
delete o["area"];
|
|
372
|
+
if (drop.heading === true)
|
|
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);
|
|
398
|
+
// R12 — FULL/DETAIL keep the raw `startDate` beside `when` as the SUBSTRATE
|
|
399
|
+
// (`startDate` = what is stored, `when` = where it sits). COMPACT drops it below
|
|
400
|
+
// (the position `when` carries is what a list needs).
|
|
401
|
+
if (!compact)
|
|
402
|
+
return o;
|
|
403
|
+
// R7 compact — default-pruning (absence = the default).
|
|
404
|
+
delete o["startDate"]; // R12 — position lives in `when`; substrate is full-tier only
|
|
405
|
+
if (o["status"] === "open")
|
|
406
|
+
delete o["status"];
|
|
407
|
+
delete o["created"];
|
|
408
|
+
delete o["modified"];
|
|
409
|
+
const notes = typeof o["notes"] === "string" ? o["notes"] : "";
|
|
410
|
+
delete o["notes"];
|
|
411
|
+
if (notes !== "")
|
|
412
|
+
o["hasNotes"] = true;
|
|
413
|
+
// The heading ref is compact-dropped everywhere (the GUI shows the project,
|
|
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"];
|
|
418
|
+
return o;
|
|
419
|
+
}
|
|
420
|
+
/** Map a plain array of items with the item shaper. */
|
|
421
|
+
function shapeList(items, drop, compact, promoter) {
|
|
422
|
+
if (!Array.isArray(items))
|
|
423
|
+
return items;
|
|
424
|
+
return items.map((i) => shapeItem(i, drop, compact, promoter));
|
|
425
|
+
}
|
|
426
|
+
/** Copy `base` and overwrite `items` with the shaped list (avoids spread-in-map). */
|
|
427
|
+
function withShapedItems(base, drop, compact, promoter) {
|
|
428
|
+
const out = { ...base };
|
|
429
|
+
out["items"] = shapeList(base["items"], drop, compact, promoter);
|
|
430
|
+
return out;
|
|
431
|
+
}
|
|
432
|
+
/** Coerce an unknown value to an array (empty when absent). */
|
|
433
|
+
const asArray = (v) => (Array.isArray(v) ? v : []);
|
|
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 };
|
|
466
|
+
/** Area-view PROJECTS list: a mixed listing of the area's project rows — keep `stage`, drop area. */
|
|
467
|
+
const AREA_PROJECTS_DROP = { area: true };
|
|
468
|
+
/**
|
|
469
|
+
* Anytime sidebar-section items: stage-PURE → drop area + the section-implied
|
|
470
|
+
* stage (R10.2). Every Anytime-view member derives `anytime`: ANYTIME_SELF
|
|
471
|
+
* (undated-active, arrived-active, arrived someday-scheduled) — an ARRIVED dated
|
|
472
|
+
* row is Anytime, not Upcoming (Upcoming is STRICTLY FUTURE, UPC1) — AND, since
|
|
473
|
+
* R13, the DEADLINE-PULLED undated Inbox/Someday rows the view now includes
|
|
474
|
+
* (BANNER1b), which derive `anytime` too (deriveStage step 2½: a Today-marked
|
|
475
|
+
* undated row is a pull → anytime). Repeating templates are excluded from the
|
|
476
|
+
* view (NOT_TEMPLATE), so no stage-`upcoming` row can appear here — still pure.
|
|
477
|
+
*/
|
|
478
|
+
const ANYTIME_SECTION_DROP = { area: true, stage: true };
|
|
479
|
+
/** Someday sidebar-section items: stage-PURE → drop area + the bucket-implied stage. */
|
|
480
|
+
const SOMEDAY_SECTION_DROP = { area: true, stage: true };
|
|
481
|
+
/** The card NODE / detail / mixed lists: keep every ref, `stage`, and `when`. */
|
|
482
|
+
const NO_DROP = {};
|
|
483
|
+
/**
|
|
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.
|
|
494
|
+
*/
|
|
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;
|
|
531
|
+
};
|
|
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);
|
|
560
|
+
const out = { ...view };
|
|
561
|
+
delete out["active"];
|
|
562
|
+
delete out["scheduled"];
|
|
563
|
+
delete out["repeating"];
|
|
564
|
+
delete out["someday"];
|
|
565
|
+
delete out["logged"];
|
|
566
|
+
delete out["loggedHeadings"];
|
|
567
|
+
delete out["headingCatalog"];
|
|
568
|
+
// Trashed children live only in `things trash` — never a project-view bucket.
|
|
569
|
+
// Delete defensively in case an untyped source carries the old key.
|
|
570
|
+
delete out["trashed"];
|
|
571
|
+
// The project card NODE keeps everything (children derive their container from
|
|
572
|
+
// it), but is still an item DTO, so the universal + R10 reshapes apply.
|
|
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);
|
|
578
|
+
out["headings"] = headings;
|
|
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);
|
|
583
|
+
return out;
|
|
584
|
+
}
|
|
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) {
|
|
595
|
+
const out = { ...view };
|
|
596
|
+
delete out["active"];
|
|
597
|
+
delete out["scheduled"];
|
|
598
|
+
delete out["repeating"];
|
|
599
|
+
delete out["someday"];
|
|
600
|
+
// No `logbook` or `trash` bucket: an area's logbook is the bounded query
|
|
601
|
+
// `things logbook --area <ref>`, and trashed rows live only in `things trash`.
|
|
602
|
+
// Delete defensively in case an untyped source carries the old keys.
|
|
603
|
+
delete out["logged"];
|
|
604
|
+
delete out["trashed"];
|
|
605
|
+
out["area"] = shapeArea(view["area"]);
|
|
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);
|
|
611
|
+
return out;
|
|
612
|
+
}
|
|
613
|
+
/** Fold an area entity's tags to string names in place (returns a shallow copy). */
|
|
614
|
+
function shapeArea(src) {
|
|
615
|
+
if (src === null || typeof src !== "object")
|
|
616
|
+
return src;
|
|
617
|
+
const o = { ...src };
|
|
618
|
+
flattenTags(o);
|
|
619
|
+
return o;
|
|
620
|
+
}
|
|
621
|
+
/** Shape sidebar sections (anytime/someday catalogues) with the section's drop spec. */
|
|
622
|
+
function shapeSections(sections, drop, compact, promoter) {
|
|
623
|
+
if (!Array.isArray(sections))
|
|
624
|
+
return sections;
|
|
625
|
+
return sections.map((s) => s === null || typeof s !== "object" ? s : withShapedItems(s, drop, compact, promoter));
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* The flat, mixed-provenance list kinds mapped to their drop spec. Only the
|
|
629
|
+
* stage-PURE catalogues (inbox/someday/logbook/trash; the section-based `anytime`
|
|
630
|
+
* is pure too, handled via shapeSections below) drop the bucket-implied `stage`.
|
|
631
|
+
* `upcoming` KEEPS it (R10.2): the Upcoming view is stage-mixed — it carries
|
|
632
|
+
* deadline-forecast stage-`anytime`/`someday` rows alongside future-dated
|
|
633
|
+
* stage-`upcoming` ones. The mixed/derived surfaces (search/changes/projects)
|
|
634
|
+
* keep it too.
|
|
635
|
+
*/
|
|
636
|
+
const FLAT_LIST_DROP = new Map([
|
|
637
|
+
["inbox", { stage: true }],
|
|
638
|
+
["upcoming", NO_DROP],
|
|
639
|
+
["logbook", { stage: true }],
|
|
640
|
+
["trash", { stage: true }],
|
|
641
|
+
["changes", NO_DROP],
|
|
642
|
+
["search", NO_DROP],
|
|
643
|
+
["projects", NO_DROP],
|
|
644
|
+
]);
|
|
645
|
+
/**
|
|
646
|
+
* Apply the universal reshapes + R6 + R7 + R10 to a read payload for one view
|
|
647
|
+
* `kind`. `full` forces the FULL tier (R7 default-pruning off, everything else
|
|
648
|
+
* applied); an unrecognized kind passes through unchanged. The input is never
|
|
649
|
+
* mutated (shallow copies throughout), so the human-render path keeps the full
|
|
650
|
+
* entities.
|
|
651
|
+
*/
|
|
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;
|
|
657
|
+
// `detail` is the FULL record and drops no ancestry / stage / `when`.
|
|
658
|
+
if (kind === "detail")
|
|
659
|
+
return shapeItem(data, NO_DROP, false, p);
|
|
660
|
+
const compact = !full;
|
|
661
|
+
const flatDrop = FLAT_LIST_DROP.get(kind);
|
|
662
|
+
if (flatDrop !== undefined)
|
|
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).
|
|
668
|
+
if (kind === "today" && data !== null && typeof data === "object") {
|
|
669
|
+
return shapeList(data["items"], TODAY_ITEM_DROP, compact, p);
|
|
670
|
+
}
|
|
671
|
+
if (kind === "anytime" && Array.isArray(data)) {
|
|
672
|
+
return shapeSections(data, ANYTIME_SECTION_DROP, compact, p); // stage-pure → drop stage
|
|
673
|
+
}
|
|
674
|
+
if (kind === "someday" && Array.isArray(data)) {
|
|
675
|
+
return shapeSections(data, SOMEDAY_SECTION_DROP, compact, p); // stage-pure → drop stage
|
|
676
|
+
}
|
|
677
|
+
if (kind === "area-view" && data !== null && typeof data === "object") {
|
|
678
|
+
return shapeAreaView(data, compact, p);
|
|
679
|
+
}
|
|
680
|
+
if (kind === "project-view" && data !== null && typeof data === "object") {
|
|
681
|
+
return shapeProjectView(data, compact, p);
|
|
682
|
+
}
|
|
683
|
+
// The `areas` listing carries Area entities whose tags fold to names.
|
|
684
|
+
if (kind === "areas" && Array.isArray(data))
|
|
685
|
+
return data.map(shapeArea);
|
|
686
|
+
// tags / legend / snapshot / diagnostics: not tag-carrying entity payloads.
|
|
687
|
+
return data;
|
|
688
|
+
}
|
|
689
|
+
/** The fixed cap on a listed candidate array; the error `message` states the total when it overflows. */
|
|
690
|
+
export const CANDIDATE_CAP = 8;
|
|
691
|
+
/** Read a container hint — a Ref `{title}` or a plain title string — to its non-empty title, else null. */
|
|
692
|
+
function candidateContainerTitle(v) {
|
|
693
|
+
if (typeof v === "string")
|
|
694
|
+
return v === "" ? null : v;
|
|
695
|
+
if (v !== null && typeof v === "object") {
|
|
696
|
+
const t = v["title"];
|
|
697
|
+
if (typeof t === "string" && t !== "")
|
|
698
|
+
return t;
|
|
699
|
+
}
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Project ONE entity — a materialized to-do/project/area/heading, or a thin
|
|
704
|
+
* `{uuid, title}` resolver row — to the fixed {@link CandidateRef}. The SINGLE
|
|
705
|
+
* source every error-candidate emit flows through (the did-you-mean fallback and
|
|
706
|
+
* every not-found/ambiguous resolver), so the candidate shape can never vary by
|
|
707
|
+
* site. Reuses the wire's own {@link stageOf}/{@link whenOf} derivations — the
|
|
708
|
+
* lifecycle words are never re-derived here.
|
|
709
|
+
*/
|
|
710
|
+
export function candidateRef(type, src) {
|
|
711
|
+
const s = (src ?? {});
|
|
712
|
+
const out = {
|
|
713
|
+
uuid: typeof s["uuid"] === "string" ? s["uuid"] : "",
|
|
714
|
+
title: typeof s["title"] === "string" ? s["title"] : "",
|
|
715
|
+
};
|
|
716
|
+
// Absent `type` = to-do — emit it only for the other kinds.
|
|
717
|
+
if (type !== "to-do")
|
|
718
|
+
out.type = type;
|
|
719
|
+
const area = candidateContainerTitle(s["area"]);
|
|
720
|
+
if (area !== null)
|
|
721
|
+
out.area = area;
|
|
722
|
+
const project = candidateContainerTitle(s["project"]) ?? candidateContainerTitle(s["headingProject"]);
|
|
723
|
+
if (project !== null)
|
|
724
|
+
out.project = project;
|
|
725
|
+
// stage/when only for the task kinds, and only when the source carries the
|
|
726
|
+
// materialized lifecycle substrate (`start`) — a thin uuid+title resolver row
|
|
727
|
+
// does not, so the keys stay absent (presence-keyed, like the wire).
|
|
728
|
+
if ((type === "to-do" || type === "project") && typeof s["start"] === "string") {
|
|
729
|
+
const stage = stageOf(s);
|
|
730
|
+
out.stage = stage;
|
|
731
|
+
const when = whenOf(s, stage);
|
|
732
|
+
if (when !== undefined)
|
|
733
|
+
out.when = when;
|
|
734
|
+
}
|
|
735
|
+
return out;
|
|
736
|
+
}
|
|
737
|
+
//# sourceMappingURL=shape.js.map
|