@wirecat/cli-core 0.512.0 → 0.514.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/CHANGELOG.md +46 -0
- package/dist/cli/messenger/tasks-command.js +4 -5
- package/dist/entities/index.d.ts +27 -0
- package/dist/entities/task-results.d.ts +15 -0
- package/dist/entities/task-results.js +4 -1
- package/dist/entities/task.d.ts +17 -0
- package/dist/entities/task.js +12 -1
- package/dist/errors/reasons.js +1 -0
- package/dist/runs/error-detail.js +3 -0
- package/dist/store/index.d.ts +2 -2
- package/dist/store/index.js +1 -1
- package/dist/store/sqlite/manifest.js +1 -0
- package/dist/store/sqlite/migrations.generated.js +12 -0
- package/dist/store/sqlite/schema.d.ts +48 -0
- package/dist/store/sqlite/schema.js +3 -0
- package/dist/store/sqlite/tags.d.ts +1 -2
- package/dist/store/sqlite/tags.js +18 -3
- package/dist/store/sqlite/task-board.js +64 -5
- package/dist/store/sqlite/task-close-store.js +4 -3
- package/dist/store/sqlite/task-relations.js +1 -1
- package/dist/store/sqlite/tasks.js +11 -1
- package/dist/store/sqlite/work-tasks.d.ts +45 -8
- package/dist/store/sqlite/work-tasks.js +137 -26
- package/dist/tasks/index.d.ts +1 -0
- package/dist/tasks/index.js +1 -0
- package/dist/tasks/priority.d.ts +18 -0
- package/dist/tasks/priority.js +37 -0
- package/package.json +1 -1
- package/parity.json +2 -6
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,52 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.514.0 — 01.11.2026
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **A failed run's record masks a WhatsApp id as `[id]`**: `<number>[:device]@s.whatsapp.net`, a
|
|
15
|
+
group's `<number>-<number>@g.us`, `@c.us`, `@broadcast`, `@newsletter` and an `@lid` of any length. Before, such an
|
|
16
|
+
id was masked only when it looked like an email, and a 16-digit `@lid` or a `@newsletter` id stayed in the record.
|
|
17
|
+
- **The parity check requires `projects` and `projects evidence` in max and tg**, which ship them in max-cli 0.94.0
|
|
18
|
+
and tg-cli 0.110.0 (tg-cli#798, max-cli#886); wa stays planned.
|
|
19
|
+
|
|
20
|
+
## 0.513.0 — 01.11.2026
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **`tasks.board(query, { limit, offset, sort, descending, zone })` and `tasks.boardCounts(query)`** list the owner's
|
|
25
|
+
project tasks across every project (an account's inbox is left out) with a Lucene query over `PROJECT_TASK_FIELDS`:
|
|
26
|
+
`state`, `kind`, `project` (`project:*` for all), `assignee` (`any`, `none`, `"person:12"`), `priority`,
|
|
27
|
+
`due` and `closed` with ranges, `overdue`, `ready`, and `text`, the words of the title and description
|
|
28
|
+
(prefixes; a task key too). The board sorts by `TASK_SORTS` and returns the total; the counts give how many tasks
|
|
29
|
+
hold each project, assignee, priority and state, each without the query's own terms on that field.
|
|
30
|
+
- **Store version 41: a decimal priority, an agent and a revision on every task** (`minCompatible` stays 34).
|
|
31
|
+
`tasks.priority_score` holds 0 to 1, higher first; `priorityLabel` reads it as low, medium, high or critical by
|
|
32
|
+
quarters. The old 0 (urgent) … 4 (low) became 0.875, 0.625, 0.375, 0.1875 and 0.0625, and the integer `priority`
|
|
33
|
+
column is still written from the score, so an older build reads a familiar number; when an older build writes the
|
|
34
|
+
integer, a trigger moves the score with it. `tasks.agent` names the agent working on a task, apart from its
|
|
35
|
+
assignees and its claim. `tasks.revision` is bumped by a trigger on every update, by any build.
|
|
36
|
+
- **`tasks.move(id, { status, before | after }, by)`** puts a project task beside another in its board column by giving
|
|
37
|
+
it a priority between its neighbours; the answer says when the label changed, and which tasks were spread out again
|
|
38
|
+
when the neighbours left no room. Into an empty column it needs no neighbour.
|
|
39
|
+
- **A stale write is refused**: `edit`, `assign`, `unassign` and `move` take `{ revision }` and refuse another with
|
|
40
|
+
`configuration_error`, reason `task.stale`, and the task's current revision. `assign` and `unassign` now bump the
|
|
41
|
+
task's revision too.
|
|
42
|
+
- **Every task write is in `task_events`**, in the same transaction: new kinds `agent`, `claimed`, `released`,
|
|
43
|
+
`next-check` (a follow-up time used to leave no event), `resolution`, `verdict`, `close-certainty` and `tags` (a
|
|
44
|
+
tag added to or removed from a task). The event id only grows, so it orders changes.
|
|
45
|
+
- **`priority:` and `agent:` query fields** on the board: `priority:high`, `priority:none`, `priority:[medium TO
|
|
46
|
+
critical]`, `priority:[0.6 TO 1]`; `agent:hermes`, `agent:none`.
|
|
47
|
+
- **The `task` entity** may carry `priority_label`, `agent`, `revision`, `assignments`, `ready` and `blocked_by`, as a
|
|
48
|
+
project task list gives them.
|
|
49
|
+
|
|
50
|
+
### Changed — may break callers
|
|
51
|
+
|
|
52
|
+
- **A task's `priority` is a number from 0 to 1, higher first**, in `TaskDetails`, `TaskFields` and the task card
|
|
53
|
+
(`priority_label` beside it); it was 0 (urgent) … 4 (low). Lists order by it highest first. `tasks update
|
|
54
|
+
--priority` takes low, medium, high, critical or none, and still reads 0–4 as before.
|
|
55
|
+
|
|
10
56
|
## 0.512.0 — 01.11.2026
|
|
11
57
|
|
|
12
58
|
### Added
|
|
@@ -8,6 +8,7 @@ import { aheadOf, momentOf } from "../../services/moment.js";
|
|
|
8
8
|
import { unlistedNote } from "../../services/task-categories.js";
|
|
9
9
|
import { ARRIVAL_WINDOW, applyTaskRulesToChats } from "../../services/task-rules.js";
|
|
10
10
|
import { CLAIM_LEASE_MS, closedStateOf, TASK_PAIR_QUERY, TASK_QUERY, taskLinkKindOf, taskTypeOf, } from "../../services/tasks.js";
|
|
11
|
+
import { PRIORITY_LABELS, priorityOf } from "../../tasks/index.js";
|
|
11
12
|
import { actorOf, holderOf as holderIn } from "../author.js";
|
|
12
13
|
import { environmentOf } from "../context.js";
|
|
13
14
|
import { destroysNothing, LOCAL_READ, LOCAL_WRITE, ownerOnlyOption } from "../effects.js";
|
|
@@ -35,6 +36,8 @@ const suggestionLine = ({ as, by, certainty, reason }) => `close suggested: ${as
|
|
|
35
36
|
const card = (task) => [
|
|
36
37
|
`${task.key} ${task.status} ${task.type} ${task.category ?? "—"} ${task.title}`,
|
|
37
38
|
`due ${when(task.dueAt)} next check ${when(task.nextCheckAt)} source ${task.source ?? "—"}`,
|
|
39
|
+
...(task.priorityLabel === null ? [] : [`priority ${task.priorityLabel}`]),
|
|
40
|
+
...(task.agent === null ? [] : [`agent ${task.agent}`]),
|
|
38
41
|
...(task.claimedBy === null ? [] : [`claimed by ${task.claimedBy} until ${when(task.claimedUntil)}`]),
|
|
39
42
|
...task.assignments.filter((one) => one.role === "assignee").map((one) => `assignee ${one.to}`),
|
|
40
43
|
...(task.closeSuggestion === null ? [] : [suggestionLine(task.closeSuggestion)]),
|
|
@@ -205,11 +208,7 @@ export const tasksCommand = (messenger) => {
|
|
|
205
208
|
.description("update a task's text, schedule, category, assignees, priority or parent; nothing reaches the messenger")
|
|
206
209
|
.argument("<task>", "the task's id or key, as tasks list shows it")
|
|
207
210
|
.option("--category <name>", `${categoryHelp}; a change is kept in the task's history`)
|
|
208
|
-
.option("--priority <n>", "
|
|
209
|
-
if (!/^[0-4]$/.test(value))
|
|
210
|
-
throw new CliError("validation_error", "--priority takes 0 through 4");
|
|
211
|
-
return Number(value);
|
|
212
|
-
})
|
|
211
|
+
.option("--priority <n>", `${PRIORITY_LABELS.join(", ")} or none; 0 (urgent) … 4 (low) still read as before`, priorityOf)
|
|
213
212
|
.option("--parent <id>", "parent task in the same project; none clears it"), "send Alex the API token").action(async function (id) {
|
|
214
213
|
const context = messengerContext(this, messenger);
|
|
215
214
|
refuseLocalWrite(context, messenger.app.command, "tasks.update");
|
package/dist/entities/index.d.ts
CHANGED
|
@@ -4765,6 +4765,18 @@ export declare const ENTITIES: {
|
|
|
4765
4765
|
readonly chat: v.OptionalSchema<v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>, undefined>;
|
|
4766
4766
|
readonly source_message: v.OptionalSchema<v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>, undefined>;
|
|
4767
4767
|
readonly priority: v.OptionalSchema<v.NullableSchema<v.NumberSchema<undefined>, undefined>, undefined>;
|
|
4768
|
+
readonly priority_label: v.OptionalSchema<v.NullableSchema<v.PicklistSchema<readonly ["low", "medium", "high", "critical"], undefined>, undefined>, undefined>;
|
|
4769
|
+
readonly agent: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
4770
|
+
readonly revision: v.OptionalSchema<v.NumberSchema<undefined>, undefined>;
|
|
4771
|
+
readonly assignments: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
|
|
4772
|
+
readonly to: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
4773
|
+
readonly role: v.PicklistSchema<["assignee", "reviewer", "watcher"], undefined>;
|
|
4774
|
+
}, undefined>, undefined>, undefined>;
|
|
4775
|
+
readonly ready: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
4776
|
+
readonly blocked_by: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
|
|
4777
|
+
readonly task: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
4778
|
+
readonly state: v.PicklistSchema<["open", "dismissed", "unavailable"], undefined>;
|
|
4779
|
+
}, undefined>, undefined>, undefined>;
|
|
4768
4780
|
readonly parent: v.OptionalSchema<v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>, undefined>;
|
|
4769
4781
|
readonly due_at: v.OptionalSchema<v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>, undefined>;
|
|
4770
4782
|
readonly closed_at: v.OptionalSchema<v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>, undefined>;
|
|
@@ -4822,6 +4834,9 @@ export declare const ENTITIES: {
|
|
|
4822
4834
|
readonly type: v.PicklistSchema<readonly ["bug", "feature", "chore", "question", "request", "mention", "promise"], undefined>;
|
|
4823
4835
|
readonly status: v.PicklistSchema<readonly ["open", "todo", "in_progress", "done", "dismissed"], undefined>;
|
|
4824
4836
|
readonly priority: v.NullableSchema<v.NumberSchema<undefined>, undefined>;
|
|
4837
|
+
readonly priority_label: v.NullableSchema<v.PicklistSchema<readonly ["low", "medium", "high", "critical"], undefined>, undefined>;
|
|
4838
|
+
readonly agent: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
4839
|
+
readonly revision: v.NumberSchema<undefined>;
|
|
4825
4840
|
readonly parent: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
4826
4841
|
readonly source: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
4827
4842
|
readonly due_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
@@ -4923,6 +4938,9 @@ export declare const ENTITIES: {
|
|
|
4923
4938
|
readonly type: v.PicklistSchema<readonly ["bug", "feature", "chore", "question", "request", "mention", "promise"], undefined>;
|
|
4924
4939
|
readonly status: v.PicklistSchema<readonly ["open", "todo", "in_progress", "done", "dismissed"], undefined>;
|
|
4925
4940
|
readonly priority: v.NullableSchema<v.NumberSchema<undefined>, undefined>;
|
|
4941
|
+
readonly priority_label: v.NullableSchema<v.PicklistSchema<readonly ["low", "medium", "high", "critical"], undefined>, undefined>;
|
|
4942
|
+
readonly agent: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
4943
|
+
readonly revision: v.NumberSchema<undefined>;
|
|
4926
4944
|
readonly parent: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
4927
4945
|
readonly source: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
4928
4946
|
readonly due_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
@@ -5025,6 +5043,9 @@ export declare const ENTITIES: {
|
|
|
5025
5043
|
readonly type: v.PicklistSchema<readonly ["bug", "feature", "chore", "question", "request", "mention", "promise"], undefined>;
|
|
5026
5044
|
readonly status: v.PicklistSchema<readonly ["open", "todo", "in_progress", "done", "dismissed"], undefined>;
|
|
5027
5045
|
readonly priority: v.NullableSchema<v.NumberSchema<undefined>, undefined>;
|
|
5046
|
+
readonly priority_label: v.NullableSchema<v.PicklistSchema<readonly ["low", "medium", "high", "critical"], undefined>, undefined>;
|
|
5047
|
+
readonly agent: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
5048
|
+
readonly revision: v.NumberSchema<undefined>;
|
|
5028
5049
|
readonly parent: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
5029
5050
|
readonly source: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
5030
5051
|
readonly due_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
@@ -5155,6 +5176,9 @@ export declare const ENTITIES: {
|
|
|
5155
5176
|
readonly type: v.PicklistSchema<readonly ["bug", "feature", "chore", "question", "request", "mention", "promise"], undefined>;
|
|
5156
5177
|
readonly status: v.PicklistSchema<readonly ["open", "todo", "in_progress", "done", "dismissed"], undefined>;
|
|
5157
5178
|
readonly priority: v.NullableSchema<v.NumberSchema<undefined>, undefined>;
|
|
5179
|
+
readonly priority_label: v.NullableSchema<v.PicklistSchema<readonly ["low", "medium", "high", "critical"], undefined>, undefined>;
|
|
5180
|
+
readonly agent: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
5181
|
+
readonly revision: v.NumberSchema<undefined>;
|
|
5158
5182
|
readonly parent: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
5159
5183
|
readonly source: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
5160
5184
|
readonly due_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
@@ -5250,6 +5274,9 @@ export declare const ENTITIES: {
|
|
|
5250
5274
|
readonly type: v.PicklistSchema<readonly ["bug", "feature", "chore", "question", "request", "mention", "promise"], undefined>;
|
|
5251
5275
|
readonly status: v.PicklistSchema<readonly ["open", "todo", "in_progress", "done", "dismissed"], undefined>;
|
|
5252
5276
|
readonly priority: v.NullableSchema<v.NumberSchema<undefined>, undefined>;
|
|
5277
|
+
readonly priority_label: v.NullableSchema<v.PicklistSchema<readonly ["low", "medium", "high", "critical"], undefined>, undefined>;
|
|
5278
|
+
readonly agent: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
5279
|
+
readonly revision: v.NumberSchema<undefined>;
|
|
5253
5280
|
readonly parent: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
5254
5281
|
readonly source: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
5255
5282
|
readonly due_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
@@ -9,6 +9,9 @@ export declare const taskCard: v.StrictObjectSchema<{
|
|
|
9
9
|
readonly type: v.PicklistSchema<readonly ["bug", "feature", "chore", "question", "request", "mention", "promise"], undefined>;
|
|
10
10
|
readonly status: v.PicklistSchema<readonly ["open", "todo", "in_progress", "done", "dismissed"], undefined>;
|
|
11
11
|
readonly priority: v.NullableSchema<v.NumberSchema<undefined>, undefined>;
|
|
12
|
+
readonly priority_label: v.NullableSchema<v.PicklistSchema<readonly ["low", "medium", "high", "critical"], undefined>, undefined>;
|
|
13
|
+
readonly agent: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
14
|
+
readonly revision: v.NumberSchema<undefined>;
|
|
12
15
|
readonly parent: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
13
16
|
readonly source: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
14
17
|
readonly due_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
@@ -110,6 +113,9 @@ export declare const taskCreated: v.StrictObjectSchema<{
|
|
|
110
113
|
readonly type: v.PicklistSchema<readonly ["bug", "feature", "chore", "question", "request", "mention", "promise"], undefined>;
|
|
111
114
|
readonly status: v.PicklistSchema<readonly ["open", "todo", "in_progress", "done", "dismissed"], undefined>;
|
|
112
115
|
readonly priority: v.NullableSchema<v.NumberSchema<undefined>, undefined>;
|
|
116
|
+
readonly priority_label: v.NullableSchema<v.PicklistSchema<readonly ["low", "medium", "high", "critical"], undefined>, undefined>;
|
|
117
|
+
readonly agent: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
118
|
+
readonly revision: v.NumberSchema<undefined>;
|
|
113
119
|
readonly parent: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
114
120
|
readonly source: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
115
121
|
readonly due_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
@@ -212,6 +218,9 @@ export declare const taskLinks: v.UnionSchema<[v.StrictObjectSchema<{
|
|
|
212
218
|
readonly type: v.PicklistSchema<readonly ["bug", "feature", "chore", "question", "request", "mention", "promise"], undefined>;
|
|
213
219
|
readonly status: v.PicklistSchema<readonly ["open", "todo", "in_progress", "done", "dismissed"], undefined>;
|
|
214
220
|
readonly priority: v.NullableSchema<v.NumberSchema<undefined>, undefined>;
|
|
221
|
+
readonly priority_label: v.NullableSchema<v.PicklistSchema<readonly ["low", "medium", "high", "critical"], undefined>, undefined>;
|
|
222
|
+
readonly agent: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
223
|
+
readonly revision: v.NumberSchema<undefined>;
|
|
215
224
|
readonly parent: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
216
225
|
readonly source: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
217
226
|
readonly due_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
@@ -342,6 +351,9 @@ export declare const taskMerge: v.StrictObjectSchema<{
|
|
|
342
351
|
readonly type: v.PicklistSchema<readonly ["bug", "feature", "chore", "question", "request", "mention", "promise"], undefined>;
|
|
343
352
|
readonly status: v.PicklistSchema<readonly ["open", "todo", "in_progress", "done", "dismissed"], undefined>;
|
|
344
353
|
readonly priority: v.NullableSchema<v.NumberSchema<undefined>, undefined>;
|
|
354
|
+
readonly priority_label: v.NullableSchema<v.PicklistSchema<readonly ["low", "medium", "high", "critical"], undefined>, undefined>;
|
|
355
|
+
readonly agent: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
356
|
+
readonly revision: v.NumberSchema<undefined>;
|
|
345
357
|
readonly parent: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
346
358
|
readonly source: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
347
359
|
readonly due_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
@@ -437,6 +449,9 @@ export declare const taskUnmerge: v.StrictObjectSchema<{
|
|
|
437
449
|
readonly type: v.PicklistSchema<readonly ["bug", "feature", "chore", "question", "request", "mention", "promise"], undefined>;
|
|
438
450
|
readonly status: v.PicklistSchema<readonly ["open", "todo", "in_progress", "done", "dismissed"], undefined>;
|
|
439
451
|
readonly priority: v.NullableSchema<v.NumberSchema<undefined>, undefined>;
|
|
452
|
+
readonly priority_label: v.NullableSchema<v.PicklistSchema<readonly ["low", "medium", "high", "critical"], undefined>, undefined>;
|
|
453
|
+
readonly agent: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
454
|
+
readonly revision: v.NumberSchema<undefined>;
|
|
440
455
|
readonly parent: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
441
456
|
readonly source: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
442
457
|
readonly due_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as v from "valibot";
|
|
2
2
|
import { TASK_TYPES } from "../domain/knowledge.js";
|
|
3
3
|
import { LINK_KINDS, LINK_SOURCES } from "../store/sqlite/link-kinds.js";
|
|
4
|
-
import { TASK_KINDS, TASK_ORIGINS, TASK_STATES, TASK_VERDICTS } from "../tasks/index.js";
|
|
4
|
+
import { PRIORITY_LABELS, TASK_KINDS, TASK_ORIGINS, TASK_STATES, TASK_VERDICTS } from "../tasks/index.js";
|
|
5
5
|
import { count, id, reference, text, time } from "./fields.js";
|
|
6
6
|
import { closeSuggestion } from "./task.js";
|
|
7
7
|
import { taskPair } from "./task-pair.js";
|
|
@@ -30,6 +30,9 @@ export const taskCard = v.strictObject({
|
|
|
30
30
|
type: v.picklist(TASK_TYPES),
|
|
31
31
|
status: v.picklist(TASK_STATES),
|
|
32
32
|
priority: v.nullable(v.number()),
|
|
33
|
+
priority_label: v.nullable(v.picklist(PRIORITY_LABELS)),
|
|
34
|
+
agent: v.nullable(text),
|
|
35
|
+
revision: v.number(),
|
|
33
36
|
parent: v.nullable(reference),
|
|
34
37
|
source: v.nullable(text),
|
|
35
38
|
due_at: nullableTime,
|
package/dist/entities/task.d.ts
CHANGED
|
@@ -24,7 +24,24 @@ export declare const task: v.StrictObjectSchema<{
|
|
|
24
24
|
readonly chat: v.OptionalSchema<v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>, undefined>;
|
|
25
25
|
/** The message it came from. */
|
|
26
26
|
readonly source_message: v.OptionalSchema<v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>, undefined>;
|
|
27
|
+
/** 0 to 1, higher first; `priority_label` reads it as low, medium, high or critical. */
|
|
27
28
|
readonly priority: v.OptionalSchema<v.NullableSchema<v.NumberSchema<undefined>, undefined>, undefined>;
|
|
29
|
+
readonly priority_label: v.OptionalSchema<v.NullableSchema<v.PicklistSchema<readonly ["low", "medium", "high", "critical"], undefined>, undefined>, undefined>;
|
|
30
|
+
/** The agent working on it, apart from its assignees and its claim. */
|
|
31
|
+
readonly agent: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
32
|
+
/** Give it back with `--revision` to refuse a write made on a stale copy. */
|
|
33
|
+
readonly revision: v.OptionalSchema<v.NumberSchema<undefined>, undefined>;
|
|
34
|
+
readonly assignments: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
|
|
35
|
+
readonly to: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
36
|
+
readonly role: v.PicklistSchema<["assignee", "reviewer", "watcher"], undefined>;
|
|
37
|
+
}, undefined>, undefined>, undefined>;
|
|
38
|
+
/** Open work whose blocked-by tasks are all done. */
|
|
39
|
+
readonly ready: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
40
|
+
/** The blocked-by tasks not done yet: open (any open state), dismissed, or unavailable. */
|
|
41
|
+
readonly blocked_by: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
|
|
42
|
+
readonly task: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
43
|
+
readonly state: v.PicklistSchema<["open", "dismissed", "unavailable"], undefined>;
|
|
44
|
+
}, undefined>, undefined>, undefined>;
|
|
28
45
|
readonly parent: v.OptionalSchema<v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>, undefined>;
|
|
29
46
|
readonly due_at: v.OptionalSchema<v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>, undefined>;
|
|
30
47
|
readonly closed_at: v.OptionalSchema<v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>, undefined>;
|
package/dist/entities/task.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as v from "valibot";
|
|
2
2
|
import { TASK_TYPES } from "../domain/knowledge.js";
|
|
3
|
-
import { TASK_ORIGINS, TASK_STATES } from "../tasks/index.js";
|
|
3
|
+
import { PRIORITY_LABELS, TASK_ORIGINS, TASK_STATES } from "../tasks/index.js";
|
|
4
4
|
import { id, reference, text, time } from "./fields.js";
|
|
5
5
|
export const closeSuggestion = v.strictObject({
|
|
6
6
|
as: v.picklist(["done", "dismissed"]),
|
|
@@ -27,7 +27,18 @@ export const task = v.strictObject({
|
|
|
27
27
|
chat: v.optional(v.nullable(reference)),
|
|
28
28
|
/** The message it came from. */
|
|
29
29
|
source_message: v.optional(v.nullable(reference)),
|
|
30
|
+
/** 0 to 1, higher first; `priority_label` reads it as low, medium, high or critical. */
|
|
30
31
|
priority: v.optional(v.nullable(v.number())),
|
|
32
|
+
priority_label: v.optional(v.nullable(v.picklist(PRIORITY_LABELS))),
|
|
33
|
+
/** The agent working on it, apart from its assignees and its claim. */
|
|
34
|
+
agent: v.optional(v.nullable(text)),
|
|
35
|
+
/** Give it back with `--revision` to refuse a write made on a stale copy. */
|
|
36
|
+
revision: v.optional(v.number()),
|
|
37
|
+
assignments: v.optional(v.array(v.strictObject({ to: reference, role: v.picklist(["assignee", "reviewer", "watcher"]) }))),
|
|
38
|
+
/** Open work whose blocked-by tasks are all done. */
|
|
39
|
+
ready: v.optional(v.boolean()),
|
|
40
|
+
/** The blocked-by tasks not done yet: open (any open state), dismissed, or unavailable. */
|
|
41
|
+
blocked_by: v.optional(v.array(v.strictObject({ task: reference, state: v.picklist(["open", "dismissed", "unavailable"]) }))),
|
|
31
42
|
parent: v.optional(v.nullable(reference)),
|
|
32
43
|
due_at: v.optional(v.nullable(time)),
|
|
33
44
|
closed_at: v.optional(v.nullable(time)),
|
package/dist/errors/reasons.js
CHANGED
|
@@ -120,6 +120,7 @@ const OTHERS = [
|
|
|
120
120
|
validation("task.category-unknown", "the category is not in the task category list"),
|
|
121
121
|
validation("task.claimed", "someone else holds the task"),
|
|
122
122
|
validation("task.closed", "the task is already closed"),
|
|
123
|
+
["task.stale", "configuration_error", "the task changed since it was read; read it again"],
|
|
123
124
|
validation("task.dependency-cycle", "the task would depend on itself"),
|
|
124
125
|
validation("task.duplicate-candidates", "open tasks may be the same job: attach to one, or add it as new"),
|
|
125
126
|
validation("task.certainty-required", "an agent closes a task with its certainty, 0 to 1"),
|
|
@@ -117,6 +117,8 @@ export const endpointTemplate = (raw) => {
|
|
|
117
117
|
/** A phone number as people type it with its country code, and one written as digits only without the "+". */
|
|
118
118
|
export const INTERNATIONAL_PHONE = /\+\d[\d\s()-]{6,}\d/.source;
|
|
119
119
|
export const BARE_PHONE = /(^|[^\w-])\d{11,15}\b/.source;
|
|
120
|
+
/** A WhatsApp jid: `<number>[:device]@s.whatsapp.net`, a group's `<number>-<time>@g.us`, an `@lid` of any length. */
|
|
121
|
+
const MESSENGER_ID = /\b\d+(?:-\d+)?(?::\d+)?@(?:s\.whatsapp\.net|g\.us|c\.us|lid|broadcast|newsletter)\b/g;
|
|
120
122
|
/**
|
|
121
123
|
* An error's sentence with what could be somebody's words or a secret taken out: anything in quotes, an email, a
|
|
122
124
|
* phone number, a secret `withoutSecrets` finds, a bot token, a bearer or a long opaque string, a URL's query, the
|
|
@@ -128,6 +130,7 @@ export const redactText = (text) => {
|
|
|
128
130
|
let out = home && home !== "/" ? text.split(home).join("~") : text;
|
|
129
131
|
out = withoutSecrets(out.replace(/"[^"\n]*"|“[^”\n]*”|«[^»\n]*»/g, '"[redacted]"'))
|
|
130
132
|
.replace(/\b(bearer|basic|token|key|password|secret)(\s*[:=]\s*|\s+)[^\s'"]+/gi, "$1$2[redacted]")
|
|
133
|
+
.replace(MESSENGER_ID, "[id]")
|
|
131
134
|
.replace(/[\w.+-]+@[\w-]+\.[\w.-]+/g, "[email]")
|
|
132
135
|
.replace(new RegExp(INTERNATIONAL_PHONE, "g"), "[number]")
|
|
133
136
|
.replace(new RegExp(BARE_PHONE, "g"), "$1[number]")
|
package/dist/store/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type { TagRedirectIndex, TagRedirectResolution, TagRedirectSnapshot } fro
|
|
|
5
5
|
export { createTagRedirectIndex } from "../domain/tag-redirects.js";
|
|
6
6
|
export { note } from "../entities/note.js";
|
|
7
7
|
export type { FindingEvidence, FindingOutcome, MeetingFactFinding, MeetingFinding, MeetingFindingsInput, MeetingFindingsReceipt, MeetingTaskFinding, } from "../meetings/index.js";
|
|
8
|
-
export { CLOSED_STATES, isClosedState, TASK_STATES } from "../tasks/index.js";
|
|
8
|
+
export { CLOSED_STATES, isClosedState, PRIORITY_LABELS, type PriorityLabel, priorityLabel, priorityOf, TASK_STATES, type TaskState, } from "../tasks/index.js";
|
|
9
9
|
export type { AccountMail, AccountMeetings, AccountStore } from "./account-store.js";
|
|
10
10
|
export type { DocumentTransferOptions, DocumentTransferPreview, DocumentTransferReceipt, } from "./document-transfer/index.js";
|
|
11
11
|
export { applyDocumentTransfer, previewDocumentTransfer } from "./document-transfer/index.js";
|
|
@@ -65,6 +65,6 @@ export type { TaskRelation, TaskRelationKind, TaskRelationsStore } from "./sqlit
|
|
|
65
65
|
export { TASK_RELATION_KINDS } from "./sqlite/task-relations.js";
|
|
66
66
|
export type { StoreTaskStore } from "./sqlite/tasks.js";
|
|
67
67
|
export type { TagPreflight, VocabularyAlias, VocabularyAliasReceipt, VocabularyAudit, VocabularyAuditOptions, VocabularyCatalog, VocabularyEntry, VocabularyKind, VocabularyOptions, VocabularyResolution, VocabularyStore, VocabularyTarget, } from "./sqlite/vocabulary.js";
|
|
68
|
-
export type { BoardCounts, BoardOptions, NewWorkTask, TaskAssignment, TaskDetails, TaskEvent, TaskFields, TaskRole, TaskType, WorkTaskType, } from "./sqlite/work-tasks.js";
|
|
68
|
+
export type { BoardCounts, BoardOptions, NewWorkTask, RevisionCheck, TaskAssignment, TaskDetails, TaskEvent, TaskFields, TaskMove, TaskMoved, TaskRole, TaskType, WorkTaskType, } from "./sqlite/work-tasks.js";
|
|
69
69
|
export { TASK_EVENT_KINDS, TASK_ROLES } from "./sqlite/work-tasks.js";
|
|
70
70
|
export { type AccountAccounts, type AccountAttachments, type AccountCounters, type AccountKey, type AccountMentions, type AccountMessageFilter, type AccountMessages, type AccountPeople, type AccountsStore, type AccountVectors, type AttachmentsStore, type AttachmentTextEntry, type AttachmentView, type ChatStats, type ChatsStore, type ConversationsStore, type CountersStore, type DeletionScope, type Delta, type FileAttachment, type IdentityRef, type IngestedVia, type LinkedIdentity, type LinkOptions, MENTION_QUERY, MENTION_STATUSES, type MentionsStore, type MessageFilter, type MessageStore, type MessagesStore, OTHER_SOURCES, openStore, type PeopleStore, type PersonFacts, type PersonRecord, type QueriesStore, type Range, type RankedEvidence, type RankedStoreFound, type RankedStoreRow, type RankingEvidenceItem, type RankingEvidenceRequest, type RankingRequest, type RosterStore, type SearchCommand, type SearchesStore, type SearchIndexStore, type SearchRecord, type StoredAccount, type StoredChatFilter, type StoredHit, type StoredSearch, type StoredTag, type StoreOptions, type SyncStore, type TagFilter, type TagsStore, type TagTarget, type TaskTagTarget, type TextOrigin, type VectorsStore, } from "./store.js";
|
package/dist/store/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { FACT_STATUSES, FACT_TYPES, MEMORY_KINDS, MEMORY_SCOPES, MEMORY_STATUSES, TASK_TYPES, WORK_TASK_TYPES, } from "../domain/knowledge.js";
|
|
2
2
|
export { createTagRedirectIndex } from "../domain/tag-redirects.js";
|
|
3
3
|
export { note } from "../entities/note.js";
|
|
4
|
-
export { CLOSED_STATES, isClosedState, TASK_STATES } from "../tasks/index.js";
|
|
4
|
+
export { CLOSED_STATES, isClosedState, PRIORITY_LABELS, priorityLabel, priorityOf, TASK_STATES, } from "../tasks/index.js";
|
|
5
5
|
export { applyDocumentTransfer, previewDocumentTransfer } from "./document-transfer/index.js";
|
|
6
6
|
export { PRAGMAS } from "./driver.js";
|
|
7
7
|
export { EVIDENCE_FINDINGS_KIND, validateEvidenceFindings, } from "./evidence-findings.js";
|
|
@@ -54,6 +54,7 @@ export const MANIFEST = [
|
|
|
54
54
|
{ name: "20261029224107_version-38-folded-triggers", version: 38, minCompatible: 34 },
|
|
55
55
|
{ name: "20261030114008_version-39-email-locations", version: 39, minCompatible: 34 },
|
|
56
56
|
{ name: "20261030225520_version-40-note-trigrams", version: 40, minCompatible: 34 },
|
|
57
|
+
{ name: "20261031230945_version-41-task-fields", version: 41, minCompatible: 34 },
|
|
57
58
|
];
|
|
58
59
|
export const generatedMigrations = (manifest = MANIFEST, generated = GENERATED) => {
|
|
59
60
|
const migrations = [];
|
|
@@ -847,5 +847,17 @@ export const GENERATED = [
|
|
|
847
847
|
"CREATE TRIGGER document_trigrams_bd BEFORE DELETE ON documents BEGIN\n DELETE FROM document_trigrams WHERE rowid = old.id;\nEND;",
|
|
848
848
|
"-- The trigrams hold JS-normalized text, which SQL cannot produce: fillTrigrams fills the rows stored before this\n-- version, up to `watermark`, and a search reads the rest of them by their text meanwhile.\nINSERT INTO search_index_state (name, watermark, filled_through, terms_through, normalizer_version)\n SELECT 'note_trigrams', coalesce(max(id), 0), 0, 0, 1 FROM notes\n UNION ALL SELECT 'document_trigrams', coalesce(max(id), 0), 0, 0, 1 FROM documents;"
|
|
849
849
|
]
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
"name": "20261031230945_version-41-task-fields",
|
|
853
|
+
"statements": [
|
|
854
|
+
"ALTER TABLE `tasks` ADD `priority_score` real;",
|
|
855
|
+
"ALTER TABLE `tasks` ADD `agent` text;",
|
|
856
|
+
"ALTER TABLE `tasks` ADD `revision` integer DEFAULT 1 NOT NULL;",
|
|
857
|
+
"-- 0 urgent … 4 low land inside the critical, high, medium and low quarters, with room on either side for moves.\nUPDATE tasks SET priority_score = CASE priority WHEN 0 THEN 0.875 WHEN 1 THEN 0.625 WHEN 2 THEN 0.375 WHEN 3 THEN 0.1875 WHEN 4 THEN 0.0625 END WHERE priority IS NOT NULL;",
|
|
858
|
+
"-- A build before version 41 writes only `priority`; these keep the score in step with what it wrote.\nCREATE TRIGGER tasks_priority_score_ai AFTER INSERT ON tasks WHEN new.priority IS NOT NULL AND new.priority_score IS NULL BEGIN\n UPDATE tasks SET priority_score = CASE new.priority WHEN 0 THEN 0.875 WHEN 1 THEN 0.625 WHEN 2 THEN 0.375 WHEN 3 THEN 0.1875 WHEN 4 THEN 0.0625 END WHERE id = new.id;\nEND;",
|
|
859
|
+
"CREATE TRIGGER tasks_priority_score_au AFTER UPDATE OF priority ON tasks WHEN new.priority IS NOT old.priority AND new.priority_score IS old.priority_score BEGIN\n UPDATE tasks SET priority_score = CASE new.priority WHEN 0 THEN 0.875 WHEN 1 THEN 0.625 WHEN 2 THEN 0.375 WHEN 3 THEN 0.1875 WHEN 4 THEN 0.0625 END WHERE id = new.id;\nEND;",
|
|
860
|
+
"-- Every build's write bumps it, older ones too, so a stale copy is caught whoever wrote last.\nCREATE TRIGGER tasks_revision_au AFTER UPDATE ON tasks WHEN new.revision IS old.revision BEGIN\n UPDATE tasks SET revision = old.revision + 1 WHERE id = new.id;\nEND;"
|
|
861
|
+
]
|
|
850
862
|
}
|
|
851
863
|
];
|
|
@@ -12683,6 +12683,54 @@ export declare const tasks: import("drizzle-orm/sqlite-core").SQLiteTableWithCol
|
|
|
12683
12683
|
identity: undefined;
|
|
12684
12684
|
generated: undefined;
|
|
12685
12685
|
}, {}>;
|
|
12686
|
+
priorityScore: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
12687
|
+
name: string;
|
|
12688
|
+
tableName: "tasks";
|
|
12689
|
+
dataType: "number double";
|
|
12690
|
+
data: number;
|
|
12691
|
+
driverParam: number;
|
|
12692
|
+
notNull: false;
|
|
12693
|
+
hasDefault: false;
|
|
12694
|
+
isPrimaryKey: false;
|
|
12695
|
+
isAutoincrement: false;
|
|
12696
|
+
hasRuntimeDefault: false;
|
|
12697
|
+
enumValues: undefined;
|
|
12698
|
+
baseColumn: never;
|
|
12699
|
+
identity: undefined;
|
|
12700
|
+
generated: undefined;
|
|
12701
|
+
}, {}>;
|
|
12702
|
+
agent: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
12703
|
+
name: string;
|
|
12704
|
+
tableName: "tasks";
|
|
12705
|
+
dataType: "string";
|
|
12706
|
+
data: string;
|
|
12707
|
+
driverParam: string;
|
|
12708
|
+
notNull: false;
|
|
12709
|
+
hasDefault: false;
|
|
12710
|
+
isPrimaryKey: false;
|
|
12711
|
+
isAutoincrement: false;
|
|
12712
|
+
hasRuntimeDefault: false;
|
|
12713
|
+
enumValues: [string, ...string[]];
|
|
12714
|
+
baseColumn: never;
|
|
12715
|
+
identity: undefined;
|
|
12716
|
+
generated: undefined;
|
|
12717
|
+
}, {}>;
|
|
12718
|
+
revision: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
12719
|
+
name: string;
|
|
12720
|
+
tableName: "tasks";
|
|
12721
|
+
dataType: "number int53";
|
|
12722
|
+
data: number;
|
|
12723
|
+
driverParam: number;
|
|
12724
|
+
notNull: true;
|
|
12725
|
+
hasDefault: true;
|
|
12726
|
+
isPrimaryKey: false;
|
|
12727
|
+
isAutoincrement: false;
|
|
12728
|
+
hasRuntimeDefault: false;
|
|
12729
|
+
enumValues: undefined;
|
|
12730
|
+
baseColumn: never;
|
|
12731
|
+
identity: undefined;
|
|
12732
|
+
generated: undefined;
|
|
12733
|
+
}, {}>;
|
|
12686
12734
|
};
|
|
12687
12735
|
dialect: 'sqlite';
|
|
12688
12736
|
isAlias: false;
|
|
@@ -1266,6 +1266,9 @@ export const tasks = sqliteTable("tasks", {
|
|
|
1266
1266
|
closeSuggestedCertainty: real("close_suggested_certainty"),
|
|
1267
1267
|
closeSuggestedReason: text("close_suggested_reason"),
|
|
1268
1268
|
closeSuggestedAt: integer("close_suggested_at"),
|
|
1269
|
+
priorityScore: real("priority_score"),
|
|
1270
|
+
agent: text("agent"),
|
|
1271
|
+
revision: integer("revision").notNull().default(1),
|
|
1269
1272
|
}, (table) => [
|
|
1270
1273
|
unique().on(table.projectId, table.number),
|
|
1271
1274
|
index("tasks_by_title_folded").on(table.titleFolded),
|
|
@@ -73,10 +73,9 @@ export declare const canonicalTagNamed: (database: CacheDatabase, name: string)
|
|
|
73
73
|
export declare const createTag: (context: StoreContext, name: string, kind: TagKind, by: "owner" | "agent", options?: TagCreationOptions) => TagRow;
|
|
74
74
|
/** The tag of that name, made as a plain tag when no tag or topic holds the name yet. */
|
|
75
75
|
export declare const ensureTag: (context: StoreContext, name: string) => TagRow;
|
|
76
|
-
/** The tags it did not have before, in the order given; a tag the owner adds becomes the owner's. */
|
|
77
76
|
export declare const addTags: (context: StoreContext, thing: Thing, tags: string[], source?: TaggingSource, author?: Actor) => string[];
|
|
78
77
|
/** An explicit removal withdraws the effective label, restricted to the requested origin when supplied. */
|
|
79
|
-
export declare const removeTags: (
|
|
78
|
+
export declare const removeTags: (context: StoreContext, thing: Thing, tags: string[], source?: "manual" | "auto") => string[];
|
|
80
79
|
export interface Label {
|
|
81
80
|
tag: string;
|
|
82
81
|
/** `file` when only a note's file states it; the owner's own label otherwise. */
|
|
@@ -111,13 +111,23 @@ export const ensureTag = (context, name) => {
|
|
|
111
111
|
return canonicalTagNamed(context.database, name) ?? createTag(context, name, "tag", "owner");
|
|
112
112
|
};
|
|
113
113
|
/** The tags it did not have before, in the order given; a tag the owner adds becomes the owner's. */
|
|
114
|
+
/** A task's tag change is part of its history, and of its revision. */
|
|
115
|
+
const taskTagsChanged = (context, thing, changes, author) => {
|
|
116
|
+
if (thing.type !== "task" || [...(changes.added ?? []), ...(changes.removed ?? [])].length === 0)
|
|
117
|
+
return;
|
|
118
|
+
const at = context.now();
|
|
119
|
+
context.database.prepare("UPDATE tasks SET updated_at = ? WHERE id = ?").run(at, thing.id);
|
|
120
|
+
context.database
|
|
121
|
+
.prepare("INSERT INTO task_events (task_id, actor_type, actor_id, kind, changes, created_at) VALUES (?, ?, ?, 'tags', ?, ?)")
|
|
122
|
+
.run(thing.id, author?.type ?? null, author?.id ?? null, JSON.stringify({ tags: [changes.removed ?? [], changes.added ?? []] }), at);
|
|
123
|
+
};
|
|
114
124
|
export const addTags = (context, thing, tags, source = "owner", author) => {
|
|
115
125
|
const { database, now } = context;
|
|
116
126
|
const insert = database.prepare("INSERT INTO taggings (tag_id, taggable_type, taggable_id, main, source, author_type, author_id, created_at, updated_at) " +
|
|
117
127
|
"VALUES (?, ?, ?, 0, ?, ?, ?, ?, ?) ON CONFLICT (tag_id, taggable_type, taggable_id) DO NOTHING");
|
|
118
128
|
const claim = database.prepare("UPDATE taggings SET source = 'owner', updated_at = ? WHERE tag_id = ? AND taggable_type = ? AND taggable_id = ? AND source <> 'owner'");
|
|
119
129
|
const at = now();
|
|
120
|
-
|
|
130
|
+
const fresh = tags.filter((name) => {
|
|
121
131
|
const tag = ensureTag(context, name);
|
|
122
132
|
const held = database
|
|
123
133
|
.prepare("SELECT source,author_type,author_id FROM taggings WHERE tag_id IN (SELECT ? UNION ALL SELECT source_id FROM tag_redirects WHERE survivor_id=?) AND taggable_type=? AND taggable_id=?")
|
|
@@ -129,11 +139,14 @@ export const addTags = (context, thing, tags, source = "owner", author) => {
|
|
|
129
139
|
claim.run(at, tag.id, thing.type, thing.id);
|
|
130
140
|
return added && held.length === 0;
|
|
131
141
|
});
|
|
142
|
+
taskTagsChanged(context, thing, { added: fresh }, author);
|
|
143
|
+
return fresh;
|
|
132
144
|
};
|
|
133
145
|
/** An explicit removal withdraws the effective label, restricted to the requested origin when supplied. */
|
|
134
|
-
export const removeTags = (
|
|
146
|
+
export const removeTags = (context, thing, tags, source) => {
|
|
147
|
+
const { database } = context;
|
|
135
148
|
const remove = database.prepare("DELETE FROM taggings WHERE tag_id=? AND taggable_type=? AND taggable_id=?");
|
|
136
|
-
|
|
149
|
+
const removed = tags.filter((name) => {
|
|
137
150
|
const tag = canonicalTagNamed(database, name);
|
|
138
151
|
if (!tag)
|
|
139
152
|
return false;
|
|
@@ -171,6 +184,8 @@ export const removeTags = ({ database }, thing, tags, source) => {
|
|
|
171
184
|
}
|
|
172
185
|
return changed;
|
|
173
186
|
});
|
|
187
|
+
taskTagsChanged(context, thing, { removed });
|
|
188
|
+
return removed;
|
|
174
189
|
};
|
|
175
190
|
/** A thing's tags by name, with whether only a file states each. */
|
|
176
191
|
/** A tagging of something still there: a deleted document keeps its tags but is not counted or listed by them. */
|
|
@@ -2,7 +2,7 @@ import { CliError } from "@wirecat/cli-base";
|
|
|
2
2
|
import { OPERATORS_BY_TYPE, walkQuery, } from "@wirecat/cli-base/query";
|
|
3
3
|
import { TASK_TYPES } from "../../domain/knowledge.js";
|
|
4
4
|
import { taskWordsOf } from "../../search/task-similarity.js";
|
|
5
|
-
import { TASK_STATES } from "../../tasks/index.js";
|
|
5
|
+
import { PRIORITY_LABELS, priorityBand, TASK_STATES } from "../../tasks/index.js";
|
|
6
6
|
import { INBOX_KEY_LENGTH } from "./task-keys.js";
|
|
7
7
|
export const TASK_SORTS = ["priority", "due", "created", "updated", "key", "state"];
|
|
8
8
|
const field = (name, type, example, more = {}) => ({
|
|
@@ -22,7 +22,11 @@ export const PROJECT_TASK_FIELDS = [
|
|
|
22
22
|
field("assignee", "kw", 'assignee:"person:12"', {
|
|
23
23
|
relation: "who does it: any, none, or a person (person:12 in quotes, or the bare 12)",
|
|
24
24
|
}),
|
|
25
|
-
field("priority", "
|
|
25
|
+
field("priority", "kw", "priority:high", {
|
|
26
|
+
operators: ["term", "range", "wildcard"],
|
|
27
|
+
relation: "low, medium, high, critical or none; a range of labels or of the 0–1 number: priority:[high TO critical]",
|
|
28
|
+
}),
|
|
29
|
+
field("agent", "kw", "agent:hermes", { relation: "the agent working on it, by name; none or any" }),
|
|
26
30
|
field("due", "time", "due:[today TO 2026-12-31]", { relation: "the due date; NOT due:* has none" }),
|
|
27
31
|
field("overdue", "bool", "overdue:true", { relation: "open work whose due date has passed" }),
|
|
28
32
|
field("closed", "time", "closed:>=14d", { relation: "when it was done or dismissed" }),
|
|
@@ -69,6 +73,60 @@ const assigneeWhere = (node) => {
|
|
|
69
73
|
throw new CliError("validation_error", `assignee: takes any, none or "person:<id>", not "${node.value}"`);
|
|
70
74
|
return { sql: `EXISTS (${ASSIGNED} AND a.assignee_id = ?)`, params: [Number(id)] };
|
|
71
75
|
};
|
|
76
|
+
const bound = (value, side, node) => {
|
|
77
|
+
const word = value.trim().toLowerCase();
|
|
78
|
+
if (word === "*")
|
|
79
|
+
return undefined;
|
|
80
|
+
if (PRIORITY_LABELS.includes(word))
|
|
81
|
+
return priorityBand(word)[side];
|
|
82
|
+
const number = Number(word);
|
|
83
|
+
if (word === "" || !Number.isFinite(number) || number < 0 || number > 1)
|
|
84
|
+
throw new CliError("validation_error", `priority: takes ${PRIORITY_LABELS.join(", ")}, none, or 0 to 1, not "${node.value}"`);
|
|
85
|
+
return number;
|
|
86
|
+
};
|
|
87
|
+
/** A label is its quarter; critical takes 1 itself. */
|
|
88
|
+
const priorityWhere = (node) => {
|
|
89
|
+
const score = "t.priority_score";
|
|
90
|
+
if (node.operator === "wildcard") {
|
|
91
|
+
if (node.value === "*")
|
|
92
|
+
return { sql: `${score} IS NOT NULL`, params: [] };
|
|
93
|
+
throw new CliError("validation_error", "priority: takes a label, none, or a range");
|
|
94
|
+
}
|
|
95
|
+
if (node.operator !== "range") {
|
|
96
|
+
const word = node.value.trim().toLowerCase();
|
|
97
|
+
if (word === "none")
|
|
98
|
+
return { sql: `${score} IS NULL`, params: [] };
|
|
99
|
+
if (!PRIORITY_LABELS.includes(word))
|
|
100
|
+
throw new CliError("validation_error", `priority: takes ${PRIORITY_LABELS.join(", ")} or none; a number goes in a range`);
|
|
101
|
+
const { from, to } = priorityBand(word);
|
|
102
|
+
return { sql: `${score} >= ? AND ${score} ${to === 1 ? "<=" : "<"} ?`, params: [from, to] };
|
|
103
|
+
}
|
|
104
|
+
const lower = bound(node.value, "from", node);
|
|
105
|
+
const upperWord = (node.upper ?? "*").trim().toLowerCase();
|
|
106
|
+
const upper = bound(upperWord, "to", node);
|
|
107
|
+
const labelUpper = PRIORITY_LABELS.includes(upperWord);
|
|
108
|
+
const parts = [`${score} IS NOT NULL`];
|
|
109
|
+
const params = [];
|
|
110
|
+
if (lower !== undefined) {
|
|
111
|
+
parts.push(`${score} ${node.lower_inclusive === false ? ">" : ">="} ?`);
|
|
112
|
+
params.push(lower);
|
|
113
|
+
}
|
|
114
|
+
if (upper !== undefined) {
|
|
115
|
+
parts.push(`${score} ${labelUpper ? (upper === 1 ? "<=" : "<") : node.upper_inclusive === false ? "<" : "<="} ?`);
|
|
116
|
+
params.push(upper);
|
|
117
|
+
}
|
|
118
|
+
return { sql: parts.join(" AND "), params };
|
|
119
|
+
};
|
|
120
|
+
const agentWhere = (node) => {
|
|
121
|
+
const value = node.value.trim().toLowerCase();
|
|
122
|
+
if (value === "none")
|
|
123
|
+
return { sql: "t.agent IS NULL", params: [] };
|
|
124
|
+
if (value === "any" || (node.operator === "wildcard" && value === "*"))
|
|
125
|
+
return { sql: "t.agent IS NOT NULL", params: [] };
|
|
126
|
+
return node.operator === "wildcard"
|
|
127
|
+
? { sql: "lower(t.agent) GLOB ?", params: [value] }
|
|
128
|
+
: { sql: "lower(t.agent) = ?", params: [value] };
|
|
129
|
+
};
|
|
72
130
|
const truth = (node, sql) => ({
|
|
73
131
|
sql: node.value.toLowerCase() === "true" ? sql : `NOT (${sql})`,
|
|
74
132
|
params: [],
|
|
@@ -79,7 +137,8 @@ export const projectTaskColumns = (now) => ({
|
|
|
79
137
|
kind: { type: "enum", sql: "t.type" },
|
|
80
138
|
project: { where: projectWhere },
|
|
81
139
|
assignee: { where: assigneeWhere },
|
|
82
|
-
priority: {
|
|
140
|
+
priority: { where: priorityWhere },
|
|
141
|
+
agent: { where: agentWhere },
|
|
83
142
|
due: { type: "time", sql: "t.due_at" },
|
|
84
143
|
closed: { type: "time", sql: "t.closed_at" },
|
|
85
144
|
overdue: { where: (node) => truth(node, `${OPEN} AND t.due_at IS NOT NULL AND t.due_at < ${Number(now)}`) },
|
|
@@ -104,14 +163,14 @@ const STATE_ORDER = `CASE t.status ${TASK_STATES.map((state, at) => `WHEN '${sta
|
|
|
104
163
|
export const boardOrder = (sort, descending) => {
|
|
105
164
|
const direction = descending ? "DESC" : "ASC";
|
|
106
165
|
const first = {
|
|
107
|
-
priority: [
|
|
166
|
+
priority: ["t.priority_score IS NULL", `t.priority_score ${descending ? "ASC" : "DESC"}`],
|
|
108
167
|
due: ["t.due_at IS NULL", `t.due_at ${direction}`],
|
|
109
168
|
created: [`t.created_at ${direction}`],
|
|
110
169
|
updated: [`t.updated_at ${direction}`],
|
|
111
170
|
key: [`(SELECT key FROM projects WHERE id = t.project_id) ${direction}`, `t.number ${direction}`],
|
|
112
171
|
state: [`${STATE_ORDER} ${direction}`],
|
|
113
172
|
}[sort];
|
|
114
|
-
return [...first, "
|
|
173
|
+
return [...first, "t.priority_score IS NULL", "t.priority_score DESC", "t.created_at", "t.id"].join(", ");
|
|
115
174
|
};
|
|
116
175
|
/** The query without its required terms on `name`: a picker counts the choices its own filter hides. */
|
|
117
176
|
export const withoutField = (query, name) => {
|
|
@@ -52,9 +52,10 @@ export const taskCloseOver = (context) => {
|
|
|
52
52
|
}),
|
|
53
53
|
recordCloseCertainty: async (id, certainty) => write(database, () => {
|
|
54
54
|
const row = liveRow(id);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
const value = certaintyOf(certainty);
|
|
56
|
+
database.prepare("UPDATE tasks SET close_certainty = ? WHERE id = ?").run(value, Number(row.id));
|
|
57
|
+
if ((row.close_certainty ?? null) !== value)
|
|
58
|
+
recordTaskEvent(database, Number(row.id), null, "close-certainty", { close_certainty: [row.close_certainty ?? null, value] }, now());
|
|
58
59
|
}),
|
|
59
60
|
};
|
|
60
61
|
};
|
|
@@ -155,7 +155,7 @@ export const taskRelationsStoreOver = (context, record) => {
|
|
|
155
155
|
if (!row)
|
|
156
156
|
throw new CliError("not_found", "the owner project is unavailable");
|
|
157
157
|
const rows = database
|
|
158
|
-
.prepare("SELECT coalesce(t.package_id,t.key) AS id,t.key,substr(t.title,1,300) AS title FROM tasks t WHERE t.project_id=? AND t.status NOT IN ('done','dismissed') AND t.deleted_at IS NULL AND NOT EXISTS (SELECT 1 FROM links l LEFT JOIN tasks b ON b.id=l.to_id LEFT JOIN projects p ON p.id=b.project_id WHERE l.from_type='task' AND l.to_type='task' AND l.kind='blocked-by' AND l.from_id=t.id AND l.confirmed=1 AND l.anchor IS NULL AND (b.id IS NULL OR b.deleted_at IS NOT NULL OR p.deleted_at IS NOT NULL OR b.status!='done')) ORDER BY
|
|
158
|
+
.prepare("SELECT coalesce(t.package_id,t.key) AS id,t.key,substr(t.title,1,300) AS title FROM tasks t WHERE t.project_id=? AND t.status NOT IN ('done','dismissed') AND t.deleted_at IS NULL AND NOT EXISTS (SELECT 1 FROM links l LEFT JOIN tasks b ON b.id=l.to_id LEFT JOIN projects p ON p.id=b.project_id WHERE l.from_type='task' AND l.to_type='task' AND l.kind='blocked-by' AND l.from_id=t.id AND l.confirmed=1 AND l.anchor IS NULL AND (b.id IS NULL OR b.deleted_at IS NOT NULL OR p.deleted_at IS NOT NULL OR b.status!='done')) ORDER BY t.priority_score IS NULL,t.priority_score DESC,t.number,t.id LIMIT ?")
|
|
159
159
|
.all(Number(row.id), limit + 1);
|
|
160
160
|
return {
|
|
161
161
|
items: rows
|
|
@@ -197,7 +197,10 @@ export const taskStoreOver = (context) => {
|
|
|
197
197
|
const row = required(id);
|
|
198
198
|
const at = now();
|
|
199
199
|
write(database, () => {
|
|
200
|
+
const before = database.prepare("SELECT resolution FROM tasks WHERE id = ?").get(row)?.resolution ?? null;
|
|
200
201
|
database.prepare("UPDATE tasks SET resolution = ?, updated_at = ? WHERE id = ?").run(text, at, row);
|
|
202
|
+
if (before !== text)
|
|
203
|
+
recordTaskEvent(database, row, null, "resolution", { resolution: [before, text] }, at);
|
|
201
204
|
if (!by)
|
|
202
205
|
return;
|
|
203
206
|
database
|
|
@@ -214,7 +217,14 @@ export const taskStoreOver = (context) => {
|
|
|
214
217
|
judge: async (id, verdict) => {
|
|
215
218
|
if (verdict !== null && !TASK_VERDICTS.includes(verdict))
|
|
216
219
|
throw new CliError("validation_error", `a verdict is one of ${TASK_VERDICTS.join(", ")}`);
|
|
217
|
-
|
|
220
|
+
const row = required(id);
|
|
221
|
+
write(database, () => {
|
|
222
|
+
const at = now();
|
|
223
|
+
const before = database.prepare("SELECT verdict FROM tasks WHERE id = ?").get(row)?.verdict ?? null;
|
|
224
|
+
database.prepare("UPDATE tasks SET verdict = ?, updated_at = ? WHERE id = ?").run(verdict, at, row);
|
|
225
|
+
if (before !== verdict)
|
|
226
|
+
recordTaskEvent(database, row, null, "verdict", { verdict: [before, verdict] }, at);
|
|
227
|
+
});
|
|
218
228
|
},
|
|
219
229
|
};
|
|
220
230
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { QueryAst } from "@wirecat/cli-base/query";
|
|
2
2
|
import { TASK_TYPES, WORK_TASK_TYPES } from "../../domain/knowledge.js";
|
|
3
|
-
import { type TaskState, type TaskVerdict } from "../../tasks/index.js";
|
|
3
|
+
import { type PriorityLabel, type TaskState, type TaskVerdict } from "../../tasks/index.js";
|
|
4
4
|
import type { CacheDatabase } from "../driver.js";
|
|
5
5
|
import type { Actor } from "./actors.js";
|
|
6
6
|
import { type Author } from "./memories.js";
|
|
@@ -10,15 +10,17 @@ import { type TaskSort } from "./task-board.js";
|
|
|
10
10
|
import { type CloseSuggestion } from "./task-close.js";
|
|
11
11
|
import { type TaskRelationsStore } from "./task-relations.js";
|
|
12
12
|
export declare const TASK_ROLES: readonly ["assignee", "reviewer", "watcher"];
|
|
13
|
-
export declare const TASK_EVENT_KINDS: readonly ["created", "title", "description", "status", "assigned", "unassigned", "due", "priority", "moved", "linked", "unlinked", "category", "close-suggestion"];
|
|
13
|
+
export declare const TASK_EVENT_KINDS: readonly ["created", "title", "description", "status", "assigned", "unassigned", "due", "priority", "moved", "linked", "unlinked", "category", "close-suggestion", "agent", "claimed", "released", "next-check", "resolution", "verdict", "close-certainty", "tags"];
|
|
14
14
|
export type WorkTaskType = (typeof WORK_TASK_TYPES)[number];
|
|
15
15
|
export type TaskType = (typeof TASK_TYPES)[number];
|
|
16
16
|
export type TaskRole = (typeof TASK_ROLES)[number];
|
|
17
17
|
export interface TaskFields {
|
|
18
18
|
title?: string;
|
|
19
19
|
description?: string | null;
|
|
20
|
-
/** 0
|
|
20
|
+
/** 0 to 1, higher first: `priorityOf` reads a label or the old 0–4. */
|
|
21
21
|
priority?: number | null;
|
|
22
|
+
/** The agent working on it, by name; `null` clears it. */
|
|
23
|
+
agent?: string | null;
|
|
22
24
|
/** A task reference (`task:<id>`) or id in the same project; `null` makes it top-level. */
|
|
23
25
|
parent?: string | null;
|
|
24
26
|
dueAt?: Date | null;
|
|
@@ -50,7 +52,12 @@ export interface TaskDetails {
|
|
|
50
52
|
description: string | null;
|
|
51
53
|
type: TaskType;
|
|
52
54
|
status: TaskState;
|
|
55
|
+
/** 0 to 1, higher first. */
|
|
53
56
|
priority: number | null;
|
|
57
|
+
priorityLabel: PriorityLabel | null;
|
|
58
|
+
agent: string | null;
|
|
59
|
+
/** Bumped by every write; give it back to refuse a write made on a stale copy. */
|
|
60
|
+
revision: number;
|
|
54
61
|
parent: string | null;
|
|
55
62
|
/** What a task found in a message points at; null for a task made here. */
|
|
56
63
|
source: string | null;
|
|
@@ -71,6 +78,26 @@ export interface TaskDetails {
|
|
|
71
78
|
createdAt: string;
|
|
72
79
|
updatedAt: string;
|
|
73
80
|
}
|
|
81
|
+
export interface RevisionCheck {
|
|
82
|
+
/** The revision the caller read; a write on any other is refused (`task.stale`). */
|
|
83
|
+
revision?: number;
|
|
84
|
+
}
|
|
85
|
+
export interface TaskMove {
|
|
86
|
+
status?: TaskState;
|
|
87
|
+
before?: string;
|
|
88
|
+
after?: string;
|
|
89
|
+
}
|
|
90
|
+
export interface TaskMoved {
|
|
91
|
+
task: TaskDetails;
|
|
92
|
+
/** The labels before and after, which differ when the move crossed a band edge. */
|
|
93
|
+
label: {
|
|
94
|
+
from: PriorityLabel | null;
|
|
95
|
+
to: PriorityLabel | null;
|
|
96
|
+
changed: boolean;
|
|
97
|
+
};
|
|
98
|
+
/** Other tasks of the column given a new priority to make room, by key. */
|
|
99
|
+
renumbered: string[];
|
|
100
|
+
}
|
|
74
101
|
export interface BoardOptions {
|
|
75
102
|
limit: number;
|
|
76
103
|
offset?: number;
|
|
@@ -93,8 +120,9 @@ export interface BoardCounts {
|
|
|
93
120
|
name: string | null;
|
|
94
121
|
count: number;
|
|
95
122
|
}[];
|
|
123
|
+
/** By label; `none` for the tasks with no priority. */
|
|
96
124
|
priority: {
|
|
97
|
-
value:
|
|
125
|
+
value: PriorityLabel | "none";
|
|
98
126
|
count: number;
|
|
99
127
|
}[];
|
|
100
128
|
state: {
|
|
@@ -114,9 +142,15 @@ export interface TaskEvent {
|
|
|
114
142
|
/** Tasks beyond the package's four message kinds, and the fields the package does not know, on any task. */
|
|
115
143
|
export interface WorkTasks extends TaskRelationsStore {
|
|
116
144
|
create(input: NewWorkTask, by: Author): Promise<TaskDetails>;
|
|
117
|
-
edit(id: string, fields: TaskFields, by: Author): Promise<TaskDetails>;
|
|
118
|
-
assign(id: string, assignment: TaskAssignment, by: Author): Promise<TaskDetails>;
|
|
119
|
-
unassign(id: string, assignment: TaskAssignment, by: Author): Promise<TaskDetails>;
|
|
145
|
+
edit(id: string, fields: TaskFields, by: Author, check?: RevisionCheck): Promise<TaskDetails>;
|
|
146
|
+
assign(id: string, assignment: TaskAssignment, by: Author, check?: RevisionCheck): Promise<TaskDetails>;
|
|
147
|
+
unassign(id: string, assignment: TaskAssignment, by: Author, check?: RevisionCheck): Promise<TaskDetails>;
|
|
148
|
+
/**
|
|
149
|
+
* Puts a project task just before or after another in a board column, by giving it a priority between its new
|
|
150
|
+
* neighbours; `status` moves it to that column in the same write. When the neighbours leave no room, that
|
|
151
|
+
* priority band of the column is spread out again, and each task given a new number is named.
|
|
152
|
+
*/
|
|
153
|
+
move(id: string, move: TaskMove, by: Author, check?: RevisionCheck): Promise<TaskMoved>;
|
|
120
154
|
/** `person:<id>` of whoever sent the task's source message; `undefined` for the owner, a note or an unknown sender. */
|
|
121
155
|
sourceSender(id: string): Promise<string | undefined>;
|
|
122
156
|
details(id: string): Promise<TaskDetails>;
|
|
@@ -159,10 +193,13 @@ export interface WorkTasks extends TaskRelationsStore {
|
|
|
159
193
|
/** Gives a task back; only its holder or the owner may. */
|
|
160
194
|
release(id: string, holder: string): Promise<TaskDetails>;
|
|
161
195
|
}
|
|
196
|
+
type Row = Record<string, unknown>;
|
|
162
197
|
export declare const recordTaskEvent: (database: CacheDatabase, task: number, actor: Actor | null, kind: TaskEvent["kind"], changes: TaskEvent["changes"], at: number) => void;
|
|
198
|
+
/** What a write made on a stale copy is refused with: the revision the caller read is not the task's now. */
|
|
199
|
+
export declare const checkRevision: (row: Row, expected: number | undefined) => void;
|
|
163
200
|
type WorkTaskOperations = Omit<WorkTasks, "create" | "assign"> & {
|
|
164
201
|
create(input: NewWorkTask, by: Author): TaskDetails;
|
|
165
|
-
assign(id: string, assignment: TaskAssignment, by: Author): TaskDetails;
|
|
202
|
+
assign(id: string, assignment: TaskAssignment, by: Author, check?: RevisionCheck): TaskDetails;
|
|
166
203
|
};
|
|
167
204
|
export declare const workTaskOperationsOver: (context: StoreContext) => WorkTaskOperations;
|
|
168
205
|
export declare const workTasksOver: (context: StoreContext) => WorkTasks;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { TASK_TYPES, WORK_TASK_TYPES } from "../../domain/knowledge.js";
|
|
3
|
-
import { isClosedState, TASK_STATES } from "../../tasks/index.js";
|
|
3
|
+
import { checkedScore, isClosedState, legacyPriority, PRIORITY_LABELS, priorityBand, priorityLabel, TASK_STATES, } from "../../tasks/index.js";
|
|
4
4
|
import { folded } from "./folded.js";
|
|
5
5
|
import { authorActor } from "./memories.js";
|
|
6
6
|
import { CORPORA, drainCorpus } from "./note-index.js";
|
|
@@ -28,6 +28,14 @@ export const TASK_EVENT_KINDS = [
|
|
|
28
28
|
"unlinked",
|
|
29
29
|
"category",
|
|
30
30
|
"close-suggestion",
|
|
31
|
+
"agent",
|
|
32
|
+
"claimed",
|
|
33
|
+
"released",
|
|
34
|
+
"next-check",
|
|
35
|
+
"resolution",
|
|
36
|
+
"verdict",
|
|
37
|
+
"close-certainty",
|
|
38
|
+
"tags",
|
|
31
39
|
];
|
|
32
40
|
const iso = (value) => (value == null ? null : new Date(Number(value)).toISOString());
|
|
33
41
|
export const recordTaskEvent = (database, task, actor, kind, changes, at) => {
|
|
@@ -45,10 +53,11 @@ const textOf = (value, what, max) => {
|
|
|
45
53
|
const claimOf = (row, at) => row.claimed_by != null && Number(row.claimed_until) >= at
|
|
46
54
|
? { claimedBy: String(row.claimed_by), claimedUntil: iso(row.claimed_until) }
|
|
47
55
|
: { claimedBy: null, claimedUntil: null };
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
56
|
+
/** What a write made on a stale copy is refused with: the revision the caller read is not the task's now. */
|
|
57
|
+
export const checkRevision = (row, expected) => {
|
|
58
|
+
if (expected === undefined || Number(row.revision) === expected)
|
|
59
|
+
return;
|
|
60
|
+
throw new CliError("configuration_error", `task ${String(row.key)} changed since revision ${expected}; it is at ${String(row.revision)}: read it again`, { reason: "task.stale", revision: Number(row.revision) });
|
|
52
61
|
};
|
|
53
62
|
export const workTaskOperationsOver = (context) => {
|
|
54
63
|
const { database, now } = context;
|
|
@@ -104,7 +113,10 @@ export const workTaskOperationsOver = (context) => {
|
|
|
104
113
|
description: row.description == null ? null : String(row.description),
|
|
105
114
|
type: row.type,
|
|
106
115
|
status: row.status,
|
|
107
|
-
priority: row.
|
|
116
|
+
priority: row.priority_score == null ? null : Number(row.priority_score),
|
|
117
|
+
priorityLabel: priorityLabel(row.priority_score == null ? null : Number(row.priority_score)),
|
|
118
|
+
agent: row.agent == null ? null : String(row.agent),
|
|
119
|
+
revision: Number(row.revision),
|
|
108
120
|
parent: row.parent_id == null ? null : reference(Number(row.parent_id)),
|
|
109
121
|
source: row.source_locator == null ? null : String(row.source_locator),
|
|
110
122
|
dueAt: iso(row.due_at),
|
|
@@ -125,6 +137,7 @@ export const workTaskOperationsOver = (context) => {
|
|
|
125
137
|
};
|
|
126
138
|
};
|
|
127
139
|
const stemmerFor = stemmerCache();
|
|
140
|
+
const touch = (task, at) => database.prepare("UPDATE tasks SET updated_at = ? WHERE id = ?").run(at, task);
|
|
128
141
|
const boardWhere = (query, zone, at) => {
|
|
129
142
|
if (query === undefined)
|
|
130
143
|
return { sql: BOARD_SCOPE, params: [] };
|
|
@@ -143,13 +156,13 @@ export const workTaskOperationsOver = (context) => {
|
|
|
143
156
|
const at = now();
|
|
144
157
|
const set = [];
|
|
145
158
|
const values = [];
|
|
146
|
-
const change = (column, value, event) => {
|
|
159
|
+
const change = (column, value, event, key = column) => {
|
|
147
160
|
if ((before[column] ?? null) === value)
|
|
148
161
|
return;
|
|
149
162
|
set.push(`${column} = ?`);
|
|
150
163
|
values.push(value);
|
|
151
164
|
if (event)
|
|
152
|
-
recordTaskEvent(database, task, actor, event, { [
|
|
165
|
+
recordTaskEvent(database, task, actor, event, { [key]: [before[column] ?? null, value] }, at);
|
|
153
166
|
};
|
|
154
167
|
if (fields.title !== undefined) {
|
|
155
168
|
const title = textOf(fields.title, "a task's title", 300);
|
|
@@ -158,15 +171,19 @@ export const workTaskOperationsOver = (context) => {
|
|
|
158
171
|
}
|
|
159
172
|
if (fields.description !== undefined)
|
|
160
173
|
change("description", fields.description === null ? null : textOf(fields.description, "a description", 20000), created ? undefined : "description");
|
|
161
|
-
if (fields.priority !== undefined)
|
|
162
|
-
|
|
174
|
+
if (fields.priority !== undefined) {
|
|
175
|
+
const score = fields.priority === null ? null : checkedScore(fields.priority);
|
|
176
|
+
change("priority_score", score, "priority", "priority");
|
|
177
|
+
change("priority", legacyPriority(score));
|
|
178
|
+
}
|
|
179
|
+
if (fields.agent !== undefined)
|
|
180
|
+
change("agent", fields.agent === null ? null : textOf(fields.agent, "an agent's name", 100), "agent");
|
|
163
181
|
if (fields.parent !== undefined)
|
|
164
182
|
change("parent_id", fields.parent === null ? null : parentOf(task, Number(before.project_id), fields.parent));
|
|
165
183
|
if (fields.dueAt !== undefined)
|
|
166
184
|
change("due_at", fields.dueAt === null ? null : fields.dueAt.getTime(), "due");
|
|
167
|
-
// Moved on every look, so no event: the history would be mostly check-ins.
|
|
168
185
|
if (fields.nextCheckAt !== undefined)
|
|
169
|
-
change("next_check_at", fields.nextCheckAt === null ? null : fields.nextCheckAt.getTime());
|
|
186
|
+
change("next_check_at", fields.nextCheckAt === null ? null : fields.nextCheckAt.getTime(), "next-check");
|
|
170
187
|
if (fields.status !== undefined) {
|
|
171
188
|
if (!TASK_STATES.includes(fields.status))
|
|
172
189
|
throw new CliError("validation_error", `a task's status is one of ${TASK_STATES.join(", ")}`);
|
|
@@ -211,6 +228,67 @@ export const workTaskOperationsOver = (context) => {
|
|
|
211
228
|
return;
|
|
212
229
|
database.prepare(`UPDATE tasks SET ${set.join(", ")}, updated_at = ? WHERE id = ?`).run(...values, at, task);
|
|
213
230
|
};
|
|
231
|
+
/** The column's tasks in board order, the moving one left out; no priority reads as 0, the bottom of low. */
|
|
232
|
+
const columnOf = (task, status) => database
|
|
233
|
+
.prepare(`SELECT t.id, t.key, coalesce(t.priority_score, 0) AS score FROM tasks t WHERE ${BOARD_SCOPE} ` +
|
|
234
|
+
`AND t.status = ? AND t.id <> ? ORDER BY ${boardOrder("priority", false)}`)
|
|
235
|
+
.all(status, task)
|
|
236
|
+
.map((row) => ({ id: Number(row.id), key: String(row.key), score: Number(row.score) }));
|
|
237
|
+
const moveTask = (task, row, move, actor) => {
|
|
238
|
+
if (move.before !== undefined && move.after !== undefined)
|
|
239
|
+
throw new CliError("validation_error", "give --before or --after, not both");
|
|
240
|
+
if (!database.prepare(`SELECT 1 FROM tasks t WHERE t.id = ? AND ${BOARD_SCOPE}`).get(task))
|
|
241
|
+
throw new CliError("validation_error", "only a project task moves on the board");
|
|
242
|
+
const status = move.status ?? row.status;
|
|
243
|
+
const column = columnOf(task, status);
|
|
244
|
+
const neighbour = move.before ?? move.after;
|
|
245
|
+
if (neighbour === undefined) {
|
|
246
|
+
if (column.length > 0 || move.status === undefined)
|
|
247
|
+
throw new CliError("validation_error", "say where it goes: --before <key> or --after <key>");
|
|
248
|
+
const from = priorityLabel(row.priority_score == null ? null : Number(row.priority_score));
|
|
249
|
+
edit(task, { status: move.status }, actor);
|
|
250
|
+
return { label: { from, to: from, changed: false }, renumbered: [] };
|
|
251
|
+
}
|
|
252
|
+
const target = required(neighbour);
|
|
253
|
+
const at = column.findIndex(({ id }) => id === target);
|
|
254
|
+
if (at === -1)
|
|
255
|
+
throw new CliError("validation_error", `${move.before ?? move.after} is not a ${status} task of a project; a task moves beside one in its column`);
|
|
256
|
+
const index = move.before === undefined ? at + 1 : at;
|
|
257
|
+
const above = column[index - 1];
|
|
258
|
+
const below = column[index];
|
|
259
|
+
const STEP = 1 / 64;
|
|
260
|
+
const score = above === undefined
|
|
261
|
+
? Math.min(1, (below?.score ?? 0) + STEP)
|
|
262
|
+
: below === undefined
|
|
263
|
+
? Math.max(0, above.score - STEP)
|
|
264
|
+
: (above.score + below.score) / 2;
|
|
265
|
+
const roomy = (above === undefined || score < above.score) && (below === undefined || score > below.score);
|
|
266
|
+
const from = priorityLabel(row.priority_score == null ? null : Number(row.priority_score));
|
|
267
|
+
const renumbered = [];
|
|
268
|
+
let given = score;
|
|
269
|
+
if (!roomy || (above !== undefined && below !== undefined && above.score - below.score < 1e-9)) {
|
|
270
|
+
const label = priorityLabel((above ?? below)?.score ?? 0);
|
|
271
|
+
const band = priorityBand(label);
|
|
272
|
+
const inBand = (one) => priorityLabel(one.score) === label;
|
|
273
|
+
const ordered = [...column.slice(0, index), { id: task, key: String(row.key), score }, ...column.slice(index)];
|
|
274
|
+
const spread = ordered.filter((one) => one.id === task || inBand(one));
|
|
275
|
+
const gap = (band.to - band.from) / (spread.length + 1);
|
|
276
|
+
spread.forEach((one, place) => {
|
|
277
|
+
const value = band.to - gap * (place + 1);
|
|
278
|
+
if (one.id === task) {
|
|
279
|
+
given = value;
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (one.score === value)
|
|
283
|
+
return;
|
|
284
|
+
edit(one.id, { priority: value }, actor);
|
|
285
|
+
renumbered.push(one.key);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
edit(task, { priority: given, ...(move.status === undefined ? {} : { status: move.status }) }, actor);
|
|
289
|
+
const to = priorityLabel(given);
|
|
290
|
+
return { label: { from, to, changed: from !== to }, renumbered };
|
|
291
|
+
};
|
|
214
292
|
return {
|
|
215
293
|
...taskRelationsStoreOver(context, (task, actor, kind, changes, at) => recordTaskEvent(database, task, actor, kind, changes, at)),
|
|
216
294
|
create: (input, by) => {
|
|
@@ -237,16 +315,29 @@ export const workTaskOperationsOver = (context) => {
|
|
|
237
315
|
});
|
|
238
316
|
return details(made);
|
|
239
317
|
},
|
|
240
|
-
edit: async (id, fields, by) => {
|
|
318
|
+
edit: async (id, fields, by, check = {}) => {
|
|
241
319
|
const task = required(id);
|
|
242
|
-
write(database, () =>
|
|
320
|
+
write(database, () => {
|
|
321
|
+
checkRevision(rowById(task), check.revision);
|
|
322
|
+
edit(task, fields, authorActor(context, by).actor);
|
|
323
|
+
});
|
|
243
324
|
return details(task);
|
|
244
325
|
},
|
|
245
|
-
|
|
326
|
+
move: async (id, move, by, check = {}) => {
|
|
327
|
+
const task = required(id);
|
|
328
|
+
return write(database, () => {
|
|
329
|
+
const row = rowById(task);
|
|
330
|
+
checkRevision(row, check.revision);
|
|
331
|
+
const moved = moveTask(task, row, move, authorActor(context, by).actor);
|
|
332
|
+
return { ...moved, task: details(task) };
|
|
333
|
+
});
|
|
334
|
+
},
|
|
335
|
+
assign: (id, { to, role }, by, check = {}) => {
|
|
246
336
|
if (!TASK_ROLES.includes(role))
|
|
247
337
|
throw new CliError("validation_error", `a role is one of ${TASK_ROLES.join(", ")}`);
|
|
248
338
|
const task = required(id);
|
|
249
339
|
write(database, () => {
|
|
340
|
+
checkRevision(rowById(task), check.revision);
|
|
250
341
|
const person = personOf(to);
|
|
251
342
|
const held = database
|
|
252
343
|
.prepare("SELECT 1 FROM task_assignments WHERE task_id = ? AND assignee_type = 'person' AND assignee_id = ? AND role = ?")
|
|
@@ -257,6 +348,7 @@ export const workTaskOperationsOver = (context) => {
|
|
|
257
348
|
database
|
|
258
349
|
.prepare("INSERT INTO task_assignments (task_id, assignee_type, assignee_id, role, created_at, updated_at) VALUES (?, 'person', ?, ?, ?, ?)")
|
|
259
350
|
.run(task, person, role, at, at);
|
|
351
|
+
touch(task, at);
|
|
260
352
|
const changes = { [role]: [null, `person:${person}`] };
|
|
261
353
|
recordTaskEvent(database, task, authorActor(context, by).actor, "assigned", changes, at);
|
|
262
354
|
});
|
|
@@ -283,17 +375,20 @@ export const workTaskOperationsOver = (context) => {
|
|
|
283
375
|
.get(thing.id);
|
|
284
376
|
return row ? `person:${String(row.id)}` : undefined;
|
|
285
377
|
},
|
|
286
|
-
unassign: async (id, { to, role }, by) => {
|
|
378
|
+
unassign: async (id, { to, role }, by, check = {}) => {
|
|
287
379
|
const task = required(id);
|
|
288
380
|
write(database, () => {
|
|
381
|
+
checkRevision(rowById(task), check.revision);
|
|
289
382
|
const person = personOf(to);
|
|
290
383
|
const removed = database
|
|
291
384
|
.prepare("DELETE FROM task_assignments WHERE task_id = ? AND assignee_type = 'person' AND assignee_id = ? AND role = ?")
|
|
292
385
|
.run(task, person, role).changes;
|
|
293
386
|
if (removed === 0)
|
|
294
387
|
return;
|
|
388
|
+
const at = now();
|
|
389
|
+
touch(task, at);
|
|
295
390
|
const changes = { [role]: [`person:${person}`, null] };
|
|
296
|
-
recordTaskEvent(database, task, authorActor(context, by).actor, "unassigned", changes,
|
|
391
|
+
recordTaskEvent(database, task, authorActor(context, by).actor, "unassigned", changes, at);
|
|
297
392
|
});
|
|
298
393
|
return details(task);
|
|
299
394
|
},
|
|
@@ -317,17 +412,23 @@ export const workTaskOperationsOver = (context) => {
|
|
|
317
412
|
.prepare("SELECT * FROM tasks t WHERE deleted_at IS NULL AND (? IS NULL OR project_id = ?) AND (? IS NULL OR type = ?) " +
|
|
318
413
|
"AND (? IS NULL OR status = ?) AND (? IS NULL OR EXISTS (SELECT 1 FROM task_assignments a " +
|
|
319
414
|
"WHERE a.task_id = t.id AND a.assignee_type = 'person' AND a.assignee_id = ?)) " +
|
|
320
|
-
`AND (${where?.sql || "1"}) ORDER BY
|
|
415
|
+
`AND (${where?.sql || "1"}) ORDER BY priority_score IS NULL, priority_score DESC, created_at, id LIMIT ?`)
|
|
321
416
|
.all(projectId, projectId, type ?? null, type ?? null, status ?? null, status ?? null, person, person, ...(where?.params ?? []), Math.min(Math.max(limit, 1), 500))
|
|
322
417
|
.map(detailsOf);
|
|
323
418
|
},
|
|
324
419
|
claim: async (id, holder, until) => {
|
|
325
420
|
const task = required(id);
|
|
326
421
|
const at = now();
|
|
327
|
-
const won = write(database, () =>
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
422
|
+
const won = write(database, () => {
|
|
423
|
+
const before = claimOf(rowById(task), at).claimedBy;
|
|
424
|
+
const claimed = database
|
|
425
|
+
.prepare("UPDATE tasks SET claimed_by = ?, claimed_until = ?, updated_at = ? " +
|
|
426
|
+
"WHERE id = ? AND status NOT IN ('done', 'dismissed') AND (claimed_by IS NULL OR claimed_until < ? OR claimed_by = ?) RETURNING id")
|
|
427
|
+
.get(holder, until.getTime(), at, task, at, holder);
|
|
428
|
+
if (claimed)
|
|
429
|
+
recordTaskEvent(database, task, null, "claimed", { claimed_by: [before, holder] }, at);
|
|
430
|
+
return claimed;
|
|
431
|
+
});
|
|
331
432
|
if (!won) {
|
|
332
433
|
const row = rowById(task);
|
|
333
434
|
if (isClosedState(String(row.status)))
|
|
@@ -346,9 +447,12 @@ export const workTaskOperationsOver = (context) => {
|
|
|
346
447
|
throw new CliError("validation_error", `${claimedBy} holds task ${String(row.key)}; only they or the owner release it`, {
|
|
347
448
|
reason: "task.claimed",
|
|
348
449
|
});
|
|
450
|
+
const at = now();
|
|
349
451
|
database
|
|
350
452
|
.prepare("UPDATE tasks SET claimed_by = NULL, claimed_until = NULL, updated_at = ? WHERE id = ?")
|
|
351
|
-
.run(
|
|
453
|
+
.run(at, task);
|
|
454
|
+
if (claimedBy !== null)
|
|
455
|
+
recordTaskEvent(database, task, null, "released", { claimed_by: [claimedBy, null] }, at);
|
|
352
456
|
});
|
|
353
457
|
return details(task);
|
|
354
458
|
},
|
|
@@ -384,13 +488,20 @@ export const workTaskOperationsOver = (context) => {
|
|
|
384
488
|
count: Number(row.n),
|
|
385
489
|
}))
|
|
386
490
|
.sort((a, b) => b.count - a.count || a.value.localeCompare(b.value));
|
|
387
|
-
const priorities = new Map(
|
|
491
|
+
const priorities = new Map();
|
|
492
|
+
for (const row of grouped("priority", "t.priority_score", "", "t.priority_score")) {
|
|
493
|
+
const label = priorityLabel(row.priority_score == null ? null : Number(row.priority_score)) ?? "none";
|
|
494
|
+
priorities.set(label, (priorities.get(label) ?? 0) + Number(row.n));
|
|
495
|
+
}
|
|
388
496
|
const states = new Map(grouped("state", "t.status", "", "t.status").map((row) => [row.status, Number(row.n)]));
|
|
389
497
|
return {
|
|
390
498
|
total: countOf(boardWhere(query, zone, at)),
|
|
391
499
|
project,
|
|
392
500
|
assignee,
|
|
393
|
-
priority: [
|
|
501
|
+
priority: [...[...PRIORITY_LABELS].reverse(), "none"].map((value) => ({
|
|
502
|
+
value,
|
|
503
|
+
count: priorities.get(value) ?? 0,
|
|
504
|
+
})),
|
|
394
505
|
state: TASK_STATES.map((value) => ({ value, count: states.get(value) ?? 0 })),
|
|
395
506
|
};
|
|
396
507
|
},
|
|
@@ -431,6 +542,6 @@ export const workTasksOver = (context) => {
|
|
|
431
542
|
return {
|
|
432
543
|
...operations,
|
|
433
544
|
create: async (input, by) => operations.create(input, by),
|
|
434
|
-
assign: async (id, assignment, by) => operations.assign(id, assignment, by),
|
|
545
|
+
assign: async (id, assignment, by, check) => operations.assign(id, assignment, by, check),
|
|
435
546
|
};
|
|
436
547
|
};
|
package/dist/tasks/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { CLOSED_STATES, type ClosedState, closeTask, isClosedState, TASK_KINDS, TASK_ORIGINS, TASK_STATES, TASK_VERDICTS, type Task, type TaskKind, type TaskOrigin, type TaskState, type TaskVerdict, } from "./model.js";
|
|
2
|
+
export { checkedScore, LEGACY_PRIORITY_SCORES, legacyPriority, PRIORITY_LABELS, type PriorityLabel, priorityBand, priorityLabel, priorityOf, } from "./priority.js";
|
|
2
3
|
export { type CloseOptions, createTaskService, type GroupStats, type NewTask, type TaskService, type TaskServiceOptions, } from "./service.js";
|
|
3
4
|
export { matches, type TaskFilter, type TaskStore } from "./store.js";
|
package/dist/tasks/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { CLOSED_STATES, closeTask, isClosedState, TASK_KINDS, TASK_ORIGINS, TASK_STATES, TASK_VERDICTS, } from "./model.js";
|
|
2
|
+
export { checkedScore, LEGACY_PRIORITY_SCORES, legacyPriority, PRIORITY_LABELS, priorityBand, priorityLabel, priorityOf, } from "./priority.js";
|
|
2
3
|
export { createTaskService, } from "./service.js";
|
|
3
4
|
export { matches } from "./store.js";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Quarters of [0, 1], lowest first; a person reads the label, a board column is ordered by the number. */
|
|
2
|
+
export declare const PRIORITY_LABELS: readonly ["low", "medium", "high", "critical"];
|
|
3
|
+
export type PriorityLabel = (typeof PRIORITY_LABELS)[number];
|
|
4
|
+
/** The scores of the old 0 (urgent) … 4 (low), as store version 41 converted them. */
|
|
5
|
+
export declare const LEGACY_PRIORITY_SCORES: readonly [0.875, 0.625, 0.375, 0.1875, 0.0625];
|
|
6
|
+
export declare const priorityLabel: (score: number | null) => PriorityLabel | null;
|
|
7
|
+
export declare const priorityBand: (label: PriorityLabel) => {
|
|
8
|
+
from: number;
|
|
9
|
+
to: number;
|
|
10
|
+
};
|
|
11
|
+
/** The 0–4 integer a build before store version 41 reads: critical 0 … low 3, the bottom of low 4. */
|
|
12
|
+
export declare const legacyPriority: (score: number | null) => number | null;
|
|
13
|
+
export declare const checkedScore: (score: number) => number;
|
|
14
|
+
/**
|
|
15
|
+
* A priority as typed: a label, or 0 (urgent) … 4 (low) as before, which keeps the meaning it had.
|
|
16
|
+
* `none` clears it.
|
|
17
|
+
*/
|
|
18
|
+
export declare const priorityOf: (value: string) => number | null;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
/** Quarters of [0, 1], lowest first; a person reads the label, a board column is ordered by the number. */
|
|
3
|
+
export const PRIORITY_LABELS = ["low", "medium", "high", "critical"];
|
|
4
|
+
/** The number a label is given: the middle of its quarter, so a move leaves room on either side. */
|
|
5
|
+
const MIDDLE = { low: 0.125, medium: 0.375, high: 0.625, critical: 0.875 };
|
|
6
|
+
/** The scores of the old 0 (urgent) … 4 (low), as store version 41 converted them. */
|
|
7
|
+
export const LEGACY_PRIORITY_SCORES = [0.875, 0.625, 0.375, 0.1875, 0.0625];
|
|
8
|
+
export const priorityLabel = (score) => score === null ? null : PRIORITY_LABELS[Math.min(3, Math.max(0, Math.floor(score * 4)))];
|
|
9
|
+
export const priorityBand = (label) => {
|
|
10
|
+
const at = PRIORITY_LABELS.indexOf(label);
|
|
11
|
+
return { from: at / 4, to: (at + 1) / 4 };
|
|
12
|
+
};
|
|
13
|
+
/** The 0–4 integer a build before store version 41 reads: critical 0 … low 3, the bottom of low 4. */
|
|
14
|
+
export const legacyPriority = (score) => score === null
|
|
15
|
+
? null
|
|
16
|
+
: score < 0.125
|
|
17
|
+
? 4
|
|
18
|
+
: 3 - PRIORITY_LABELS.indexOf(priorityLabel(score));
|
|
19
|
+
export const checkedScore = (score) => {
|
|
20
|
+
if (!Number.isFinite(score) || score < 0 || score > 1)
|
|
21
|
+
throw new CliError("validation_error", "a priority score is 0 to 1");
|
|
22
|
+
return score;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* A priority as typed: a label, or 0 (urgent) … 4 (low) as before, which keeps the meaning it had.
|
|
26
|
+
* `none` clears it.
|
|
27
|
+
*/
|
|
28
|
+
export const priorityOf = (value) => {
|
|
29
|
+
const word = value.trim().toLowerCase();
|
|
30
|
+
if (word === "none")
|
|
31
|
+
return null;
|
|
32
|
+
if (PRIORITY_LABELS.includes(word))
|
|
33
|
+
return MIDDLE[word];
|
|
34
|
+
if (/^[0-4]$/.test(word))
|
|
35
|
+
return LEGACY_PRIORITY_SCORES[Number(word)];
|
|
36
|
+
throw new CliError("validation_error", `a priority is ${PRIORITY_LABELS.join(", ")} or none; 0 (urgent) … 4 (low) still work as critical, high, medium, low, low`);
|
|
37
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.514.0",
|
|
4
4
|
"description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|
package/parity.json
CHANGED
|
@@ -3678,18 +3678,14 @@
|
|
|
3678
3678
|
}
|
|
3679
3679
|
},
|
|
3680
3680
|
"projects": {
|
|
3681
|
-
"in": [],
|
|
3681
|
+
"in": ["max", "tg"],
|
|
3682
3682
|
"planned": {
|
|
3683
|
-
"max": "max-cli#886, on the cli-core release with this row",
|
|
3684
|
-
"tg": "tg-cli#798, on the cli-core release with this row",
|
|
3685
3683
|
"wa": "wa backlog: mount projectsCommand once wa is on cli-core 0.504.0"
|
|
3686
3684
|
}
|
|
3687
3685
|
},
|
|
3688
3686
|
"projects evidence": {
|
|
3689
|
-
"in": [],
|
|
3687
|
+
"in": ["max", "tg"],
|
|
3690
3688
|
"planned": {
|
|
3691
|
-
"max": "max-cli#886, on the cli-core release with this row",
|
|
3692
|
-
"tg": "tg-cli#798, on the cli-core release with this row",
|
|
3693
3689
|
"wa": "wa backlog: mount projectsCommand once wa is on cli-core 0.504.0"
|
|
3694
3690
|
},
|
|
3695
3691
|
"options": {
|