things-api 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli/canonical-ref.d.ts +21 -0
- package/dist/cli/canonical-ref.js +23 -0
- package/dist/cli/canonical-ref.js.map +1 -0
- package/dist/cli/commands/area.js +27 -11
- package/dist/cli/commands/area.js.map +1 -1
- package/dist/cli/commands/install-skill.d.ts +2 -2
- package/dist/cli/commands/install-skill.js +29 -0
- package/dist/cli/commands/install-skill.js.map +1 -1
- package/dist/cli/commands/mcp.js +12 -1
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/project.js +73 -21
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/cli/commands/reads.d.ts +5 -1
- package/dist/cli/commands/reads.js +10 -3
- package/dist/cli/commands/reads.js.map +1 -1
- package/dist/cli/commands/setup.js +11 -4
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/show.js +45 -30
- package/dist/cli/commands/show.js.map +1 -1
- package/dist/cli/commands/todo.js +1 -1
- package/dist/cli/commands/todo.js.map +1 -1
- package/dist/cli/commands/writes.js +31 -6
- package/dist/cli/commands/writes.js.map +1 -1
- package/dist/cli/did-you-mean.js +19 -5
- package/dist/cli/did-you-mean.js.map +1 -1
- package/dist/cli/dry-run.d.ts +55 -0
- package/dist/cli/dry-run.js +92 -0
- package/dist/cli/dry-run.js.map +1 -0
- package/dist/cli/help.js +7 -6
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.js +7 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/read-driver.d.ts +16 -6
- package/dist/cli/read-driver.js +19 -18
- package/dist/cli/read-driver.js.map +1 -1
- package/dist/cli/ref-render.d.ts +14 -0
- package/dist/cli/ref-render.js +9 -0
- package/dist/cli/ref-render.js.map +1 -0
- package/dist/cli/render.d.ts +21 -6
- package/dist/cli/render.js +73 -34
- package/dist/cli/render.js.map +1 -1
- package/dist/cli/resolve-invocation.d.ts +5 -3
- package/dist/cli/resolve-invocation.js +13 -4
- package/dist/cli/resolve-invocation.js.map +1 -1
- package/dist/client.d.ts +18 -7
- package/dist/client.js +2 -1
- package/dist/client.js.map +1 -1
- package/dist/contracts.d.ts +12 -1
- package/dist/contracts.js +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/db/baselines/db-v26.js +4 -1
- package/dist/db/baselines/db-v26.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +81 -42
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/entities.d.ts +11 -1
- package/dist/model/entities.js.map +1 -1
- package/dist/model/mappers.js +3 -0
- package/dist/model/mappers.js.map +1 -1
- package/dist/model/serialize.js +15 -3
- package/dist/model/serialize.js.map +1 -1
- package/dist/read/area-filter.d.ts +2 -2
- package/dist/read/area-filter.js +5 -6
- package/dist/read/area-filter.js.map +1 -1
- package/dist/read/area-view.d.ts +11 -0
- package/dist/read/area-view.js +5 -1
- package/dist/read/area-view.js.map +1 -1
- package/dist/read/project-view.d.ts +61 -1
- package/dist/read/project-view.js +74 -9
- package/dist/read/project-view.js.map +1 -1
- package/dist/read/queries.d.ts +71 -2
- package/dist/read/queries.js +199 -10
- package/dist/read/queries.js.map +1 -1
- package/dist/read/scope.d.ts +2 -2
- package/dist/read/scope.js +5 -6
- package/dist/read/scope.js.map +1 -1
- package/dist/read/shape.d.ts +36 -17
- package/dist/read/shape.js +251 -200
- package/dist/read/shape.js.map +1 -1
- package/dist/read/show-target.js +52 -33
- package/dist/read/show-target.js.map +1 -1
- package/dist/read/truncation.d.ts +0 -7
- package/dist/read/truncation.js +24 -14
- package/dist/read/truncation.js.map +1 -1
- package/dist/read/views.d.ts +19 -7
- package/dist/read/views.js +13 -12
- package/dist/read/views.js.map +1 -1
- package/dist/surface-copy.d.ts +7 -0
- package/dist/surface-copy.js +7 -0
- package/dist/surface-copy.js.map +1 -1
- package/dist/write/commands.js +3 -1
- package/dist/write/commands.js.map +1 -1
- package/dist/write/move.d.ts +10 -6
- package/dist/write/move.js +417 -76
- package/dist/write/move.js.map +1 -1
- package/dist/write/operations.d.ts +1 -1
- package/dist/write/operations.js.map +1 -1
- package/dist/write/pre-state.d.ts +10 -0
- package/dist/write/pre-state.js +118 -41
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/reorder.js +263 -29
- package/dist/write/reorder.js.map +1 -1
- package/package.json +1 -1
- package/schema/envelope.schema.json +14 -0
- package/skills/things-cli/SKILL.md +6 -4
- package/skills/things-cli/references/contracts.md +3 -1
- package/skills/things-cli/references/errors.md +9 -1
- package/skills/things-cli/references/model.md +6 -4
- package/skills/things-cli/references/ordering.md +21 -4
package/dist/write/move.js
CHANGED
|
@@ -8,13 +8,15 @@ import { runMutation } from "./pipeline.js";
|
|
|
8
8
|
import { runReorder } from "./reorder.js";
|
|
9
9
|
function loadRow(db, uuid) {
|
|
10
10
|
const row = db
|
|
11
|
-
.prepare("SELECT uuid, title, type, project, area, heading, start, startDate, startBucket, " +
|
|
12
|
-
"rt1_recurrenceRule AS rule, repeater
|
|
11
|
+
.prepare("SELECT uuid, title, type, project, area, heading, start, startDate, startBucket, deadline, " +
|
|
12
|
+
"rt1_recurrenceRule AS rule, repeater, rt1_nextInstanceStartDate AS projectionDay " +
|
|
13
|
+
"FROM TMTask WHERE uuid = ?")
|
|
13
14
|
.get(uuid);
|
|
14
15
|
if (row === undefined)
|
|
15
16
|
return undefined;
|
|
16
|
-
const { rule, repeater, ...rest } = row;
|
|
17
|
-
|
|
17
|
+
const { rule, repeater, projectionDay, ...rest } = row;
|
|
18
|
+
const isTemplate = rule !== null || repeater !== null;
|
|
19
|
+
return { ...rest, isTemplate, templateProjectionDay: isTemplate ? projectionDay : null };
|
|
18
20
|
}
|
|
19
21
|
const KIND_LABEL = { 0: "to-do", 1: "project", 2: "heading" };
|
|
20
22
|
/**
|
|
@@ -58,6 +60,33 @@ function scheduleBucket(row, packedToday) {
|
|
|
58
60
|
return "someday";
|
|
59
61
|
return "anytime";
|
|
60
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* The packed future-deadline day of a DEADLINE-FORECAST row (DLBNC / #383, #385),
|
|
65
|
+
* or null. The §9o forecast cohort: a to-do (type=0) OR project (type=1) with NO
|
|
66
|
+
* `startDate`, someday/anytime stage (`start IN (1,2)`), and a strictly-FUTURE
|
|
67
|
+
* `deadline` rests on that deadline day's ROOT Upcoming day-block on the shared
|
|
68
|
+
* `todayIndex` axis (UPCDL-1a/§9o, GUI-confirmed DLBNC-1d; forecast PROJECTS carry
|
|
69
|
+
* the axis identically — PROJDL-2a/2b/2c, #385). It reorders there via the deadline-
|
|
70
|
+
* cycle (URL `deadline=` clear + re-set — `update` for a to-do, `update-project`
|
|
71
|
+
* for a project), NOT the someday/anytime `index` lever — so the planner routes it
|
|
72
|
+
* to the `day` scope keyed on the deadline. INBOX-stage rows (`start=0`) rest OFF
|
|
73
|
+
* the axis (todayIndex=0) — excluded here; a today/past deadline is NOT a future
|
|
74
|
+
* day-block (the row renders in its someday/anytime bucket with an overdue badge) —
|
|
75
|
+
* excluded, matching the strictly-future gate scheduled rows use. Headings (type=2)
|
|
76
|
+
* are excluded. PROJSTAR-safe: the project deadline-cycle never flips `start` to 1
|
|
77
|
+
* (no accidental Today star) and preserves `index`/area-FK/tags (PROJDL-2b/2b').
|
|
78
|
+
*/
|
|
79
|
+
function forecastDeadlineDay(row, packedToday) {
|
|
80
|
+
if (row.type !== 0 && row.type !== 1)
|
|
81
|
+
return null;
|
|
82
|
+
if (row.startDate !== null)
|
|
83
|
+
return null;
|
|
84
|
+
if (row.start !== 1 && row.start !== 2)
|
|
85
|
+
return null;
|
|
86
|
+
if (row.deadline === null || row.deadline <= packedToday)
|
|
87
|
+
return null;
|
|
88
|
+
return row.deadline;
|
|
89
|
+
}
|
|
61
90
|
/** An app-default target for a bounce-dependent placement while bounce is off. */
|
|
62
91
|
function bounceDisabledTarget(what) {
|
|
63
92
|
return {
|
|
@@ -90,7 +119,24 @@ function bounceDisabledTarget(what) {
|
|
|
90
119
|
*/
|
|
91
120
|
function reorderTargetOf(row, isTodo, packedToday, bounceEnabled) {
|
|
92
121
|
if (row.isTemplate) {
|
|
93
|
-
|
|
122
|
+
// A repeating template's Upcoming-day-block projection is a first-class todayIndex
|
|
123
|
+
// member of its projection day (TMPLSORT/PTMPL): route it to the day-group scope so
|
|
124
|
+
// it rides the template-aware leg family in reorder.ts — the native one-call `list
|
|
125
|
+
// "Tomorrow"` wire when the projection == tomorrow (both to-do AND project
|
|
126
|
+
// templates: TMPLSORT-3c-Tomorrow / PTMPL-B5), else the `day` scope (a TO-DO
|
|
127
|
+
// template front-inserts via a single-id `list "Upcoming"` leg; a PROJECT template
|
|
128
|
+
// is byte-untouched under the suffix rule). A template with no strictly-future
|
|
129
|
+
// projection has no wired day-block surface — app-default, honest.
|
|
130
|
+
const day = rowDayKey(row, packedToday);
|
|
131
|
+
if (day === null) {
|
|
132
|
+
return {
|
|
133
|
+
scope: null,
|
|
134
|
+
reason: "a repeating template with no strictly-future projection (§9e)",
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return day === packedTomorrowOf(packedToday)
|
|
138
|
+
? { scope: "tomorrow", day }
|
|
139
|
+
: { scope: "day", day };
|
|
94
140
|
}
|
|
95
141
|
const bucket = scheduleBucket(row, packedToday);
|
|
96
142
|
// A same-day (today-proper) or future scheduled day, startBucket=0 — the
|
|
@@ -114,6 +160,17 @@ function reorderTargetOf(row, isTodo, packedToday, bounceEnabled) {
|
|
|
114
160
|
// so they take the dated bounce even on tomorrow.
|
|
115
161
|
const isTomorrow = row.startBucket === 0 && row.startDate === packedTomorrowOf(packedToday);
|
|
116
162
|
if (isTodo) {
|
|
163
|
+
// DEADLINE-FORECAST members route FIRST (DLBNC / #383). A someday/anytime-stage
|
|
164
|
+
// to-do (no startDate) with a future deadline is a first-class member of that
|
|
165
|
+
// deadline day's Upcoming block — it reorders on the block's todayIndex axis via
|
|
166
|
+
// the deadline-cycle (URL deadline= clear + re-set), never the someday/anytime
|
|
167
|
+
// index lever. ALWAYS the `day` scope keyed on the deadline — NEVER the native
|
|
168
|
+
// `list "Tomorrow"` sort even when the deadline is tomorrow, because that surface
|
|
169
|
+
// RE-DATES a forecast row (stamps a startDate, UPCDL-5), ejecting it from the
|
|
170
|
+
// forecast cohort. The deadline-cycle is public-URL-only (no experimental gate).
|
|
171
|
+
const fDay = forecastDeadlineDay(row, packedToday);
|
|
172
|
+
if (fDay !== null)
|
|
173
|
+
return { scope: "day", day: fDay };
|
|
117
174
|
// ARRIVED Today-view members route DATE-FIRST, before any container branch.
|
|
118
175
|
// A dated row whose day has landed (startDate <= today) is a Today member —
|
|
119
176
|
// the GUI renders arrived/today-dated rows in the TODAY view; the Upcoming
|
|
@@ -262,6 +319,20 @@ function indexAxisTargetOf(row, bounceEnabled) {
|
|
|
262
319
|
// shared loose index bucket and spuriously "dual-axis ambiguous".
|
|
263
320
|
return reorderTargetOf(asAnytime, row.type === 0, 0, bounceEnabled);
|
|
264
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* The INDEX-axis reorder target of a DEADLINE-FORECAST row (§9o dual-citizen) —
|
|
324
|
+
* its container's someday/anytime `index` order, the alternative to its Upcoming
|
|
325
|
+
* day-block todayIndex axis. Classified by STRIPPING the deadline so
|
|
326
|
+
* reorderTargetOf routes to the container index (someday/anytime/project/area/
|
|
327
|
+
* heading-someday), never the `day` scope the forecast day-block routes to. Used
|
|
328
|
+
* both to name the container spelling in the dual-axis refusal and to classify a
|
|
329
|
+
* forecast row that an explicit `--in <container>` / `--in someday|anytime` forces
|
|
330
|
+
* onto its index axis (the bug-fix: `--in` must never be overridden by the day
|
|
331
|
+
* auto-route).
|
|
332
|
+
*/
|
|
333
|
+
function forecastIndexTargetOf(row, packedToday, bounceEnabled) {
|
|
334
|
+
return reorderTargetOf({ ...row, deadline: null }, row.type === 0, packedToday, bounceEnabled);
|
|
335
|
+
}
|
|
265
336
|
/**
|
|
266
337
|
* True when a row's INDEX-axis target preserves the Today/Evening flag. TWO
|
|
267
338
|
* flag-safe families:
|
|
@@ -311,11 +382,38 @@ function describeSetLocation(rows, packedToday) {
|
|
|
311
382
|
const buckets = [...new Set(rows.map((r) => scheduleBucket(r, packedToday)))];
|
|
312
383
|
return `in the ${buckets.join(" / ")} bucket`;
|
|
313
384
|
}
|
|
314
|
-
/**
|
|
385
|
+
/** The DIRECT container a row sits in, named for a refusal (its title, or "the loose list"). */
|
|
386
|
+
function describeDirectContainer(deps, row) {
|
|
387
|
+
const c = indexAxisContainerOf(row);
|
|
388
|
+
return c === null ? "the loose list" : `"${containerLabel(deps, c)}"`;
|
|
389
|
+
}
|
|
390
|
+
/** Per-movee day membership (`uuid on YYYY-MM-DD`, or off-day), for the `--in upcoming` spread refusal. */
|
|
391
|
+
function describeDayMembership(rows, packedToday) {
|
|
392
|
+
return rows
|
|
393
|
+
.map((r) => {
|
|
394
|
+
const k = rowDayKey(r, packedToday);
|
|
395
|
+
return k === null ? `${r.uuid} (not on a future day)` : `${r.uuid} on ${decodePackedDate(k)}`;
|
|
396
|
+
})
|
|
397
|
+
.join("; ");
|
|
398
|
+
}
|
|
399
|
+
/** Parse a raw `--in <target>` into a list axis, a day-axis token, or a container ref. */
|
|
315
400
|
function parseInTarget(deps, raw, rows) {
|
|
316
401
|
const norm = raw.trim().toLowerCase();
|
|
317
402
|
if (IN_AXES.includes(norm))
|
|
318
403
|
return { axis: norm };
|
|
404
|
+
// Day-axis tokens (checked before container resolution so an ISO date is never
|
|
405
|
+
// read as a container title). `upcoming` is the shared-future-day proxy; a
|
|
406
|
+
// YYYY-MM-DD names one exact Upcoming day-block.
|
|
407
|
+
if (norm === "upcoming")
|
|
408
|
+
return { upcoming: true };
|
|
409
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(norm)) {
|
|
410
|
+
try {
|
|
411
|
+
return { day: encodePackedDate(norm) };
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
return { error: `--in "${raw}" is not a valid calendar date (expected YYYY-MM-DD)` };
|
|
415
|
+
}
|
|
416
|
+
}
|
|
319
417
|
if (norm === "loose") {
|
|
320
418
|
return {
|
|
321
419
|
error: '`--in loose` is not valid — "loose" is a read view, not a reorder bucket; ' +
|
|
@@ -345,7 +443,7 @@ function parseInTarget(deps, raw, rows) {
|
|
|
345
443
|
}
|
|
346
444
|
return {
|
|
347
445
|
error: `--in "${raw}" did not resolve to a project, area, or heading ` +
|
|
348
|
-
"(or one of today | evening | anytime | someday | inbox)",
|
|
446
|
+
"(or one of today | evening | anytime | someday | inbox | upcoming | a YYYY-MM-DD day)",
|
|
349
447
|
};
|
|
350
448
|
}
|
|
351
449
|
/**
|
|
@@ -375,20 +473,76 @@ function resolveReorderAxis(deps, op, rows, inTarget, packedToday, verb, positio
|
|
|
375
473
|
// single-bucket guard; view=null means "not a clean view set".)
|
|
376
474
|
const views = new Set(coherence.map((r) => viewOf(r, packedToday)));
|
|
377
475
|
const view = views.size === 1 ? [...views][0] : null;
|
|
476
|
+
// An INDEX bucket sorts each object KIND in its OWN rank space (an area's someday
|
|
477
|
+
// to-dos and someday projects are DIFFERENT index buckets — spec axis-isolation),
|
|
478
|
+
// so a MIXED to-do+project movee set has NO shared container index: only its
|
|
479
|
+
// GLOBAL axis (the Today/Evening view or the Upcoming day-block, both of which
|
|
480
|
+
// intermix kinds) is coherent. Nulling the index target for a cross-kind set keeps
|
|
481
|
+
// the dual-axis refusals (view + forecast) and the mixed auto-route from treating
|
|
482
|
+
// it as container-sortable; it falls through to its global axis instead. (A wrong-
|
|
483
|
+
// kind movee that ISN'T bound for a global axis was already refused upstream in
|
|
484
|
+
// runInPlaceReorder, so the only cross-kind sets that reach here are global ones.)
|
|
485
|
+
const sameKind = new Set(rows.map((r) => r.type)).size === 1;
|
|
378
486
|
// The shared index-axis container target, if the whole coherence set has one
|
|
379
487
|
// (classifying each row as though it sat in its container's anytime bucket).
|
|
380
488
|
const indexTargets = coherence.map((r) => indexAxisTargetOf(r, bounceEnabled));
|
|
381
489
|
const indexKeys = new Set(indexTargets.map(containerKey));
|
|
382
|
-
const indexTarget = indexKeys.size === 1 && indexTargets[0]?.scope != null
|
|
490
|
+
const indexTarget = sameKind && indexKeys.size === 1 && indexTargets[0]?.scope != null
|
|
383
491
|
? indexTargets[0]
|
|
384
492
|
: null;
|
|
385
|
-
// Force the CONTAINER index axis for
|
|
386
|
-
// their natural target (used when --in names a container).
|
|
387
|
-
|
|
493
|
+
// Force the CONTAINER index axis for a dual-axis row, leaving single-axis rows on
|
|
494
|
+
// their natural target (used when --in names a container or a loose stage list).
|
|
495
|
+
// A Today/Evening member forces onto its container index (view axis stripped); a
|
|
496
|
+
// DEADLINE-FORECAST row forces onto its someday/anytime container index (day-block
|
|
497
|
+
// axis stripped, §9o) — without this a forecast row would classify to the `day`
|
|
498
|
+
// scope and the explicit `--in` would be overridden by the day route.
|
|
499
|
+
const indexClassifier = (r) => {
|
|
500
|
+
if (viewOf(r, packedToday) !== null)
|
|
501
|
+
return indexAxisTargetOf(r, bounceEnabled);
|
|
502
|
+
if (forecastDeadlineDay(r, packedToday) !== null)
|
|
503
|
+
return forecastIndexTargetOf(r, packedToday, bounceEnabled);
|
|
504
|
+
return base(r);
|
|
505
|
+
};
|
|
388
506
|
if (inTarget !== undefined) {
|
|
389
507
|
const parsed = parseInTarget(deps, inTarget, rows);
|
|
390
508
|
if ("error" in parsed)
|
|
391
509
|
return { refused: refused(op, "usage", parsed.error) };
|
|
510
|
+
// `--in <YYYY-MM-DD>` — one exact Upcoming day-block (the day axis). The date
|
|
511
|
+
// must be strictly future, and every movee must be a member of that day
|
|
512
|
+
// (scheduled startDate == date, or a forecast deadline == date per §9o). No
|
|
513
|
+
// shared-container requirement — a day-block is ONE cross-container axis.
|
|
514
|
+
if ("day" in parsed) {
|
|
515
|
+
const day = parsed.day;
|
|
516
|
+
if (day <= packedToday) {
|
|
517
|
+
return {
|
|
518
|
+
refused: refused(op, "usage", `--in ${decodePackedDate(day)} is not a future day — that date is today or in the past; ` +
|
|
519
|
+
"use --in today to reorder the Today view", "name a strictly-future YYYY-MM-DD day, or --in today for the Today view"),
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
const notMembers = rows.filter((r) => rowDayKey(r, packedToday) !== day);
|
|
523
|
+
if (notMembers.length > 0) {
|
|
524
|
+
return {
|
|
525
|
+
refused: refused(op, "usage", `--in ${decodePackedDate(day)} but these items are not on that day: ` +
|
|
526
|
+
notMembers.map((r) => r.uuid).join(", ") +
|
|
527
|
+
" — a movee must be scheduled for it, or carry it as a deadline", "name the day the items actually share, or omit --in if they share one"),
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
return { dayAxis: day };
|
|
531
|
+
}
|
|
532
|
+
// `--in upcoming` — the single future day the whole set shares (the day axis),
|
|
533
|
+
// derived via sharedFutureDay. Stage-agnostic across scheduled + forecast, no
|
|
534
|
+
// container requirement; a set spanning days (or with an off-day member) refuses
|
|
535
|
+
// with the per-item days listed.
|
|
536
|
+
if ("upcoming" in parsed) {
|
|
537
|
+
const day = sharedFutureDay(rows, packedToday);
|
|
538
|
+
if (day === null) {
|
|
539
|
+
return {
|
|
540
|
+
refused: refused(op, "blocked", "--in upcoming needs every item on ONE shared future day, but they are not: " +
|
|
541
|
+
describeDayMembership(rows, packedToday), "name the exact day with --in YYYY-MM-DD, or reorder one day at a time"),
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
return { dayAxis: day };
|
|
545
|
+
}
|
|
392
546
|
if ("axis" in parsed) {
|
|
393
547
|
const axis = parsed.axis;
|
|
394
548
|
if (axis === "today" || axis === "evening") {
|
|
@@ -421,19 +575,34 @@ function resolveReorderAxis(deps, op, rows, inTarget, packedToday, verb, positio
|
|
|
421
575
|
: ""), `reorder them in the view with --in ${view}, or reschedule them off Today first`),
|
|
422
576
|
};
|
|
423
577
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
578
|
+
// Sanity + membership (spec rule 4). Every movee must BE that stage; and for
|
|
579
|
+
// anytime/someday every movee must share ONE direct container (same project,
|
|
580
|
+
// heading, area, or all loose — the index bucket they re-rank in). inbox needs
|
|
581
|
+
// only the stage check (inbox items are container-less by nature).
|
|
582
|
+
const wrongStage = rows.filter((r) => scheduleBucket(r, packedToday) !== axis);
|
|
583
|
+
if (wrongStage.length > 0) {
|
|
429
584
|
return {
|
|
430
|
-
refused: refused(op, "usage", `--in ${axis} but
|
|
431
|
-
(
|
|
432
|
-
|
|
433
|
-
: ""), "omit --in and reorder the items where they are, or move them first"),
|
|
585
|
+
refused: refused(op, "usage", `--in ${axis} but not every item is ${axis}-stage — ` +
|
|
586
|
+
`${describeSetLocation(wrongStage, packedToday)}: ` +
|
|
587
|
+
wrongStage.map((r) => r.uuid).join(", "), "omit --in and reorder the items where they are, or reschedule them first"),
|
|
434
588
|
};
|
|
435
589
|
}
|
|
436
|
-
|
|
590
|
+
if (axis !== "inbox") {
|
|
591
|
+
const containers = new Set(rows.map((r) => structuralKey(r, packedToday)));
|
|
592
|
+
if (containers.size > 1) {
|
|
593
|
+
return {
|
|
594
|
+
refused: refused(op, "usage", `--in ${axis} but the items are in different containers (` +
|
|
595
|
+
rows.map((r) => `${r.uuid} in ${describeDirectContainer(deps, r)}`).join("; ") +
|
|
596
|
+
`) — a single --in ${axis} reorder needs them all in one project, heading, or ` +
|
|
597
|
+
"area, or all loose", "reorder within one container at a time, or move them together first"),
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
// The stage list axis is an INDEX axis — it suppresses the day auto-route so a
|
|
602
|
+
// forecast someday/anytime row re-ranks its container index here (the bug-fix:
|
|
603
|
+
// `--in someday` on a same-day forecast set compiles the index re-rank, not the
|
|
604
|
+
// deadline day bounce); indexClassifier strips the deadline for that.
|
|
605
|
+
return { targetOf: indexClassifier, indexAxis: true };
|
|
437
606
|
}
|
|
438
607
|
// A container ref (project/area/heading).
|
|
439
608
|
const container = parsed.container;
|
|
@@ -472,6 +641,32 @@ function resolveReorderAxis(deps, op, rows, inTarget, packedToday, verb, positio
|
|
|
472
641
|
"(todayIndex slots) and the container (index slots); say which with --in", `--in ${view} to reorder the ${view} view, or ${inSpellingFor(deps, indexTarget)} to reorder within the container`),
|
|
473
642
|
};
|
|
474
643
|
}
|
|
644
|
+
// A FORECAST set coherent on BOTH axes (§9o dual-citizen): every movee a deadline-
|
|
645
|
+
// forecast member of ONE shared day AND all sharing one container index bucket.
|
|
646
|
+
// Ambiguous between the day-block (todayIndex) and the container (index) — refuse,
|
|
647
|
+
// echoing the REAL date and the container/list spelling. Forecast rows are never
|
|
648
|
+
// Today/Evening members, so this is the forecast twin of the view-member refusal
|
|
649
|
+
// above. (A forecast set spanning containers, or mixed with scheduled rows, is
|
|
650
|
+
// coherent on only the day axis → it auto-routes to `day` below, no refusal.)
|
|
651
|
+
const forecastDay = sharedForecastDay(rows, packedToday);
|
|
652
|
+
if (rows.length >= 2 && view === null && forecastDay !== null) {
|
|
653
|
+
const fTargets = rows.map((r) => forecastIndexTargetOf(r, packedToday, bounceEnabled));
|
|
654
|
+
const fKeys = new Set(fTargets.map(containerKey));
|
|
655
|
+
// A cross-kind forecast set has no shared container index (kinds isolate), so it
|
|
656
|
+
// is coherent on ONLY the day-block axis — not dual-axis. Auto-route it there
|
|
657
|
+
// (below) rather than offering a container spelling that would then kind-refuse.
|
|
658
|
+
const fIndexTarget = sameKind && fKeys.size === 1 && fTargets[0]?.scope != null
|
|
659
|
+
? fTargets[0]
|
|
660
|
+
: null;
|
|
661
|
+
if (fIndexTarget !== null) {
|
|
662
|
+
const date = decodePackedDate(forecastDay);
|
|
663
|
+
return {
|
|
664
|
+
refused: refused(op, "blocked", `these items are forecast members of the ${date} Upcoming day-block that also share ` +
|
|
665
|
+
`${describeScope(fIndexTarget)} — the reorder is ambiguous between the day-block ` +
|
|
666
|
+
"(todayIndex slots) and the container (index slots); say which with --in", `--in ${date} to reorder the day-block, or ${inSpellingFor(deps, fIndexTarget)} to reorder within the container`),
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
}
|
|
475
670
|
// A MIXED dual-axis set (some Today/Evening-flagged, some plain, all sharing one
|
|
476
671
|
// flag-safe container index) is NOT ambiguous: the plain members are not view
|
|
477
672
|
// members, so there is no shared view to reorder them in — the container index is
|
|
@@ -529,6 +724,37 @@ function refused(op, refusal, detail, remediation) {
|
|
|
529
724
|
...(remediation !== undefined && { remediation }),
|
|
530
725
|
};
|
|
531
726
|
}
|
|
727
|
+
/**
|
|
728
|
+
* Map a non-ok reorder PLACEMENT from a pure in-place reposition (no membership
|
|
729
|
+
* leg ran, so NOTHING landed) to a terminal MoveResult. A `blocked` placement is
|
|
730
|
+
* a genuine refusal — the reorder never touched the app (an H-REORDER-SCOPE
|
|
731
|
+
* template suffix / experimental-off block) — so it HOISTS to a canonical
|
|
732
|
+
* top-level `move-refused` carrying the hazard: exit 4 (Blocked) + the
|
|
733
|
+
* `blocked:<hazard>` code + `BLOCKED (<hazard>)` copy, the SAME surface a direct
|
|
734
|
+
* `things reorder` block gets, instead of being buried under a generic
|
|
735
|
+
* leg-failed with a verify-failed (exit 3) code. Every other non-ok kind (a
|
|
736
|
+
* mid-bounce `bounce-aborted`, a `verify-failed`) is a leg that DID run and did
|
|
737
|
+
* not complete, so it stays `move-leg-failed`.
|
|
738
|
+
*/
|
|
739
|
+
function repositionFailed(op, placement) {
|
|
740
|
+
if (placement.kind === "blocked") {
|
|
741
|
+
return {
|
|
742
|
+
kind: "move-refused",
|
|
743
|
+
op,
|
|
744
|
+
refusal: "blocked",
|
|
745
|
+
detail: placement.detail,
|
|
746
|
+
...(placement.remediation !== undefined && { remediation: placement.remediation }),
|
|
747
|
+
...(placement.hazard !== undefined && { hazard: placement.hazard }),
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
return {
|
|
751
|
+
kind: "move-leg-failed",
|
|
752
|
+
op,
|
|
753
|
+
detail: `the reorder leg did not complete (${placement.kind})`,
|
|
754
|
+
failed: placement,
|
|
755
|
+
completed: [],
|
|
756
|
+
};
|
|
757
|
+
}
|
|
532
758
|
// The ratified teaching errors (spec §4 bare-invocation block, §5, §7).
|
|
533
759
|
const BARE_TODO_MOVE = "`todo move` needs a destination or a position. To change what a to-do belongs to, name " +
|
|
534
760
|
"one of --to-project / --to-heading / --to-area (or --no-heading / --loose to detach). To " +
|
|
@@ -905,6 +1131,36 @@ export async function runProjectMove(deps, request, options = {}) {
|
|
|
905
1131
|
* a contiguous block at the EARLIEST movee's current slot, in argument order
|
|
906
1132
|
* (spec §4 — `--first` is NOT implied). Cross-container operands fail closed.
|
|
907
1133
|
*/
|
|
1134
|
+
/**
|
|
1135
|
+
* The single-KIND refusal for an INDEX-axis reorder (a stage list, or a project/
|
|
1136
|
+
* area/heading container `index`). An index bucket sorts each object KIND in its
|
|
1137
|
+
* OWN rank space, so a to-do+project mix cannot re-rank in one call — even when the
|
|
1138
|
+
* kinds share a container. A HEADING is never an index/day/view member at all: its
|
|
1139
|
+
* order is the project's heading axis. The GLOBAL day/Today/Evening axes intermix
|
|
1140
|
+
* kinds and never reach here. Copy is tailored per case, naming each movee's kind.
|
|
1141
|
+
*/
|
|
1142
|
+
function indexKindRefusal(op, rows) {
|
|
1143
|
+
const headings = rows.filter((r) => r.type === 2);
|
|
1144
|
+
if (headings.length > 0) {
|
|
1145
|
+
return refused(op, "usage", "a heading has no stage, schedule, or day order of its own — it is never a Today/Evening, " +
|
|
1146
|
+
"day-block, or stage-list member; a heading's order is the project's heading axis, not a " +
|
|
1147
|
+
`to-do/project index bucket: ${headings.map((r) => r.uuid).join(", ")}`, "reorder a project's headings with `things project move-heading <project> <headings…>`");
|
|
1148
|
+
}
|
|
1149
|
+
const want = op === "todo.move" ? 0 : 1;
|
|
1150
|
+
const named = rows.map((r) => `${r.uuid} (${KIND_LABEL[r.type] ?? "item"})`).join(", ");
|
|
1151
|
+
// A homogeneous set of the OTHER kind (e.g. all projects handed to `todo reorder`):
|
|
1152
|
+
// point at that kind's own reorder verb rather than the mixed-axis message.
|
|
1153
|
+
if (rows.every((r) => r.type !== want)) {
|
|
1154
|
+
return refused(op, "usage", `\`reorder\` on this path rearranges ${want === 0 ? "to-dos" : "projects"}, but every item ` +
|
|
1155
|
+
`is a ${want === 0 ? "project" : "to-do"}: ${named}`, want === 0
|
|
1156
|
+
? "to rearrange projects use `things project move <refs…> --first/--last/--before/--after`"
|
|
1157
|
+
: "to rearrange to-dos use `things todo reorder <refs…>`");
|
|
1158
|
+
}
|
|
1159
|
+
// A genuine to-do + project mix bound for an index bucket.
|
|
1160
|
+
return refused(op, "usage", "one kind at a time — an index bucket sorts to-dos and projects in separate order-spaces (a " +
|
|
1161
|
+
`shared container does not merge them), so a mixed set cannot re-rank in one call: ${named}`, "reorder the to-dos and the projects in separate calls; only the Today/Evening and day-block " +
|
|
1162
|
+
"axes (--in today | evening | upcoming | a YYYY-MM-DD day) sort both kinds together");
|
|
1163
|
+
}
|
|
908
1164
|
export async function runInPlaceReorder(deps, op, request, options = {}) {
|
|
909
1165
|
const now = deps.now?.() ?? new Date();
|
|
910
1166
|
const packedToday = encodePackedDate(localToday(now));
|
|
@@ -933,61 +1189,137 @@ export async function runInPlaceReorder(deps, op, request, options = {}) {
|
|
|
933
1189
|
wrongKind.push({ ref: r.uuid, kind: KIND_LABEL[row.type] ?? "item" });
|
|
934
1190
|
rows.push(row);
|
|
935
1191
|
}
|
|
936
|
-
//
|
|
937
|
-
// reorder: `today` (O12 — `list "Today"` takes projects
|
|
938
|
-
// EVEORD — projects share the evening axis, per-type
|
|
939
|
-
// day-group scopes (`day`/`tomorrow
|
|
940
|
-
//
|
|
941
|
-
//
|
|
942
|
-
//
|
|
943
|
-
//
|
|
944
|
-
//
|
|
1192
|
+
// Kind rule (spec: index-axis isolation). The GLOBAL todayIndex axes INTERMIX
|
|
1193
|
+
// object kinds in one reorder: `today` (O12 — `list "Today"` takes projects
|
|
1194
|
+
// inline), `evening` (SIT4 EVEORD — projects share the evening axis, per-type
|
|
1195
|
+
// bounce legs), and the future day-group scopes (`day`/`tomorrow`/`upcoming`/an
|
|
1196
|
+
// `<ISO>` day — SIT4 DAYBNC + TOMORROWLIST accept area-less project rows; DEADLINE-
|
|
1197
|
+
// FORECAST projects join that block via the update-project deadline-cycle, PROJDL-
|
|
1198
|
+
// 2a/2c #385). An INDEX axis (a stage list `anytime`/`someday`/`inbox`, or a
|
|
1199
|
+
// project/area/heading container `index`) sorts each KIND in its OWN rank space, so
|
|
1200
|
+
// it takes ONE kind only — an area's someday to-dos and someday projects are
|
|
1201
|
+
// DIFFERENT index buckets, so a shared container does not merge them.
|
|
1202
|
+
//
|
|
1203
|
+
// A movee kind mismatched to the reorder verb (a project/heading on `todo reorder`)
|
|
1204
|
+
// is therefore legal ONLY when the whole set lands on a GLOBAL axis. It lands there
|
|
1205
|
+
// when NO `--in` forces an index axis AND every movee is a today/evening/scheduled/
|
|
1206
|
+
// forecast member (the bare set then auto-routes to a day/today scope whose wire
|
|
1207
|
+
// list is type IN (0,1)). An explicit `--in anytime|someday|inbox|<container>`
|
|
1208
|
+
// forces the index axis regardless of bucket, so it NEVER intermixes — this is what
|
|
1209
|
+
// closes the #387-opened trap where a same-day forecast set's index token slipped
|
|
1210
|
+
// the bucket-only relaxation.
|
|
1211
|
+
const inNorm = request.in?.trim().toLowerCase();
|
|
1212
|
+
const inForcesIndex = inNorm !== undefined &&
|
|
1213
|
+
inNorm !== "today" &&
|
|
1214
|
+
inNorm !== "evening" &&
|
|
1215
|
+
inNorm !== "upcoming" &&
|
|
1216
|
+
!/^\d{4}-\d{2}-\d{2}$/.test(inNorm);
|
|
945
1217
|
const globalAxisIntermix = isTodo &&
|
|
946
1218
|
rows.length > 0 &&
|
|
1219
|
+
!inForcesIndex &&
|
|
947
1220
|
rows.every((r) => {
|
|
948
1221
|
const b = scheduleBucket(r, packedToday);
|
|
949
|
-
return b === "today" ||
|
|
1222
|
+
return (b === "today" ||
|
|
1223
|
+
b === "evening" ||
|
|
1224
|
+
b.startsWith("scheduled:") ||
|
|
1225
|
+
forecastDeadlineDay(r, packedToday) !== null ||
|
|
1226
|
+
// A repeating TEMPLATE with a strictly-future projection is a first-class
|
|
1227
|
+
// day-block todayIndex member too (mirrors rowDayKey's template branch,
|
|
1228
|
+
// #393). Its startDate is NULL and it carries no deadline, so it satisfies
|
|
1229
|
+
// NEITHER branch above — without this disjunct a MIXED-kind day set that
|
|
1230
|
+
// also holds a template fails `.every()`, `globalAxisIntermix` goes false,
|
|
1231
|
+
// and the upstream indexKindRefusal blocks the set before the day-axis
|
|
1232
|
+
// resolver (which #393 taught to admit templates) ever runs. That is Mike's
|
|
1233
|
+
// "both kinds + template, one op" interleave, so the template must count as
|
|
1234
|
+
// a day-group member here exactly as it does downstream.
|
|
1235
|
+
(r.isTemplate && r.templateProjectionDay !== null && r.templateProjectionDay > packedToday));
|
|
950
1236
|
});
|
|
951
1237
|
if (wrongKind.length > 0 && !globalAxisIntermix) {
|
|
952
|
-
|
|
953
|
-
const allProjects = isTodo && rows.length > 0 && rows.every((r) => r.type === 1);
|
|
954
|
-
return refused(op, "usage", `homogeneous kinds required — \`reorder\` rearranges to-dos ` +
|
|
955
|
-
"(the Today, This Evening, and future day-group lists also accept project rows " +
|
|
956
|
-
`intermixed with to-dos); not: ${list}`, allProjects
|
|
957
|
-
? "to rearrange projects in their sidebar/area/someday order use `things project move " +
|
|
958
|
-
"<refs…> --first/--last/--before/--after`"
|
|
959
|
-
: undefined);
|
|
1238
|
+
return indexKindRefusal(op, rows);
|
|
960
1239
|
}
|
|
961
1240
|
return repositionInPlace(deps, op, rows, request.position, packedToday, options, "reorder", request.in);
|
|
962
1241
|
}
|
|
963
1242
|
// ---------------------------------------------------------------- shared core
|
|
964
1243
|
/**
|
|
965
|
-
* The single
|
|
966
|
-
*
|
|
967
|
-
*
|
|
968
|
-
*
|
|
969
|
-
*
|
|
970
|
-
*
|
|
971
|
-
|
|
972
|
-
|
|
1244
|
+
* The packed day a single row contributes to a shared future day-group, or null.
|
|
1245
|
+
* A SCHEDULED row (to-do or project) on a strictly-future startBucket=0 day
|
|
1246
|
+
* contributes its `startDate`; a DEADLINE-FORECAST to-do (startDate NULL, future
|
|
1247
|
+
* `deadline`, start IN (1,2) — DLBNC/§9o) contributes its `deadline` (they share the
|
|
1248
|
+
* one block todayIndex axis). Any other row (arrived, undated non-forecast, off the
|
|
1249
|
+
* Today axis) contributes null.
|
|
1250
|
+
*/
|
|
1251
|
+
function rowDayKey(row, packedToday) {
|
|
1252
|
+
// A repeating TEMPLATE contributes its PROJECTION day (rt1_nextInstanceStartDate),
|
|
1253
|
+
// the Upcoming day-block its rendered projection row sits in (TMPLSORT/PTMPL). Its
|
|
1254
|
+
// startDate/deadline are NULL (or non-block), so it would otherwise contribute null;
|
|
1255
|
+
// strictly-future gate mirrors the scheduled/forecast branches (an arrived/absent
|
|
1256
|
+
// projection is not a future day-group member).
|
|
1257
|
+
if (row.isTemplate) {
|
|
1258
|
+
return row.templateProjectionDay !== null && row.templateProjectionDay > packedToday
|
|
1259
|
+
? row.templateProjectionDay
|
|
1260
|
+
: null;
|
|
1261
|
+
}
|
|
1262
|
+
if (row.startDate !== null &&
|
|
1263
|
+
row.startBucket === 0 &&
|
|
1264
|
+
row.startDate > packedToday &&
|
|
1265
|
+
(row.type === 0 || row.type === 1)) {
|
|
1266
|
+
return row.startDate;
|
|
1267
|
+
}
|
|
1268
|
+
return forecastDeadlineDay(row, packedToday);
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* The single STRICTLY-FUTURE day every movee shares, or null — the precondition for
|
|
1272
|
+
* the SIT4 dated `day` bounce (and the one-call `tomorrow` sort). Members are
|
|
1273
|
+
* SCHEDULED to-dos in ANY container AND scheduled PROJECT rows (area-less OR area-
|
|
1274
|
+
* direct — SIT5 AREAPROJDAY proved the update-project when= legs preserve the area
|
|
1275
|
+
* FK) sharing a future startBucket=0 `startDate`, PLUS DEADLINE-FORECAST to-dos
|
|
1276
|
+
* (DLBNC/§9o) whose future `deadline` equals that same day — all on the ONE Upcoming
|
|
1277
|
+
* day-block todayIndex axis, so a scheduled+forecast mix is one group. A row off the
|
|
1278
|
+
* day, a template, or an undated/arrived non-forecast row breaks the group and falls
|
|
1279
|
+
* through to the normal single-container guard. (Templates never reach here: both
|
|
1280
|
+
* `startDate` and `deadline` fail the strictly-future key for a resting template.)
|
|
973
1281
|
*/
|
|
974
1282
|
function sharedFutureDay(rows, packedToday) {
|
|
975
1283
|
const first = rows[0];
|
|
976
1284
|
if (first === undefined)
|
|
977
1285
|
return null;
|
|
978
|
-
const day = first
|
|
979
|
-
if (day === null
|
|
1286
|
+
const day = rowDayKey(first, packedToday);
|
|
1287
|
+
if (day === null)
|
|
980
1288
|
return null;
|
|
981
1289
|
for (const r of rows) {
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
if (r
|
|
1290
|
+
// Templates NO LONGER break the group: a strictly-future template projection is
|
|
1291
|
+
// a first-class day-block member (TMPLSORT-3c-Tomorrow / PTMPL-B5), reached on the
|
|
1292
|
+
// `tomorrow` native wire or the `day` per-class leg family (reorder.ts). rowDayKey
|
|
1293
|
+
// returns its projection day, so an on-day template shares the group.
|
|
1294
|
+
if (rowDayKey(r, packedToday) !== day)
|
|
987
1295
|
return null;
|
|
988
1296
|
}
|
|
989
1297
|
return day;
|
|
990
1298
|
}
|
|
1299
|
+
/** Whether any row in the group is a deadline-forecast member (DLBNC/§9o). */
|
|
1300
|
+
function hasForecastMember(rows, packedToday) {
|
|
1301
|
+
return rows.some((r) => forecastDeadlineDay(r, packedToday) !== null);
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* The single shared FORECAST deadline day when EVERY row is a deadline-forecast
|
|
1305
|
+
* member (§9o) of that one day, else null. Stricter than {@link sharedFutureDay}:
|
|
1306
|
+
* a scheduled row (or a forecast row on a different day) breaks it — so a
|
|
1307
|
+
* scheduled-only or scheduled+forecast-mixed set returns null (single-axis, day-
|
|
1308
|
+
* only). Gates the forecast dual-axis refusal: only an all-forecast, one-day set
|
|
1309
|
+
* has a coherent container-index alternative to the day-block.
|
|
1310
|
+
*/
|
|
1311
|
+
function sharedForecastDay(rows, packedToday) {
|
|
1312
|
+
const first = rows[0];
|
|
1313
|
+
if (first === undefined)
|
|
1314
|
+
return null;
|
|
1315
|
+
const day = forecastDeadlineDay(first, packedToday);
|
|
1316
|
+
if (day === null)
|
|
1317
|
+
return null;
|
|
1318
|
+
for (const r of rows)
|
|
1319
|
+
if (forecastDeadlineDay(r, packedToday) !== day)
|
|
1320
|
+
return null;
|
|
1321
|
+
return day;
|
|
1322
|
+
}
|
|
991
1323
|
/**
|
|
992
1324
|
* Reposition a shared FUTURE day-group on the global todayIndex axis — the SIT4
|
|
993
1325
|
* dated `day` bounce (loose/direct-area/headed/cross-container to-dos + area-less
|
|
@@ -1040,13 +1372,7 @@ async function runDayGroupReposition(deps, op, rows, position, options, scope, d
|
|
|
1040
1372
|
};
|
|
1041
1373
|
}
|
|
1042
1374
|
if (placement.kind !== "ok") {
|
|
1043
|
-
return
|
|
1044
|
-
kind: "move-leg-failed",
|
|
1045
|
-
op,
|
|
1046
|
-
detail: `the reorder leg did not complete (${placement.kind})`,
|
|
1047
|
-
failed: placement,
|
|
1048
|
-
completed: [],
|
|
1049
|
-
};
|
|
1375
|
+
return repositionFailed(op, placement);
|
|
1050
1376
|
}
|
|
1051
1377
|
return {
|
|
1052
1378
|
kind: "move-ok",
|
|
@@ -1073,6 +1399,18 @@ async function repositionInPlace(deps, op, rows, position, packedToday, options,
|
|
|
1073
1399
|
const axis = resolveReorderAxis(deps, op, rows, inTarget, packedToday, verb, position);
|
|
1074
1400
|
if ("refused" in axis)
|
|
1075
1401
|
return axis.refused;
|
|
1402
|
+
// An explicit day-axis token (`--in <YYYY-MM-DD>` / `--in upcoming`) routes straight
|
|
1403
|
+
// to the cross-container day-group reposition — membership + future-ness were
|
|
1404
|
+
// validated in resolveReorderAxis. Mirror the auto-route's scope pick: the native
|
|
1405
|
+
// one-call `tomorrow` sort only for a non-forecast group whose day is tomorrow (that
|
|
1406
|
+
// surface re-dates a forecast row), else the dated `day` bounce.
|
|
1407
|
+
if ("dayAxis" in axis) {
|
|
1408
|
+
const day = axis.dayAxis;
|
|
1409
|
+
const scope = !hasForecastMember(rows, packedToday) && day === packedTomorrowOf(packedToday)
|
|
1410
|
+
? "tomorrow"
|
|
1411
|
+
: "day";
|
|
1412
|
+
return runDayGroupReposition(deps, op, rows, position, options, scope, day);
|
|
1413
|
+
}
|
|
1076
1414
|
const targetOf = axis.targetOf;
|
|
1077
1415
|
// On a forced/auto INDEX axis (a dual-axis set reordered within its container),
|
|
1078
1416
|
// the display-bucket coherence guards below are the WRONG check: the members
|
|
@@ -1084,18 +1422,27 @@ async function repositionInPlace(deps, op, rows, position, packedToday, options,
|
|
|
1084
1422
|
// A shared FUTURE day-group (single- OR cross-container) rides the global
|
|
1085
1423
|
// todayIndex axis, not the normal single-container reorder: the SIT4 dated `day`
|
|
1086
1424
|
// bounce (loose/direct-area/headed/cross-container to-dos + area-less project
|
|
1087
|
-
// rows, any mix), or the one-call `list "Tomorrow"`
|
|
1088
|
-
//
|
|
1089
|
-
//
|
|
1090
|
-
//
|
|
1091
|
-
//
|
|
1425
|
+
// rows + DEADLINE-FORECAST to-dos, any mix), or the one-call `list "Tomorrow"`
|
|
1426
|
+
// sort when the day is tomorrow. Two exceptions force the `day` scope (never the
|
|
1427
|
+
// native short-cuts): a group CONTAINING a forecast row NEVER rides `list
|
|
1428
|
+
// "Tomorrow"` (that surface re-dates a forecast row, UPCDL-5) NOR the single-
|
|
1429
|
+
// project container-day native re-rank (a forecast row has no startDate, so it is
|
|
1430
|
+
// not a container-day scheduled child) — the deadline-cycle `day` bounce is the
|
|
1431
|
+
// one surface that serves the mixed group. Otherwise a single UNHEADED PROJECT
|
|
1432
|
+
// container's same-day scheduled children ride the cheaper atomic native
|
|
1433
|
+
// container-day re-rank (fall through to reorderTargetOf → container-day).
|
|
1434
|
+
// Templates are excluded by sharedFutureDay. The auto-route NEVER fires on a
|
|
1435
|
+
// forced/auto INDEX axis (indexAxis === true): an explicit `--in someday` /
|
|
1436
|
+
// `--in anytime` / `--in <container>` on a same-day forecast set is the certified
|
|
1437
|
+
// container `index` re-rank, and must not be overridden by the day bounce.
|
|
1092
1438
|
const structKeys = new Set(rows.map((r) => structuralKey(r, packedToday)));
|
|
1093
1439
|
const sharedDay = sharedFutureDay(rows, packedToday);
|
|
1094
|
-
if (sharedDay !== null) {
|
|
1440
|
+
if (sharedDay !== null && !indexAxis) {
|
|
1441
|
+
const forecastInGroup = hasForecastMember(rows, packedToday);
|
|
1095
1442
|
const soleStruct = structKeys.size === 1 ? structKeys.values().next().value : null;
|
|
1096
|
-
const singleProjectContainer = soleStruct?.startsWith("project:") ?? false;
|
|
1443
|
+
const singleProjectContainer = !forecastInGroup && (soleStruct?.startsWith("project:") ?? false);
|
|
1097
1444
|
if (!singleProjectContainer) {
|
|
1098
|
-
const scope = sharedDay === packedTomorrowOf(packedToday) ? "tomorrow" : "day";
|
|
1445
|
+
const scope = !forecastInGroup && sharedDay === packedTomorrowOf(packedToday) ? "tomorrow" : "day";
|
|
1099
1446
|
return runDayGroupReposition(deps, op, rows, position, options, scope, sharedDay);
|
|
1100
1447
|
}
|
|
1101
1448
|
}
|
|
@@ -1204,13 +1551,7 @@ async function repositionInPlace(deps, op, rows, position, packedToday, options,
|
|
|
1204
1551
|
};
|
|
1205
1552
|
}
|
|
1206
1553
|
if (placement.kind !== "ok") {
|
|
1207
|
-
return
|
|
1208
|
-
kind: "move-leg-failed",
|
|
1209
|
-
op,
|
|
1210
|
-
detail: `the reorder leg did not complete (${placement.kind})`,
|
|
1211
|
-
failed: placement,
|
|
1212
|
-
completed: [],
|
|
1213
|
-
};
|
|
1554
|
+
return repositionFailed(op, placement);
|
|
1214
1555
|
}
|
|
1215
1556
|
return {
|
|
1216
1557
|
kind: "move-ok",
|