@gavin-hu/my-pi-agent 0.20.0 → 0.22.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 +46 -30
- package/README.zh-CN.md +40 -25
- package/assets/screens/board.png +0 -0
- package/assets/screens/chrome.png +0 -0
- package/assets/screens/file-browse.png +0 -0
- package/assets/screens/plan-review.png +0 -0
- package/assets/screens/plan.png +0 -0
- package/docs/extensions/README.md +22 -23
- package/docs/extensions/board.md +397 -168
- package/docs/extensions/bytefly.md +234 -197
- package/docs/extensions/check.md +15 -17
- package/docs/extensions/context.md +35 -15
- package/docs/extensions/file-browse.md +57 -63
- package/docs/extensions/file-open.md +9 -11
- package/docs/extensions/file-read.md +12 -14
- package/docs/extensions/goal.md +15 -17
- package/docs/extensions/job.md +54 -42
- package/docs/extensions/memory.md +17 -19
- package/docs/extensions/plan.md +197 -140
- package/docs/extensions/question.md +16 -18
- package/docs/extensions/rewind.md +17 -19
- package/docs/extensions/serve.md +145 -0
- package/docs/extensions/session-bus.md +38 -16
- package/docs/extensions/subagent.md +23 -25
- package/docs/extensions/team.md +83 -44
- package/docs/extensions/todo.md +18 -18
- package/docs/extensions/watch.md +31 -31
- package/docs/extensions/web-access.md +2 -4
- package/docs/extensions/wechat.md +34 -35
- package/docs/extensions/worktree.md +56 -50
- package/extensions/board/commands.ts +79 -25
- package/extensions/board/format.ts +43 -24
- package/extensions/board/index.ts +17 -4
- package/extensions/board/input.ts +4 -9
- package/extensions/board/join.ts +5 -20
- package/extensions/board/lazy-page.ts +40 -0
- package/extensions/board/page.ts +65 -93
- package/extensions/board/result.ts +30 -39
- package/extensions/board/rows.ts +62 -25
- package/extensions/board/schema.ts +49 -11
- package/extensions/board/snapshot.ts +8 -6
- package/extensions/board/tone.ts +22 -0
- package/extensions/board/tools.ts +154 -55
- package/extensions/board/transport/router.ts +3 -5
- package/extensions/board/transport/server.ts +59 -28
- package/extensions/board/types.ts +36 -42
- package/extensions/board/view/card.ts +229 -0
- package/extensions/board/view/client.ts +35 -16
- package/extensions/board/view/dialog.ts +169 -6
- package/extensions/board/view/empty.ts +47 -0
- package/extensions/board/view/keys.ts +13 -2
- package/extensions/board/view/live.ts +90 -28
- package/extensions/board/view/names.ts +13 -4
- package/extensions/board/view/page.ts +38 -13
- package/extensions/board/view/render.ts +219 -277
- package/extensions/board/view/styles.ts +130 -32
- package/extensions/board/view/team.ts +297 -76
- package/extensions/bytefly/block/fallback.ts +1 -1
- package/extensions/bytefly/doctor/runtime.ts +5 -2
- package/extensions/bytefly/footer/collect/label.ts +60 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +8 -1
- package/extensions/bytefly/footer/config.ts +7 -8
- package/extensions/bytefly/footer/register.ts +33 -4
- package/extensions/bytefly/footer/render/footer.ts +18 -4
- package/extensions/bytefly/footer/render/key.ts +46 -0
- package/extensions/bytefly/footer/render/layout.ts +13 -3
- package/extensions/bytefly/footer/render/lines.ts +10 -7
- package/extensions/bytefly/footer/render/segments.ts +41 -38
- package/extensions/bytefly/footer/render/status.ts +0 -11
- package/extensions/bytefly/footer/types.ts +7 -0
- package/extensions/bytefly/header/component.ts +4 -20
- package/extensions/bytefly/header/config.ts +1 -1
- package/extensions/bytefly/index.ts +32 -6
- package/extensions/bytefly/mode/editor.ts +115 -0
- package/extensions/bytefly/panel/commands.ts +210 -120
- package/extensions/bytefly/panel/format.ts +4 -4
- package/extensions/bytefly/panel/settings.ts +73 -6
- package/extensions/bytefly/panel/tui/keys.ts +2 -6
- package/extensions/bytefly/update/cache.ts +1 -1
- package/extensions/bytefly/update/register.ts +4 -4
- package/extensions/bytefly/working/config.ts +5 -7
- package/extensions/bytefly/working/format.ts +10 -13
- package/extensions/bytefly/working/register.ts +22 -27
- package/extensions/check/commands.ts +3 -2
- package/extensions/check/render.ts +10 -1
- package/extensions/check/{tui.ts → view/check-screens.ts} +14 -19
- package/extensions/context/commands.ts +2 -2
- package/extensions/context/{tui.ts → view/context-report.ts} +51 -19
- package/extensions/file-browse/config/config.ts +4 -19
- package/extensions/file-browse/index.ts +89 -79
- package/extensions/file-browse/live/live.ts +1 -1
- package/extensions/file-browse/transport/router.ts +12 -4
- package/extensions/file-browse/transport/server.ts +61 -28
- package/extensions/file-browse/view/chrome.ts +4 -4
- package/extensions/file-browse/view/client.ts +3 -1
- package/extensions/file-browse/view/html.ts +73 -40
- package/extensions/file-browse/view/live-client.ts +2 -2
- package/extensions/file-browse/view/markdown.ts +13 -14
- package/extensions/file-browse/view/palette.ts +2 -2
- package/extensions/file-browse/view/raw-html.ts +9 -9
- package/extensions/file-browse/view/tree.ts +1 -1
- package/extensions/file-browse/view/urls.ts +20 -6
- package/extensions/file-read/render.ts +4 -4
- package/extensions/goal/plan-route.ts +2 -2
- package/extensions/goal/tui.ts +12 -2
- package/extensions/job/child.ts +527 -0
- package/extensions/job/clock.ts +142 -0
- package/extensions/job/commands.ts +4 -2
- package/extensions/job/config.ts +8 -1
- package/extensions/job/format.ts +22 -1
- package/extensions/job/log-writer.ts +13 -2
- package/extensions/job/registry.ts +63 -7
- package/extensions/job/render.ts +34 -0
- package/extensions/job/runtime.ts +296 -655
- package/extensions/job/session.ts +132 -19
- package/extensions/job/store.ts +7 -7
- package/extensions/job/tools.ts +21 -9
- package/extensions/job/types.ts +47 -0
- package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -99
- package/extensions/job/waiters.ts +44 -1
- package/extensions/memory/commands.ts +4 -2
- package/extensions/memory/{tui.ts → view/memory-picker.ts} +12 -12
- package/extensions/plan/commands.ts +12 -9
- package/extensions/plan/feedback.ts +62 -16
- package/extensions/plan/format.ts +8 -2
- package/extensions/plan/git-bash.ts +115 -16
- package/extensions/plan/index.ts +58 -51
- package/extensions/plan/plan-index.ts +4 -2
- package/extensions/plan/plan-text.ts +114 -0
- package/extensions/plan/plans.ts +5 -54
- package/extensions/plan/policy.ts +8 -10
- package/extensions/plan/review-lazy.ts +85 -0
- package/extensions/plan/review.ts +2 -2
- package/extensions/plan/runtime.ts +61 -41
- package/extensions/plan/sections.ts +1 -1
- package/extensions/plan/steps.ts +3 -3
- package/extensions/plan/todos.ts +1 -1
- package/extensions/plan/tools/cancel.ts +27 -62
- package/extensions/plan/tools/enter.ts +55 -81
- package/extensions/plan/tools/exit.ts +221 -238
- package/extensions/plan/tools/refusal.ts +18 -0
- package/extensions/plan/tools/steps.ts +75 -92
- package/extensions/plan/tools/write.ts +77 -120
- package/extensions/plan/tools.ts +188 -30
- package/extensions/plan/tui.ts +5 -5
- package/extensions/plan/types.ts +48 -5
- package/extensions/plan/web-blocks.ts +76 -5
- package/extensions/plan/web-chrome.ts +4 -0
- package/extensions/plan/web-client.ts +102 -2
- package/extensions/plan/web-collab.ts +4 -4
- package/extensions/plan/web-comments.ts +39 -8
- package/extensions/plan/web-composer.ts +14 -3
- package/extensions/plan/web-keys.ts +149 -25
- package/extensions/plan/web-lazy.ts +53 -0
- package/extensions/plan/web-markdown.ts +10 -68
- package/extensions/plan/web-outline.ts +22 -2
- package/extensions/plan/web-page.ts +56 -86
- package/extensions/plan/web-payload.ts +188 -0
- package/extensions/plan/web-plan.ts +23 -13
- package/extensions/plan/web-router.ts +18 -9
- package/extensions/plan/web-settle.ts +14 -7
- package/extensions/plan/web-share-session.ts +24 -55
- package/extensions/plan/web-share.ts +19 -37
- package/extensions/plan/web-write.ts +112 -0
- package/extensions/plan/web.ts +67 -131
- package/extensions/question/tools.ts +6 -2
- package/extensions/question/{tui.ts → view/question-screen.ts} +11 -11
- package/extensions/rewind/commands/commands.ts +6 -3
- package/extensions/rewind/state/runtime.ts +1 -1
- package/extensions/rewind/{view/status.ts → status.ts} +4 -4
- package/extensions/rewind/view/review-tui.ts +4 -4
- package/extensions/serve/config.ts +39 -0
- package/extensions/serve/index.ts +85 -0
- package/extensions/serve/registry.ts +260 -0
- package/extensions/session-bus/commands.ts +11 -6
- package/extensions/session-bus/join-request.ts +17 -9
- package/extensions/session-bus/mail-watch.ts +26 -17
- package/extensions/session-bus/protocol.ts +15 -3
- package/extensions/session-bus/resolve.ts +5 -7
- package/extensions/session-bus/result.ts +1 -27
- package/extensions/session-bus/runtime.ts +40 -12
- package/extensions/session-bus/store.ts +101 -19
- package/extensions/session-bus/tools.ts +5 -4
- package/extensions/session-bus/types.ts +19 -0
- package/extensions/session-bus/{tui.ts → view/session-list.ts} +16 -16
- package/extensions/subagent/agents.ts +1 -1
- package/extensions/subagent/commands.ts +4 -2
- package/extensions/subagent/format.ts +15 -15
- package/extensions/subagent/index.ts +1 -1
- package/extensions/subagent/render.ts +3 -3
- package/extensions/subagent/{background-tui.ts → view/agents-board.ts} +11 -11
- package/extensions/team/commands.ts +24 -5
- package/extensions/team/dispatch.ts +18 -4
- package/extensions/team/format.ts +17 -2
- package/extensions/team/index.ts +9 -8
- package/extensions/team/schema.ts +6 -5
- package/extensions/team/tools.ts +107 -45
- package/extensions/team/types.ts +3 -2
- package/extensions/todo/commands.ts +4 -2
- package/extensions/todo/format.ts +11 -0
- package/extensions/todo/render.ts +57 -0
- package/extensions/todo/runtime.ts +1 -1
- package/extensions/todo/tools.ts +1 -1
- package/extensions/todo/{tui.ts → view/todo-screen.ts} +18 -90
- package/extensions/todo/widget.ts +47 -0
- package/extensions/watch/commands.ts +4 -2
- package/extensions/watch/format.ts +36 -7
- package/extensions/watch/render.ts +57 -0
- package/extensions/watch/runtime.ts +27 -22
- package/extensions/watch/tools.ts +4 -4
- package/extensions/watch/{tui.ts → view/watch-screen.ts} +18 -80
- package/extensions/web-access/fetch/tool.ts +12 -4
- package/extensions/wechat/commands/commands.ts +18 -14
- package/extensions/wechat/index.ts +72 -37
- package/extensions/wechat/state/credentials.ts +1 -1
- package/extensions/wechat/state/state.ts +1 -1
- package/extensions/wechat/text/status.ts +112 -0
- package/extensions/wechat/types.ts +2 -2
- package/extensions/wechat/view/status-screen.ts +4 -103
- package/extensions/worktree/commands.ts +11 -9
- package/extensions/worktree/enter.ts +2 -2
- package/extensions/worktree/exit.ts +2 -2
- package/extensions/worktree/index.ts +5 -5
- package/extensions/worktree/screen.ts +3 -3
- package/extensions/worktree/status.ts +2 -2
- package/extensions/worktree/tools.ts +113 -116
- package/lib/README.md +28 -13
- package/lib/board/actions.ts +111 -0
- package/lib/board/artifact.ts +149 -0
- package/lib/board/card.ts +163 -52
- package/lib/board/config.ts +58 -0
- package/lib/board/contract.ts +113 -0
- package/lib/board/join.ts +38 -0
- package/lib/board/lanes.ts +182 -0
- package/lib/board/lead.ts +130 -0
- package/lib/board/limits.ts +78 -0
- package/lib/board/links.ts +31 -10
- package/lib/board/page-pointer.ts +30 -5
- package/lib/board/read.ts +83 -0
- package/lib/board/reason.ts +102 -0
- package/lib/board/round-text.ts +68 -6
- package/lib/board/round.ts +31 -15
- package/lib/board/rules.ts +151 -307
- package/lib/board/seat.ts +35 -0
- package/lib/board/session.ts +199 -51
- package/lib/board/skipped.ts +22 -0
- package/lib/board/steps.ts +487 -0
- package/lib/board/store.ts +119 -36
- package/lib/board/tone.ts +92 -0
- package/lib/board/view.ts +81 -63
- package/lib/bus-request.ts +83 -0
- package/{extensions/watch → lib}/child.ts +40 -21
- package/lib/env.ts +65 -7
- package/lib/format.ts +13 -0
- package/lib/http/index.ts +17 -11
- package/lib/http/mount.ts +142 -0
- package/lib/http/port.ts +30 -0
- package/lib/http/serve.ts +170 -46
- package/lib/http/server.ts +3 -3
- package/lib/lazy.ts +45 -0
- package/lib/plan-entry.ts +46 -21
- package/lib/plans/paths.ts +82 -0
- package/lib/policy.ts +9 -6
- package/lib/report.ts +3 -2
- package/lib/requirements.ts +10 -10
- package/lib/session-label.ts +129 -7
- package/lib/session-roster.ts +43 -1
- package/lib/session-tool.ts +39 -22
- package/lib/stray-fields.ts +7 -2
- package/lib/tool-names.ts +5 -10
- package/lib/tui/README.md +29 -11
- package/lib/tui/chrome.ts +16 -15
- package/lib/tui/cursor.ts +18 -3
- package/lib/tui/glyphs.ts +11 -11
- package/lib/tui/render-cache.ts +29 -0
- package/lib/tui/tokens.ts +33 -2
- package/lib/tui/tool.ts +33 -4
- package/lib/web/README.md +21 -0
- package/lib/web/client-runtime.ts +27 -1
- package/lib/web/components.ts +5 -1
- package/lib/web/index.ts +1 -0
- package/lib/web/markdown.ts +83 -0
- package/lib/web/markup.ts +20 -2
- package/lib/web/runtime.ts +9 -10
- package/package.json +8 -2
- package/skills/init/SKILL.md +5 -5
- package/skills/next/SKILL.md +3 -5
- package/extensions/board/config.ts +0 -36
- package/extensions/board/seat.ts +0 -28
- package/lib/tui/index.ts +0 -34
- package/skills/project-docs/SKILL.md +0 -133
- package/skills/project-docs/references/audit.md +0 -131
- package/skills/project-docs/references/changelog.md +0 -105
- package/skills/project-docs/references/discovery.md +0 -127
- package/skills/project-docs/references/readme.md +0 -80
- package/skills/project-docs/references/release.md +0 -84
- package/skills/project-docs/references/roadmap.md +0 -93
- package/skills/project-docs/scripts/state.ts +0 -316
package/lib/board/rules.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The card lifecycle: every transition, as a pure function of the card, the
|
|
3
|
-
* operation, who asked, the clock,
|
|
3
|
+
* operation, who asked, the clock, the board's other cards, and the board's limits.
|
|
4
4
|
*
|
|
5
5
|
* One table decides who may do what — the tool, the router, the page, and `team` all
|
|
6
6
|
* route through {@link applyOp} — so a lane change cannot be reachable from one
|
|
@@ -8,41 +8,47 @@
|
|
|
8
8
|
* ownership is advisory (the bus's path claims are the lock), and the store owns the
|
|
9
9
|
* compare-and-set that makes a concurrent writer lose.
|
|
10
10
|
*
|
|
11
|
+
* Two shapes of failure, and the difference matters. A refusal is `{ ok: false }` with
|
|
12
|
+
* the reason, and the caller returns the card untouched. A *no-op* is `{ ok: true,
|
|
13
|
+
* changed: false }`: the operation's preconditions already hold (claiming a card you
|
|
14
|
+
* already hold), so nothing is written and the revision does not move.
|
|
15
|
+
*
|
|
16
|
+
* {@link mayOp} is the same decision without writing it, which is what `team` asks
|
|
17
|
+
* before it sends a handoff: a dry run is a question — "would this be allowed" — not a
|
|
18
|
+
* mutation performed as a fake session.
|
|
19
|
+
*
|
|
11
20
|
* The board index is a parameter rather than a lookup because three operations need a
|
|
12
21
|
* *different* card: a claim or a move is refused while a dependency is unfinished, and
|
|
13
|
-
* a drop is refused while somebody waits on the card. `lib/board/links.ts` answers
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* with the reason, and the caller returns the card untouched. A *no-op* is
|
|
18
|
-
* `{ ok: true, changed: false }`: the operation's preconditions already hold
|
|
19
|
-
* (claiming a card you already hold), so nothing is written and the revision does
|
|
20
|
-
* not move.
|
|
22
|
+
* a drop is refused while somebody waits on the card. `lib/board/links.ts` answers those
|
|
23
|
+
* questions; this module decides what to do with the answer. The board's settings — its WIP
|
|
24
|
+
* limits and the name its two signatures must carry — are one parameter for the same reason:
|
|
25
|
+
* the rule decides, the reader of `bytefly.json` supplies them.
|
|
21
26
|
*
|
|
22
27
|
* `add` is {@link createCard} rather than an operation, because it has no card to
|
|
23
|
-
* transition — and because a card is born `
|
|
28
|
+
* transition — and because a card is born in `backlog` with no owner, which is the one
|
|
24
29
|
* state the other operations never produce.
|
|
25
30
|
*/
|
|
26
31
|
|
|
32
|
+
import type { ArtifactCheck } from "./artifact.ts";
|
|
33
|
+
import { type Card, type CardBlock, CARD_VERSION, DEFAULT_PRIORITY, MAX_LOG, type Priority } from "./card.ts";
|
|
34
|
+
import { type BoardConfig, DEFAULT_BOARD_CONFIG } from "./config.ts";
|
|
35
|
+
import type { Lane } from "./lanes.ts";
|
|
36
|
+
import type { BoardIndex } from "./links.ts";
|
|
27
37
|
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
type Priority,
|
|
43
|
-
priorityOf,
|
|
44
|
-
} from "./card.ts";
|
|
45
|
-
import { type BoardIndex, dependedOn, normalizeDepends, waitingOn } from "./links.ts";
|
|
38
|
+
approve,
|
|
39
|
+
archive,
|
|
40
|
+
assign,
|
|
41
|
+
claim,
|
|
42
|
+
contractOp,
|
|
43
|
+
drop,
|
|
44
|
+
edit,
|
|
45
|
+
move,
|
|
46
|
+
note,
|
|
47
|
+
offer,
|
|
48
|
+
reclaim,
|
|
49
|
+
release,
|
|
50
|
+
review,
|
|
51
|
+
} from "./steps.ts";
|
|
46
52
|
|
|
47
53
|
/** Who is attempting an operation: a session when it has one, and the label it is addressed by. */
|
|
48
54
|
export interface Actor {
|
|
@@ -52,27 +58,45 @@ export interface Actor {
|
|
|
52
58
|
|
|
53
59
|
/** One named mutation of one card. */
|
|
54
60
|
export type CardOp =
|
|
61
|
+
/** Pull a backlog card into `plan`: the only way a card starts moving. */
|
|
55
62
|
| { kind: "claim" }
|
|
56
63
|
/**
|
|
57
|
-
* Hand a card to `to
|
|
58
|
-
*
|
|
64
|
+
* Hand a pool card to `to`: the owner is recorded and the card enters `plan`. A held card is
|
|
65
|
+
* refused — `reclaim` is how it changes hands — and `team assign` sends the handoff, which
|
|
66
|
+
* the state machine knows nothing about.
|
|
59
67
|
*/
|
|
60
|
-
| { kind: "assign"; to: Actor
|
|
68
|
+
| { kind: "assign"; to: Actor }
|
|
61
69
|
| { kind: "release" }
|
|
62
70
|
/**
|
|
63
|
-
* Change lane. `blockedBy`
|
|
64
|
-
* closed a card
|
|
65
|
-
*
|
|
66
|
-
* field or live outside a repository.
|
|
71
|
+
* Change lane. `blockedBy` and `blocker` are what a block records, `landed` says which
|
|
72
|
+
* commit closed a card, and `artifact` is the plan proof a card entering `execute` or
|
|
73
|
+
* `done` carries: each is read only by the lanes that ask for it.
|
|
67
74
|
*/
|
|
68
|
-
| {
|
|
69
|
-
|
|
70
|
-
|
|
75
|
+
| {
|
|
76
|
+
kind: "move";
|
|
77
|
+
lane: Lane;
|
|
78
|
+
blockedBy?: unknown;
|
|
79
|
+
blocker?: unknown;
|
|
80
|
+
landed?: { commit: string };
|
|
81
|
+
artifact?: ArtifactCheck;
|
|
82
|
+
}
|
|
83
|
+
// `text` and the contract fields are whatever the model sent: the modules that own
|
|
84
|
+
// them validate them, and the refusal they produce is the one the caller shows.
|
|
71
85
|
| { kind: "note"; text: unknown }
|
|
72
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Write the contract and the plan it is executed against. `artifact` is what the caller
|
|
88
|
+
* proved: a resolved, existing plan file, or the sentence refusing it. The state machine
|
|
89
|
+
* decides — nothing here touches a filesystem — and a contract naming neither is refused,
|
|
90
|
+
* so a caller cannot skip the check by omitting it.
|
|
91
|
+
*/
|
|
92
|
+
| { kind: "contract"; artifact?: ArtifactCheck; goal?: unknown; acceptance?: unknown; scopeOut?: unknown }
|
|
93
|
+
/** Read the contract and approve it: the lead, and never the card's holder; `text` is noted with the sign. */
|
|
94
|
+
| { kind: "approve"; text?: unknown }
|
|
95
|
+
/** Spot-check the finished work: the lead, in `execute`, and never the card's holder; `text` is noted with the sign. */
|
|
96
|
+
| { kind: "review"; text?: unknown }
|
|
73
97
|
// Every field but `kind` is optional, and an absent one is left as it was: the page
|
|
74
98
|
// sends the three it can show, and omitting a link must not clear it.
|
|
75
|
-
| { kind: "edit"; title
|
|
99
|
+
| { kind: "edit"; title?: unknown; paths?: unknown; priority?: unknown; dependsOn?: unknown }
|
|
76
100
|
| { kind: "reclaim" }
|
|
77
101
|
/**
|
|
78
102
|
* Record that the card was offered to the team: `team offer` broadcasts it on the bus
|
|
@@ -89,7 +113,10 @@ export type CardOp =
|
|
|
89
113
|
/** What an operation did: the new card, or the reason it was refused. */
|
|
90
114
|
export type OpResult = { ok: true; card: Card; changed: boolean } | { ok: false; error: string };
|
|
91
115
|
|
|
92
|
-
/**
|
|
116
|
+
/** What one operation would write, or why it may not. */
|
|
117
|
+
export type Prepared = { error: string } | { fields: Partial<Card> | null; changed: boolean };
|
|
118
|
+
|
|
119
|
+
/** A card's first state: `backlog`, no owner, one note at most. */
|
|
93
120
|
export function createCard(
|
|
94
121
|
id: string,
|
|
95
122
|
input: { title: string; paths: string[]; note?: string; priority?: Priority },
|
|
@@ -97,19 +124,19 @@ export function createCard(
|
|
|
97
124
|
now: number,
|
|
98
125
|
): Card {
|
|
99
126
|
return {
|
|
100
|
-
v:
|
|
127
|
+
v: CARD_VERSION,
|
|
101
128
|
id,
|
|
102
129
|
title: input.title,
|
|
103
|
-
|
|
130
|
+
lane: "backlog",
|
|
104
131
|
paths: [...input.paths],
|
|
105
132
|
// A new card says its priority out loud, even when it is the ordinary one, so a
|
|
106
133
|
// file on disk tells a reader the whole story without a default to know.
|
|
107
134
|
priority: input.priority ?? DEFAULT_PRIORITY,
|
|
108
135
|
notes: input.note === undefined ? [] : [{ by: actor.label, at: now, text: input.note }],
|
|
109
136
|
notesDropped: 0,
|
|
110
|
-
// A card is born in `
|
|
111
|
-
// "how long has this been waiting" gets an answer rather than an absence.
|
|
112
|
-
log: [{
|
|
137
|
+
// A card is born in `backlog`, and that is a transition like any other: a reader
|
|
138
|
+
// asking "how long has this been waiting" gets an answer rather than an absence.
|
|
139
|
+
log: [{ lane: "backlog", at: now, by: actor.label }],
|
|
113
140
|
logDropped: 0,
|
|
114
141
|
rev: 1,
|
|
115
142
|
createdAt: now,
|
|
@@ -119,17 +146,21 @@ export function createCard(
|
|
|
119
146
|
}
|
|
120
147
|
|
|
121
148
|
/**
|
|
122
|
-
*
|
|
149
|
+
* Why `actor` may not apply `op` to `card`, or `undefined` when it may.
|
|
123
150
|
*
|
|
124
|
-
* The
|
|
125
|
-
*
|
|
126
|
-
*
|
|
151
|
+
* The answer a dry run asks for: it computes everything a write would check and nothing
|
|
152
|
+
* a write would store, so `team` can find out whether a handoff is deliverable before it
|
|
153
|
+
* sends one.
|
|
127
154
|
*/
|
|
128
|
-
export function
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
155
|
+
export function mayOp(
|
|
156
|
+
card: Card,
|
|
157
|
+
op: CardOp,
|
|
158
|
+
actor: Actor,
|
|
159
|
+
board: BoardIndex,
|
|
160
|
+
config: BoardConfig = DEFAULT_BOARD_CONFIG,
|
|
161
|
+
): string | undefined {
|
|
162
|
+
const prepared = prepare(card, op, actor, 0, board, config);
|
|
163
|
+
return "error" in prepared ? prepared.error : undefined;
|
|
133
164
|
}
|
|
134
165
|
|
|
135
166
|
/**
|
|
@@ -138,151 +169,75 @@ export function holds(card: Card, actor: Actor): boolean {
|
|
|
138
169
|
* A refused operation returns the reason and no card: the caller already holds the
|
|
139
170
|
* card it read, and a "refused" copy of it would be a second version of the truth.
|
|
140
171
|
*/
|
|
141
|
-
export function applyOp(
|
|
172
|
+
export function applyOp(
|
|
173
|
+
card: Card,
|
|
174
|
+
op: CardOp,
|
|
175
|
+
actor: Actor,
|
|
176
|
+
now: number,
|
|
177
|
+
board: BoardIndex,
|
|
178
|
+
config: BoardConfig = DEFAULT_BOARD_CONFIG,
|
|
179
|
+
): OpResult {
|
|
180
|
+
const prepared = prepare(card, op, actor, now, board, config);
|
|
181
|
+
if ("error" in prepared) return { ok: false, error: prepared.error };
|
|
182
|
+
if (!prepared.changed) return { ok: true, card, changed: false };
|
|
183
|
+
// `archive` changes the pile a card's file lies in and no field at all, which is why
|
|
184
|
+
// its step has nothing to write and the store's move is the whole of the operation.
|
|
185
|
+
if (prepared.fields === null) return { ok: true, card, changed: true };
|
|
186
|
+
return { ok: true, card: write(card, actor, now, prepared.fields), changed: true };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The one place an operation becomes a write: each `step*` decides, this applies. */
|
|
190
|
+
function prepare(card: Card, op: CardOp, actor: Actor, now: number, board: BoardIndex, config: BoardConfig): Prepared {
|
|
142
191
|
switch (op.kind) {
|
|
143
192
|
case "claim":
|
|
144
|
-
return claim(card, actor,
|
|
193
|
+
return claim(card, actor, board, config.wip);
|
|
145
194
|
case "assign":
|
|
146
|
-
return assign(card, op.to,
|
|
195
|
+
return assign(card, op.to, board, config.wip);
|
|
147
196
|
case "release":
|
|
148
|
-
return release(card, actor
|
|
197
|
+
return release(card, actor);
|
|
149
198
|
case "move":
|
|
150
|
-
return move(card, op, actor, now, board);
|
|
199
|
+
return move(card, op, actor, now, board, config);
|
|
151
200
|
case "note":
|
|
152
201
|
return note(card, op.text, actor, now);
|
|
153
|
-
case "
|
|
154
|
-
return
|
|
202
|
+
case "contract":
|
|
203
|
+
return contractOp(card, op, actor);
|
|
204
|
+
case "approve":
|
|
205
|
+
return approve(card, actor, now, config, op.text);
|
|
206
|
+
case "review":
|
|
207
|
+
return review(card, actor, now, config, op.text);
|
|
155
208
|
case "edit":
|
|
156
|
-
return edit(card, op, actor,
|
|
209
|
+
return edit(card, op, actor, board);
|
|
157
210
|
case "reclaim":
|
|
158
|
-
return reclaim(card
|
|
211
|
+
return reclaim(card);
|
|
159
212
|
case "offer":
|
|
160
|
-
return offer(card,
|
|
213
|
+
return offer(card, now, board, config.wip);
|
|
161
214
|
case "archive":
|
|
162
215
|
return archive(card);
|
|
163
216
|
case "drop":
|
|
164
217
|
return drop(card, actor, board);
|
|
218
|
+
default:
|
|
219
|
+
// A caller from an older build, or a hand-written fixture, names an operation this
|
|
220
|
+
// one does not know: that is a refusal, not a crash on `undefined`.
|
|
221
|
+
return { error: "unknown board operation." };
|
|
165
222
|
}
|
|
166
223
|
}
|
|
167
224
|
|
|
168
|
-
function claim(card: Card, actor: Actor, now: number, board: BoardIndex): OpResult {
|
|
169
|
-
if (card.status === "done") return { ok: false, error: `${cardRef(card.id)} is done; it cannot be claimed.` };
|
|
170
|
-
if (holds(card, actor)) return { ok: true, card, changed: false };
|
|
171
|
-
if (card.owner !== undefined) return { ok: false, error: heldBy(card) };
|
|
172
|
-
const waiting = waitingOn(card, board);
|
|
173
|
-
if (waiting !== undefined) return { ok: false, error: waiting };
|
|
174
|
-
return {
|
|
175
|
-
ok: true,
|
|
176
|
-
card: write(card, actor, now, { status: "doing", owner: ownerOf(actor), offeredAt: undefined }),
|
|
177
|
-
changed: true,
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function assign(card: Card, to: Actor, rawNote: unknown, actor: Actor, now: number, board: BoardIndex): OpResult {
|
|
182
|
-
if (card.status === "done") return { ok: false, error: `${cardRef(card.id)} is done; it cannot be assigned.` };
|
|
183
|
-
if (card.owner !== undefined && !holds(card, to)) return { ok: false, error: heldBy(card) };
|
|
184
|
-
if (holds(card, to) && card.status === "doing") return { ok: true, card, changed: false };
|
|
185
|
-
const waiting = waitingOn(card, board);
|
|
186
|
-
if (waiting !== undefined) return { ok: false, error: waiting };
|
|
187
|
-
const fields: Partial<Card> = { status: "doing", owner: ownerOf(to), offeredAt: undefined };
|
|
188
|
-
if (rawNote !== undefined) {
|
|
189
|
-
const parsed = normalizeNote(rawNote);
|
|
190
|
-
if ("error" in parsed) return { ok: false, error: parsed.error };
|
|
191
|
-
// One revision, not two: the history says who it went to in the same write that
|
|
192
|
-
// recorded the owner, so a reader never sees the assignment without the sentence.
|
|
193
|
-
Object.assign(fields, appendNote(card, actor, now, parsed.text));
|
|
194
|
-
}
|
|
195
|
-
return { ok: true, card: write(card, actor, now, fields), changed: true };
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function release(card: Card, actor: Actor, now: number): OpResult {
|
|
199
|
-
// Finished work is history: every other transition refuses it, and a release that did
|
|
200
|
-
// not would be the one way a `done` card could go backwards — reopening the order a
|
|
201
|
-
// dependency was recorded against.
|
|
202
|
-
if (card.status === "done") return { ok: false, error: `${cardRef(card.id)} is done; it cannot be released.` };
|
|
203
|
-
if (card.owner === undefined) return { ok: false, error: `${cardRef(card.id)} is not claimed.` };
|
|
204
|
-
if (!holds(card, actor)) return { ok: false, error: `${heldBy(card)} Release is the owner's decision.` };
|
|
205
|
-
return {
|
|
206
|
-
ok: true,
|
|
207
|
-
card: write(card, actor, now, { status: "todo", owner: undefined, offeredAt: undefined }),
|
|
208
|
-
changed: true,
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function move(
|
|
213
|
-
card: Card,
|
|
214
|
-
op: Extract<CardOp, { kind: "move" }>,
|
|
215
|
-
actor: Actor,
|
|
216
|
-
now: number,
|
|
217
|
-
board: BoardIndex,
|
|
218
|
-
): OpResult {
|
|
219
|
-
const status = op.status;
|
|
220
|
-
if (card.status === "done") return { ok: false, error: `${cardRef(card.id)} is done; it cannot be moved.` };
|
|
221
|
-
if (card.owner === undefined) {
|
|
222
|
-
return { ok: false, error: `${cardRef(card.id)} is unclaimed; claim it before moving it to ${status}.` };
|
|
223
|
-
}
|
|
224
|
-
if (!holds(card, actor)) return { ok: false, error: `${heldBy(card)} Only its owner can move it.` };
|
|
225
|
-
// A block's reason is checked before the no-op: a caller that repeats a move with a new
|
|
226
|
-
// sentence meant to change it, and a refusal is clearer than a silent nothing.
|
|
227
|
-
let blockedBy: string | undefined;
|
|
228
|
-
if (status === "blocked" && op.blockedBy !== undefined) {
|
|
229
|
-
const parsed = normalizeBlockedBy(op.blockedBy);
|
|
230
|
-
if ("error" in parsed) return { ok: false, error: parsed.error };
|
|
231
|
-
blockedBy = parsed.blockedBy;
|
|
232
|
-
}
|
|
233
|
-
if (card.status === status && blockedBy === undefined) return { ok: true, card, changed: false };
|
|
234
|
-
// Parking is what a link is for, so only the two lanes that put a card back in flight
|
|
235
|
-
// are gated: a card that waits may be blocked, it may not be taken or finished.
|
|
236
|
-
if (status !== "blocked") {
|
|
237
|
-
const waiting = waitingOn(card, board);
|
|
238
|
-
if (waiting !== undefined) return { ok: false, error: waiting };
|
|
239
|
-
}
|
|
240
|
-
return { ok: true, card: write(card, actor, now, laneFields(status, blockedBy, op, now)), changed: true };
|
|
241
|
-
}
|
|
242
|
-
|
|
243
225
|
/**
|
|
244
|
-
*
|
|
226
|
+
* A card's log after one more transition, with the oldest dropped and counted.
|
|
245
227
|
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
* commit — a board outside git still finishes cards, it just cannot say with what.
|
|
228
|
+
* The block travels with the entry that parked the card, because the card's own `blocked`
|
|
229
|
+
* is deleted on the way out: without it the answer to "why did this sit for two days" dies
|
|
230
|
+
* with the resume, and the notes are the only other place it could have been written.
|
|
250
231
|
*/
|
|
251
|
-
function
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
232
|
+
function appendTransition(
|
|
233
|
+
card: Card,
|
|
234
|
+
lane: Lane,
|
|
235
|
+
actor: Actor,
|
|
255
236
|
now: number,
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
} else {
|
|
261
|
-
fields.blockedBy = undefined;
|
|
262
|
-
}
|
|
263
|
-
if (status === "done" && op.landed !== undefined) fields.landed = { commit: op.landed.commit, at: now };
|
|
264
|
-
return fields;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function note(card: Card, raw: unknown, actor: Actor, now: number): OpResult {
|
|
268
|
-
const parsed = normalizeNote(raw);
|
|
269
|
-
if ("error" in parsed) return { ok: false, error: parsed.error };
|
|
270
|
-
return { ok: true, card: write(card, actor, now, appendNote(card, actor, now, parsed.text)), changed: true };
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/** A card's notes after one more, with the oldest dropped and counted. */
|
|
274
|
-
function appendNote(card: Card, actor: Actor, now: number, text: string): Pick<Card, "notes" | "notesDropped"> {
|
|
275
|
-
const notes = [...card.notes, { by: actor.label, at: now, text }];
|
|
276
|
-
const overflow = Math.max(0, notes.length - MAX_NOTES);
|
|
277
|
-
return {
|
|
278
|
-
notes: overflow === 0 ? notes : notes.slice(overflow),
|
|
279
|
-
notesDropped: card.notesDropped + overflow,
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/** A card's log after one more transition, with the oldest dropped and counted. */
|
|
284
|
-
function appendTransition(card: Card, status: CardStatus, actor: Actor, now: number): Pick<Card, "log" | "logDropped"> {
|
|
285
|
-
const log = [...(card.log ?? []), { status, at: now, by: actor.label }];
|
|
237
|
+
block: CardBlock | undefined,
|
|
238
|
+
): Pick<Card, "log" | "logDropped"> {
|
|
239
|
+
const entry = block === undefined ? { lane, at: now, by: actor.label } : { lane, at: now, by: actor.label, block };
|
|
240
|
+
const log = [...(card.log ?? []), entry];
|
|
286
241
|
const overflow = Math.max(0, log.length - MAX_LOG);
|
|
287
242
|
return {
|
|
288
243
|
log: overflow === 0 ? log : log.slice(overflow),
|
|
@@ -290,148 +245,37 @@ function appendTransition(card: Card, status: CardStatus, actor: Actor, now: num
|
|
|
290
245
|
};
|
|
291
246
|
}
|
|
292
247
|
|
|
293
|
-
function plan(card: Card, raw: unknown, actor: Actor, now: number): OpResult {
|
|
294
|
-
if (card.status === "done") return { ok: false, error: `${cardRef(card.id)} is done; its plan cannot change.` };
|
|
295
|
-
if (!holds(card, actor)) return { ok: false, error: `${heldBy(card)} Only its owner records a plan.` };
|
|
296
|
-
const parsed = normalizePlanPath(raw);
|
|
297
|
-
if ("error" in parsed) return { ok: false, error: parsed.error };
|
|
298
|
-
if (card.plan === parsed.path) return { ok: true, card, changed: false };
|
|
299
|
-
return { ok: true, card: write(card, actor, now, { plan: parsed.path }), changed: true };
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Fix what a card says: its title, its hints, its priority, and what it waits for.
|
|
304
|
-
*
|
|
305
|
-
* The holder decides for a held card and anyone decides for an unclaimed one — and
|
|
306
|
-
* that includes a `done` card, because a title is what a reader reads, not a step of
|
|
307
|
-
* the work: a finished feature whose title says the wrong thing would stay wrong for
|
|
308
|
-
* as long as the board keeps history.
|
|
309
|
-
*
|
|
310
|
-
* Every field is optional and an absent one is left alone; a field that already holds
|
|
311
|
-
* the value asked for is not a write, so re-editing the same card is a no-op.
|
|
312
|
-
*/
|
|
313
|
-
function edit(
|
|
314
|
-
card: Card,
|
|
315
|
-
op: Extract<CardOp, { kind: "edit" }>,
|
|
316
|
-
actor: Actor,
|
|
317
|
-
now: number,
|
|
318
|
-
board: BoardIndex,
|
|
319
|
-
): OpResult {
|
|
320
|
-
if (card.owner !== undefined && !holds(card, actor)) {
|
|
321
|
-
return { ok: false, error: `${heldBy(card)} Only its owner can edit it.` };
|
|
322
|
-
}
|
|
323
|
-
const fields: Partial<Card> = {};
|
|
324
|
-
if (op.title !== undefined) {
|
|
325
|
-
const parsed = normalizeTitle(op.title);
|
|
326
|
-
if ("error" in parsed) return { ok: false, error: parsed.error };
|
|
327
|
-
if (parsed.title !== card.title) fields.title = parsed.title;
|
|
328
|
-
}
|
|
329
|
-
if (op.paths !== undefined) {
|
|
330
|
-
const parsed = normalizePathHints(op.paths);
|
|
331
|
-
if ("error" in parsed) return { ok: false, error: parsed.error };
|
|
332
|
-
if (!sameList(parsed.paths, card.paths)) fields.paths = parsed.paths;
|
|
333
|
-
}
|
|
334
|
-
if (op.priority !== undefined) {
|
|
335
|
-
const parsed = normalizePriority(op.priority);
|
|
336
|
-
if ("error" in parsed) return { ok: false, error: parsed.error };
|
|
337
|
-
if (parsed.priority !== priorityOf(card)) fields.priority = parsed.priority;
|
|
338
|
-
}
|
|
339
|
-
if (op.dependsOn !== undefined) {
|
|
340
|
-
const parsed = normalizeDepends(op.dependsOn, card.id, board);
|
|
341
|
-
if ("error" in parsed) return { ok: false, error: parsed.error };
|
|
342
|
-
if (!sameList(parsed.dependsOn, dependsOf(card))) fields.dependsOn = parsed.dependsOn;
|
|
343
|
-
}
|
|
344
|
-
if (Object.keys(fields).length === 0) return { ok: true, card, changed: false };
|
|
345
|
-
return { ok: true, card: write(card, actor, now, fields), changed: true };
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
/**
|
|
349
|
-
* Record that the card was offered to the team, and nobody has taken it.
|
|
350
|
-
*
|
|
351
|
-
* The same preconditions a claim would meet, because the offer asks a peer to claim: a
|
|
352
|
-
* held card, a finished one, or one waiting on unfinished work is refused before anything
|
|
353
|
-
* is broadcast. Re-offering refreshes the age rather than refusing as a no-op — a lead
|
|
354
|
-
* who offers again is asking again, and the age is the record of when they last did.
|
|
355
|
-
*/
|
|
356
|
-
function offer(card: Card, actor: Actor, now: number, board: BoardIndex): OpResult {
|
|
357
|
-
if (card.status === "done") return { ok: false, error: `${cardRef(card.id)} is done; it cannot be offered.` };
|
|
358
|
-
if (card.owner !== undefined) return { ok: false, error: heldBy(card) };
|
|
359
|
-
const waiting = waitingOn(card, board);
|
|
360
|
-
if (waiting !== undefined) return { ok: false, error: waiting };
|
|
361
|
-
return { ok: true, card: write(card, actor, now, { offeredAt: now }), changed: true };
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function reclaim(card: Card, actor: Actor, now: number): OpResult {
|
|
365
|
-
if (card.status === "done") return { ok: false, error: `${cardRef(card.id)} is done; there is nothing to reclaim.` };
|
|
366
|
-
if (card.owner === undefined) return { ok: false, error: `${cardRef(card.id)} has no owner to reclaim.` };
|
|
367
|
-
return {
|
|
368
|
-
ok: true,
|
|
369
|
-
card: write(card, actor, now, { status: "todo", owner: undefined, offeredAt: undefined }),
|
|
370
|
-
changed: true,
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* Set a finished card aside, out of the board and into the archive beside it.
|
|
376
|
-
*
|
|
377
|
-
* Only a finished card leaves this way: `done` is terminal, and a card still in flight
|
|
378
|
-
* has an owner, a plan, and a decision in it. No field moves and no transition is
|
|
379
|
-
* appended — the record is unchanged, and what changed is which pile its file lies in,
|
|
380
|
-
* which is the store's write. `changed` is still true, because the board lost a card.
|
|
381
|
-
*/
|
|
382
|
-
function archive(card: Card): OpResult {
|
|
383
|
-
if (card.status !== "done") {
|
|
384
|
-
return { ok: false, error: `${cardRef(card.id)} is in ${card.status}; only a finished card can be archived.` };
|
|
385
|
-
}
|
|
386
|
-
return { ok: true, card, changed: true };
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function drop(card: Card, actor: Actor, board: BoardIndex): OpResult {
|
|
390
|
-
// A drop takes its id with it, so a card another one waits on cannot leave: the
|
|
391
|
-
// remaining link would name nothing, and nothing on the board would say so.
|
|
392
|
-
const depended = dependedOn(card.id, board);
|
|
393
|
-
if (depended !== undefined) return { ok: false, error: depended };
|
|
394
|
-
if (card.status === "todo" || card.status === "done") return { ok: true, card, changed: true };
|
|
395
|
-
if (!holds(card, actor)) return { ok: false, error: `${heldBy(card)} It cannot be dropped while someone holds it.` };
|
|
396
|
-
return { ok: true, card, changed: true };
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
/** Whether two string lists are the same list, in the same order. */
|
|
400
|
-
function sameList(left: readonly string[], right: readonly string[]): boolean {
|
|
401
|
-
return left.length === right.length && left.every((entry, index) => entry === right[index]);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
248
|
/**
|
|
405
249
|
* A card with `fields` applied and its write marks moved.
|
|
406
250
|
*
|
|
407
251
|
* This is the one place a lane change is observed, because it is the one place every
|
|
408
|
-
* transition ends: a note, a
|
|
252
|
+
* transition ends: a note, a contract, or an edit writes fields and leaves the lane alone,
|
|
409
253
|
* while a claim, an assign, a release, a reclaim, and a move all name the new one. So
|
|
410
254
|
* the log is appended from here rather than from each operation, and an operation added
|
|
411
255
|
* later cannot forget it.
|
|
412
256
|
*/
|
|
413
257
|
function write(card: Card, actor: Actor, now: number, fields: Partial<Card>): Card {
|
|
414
|
-
const
|
|
258
|
+
const lane = fields.lane;
|
|
415
259
|
const transition =
|
|
416
|
-
|
|
260
|
+
lane !== undefined && lane !== card.lane
|
|
261
|
+
? appendTransition(card, lane, actor, now, lane === "blocked" ? fields.blocked : undefined)
|
|
262
|
+
: undefined;
|
|
417
263
|
const next: Card = { ...card, ...fields, ...transition, rev: card.rev + 1, updatedAt: now, by: actor.label };
|
|
418
264
|
// `owner: undefined` must clear the field rather than store a key holding it, so a
|
|
419
265
|
// released card reads back exactly like one that was never claimed. Only an
|
|
420
266
|
// explicit `owner` in `fields` may delete it: every other write keeps the holder.
|
|
421
267
|
if ("owner" in fields && fields.owner === undefined) delete next.owner;
|
|
422
|
-
// The same for
|
|
423
|
-
//
|
|
424
|
-
|
|
425
|
-
|
|
268
|
+
// The same for a block and a spot-check: a card that left `blocked` has no reason and no
|
|
269
|
+
// kind, and one that left `execute` is not reviewed, so a key holding undefined would be
|
|
270
|
+
// one more shape for a reader to know about.
|
|
271
|
+
for (const field of ["blocked", "offeredAt", "review"] as const) {
|
|
272
|
+
if (field in fields && fields[field] === undefined) delete next[field];
|
|
273
|
+
}
|
|
274
|
+
// `blocked` is a fact about the `blocked` lane and nothing else: a card back in the pool, or
|
|
275
|
+
// one claimed out of it, is not parked on that decision any more. This is here rather than
|
|
276
|
+
// in each operation because every lane change ends in this one write — so a release, a
|
|
277
|
+
// reclaim, and a claim clean it without any of them having to remember, and the transition
|
|
278
|
+
// log keeps what the park was.
|
|
279
|
+
if (next.lane !== "blocked") delete next.blocked;
|
|
426
280
|
return next;
|
|
427
281
|
}
|
|
428
|
-
|
|
429
|
-
/** The owner recorded for an actor: the id only when this actor has one. */
|
|
430
|
-
function ownerOf(actor: Actor): CardOwner {
|
|
431
|
-
return actor.id === undefined ? { label: actor.label } : { id: actor.id, label: actor.label };
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
/** The refusal for an operation on a card someone else holds. */
|
|
435
|
-
function heldBy(card: Card): string {
|
|
436
|
-
return `${cardRef(card.id)} is held by ${card.owner?.label ?? "another session"}.`;
|
|
437
|
-
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The seat: the name the page serves as, and the actor it is shown as.
|
|
3
|
+
*
|
|
4
|
+
* A page outlives the session that opened it — a `/resume`, a rename, or a fresh session
|
|
5
|
+
* taking the page over would otherwise change who the page says it is. So the page is a *seat*
|
|
6
|
+
* rather than a session: one name for its whole life, one id derived from it, and the same
|
|
7
|
+
* identity however many sessions have served it.
|
|
8
|
+
*
|
|
9
|
+
* That name is `board.lead` — the lead's team name, read by `config.ts` — because the two
|
|
10
|
+
* questions have one answer: the page belongs to the lead, and serving it is how a session
|
|
11
|
+
* *becomes* the lead (the seat outranks the session's own name while it serves, so the gate
|
|
12
|
+
* accepts it). There is no built-in seat name here: a name in this module would be a second
|
|
13
|
+
* answer to a question the config already answers.
|
|
14
|
+
*
|
|
15
|
+
* The page writes nothing, so the id decides no ownership: it is what the page's own roster and
|
|
16
|
+
* the `you` chip are read against, and it keeps a page's reads the same across a handover. The
|
|
17
|
+
* board's tools write as the session — which is why the label is shared with the session
|
|
18
|
+
* serving the page and the ids are not.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { Lead } from "./lead.ts";
|
|
22
|
+
import type { Actor } from "./rules.ts";
|
|
23
|
+
|
|
24
|
+
/** The prefix of a page's actor id, so one is never mistaken for a session id. */
|
|
25
|
+
export const SEAT_ID_PREFIX = "board:";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The actor a page reads as: the lead's name, and an id derived from it.
|
|
29
|
+
*
|
|
30
|
+
* The name arrives already normalized: `board.lead` passes `normalizeLabel` on its way out of
|
|
31
|
+
* `config.ts`, so this only derives the id — one rule for a label, in one place.
|
|
32
|
+
*/
|
|
33
|
+
export function pageActor(lead: Lead): Actor {
|
|
34
|
+
return { id: `${SEAT_ID_PREFIX}${lead.toLowerCase()}`, label: lead };
|
|
35
|
+
}
|