things-api 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/cli/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 +5 -5
- package/dist/cli/commands/area.js +39 -18
- 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 +74 -22
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/cli/commands/reads.d.ts +5 -1
- package/dist/cli/commands/reads.js +31 -15
- 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 +51 -32
- 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 +138 -67
- 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/glyphs.d.ts +9 -2
- package/dist/cli/glyphs.js +11 -5
- package/dist/cli/glyphs.js.map +1 -1
- 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 +47 -7
- package/dist/cli/read-driver.js +43 -21
- 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 +39 -25
- package/dist/cli/render.js +102 -62
- 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 +100 -32
- package/dist/client.js +63 -19
- package/dist/client.js.map +1 -1
- package/dist/contracts.d.ts +34 -33
- 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 +8 -5
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +192 -139
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/dates.d.ts +26 -0
- package/dist/model/dates.js +46 -4
- package/dist/model/dates.js.map +1 -1
- package/dist/model/entities.d.ts +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 +3 -3
- package/dist/read/area-filter.js.map +1 -1
- package/dist/read/project-view.d.ts +76 -1
- package/dist/read/project-view.js +80 -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 +3 -3
- package/dist/read/scope.js.map +1 -1
- package/dist/read/shape.d.ts +87 -9
- package/dist/read/shape.js +483 -124
- 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 +73 -7
- package/dist/read/truncation.js +62 -29
- package/dist/read/truncation.js.map +1 -1
- package/dist/read/views.d.ts +20 -7
- package/dist/read/views.js +7 -7
- package/dist/read/views.js.map +1 -1
- package/dist/surface-copy.d.ts +13 -0
- package/dist/surface-copy.js +13 -0
- package/dist/surface-copy.js.map +1 -1
- package/dist/write/batch.js +0 -1
- package/dist/write/batch.js.map +1 -1
- package/dist/write/commands.d.ts +25 -1
- package/dist/write/commands.js +259 -122
- package/dist/write/commands.js.map +1 -1
- package/dist/write/guards.js +27 -11
- package/dist/write/guards.js.map +1 -1
- package/dist/write/move.d.ts +34 -9
- package/dist/write/move.js +730 -78
- package/dist/write/move.js.map +1 -1
- package/dist/write/operations.d.ts +50 -20
- package/dist/write/operations.js +2 -2
- package/dist/write/operations.js.map +1 -1
- package/dist/write/pipeline.js +25 -4
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/pre-state.d.ts +58 -28
- package/dist/write/pre-state.js +247 -49
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/reorder.js +266 -32
- package/dist/write/reorder.js.map +1 -1
- package/dist/write/resolution-timestamps.d.ts +34 -0
- package/dist/write/resolution-timestamps.js +286 -0
- package/dist/write/resolution-timestamps.js.map +1 -0
- package/dist/write/reversibility.js +5 -5
- package/dist/write/reversibility.js.map +1 -1
- package/dist/write/scope-guard.js +0 -5
- package/dist/write/scope-guard.js.map +1 -1
- package/dist/write/undo.d.ts +0 -6
- package/dist/write/undo.js +44 -4
- package/dist/write/undo.js.map +1 -1
- package/dist/write/vectors/applescript.js +14 -4
- package/dist/write/vectors/applescript.js.map +1 -1
- package/dist/write/vectors/url-scheme.js +0 -8
- package/dist/write/vectors/url-scheme.js.map +1 -1
- package/dist/write/verify/delta.d.ts +16 -2
- package/dist/write/verify/delta.js +21 -2
- package/dist/write/verify/delta.js.map +1 -1
- package/package.json +1 -1
- package/schema/envelope.schema.json +16 -78
- package/skills/things-cli/SKILL.md +10 -6
- package/skills/things-cli/references/contracts.md +3 -2
- package/skills/things-cli/references/errors.md +9 -1
- package/skills/things-cli/references/model.md +9 -7
- package/skills/things-cli/references/ordering.md +36 -10
package/dist/write/reorder.js
CHANGED
|
@@ -302,8 +302,19 @@ function bounceFallbackOk(deps, what, kind, protocol) {
|
|
|
302
302
|
return d;
|
|
303
303
|
return { ...d, fallbackNote: fallbackNoteFor(deps, protocol) };
|
|
304
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* Whether the native `_private_experimental_ reorder` command is available — the
|
|
307
|
+
* config gate is on AND the app still declares it in its sdef (the canary). The
|
|
308
|
+
* `day`-scope template leg family gates on this too: a repeating TO-DO template's
|
|
309
|
+
* only safe day-block placement is the native single-id `list "Upcoming"` front-
|
|
310
|
+
* insert (a dated when= leg CRASHES a template, §1), so a day-group carrying any
|
|
311
|
+
* template requires the native surface (else it refuses honestly, naming them).
|
|
312
|
+
*/
|
|
313
|
+
function nativeReorderAvailable(deps) {
|
|
314
|
+
return deps.config.allowExperimental && (deps.sdefProbe ?? sdefDeclaresPrivateReorder)();
|
|
315
|
+
}
|
|
305
316
|
function resolveStrategy(deps, params) {
|
|
306
|
-
const nativeAvailable =
|
|
317
|
+
const nativeAvailable = nativeReorderAvailable(deps);
|
|
307
318
|
// Scopes whose ONLY surface is the bounce (no native command reaches them).
|
|
308
319
|
const bounceOnly = {
|
|
309
320
|
evening: { kind: "evening", what: "evening-section order" },
|
|
@@ -398,6 +409,11 @@ function resolveStrategy(deps, params) {
|
|
|
398
409
|
// this case exists only for switch exhaustiveness.
|
|
399
410
|
case "heading-someday":
|
|
400
411
|
return { kind: "ok", strategy: "native" };
|
|
412
|
+
// `upcoming` is an INTERNAL per-template front-insert leg (the `day` bounce
|
|
413
|
+
// dispatches it via runMutation directly — TMPLSORT-1); it never reaches the
|
|
414
|
+
// strategy resolver. Blocked here for exhaustiveness / defence in depth.
|
|
415
|
+
case "upcoming":
|
|
416
|
+
return blocked("the `upcoming` scope is an internal per-template front-insert leg, not a user reorder scope", "reorder a template's day-block via `things todo reorder` on its day (the planner routes it)");
|
|
401
417
|
}
|
|
402
418
|
}
|
|
403
419
|
// (The SOMEBNC-project someday-only bounce fallback for the `project` scope is
|
|
@@ -483,7 +499,7 @@ async function runHeadingSomeday(deps, params, options) {
|
|
|
483
499
|
const headingUuid = params.container?.uuid ?? null;
|
|
484
500
|
const projectUuid = headingProjectUuid(deps, headingUuid);
|
|
485
501
|
const txnId = `txn-${startedAt.getTime().toString(36)}-${process.pid.toString(36)}`;
|
|
486
|
-
const pre = computeReorderPre(deps.db, params, headingUuid, now());
|
|
502
|
+
const pre = computeReorderPre(deps.db, params, headingUuid, now(), { zone: deps.zone });
|
|
487
503
|
const preRanks = {};
|
|
488
504
|
for (const m of pre.members)
|
|
489
505
|
preRanks[m.uuid] = m.rank;
|
|
@@ -620,19 +636,38 @@ async function runBounce(deps, params, bounceKind, options) {
|
|
|
620
636
|
// The two `when=` leg values. A fixed-bucket bounce uses the spec's keywords; the
|
|
621
637
|
// DATED `day` bounce (SIT4 DAYBNC) derives them from the movees' shared day —
|
|
622
638
|
// back = the day D, away = the neighbour day D+1 (a strictly-future staging day
|
|
623
|
-
// ≠ D that keeps the transient visit out of Today). The day is read off the
|
|
624
|
-
// requested uuid
|
|
625
|
-
//
|
|
626
|
-
//
|
|
639
|
+
// ≠ D that keeps the transient visit out of Today). The day D is read off the
|
|
640
|
+
// first requested uuid — its `startDate` when scheduled, else its `deadline` when
|
|
641
|
+
// it is a deadline-forecast row (startDate NULL, start IN (1,2) — DLBNC/§9o). The
|
|
642
|
+
// planner guarantees every movee shares D; an absent/malformed day yields an empty
|
|
643
|
+
// member set and is rejected below before any dispatch, so the today fallback here
|
|
644
|
+
// only keeps the WhenValue well-typed. (The `when=` values drive the SCHEDULED
|
|
645
|
+
// rows' bounce legs; a forecast row ignores them — it rides the deadline-cycle.)
|
|
627
646
|
let dayPacked = null;
|
|
628
647
|
let awayValue;
|
|
629
648
|
let backValue;
|
|
630
649
|
if (spec.dated) {
|
|
631
650
|
const firstUuid = params.uuids[0];
|
|
651
|
+
const firstRow = firstUuid !== undefined
|
|
652
|
+
? deps.db
|
|
653
|
+
.prepare("SELECT startDate, startBucket, deadline, start, rt1_nextInstanceStartDate AS proj, " +
|
|
654
|
+
"(rt1_recurrenceRule IS NOT NULL OR repeater IS NOT NULL) AS isTemplate " +
|
|
655
|
+
"FROM TMTask WHERE uuid = ?")
|
|
656
|
+
.get(firstUuid)
|
|
657
|
+
: undefined;
|
|
658
|
+
// The day D: a template's PROJECTION day (rt1_nextInstanceStartDate), else a
|
|
659
|
+
// scheduled row's startDate, else a forecast row's deadline (mirrors
|
|
660
|
+
// computeReorderPre so the when= legs and the member set agree on D).
|
|
632
661
|
dayPacked =
|
|
633
|
-
|
|
634
|
-
?
|
|
635
|
-
:
|
|
662
|
+
firstRow === undefined
|
|
663
|
+
? null
|
|
664
|
+
: firstRow.isTemplate === 1
|
|
665
|
+
? firstRow.proj
|
|
666
|
+
: firstRow.startDate !== null && firstRow.startBucket === 0
|
|
667
|
+
? firstRow.startDate
|
|
668
|
+
: firstRow.startDate === null && (firstRow.start === 1 || firstRow.start === 2)
|
|
669
|
+
? firstRow.deadline
|
|
670
|
+
: null;
|
|
636
671
|
const iso = decodePackedDate(dayPacked);
|
|
637
672
|
backValue = iso ?? localToday(now());
|
|
638
673
|
awayValue = iso !== null ? addDaysIso(iso, 1) : localToday(now());
|
|
@@ -641,6 +676,21 @@ async function runBounce(deps, params, bounceKind, options) {
|
|
|
641
676
|
awayValue = spec.away;
|
|
642
677
|
backValue = spec.back;
|
|
643
678
|
}
|
|
679
|
+
// A day-scope DEADLINE-FORECAST row (startDate NULL, deadline set) reorders by the
|
|
680
|
+
// DLBNC deadline-cycle (URL `deadline=` clear + re-set of the SAME deadline —
|
|
681
|
+
// byte-identical, front-inserts on the block's todayIndex axis) instead of the
|
|
682
|
+
// scheduled `when=` bounce. Returns the decoded ISO to re-set, or null for a
|
|
683
|
+
// scheduled row (which takes the `when=` legs). AppleScript `due date` is
|
|
684
|
+
// certified-wrong here (lazy todayIndex, cannot clear) — URL only (DLBNC §9q).
|
|
685
|
+
const forecastLegOf = (uuid) => {
|
|
686
|
+
if (bounceKind !== "day")
|
|
687
|
+
return null;
|
|
688
|
+
const r = deps.db.prepare("SELECT startDate, deadline FROM TMTask WHERE uuid = ?").get(uuid);
|
|
689
|
+
if (r === undefined || r.startDate !== null || r.deadline === null)
|
|
690
|
+
return null;
|
|
691
|
+
const iso = decodePackedDate(r.deadline);
|
|
692
|
+
return iso === null ? null : { iso };
|
|
693
|
+
};
|
|
644
694
|
// Per-item leg op: the mixed-kind `day`/`evening` bounces pick update-project for
|
|
645
695
|
// a project row (type=1) and todo.update for a to-do (type=0); every other bounce
|
|
646
696
|
// uses one fixed op. (NEVER send a dated when= leg to a template — §1 crash — but
|
|
@@ -654,7 +704,7 @@ async function runBounce(deps, params, bounceKind, options) {
|
|
|
654
704
|
};
|
|
655
705
|
const txnId = `txn-${startedAt.getTime().toString(36)}-${process.pid.toString(36)}`;
|
|
656
706
|
// Scope/membership guard — same data the native path's guard uses.
|
|
657
|
-
const pre = computeReorderPre(deps.db, params, containerUuid, now());
|
|
707
|
+
const pre = computeReorderPre(deps.db, params, containerUuid, now(), { zone: deps.zone });
|
|
658
708
|
// Pre-ranks make the SUMMARY record the undoable unit (a single inverse
|
|
659
709
|
// reorder restores the old relative order); legs are excluded from undo.
|
|
660
710
|
const preRanks = {};
|
|
@@ -764,14 +814,82 @@ async function runBounce(deps, params, bounceKind, options) {
|
|
|
764
814
|
});
|
|
765
815
|
return result;
|
|
766
816
|
}
|
|
817
|
+
// TMPLSORT/PTMPL template leg families (the `day` scope only — the native
|
|
818
|
+
// `tomorrow` one-call wire carries templates as ordinary members). A repeating
|
|
819
|
+
// template's projection is a first-class day-block member, but a dated when=/
|
|
820
|
+
// deadline leg CRASHES it (§1), so it NEVER receives one. Split per class:
|
|
821
|
+
// - TO-DO template: a single-id `list "Upcoming"` NATIVE front-insert leg
|
|
822
|
+
// (TMPLSORT-1), interleaved into the reverse-target dispatch on the shared
|
|
823
|
+
// block min-space (TMPLSORT-2). Needs the native surface.
|
|
824
|
+
// - PROJECT template: byte-UNTOUCHED under the SUFFIX RULE — it has no headless
|
|
825
|
+
// reach on an arbitrary future day (PTMPL-B: only `list "Tomorrow"` / a GUI
|
|
826
|
+
// drag place it), so every movable front-inserts ABOVE it. Accept only a wire
|
|
827
|
+
// where the project templates form a suffix in their CURRENT relative order;
|
|
828
|
+
// refuse otherwise, naming the one achievable arrangement.
|
|
829
|
+
// The whole day dispatch is experimental-gated when ANY template is present.
|
|
830
|
+
const memberInfo = new Map(pre.members.map((m) => [m.uuid, m]));
|
|
831
|
+
const isTmpl = (u) => memberInfo.get(u)?.isTemplate === true;
|
|
832
|
+
const isProjectTemplate = (u) => isTmpl(u) && memberInfo.get(u)?.type === 1;
|
|
833
|
+
const templatesPresent = bounceKind === "day" && coBounce.some(isTmpl);
|
|
834
|
+
let dispatchRun = coBounce;
|
|
835
|
+
if (templatesPresent) {
|
|
836
|
+
const dayIso = decodePackedDate(dayPacked) ?? "the target day";
|
|
837
|
+
const templateRefusal = (detail, remediation) => {
|
|
838
|
+
const result = {
|
|
839
|
+
kind: "blocked",
|
|
840
|
+
op: "reorder",
|
|
841
|
+
reason: "hazard",
|
|
842
|
+
hazard: "H-REORDER-SCOPE",
|
|
843
|
+
detail,
|
|
844
|
+
remediation,
|
|
845
|
+
};
|
|
846
|
+
auditSummary(deps, params, startedAt, "blocked:H-REORDER-SCOPE", null, {
|
|
847
|
+
pre: preRanks,
|
|
848
|
+
txnId,
|
|
849
|
+
actor,
|
|
850
|
+
});
|
|
851
|
+
return result;
|
|
852
|
+
};
|
|
853
|
+
if (!nativeReorderAvailable(deps)) {
|
|
854
|
+
// C(iii): refuse honestly, NAMING the templates — never a dated leg (crash),
|
|
855
|
+
// never a silent skip, never a partial sort.
|
|
856
|
+
const tmpls = coBounce.filter(isTmpl);
|
|
857
|
+
return templateRefusal(`the ${dayIso} day-group contains repeating template(s) [${tmpls.join(", ")}] whose ` +
|
|
858
|
+
"day-block placement needs the native private reorder surface (a dated when= leg CRASHES " +
|
|
859
|
+
`a template — §1/§9e), but it is unavailable (${nativeUnavailableReason(deps)})`, "enable it with `things config set allow-experimental true` (and keep Things updated so the " +
|
|
860
|
+
"sdef canary passes), or reorder the day-group without the template(s)");
|
|
861
|
+
}
|
|
862
|
+
// PROJECT-template SUFFIX RULE.
|
|
863
|
+
const projectTemplates = coBounce.filter(isProjectTemplate);
|
|
864
|
+
if (projectTemplates.length > 0) {
|
|
865
|
+
const suffix = coBounce.slice(coBounce.length - projectTemplates.length);
|
|
866
|
+
const suffixContiguous = suffix.every(isProjectTemplate);
|
|
867
|
+
// Their achievable order is their CURRENT todayIndex (render) order — untouched.
|
|
868
|
+
const currentOrder = projectTemplates.toSorted((a, b) => (memberInfo.get(a)?.rank ?? 0) - (memberInfo.get(b)?.rank ?? 0));
|
|
869
|
+
const orderPreserved = suffixContiguous && suffix.every((u, i) => u === currentOrder[i]);
|
|
870
|
+
if (!orderPreserved) {
|
|
871
|
+
const movables = coBounce.filter((u) => !isProjectTemplate(u));
|
|
872
|
+
const achievable = [...movables, ...currentOrder];
|
|
873
|
+
return templateRefusal(`a repeating PROJECT template cannot be placed above a movable item on the ${dayIso} ` +
|
|
874
|
+
"day-block, and project templates cannot change their relative order there — an arbitrary " +
|
|
875
|
+
"future day gives a project template no headless reach (PTMPL-B: only the Tomorrow one-call " +
|
|
876
|
+
"sort, when the day is tomorrow, or a GUI drag place it). The only arrangement this " +
|
|
877
|
+
`day-group can reach headlessly is: ${achievable.join(", ")} (every movable above the ` +
|
|
878
|
+
"project template(s), which keep their current relative order)", "request that arrangement (place the project template(s) last, in their current order), or " +
|
|
879
|
+
"drag the project template in the app; if the day is tomorrow, reorder on Tomorrow instead");
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
// Movables + to-do templates dispatch; project templates are the untouched suffix.
|
|
883
|
+
dispatchRun = coBounce.filter((u) => !isProjectTemplate(u));
|
|
884
|
+
}
|
|
767
885
|
// Front-insert contexts (loose/area-direct) place last-first (reverse iterate,
|
|
768
886
|
// unshift); back-insert contexts (heading/project children) place first-first
|
|
769
887
|
// (forward iterate, push). Either way `placed` holds the current top-to-bottom
|
|
770
888
|
// order of the bounced block. The SAME per-item order drives the json-array
|
|
771
889
|
// collapse (array order == result index order for both directions).
|
|
772
890
|
const order = direction === "front"
|
|
773
|
-
?
|
|
774
|
-
:
|
|
891
|
+
? dispatchRun.map((_, i) => dispatchRun.length - 1 - i)
|
|
892
|
+
: dispatchRun.map((_, i) => i);
|
|
775
893
|
// BOUNCEJSON collapse (§9i): when the placement (`back`) leg is when=anytime
|
|
776
894
|
// into a loose/heading-container bucket, the whole N-item round-trip collapses
|
|
777
895
|
// to ONE pre-validated `things:///json` array (2N ops, 1 dispatch, ~7×,
|
|
@@ -783,18 +901,39 @@ async function runBounce(deps, params, bounceKind, options) {
|
|
|
783
901
|
const dispatchVector = spec.jsonCollapsible && legOp !== "per-type" ? pickDispatchVector(deps, legOp) : undefined;
|
|
784
902
|
const useJson = spec.jsonCollapsible && dispatchVector !== undefined && dispatchVector.simulates !== true;
|
|
785
903
|
if (options.dryRun === true) {
|
|
904
|
+
// Name the per-row-class legs. A `day` set may mix SCHEDULED rows (when= bounce)
|
|
905
|
+
// and DEADLINE-FORECAST rows (the DLBNC deadline-cycle: deadline= clear + re-set),
|
|
906
|
+
// so the plan discloses the leg family per class + count; every other bounce is a
|
|
907
|
+
// single when= round-trip.
|
|
908
|
+
let legNaming = `when=${awayValue} → when=${backValue}`;
|
|
909
|
+
if (bounceKind === "day") {
|
|
910
|
+
const ttN = coBounce.filter((u) => isTmpl(u) && !isProjectTemplate(u)).length;
|
|
911
|
+
const ptN = coBounce.filter(isProjectTemplate).length;
|
|
912
|
+
const fN = coBounce.filter((u) => !isTmpl(u) && forecastLegOf(u) !== null).length;
|
|
913
|
+
const sN = coBounce.length - fN - ttN - ptN;
|
|
914
|
+
const bits = [];
|
|
915
|
+
if (sN > 0)
|
|
916
|
+
bits.push(`${sN} scheduled via when=${awayValue} → when=${backValue}`);
|
|
917
|
+
if (fN > 0)
|
|
918
|
+
bits.push(`${fN} deadline-forecast via deadline-cycle (deadline= clear + re-set)`);
|
|
919
|
+
if (ttN > 0)
|
|
920
|
+
bits.push(`${ttN} to-do template(s) via single-id \`list "Upcoming"\` front-insert (umd-silent)`);
|
|
921
|
+
if (ptN > 0)
|
|
922
|
+
bits.push(`${ptN} project template(s) left byte-untouched (suffix rule — no headless reach)`);
|
|
923
|
+
legNaming = bits.join("; ");
|
|
924
|
+
}
|
|
786
925
|
const invocation = useJson
|
|
787
926
|
? `json-collapse ×${coBounce.length} (${direction}-insert, ` +
|
|
788
927
|
`${direction === "front" ? "reverse" : "forward"} array order, ` +
|
|
789
928
|
(touchedUnnamed.length > 0 ? `touches ${touchedUnnamed.length} unnamed sibling(s), ` : "") +
|
|
790
929
|
`1 dispatch / ${coBounce.length * 2} ops): ` +
|
|
791
|
-
|
|
930
|
+
`${legNaming} interleaved per item; validate-first full-abort, ` +
|
|
792
931
|
`one terminal order verify`
|
|
793
932
|
: `bounce ×${coBounce.length} (${direction}-insert, ` +
|
|
794
933
|
`${direction === "front" ? "reverse" : "forward"} order, ` +
|
|
795
934
|
(touchedUnnamed.length > 0 ? `touches ${touchedUnnamed.length} unnamed sibling(s), ` : "") +
|
|
796
935
|
`${coBounce.length * 2} legs): ` +
|
|
797
|
-
|
|
936
|
+
`${legNaming}; one verify per item round-trip`;
|
|
798
937
|
return {
|
|
799
938
|
kind: "dry-run",
|
|
800
939
|
op: "reorder",
|
|
@@ -826,8 +965,8 @@ async function runBounce(deps, params, bounceKind, options) {
|
|
|
826
965
|
const placed = [];
|
|
827
966
|
for (let step = 0; step < order.length; step++) {
|
|
828
967
|
const i = order[step];
|
|
829
|
-
const uuid =
|
|
830
|
-
const remainingBefore = () => direction === "front" ?
|
|
968
|
+
const uuid = dispatchRun[i];
|
|
969
|
+
const remainingBefore = () => direction === "front" ? dispatchRun.slice(0, i + 1) : dispatchRun.slice(i);
|
|
831
970
|
// Concurrent-edit re-check: the item must still be an eligible member.
|
|
832
971
|
const memberProblem = checkStillMember(deps, uuid, bounceKind, containerUuid, now(), dayPacked);
|
|
833
972
|
if (memberProblem !== null) {
|
|
@@ -843,31 +982,91 @@ async function runBounce(deps, params, bounceKind, options) {
|
|
|
843
982
|
cause: null,
|
|
844
983
|
};
|
|
845
984
|
}
|
|
985
|
+
// A repeating TO-DO template rides ONE native single-id `list "Upcoming"` front-
|
|
986
|
+
// insert leg (TMPLSORT-1) — NEVER a dated when=/deadline leg (§1 crash). It front-
|
|
987
|
+
// inserts on the SAME shared block min-space as the when=/deadline families, so the
|
|
988
|
+
// reverse-target dispatch interleaves it exactly (TMPLSORT-2). Project templates are
|
|
989
|
+
// excluded from `dispatchRun` (the untouched suffix), so only to-do templates reach
|
|
990
|
+
// this branch; the leg is umd-silent (§9r — disclosed in the result note).
|
|
991
|
+
if (isTmpl(uuid)) {
|
|
992
|
+
const tmplLeg = await runMutation(deps, "reorder", { scope: "upcoming", uuids: [uuid], named: [uuid] }, legOptions(options, txnId));
|
|
993
|
+
if (tmplLeg.kind !== "ok") {
|
|
994
|
+
auditSummary(deps, params, startedAt, "verify-failed:mismatch", { placed: [...placed] }, { pre: preRanks, txnId, actor });
|
|
995
|
+
return {
|
|
996
|
+
kind: "bounce-aborted",
|
|
997
|
+
op: "reorder",
|
|
998
|
+
detail: `the native \`list "Upcoming"\` front-insert leg failed for repeating template ${uuid} ` +
|
|
999
|
+
"— it was NOT moved (its position is unchanged)",
|
|
1000
|
+
placed: [...placed],
|
|
1001
|
+
remaining: remainingBefore(),
|
|
1002
|
+
cause: tmplLeg,
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
if (direction === "front")
|
|
1006
|
+
placed.unshift(uuid);
|
|
1007
|
+
else
|
|
1008
|
+
placed.push(uuid);
|
|
1009
|
+
const tmplPrefix = await pollUntilVerified(() => evaluateDelta({ mode: "ordering", key: rankKey, sequence: [...placed] }, createDbReader(deps.db), { modDates: {}, fields: {} }), options.verifyTimeoutMs ?? 4000, deps.poller ?? {});
|
|
1010
|
+
if (tmplPrefix.kind !== "ok") {
|
|
1011
|
+
auditSummary(deps, params, startedAt, "verify-failed:mismatch", { placed: [...placed] }, { pre: preRanks, txnId, actor });
|
|
1012
|
+
return {
|
|
1013
|
+
kind: "bounce-aborted",
|
|
1014
|
+
op: "reorder",
|
|
1015
|
+
detail: `placed items fell out of order after front-inserting template ${uuid} (concurrent ` +
|
|
1016
|
+
"edit?); re-run the reorder once Things is idle",
|
|
1017
|
+
placed: [...placed],
|
|
1018
|
+
remaining: direction === "front" ? dispatchRun.slice(0, i) : dispatchRun.slice(i + 1),
|
|
1019
|
+
cause: null,
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
846
1024
|
// leg 1 sends the item AWAY from its resting bucket; leg 2 returns it — the
|
|
847
|
-
// return leg is what front
|
|
848
|
-
//
|
|
1025
|
+
// return leg is what front-inserts (BOUNCE2 re-entry law). SCHEDULED rows use
|
|
1026
|
+
// the `when=` round-trip; a day-scope DEADLINE-FORECAST row (startDate NULL,
|
|
1027
|
+
// deadline set) uses the DLBNC deadline-cycle instead: leg 1 CLEARS the deadline
|
|
1028
|
+
// (transiently off the block), leg 2 RE-SETS the same deadline byte-identical
|
|
1029
|
+
// (front-inserts on the block todayIndex axis). BOTH classes pick the leg op PER
|
|
1030
|
+
// ROW TYPE — `todo.update` (→ `update?…`) for a to-do, `project.update` (→
|
|
1031
|
+
// `update-project?…`) for a project — so a forecast PROJECT rides the #385-
|
|
1032
|
+
// certified `update-project?deadline=` cycle (PROJDL-2b, PROJSTAR-safe). Both
|
|
1033
|
+
// classes front-insert at the day's global min, so one reverse-target pass
|
|
1034
|
+
// interleaves them exactly (o-suite forecast-cycle + mixed-interleave locks).
|
|
1035
|
+
const forecast = forecastLegOf(uuid);
|
|
849
1036
|
const rowLegOp = opForRow(uuid);
|
|
850
|
-
const
|
|
1037
|
+
const awayLabel = forecast !== null ? "deadline= clear" : `when=${awayValue}`;
|
|
1038
|
+
const backLabel = forecast !== null ? "deadline= re-set" : `when=${backValue}`;
|
|
1039
|
+
const leg1 = forecast !== null
|
|
1040
|
+
? await runMutation(deps, rowLegOp, { uuid, deadline: null }, legOptions(options, txnId))
|
|
1041
|
+
: await runMutation(deps, rowLegOp, { uuid, when: awayValue }, legOptions(options, txnId));
|
|
851
1042
|
if (leg1.kind !== "ok") {
|
|
852
1043
|
auditSummary(deps, params, startedAt, "verify-failed:mismatch", { placed: [...placed] }, { pre: preRanks, txnId, actor });
|
|
853
1044
|
return {
|
|
854
1045
|
kind: "bounce-aborted",
|
|
855
1046
|
op: "reorder",
|
|
856
|
-
detail: `bounce leg 1 (
|
|
1047
|
+
detail: `bounce leg 1 (${awayLabel}) failed for ${uuid} — the item was NOT moved`,
|
|
857
1048
|
placed: [...placed],
|
|
858
1049
|
remaining: remainingBefore(),
|
|
859
1050
|
cause: leg1,
|
|
860
1051
|
};
|
|
861
1052
|
}
|
|
862
1053
|
// leg 2 must follow leg 1's committed state for the same item before the next item's bounce begins
|
|
863
|
-
const leg2 =
|
|
1054
|
+
const leg2 = forecast !== null
|
|
1055
|
+
? await runMutation(deps, rowLegOp, { uuid, deadline: forecast.iso }, legOptions(options, txnId))
|
|
1056
|
+
: await runMutation(deps, rowLegOp, { uuid, when: backValue }, legOptions(options, txnId));
|
|
864
1057
|
if (leg2.kind !== "ok") {
|
|
865
1058
|
auditSummary(deps, params, startedAt, "verify-failed:mismatch", { placed: [...placed] }, { pre: preRanks, txnId, actor });
|
|
1059
|
+
const strandedState = forecast !== null
|
|
1060
|
+
? "left DEADLINE-LESS (transiently off the day-block)"
|
|
1061
|
+
: `STRANDED IN ${String(awayValue).toUpperCase()}`;
|
|
1062
|
+
const recover = forecast !== null
|
|
1063
|
+
? `re-set its deadline (${forecast.iso})`
|
|
1064
|
+
: `re-schedule it (when=${backValue})`;
|
|
866
1065
|
return {
|
|
867
1066
|
kind: "bounce-aborted",
|
|
868
1067
|
op: "reorder",
|
|
869
|
-
detail: `bounce leg 2 (
|
|
870
|
-
`${
|
|
1068
|
+
detail: `bounce leg 2 (${backLabel}) failed for ${uuid} — THE ITEM IS ${strandedState}; ` +
|
|
1069
|
+
`${recover} or fix in the app`,
|
|
871
1070
|
placed: [...placed],
|
|
872
1071
|
remaining: remainingBefore(),
|
|
873
1072
|
cause: leg2,
|
|
@@ -889,7 +1088,7 @@ async function runBounce(deps, params, bounceKind, options) {
|
|
|
889
1088
|
detail: `placed items fell out of order after bouncing ${uuid} (concurrent edit?); ` +
|
|
890
1089
|
"re-run the reorder once Things is idle",
|
|
891
1090
|
placed: [...placed],
|
|
892
|
-
remaining: direction === "front" ?
|
|
1091
|
+
remaining: direction === "front" ? dispatchRun.slice(0, i) : dispatchRun.slice(i + 1),
|
|
893
1092
|
cause: null,
|
|
894
1093
|
};
|
|
895
1094
|
}
|
|
@@ -899,6 +1098,25 @@ async function runBounce(deps, params, bounceKind, options) {
|
|
|
899
1098
|
for (const uuid of coBounce)
|
|
900
1099
|
observed[uuid] = reader.rankOf(uuid, rankKey);
|
|
901
1100
|
auditSummary(deps, params, startedAt, "ok", observed, { pre: preRanks, txnId, actor });
|
|
1101
|
+
// §9r disclosure: a template's `list "Upcoming"` front-insert leg is
|
|
1102
|
+
// userModificationDate-SILENT (a umd-diffing sync/watcher misses it), and a project
|
|
1103
|
+
// template left as the untouched suffix moved not at all — surface both so the caller
|
|
1104
|
+
// never mistakes a umd-silent placement for a no-op.
|
|
1105
|
+
const warnings = [];
|
|
1106
|
+
if (templatesPresent) {
|
|
1107
|
+
const tt = coBounce.filter((u) => isTmpl(u) && !isProjectTemplate(u));
|
|
1108
|
+
const pt = coBounce.filter(isProjectTemplate);
|
|
1109
|
+
if (tt.length > 0) {
|
|
1110
|
+
warnings.push(`${tt.length} repeating to-do template(s) were front-inserted via \`list "Upcoming"\` and ` +
|
|
1111
|
+
"are userModificationDate-SILENT (a umd-diffing sync/watcher will not see the move): " +
|
|
1112
|
+
tt.join(", "));
|
|
1113
|
+
}
|
|
1114
|
+
if (pt.length > 0) {
|
|
1115
|
+
warnings.push(`${pt.length} repeating project template(s) were left byte-untouched as the day-block suffix ` +
|
|
1116
|
+
"(no headless reach on this day — every movable was placed above them): " +
|
|
1117
|
+
pt.join(", "));
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
902
1120
|
return {
|
|
903
1121
|
kind: "ok",
|
|
904
1122
|
op: "reorder",
|
|
@@ -909,6 +1127,7 @@ async function runBounce(deps, params, bounceKind, options) {
|
|
|
909
1127
|
// A bounce reorder is a summary txn, so its token is the txn id (matches
|
|
910
1128
|
// the audit record's undoToken); pass it to `things undo --txn <token>`.
|
|
911
1129
|
undoToken: txnId,
|
|
1130
|
+
...(warnings.length > 0 && { warnings }),
|
|
912
1131
|
// Co-bounced siblings the anchor placement re-inserted (honest disclosure).
|
|
913
1132
|
...(touchedUnnamed.length > 0 && { touched: touchedUnnamed }),
|
|
914
1133
|
};
|
|
@@ -1398,7 +1617,7 @@ async function runMoveFallback(deps, params, fallback, options) {
|
|
|
1398
1617
|
const cap = deps.config.bounceMaxItems ?? BOUNCE_MAX_ITEMS;
|
|
1399
1618
|
const containerUuid = resolveContainerUuid(deps, params);
|
|
1400
1619
|
const txnId = `txn-${startedAt.getTime().toString(36)}-${process.pid.toString(36)}`;
|
|
1401
|
-
const pre = computeReorderPre(deps.db, params, containerUuid, now());
|
|
1620
|
+
const pre = computeReorderPre(deps.db, params, containerUuid, now(), { zone: deps.zone });
|
|
1402
1621
|
// proj-root BACK-inserts (forward dispatch → suffix from the first named slot);
|
|
1403
1622
|
// inbox/area FRONT-insert (reverse dispatch → prefix to the last named slot).
|
|
1404
1623
|
const direction = fallback === "proj-root" ? "back" : "front";
|
|
@@ -1969,7 +2188,8 @@ function resolveContainerUuid(deps, params) {
|
|
|
1969
2188
|
function checkStillMember(deps, uuid, bounceKind, containerUuid, now, dayPacked) {
|
|
1970
2189
|
const packedToday = encodePackedDate(localToday(now));
|
|
1971
2190
|
const row = deps.db
|
|
1972
|
-
.prepare("SELECT status, trashed, startBucket, startDate, start, type, area, project, heading " +
|
|
2191
|
+
.prepare("SELECT status, trashed, startBucket, startDate, start, type, area, project, heading, deadline, " +
|
|
2192
|
+
"rt1_recurrenceRule AS rule, repeater, rt1_nextInstanceStartDate AS proj " +
|
|
1973
2193
|
"FROM TMTask WHERE uuid = ?")
|
|
1974
2194
|
.get(uuid);
|
|
1975
2195
|
if (row === undefined)
|
|
@@ -2052,14 +2272,28 @@ function checkStillMember(deps, uuid, bounceKind, containerUuid, now, dayPacked)
|
|
|
2052
2272
|
return null;
|
|
2053
2273
|
}
|
|
2054
2274
|
case "day": {
|
|
2055
|
-
// The dated bounce's members are to-dos (any container) and scheduled
|
|
2056
|
-
// rows (area-less OR area-direct — SIT5 AREAPROJDAY) sharing the day D
|
|
2057
|
-
// startBucket=0 axis
|
|
2058
|
-
//
|
|
2275
|
+
// The dated bounce's members are SCHEDULED to-dos (any container) and scheduled
|
|
2276
|
+
// project rows (area-less OR area-direct — SIT5 AREAPROJDAY) sharing the day D
|
|
2277
|
+
// on the startBucket=0 axis, PLUS DEADLINE-FORECAST to-dos (startDate NULL,
|
|
2278
|
+
// start IN (1,2), deadline == D — DLBNC/§9o) on the SAME block todayIndex axis.
|
|
2279
|
+
// A concurrent edit that re-dates, de-schedules, evenings, clears the deadline,
|
|
2280
|
+
// or drops the row to the Inbox (start=0) ejects it from the group.
|
|
2059
2281
|
if (row.type !== 0 && row.type !== 1)
|
|
2060
2282
|
return "the item is not a to-do or project";
|
|
2061
|
-
|
|
2062
|
-
|
|
2283
|
+
// A repeating TEMPLATE stays a day-block member via its PROJECTION day
|
|
2284
|
+
// (rt1_nextInstanceStartDate — TMPLSORT/PTMPL), not startDate/deadline.
|
|
2285
|
+
if (row.rule !== null || row.repeater !== null) {
|
|
2286
|
+
return row.proj === dayPacked
|
|
2287
|
+
? null
|
|
2288
|
+
: "the template's projection day changed (its recurrence rule was edited)";
|
|
2289
|
+
}
|
|
2290
|
+
const scheduled = row.startBucket === 0 && row.startDate !== null && row.startDate === dayPacked;
|
|
2291
|
+
const forecast = row.startDate === null &&
|
|
2292
|
+
(row.start === 1 || row.start === 2) &&
|
|
2293
|
+
row.deadline !== null &&
|
|
2294
|
+
row.deadline === dayPacked;
|
|
2295
|
+
if (!scheduled && !forecast) {
|
|
2296
|
+
return "the item left the day-group (re-dated, evening-ed, de-scheduled, or its deadline changed)";
|
|
2063
2297
|
}
|
|
2064
2298
|
return null;
|
|
2065
2299
|
}
|