@zhchxiao123/dsh-devflow 0.1.1 → 0.3.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.i18n.yaml +2 -2
- package/README.md +7 -5
- package/README.zh.md +7 -5
- package/lib/index.js +240 -19
- package/lib/types/index.d.ts +151 -9
- package/lib/types/index.js +46 -2
- package/lib/types/invariant.js +1 -1
- package/lib/types/journal.d.ts +23 -2
- package/lib/types/journal.js +80 -7
- package/lib/types/stages.d.ts +43 -7
- package/lib/types/stages.js +73 -12
- package/lib/types/types.d.ts +190 -11
- package/package.json +1 -1
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/devflow/devflow/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 93ad295689a8ebb5071ee844f29db6770ed1e5fa
|
|
6
|
+
README.zh.md: ef5e88a7ecbd690af7fb2346a51de078b3a37d21
|
package/README.md
CHANGED
|
@@ -19,24 +19,26 @@ Every operation carries an explicit **devflow root** dimension: reads take an op
|
|
|
19
19
|
| `resolveCreate(request)` | Explicit defaulting: turns a caller `CreateRequest` (title, Markdown body, optional slug, actor, optional parent, optional root) into the fully specified `CreateSpec` — the slug derived from the title when omitted, the root resolved, plus the creation timestamp. |
|
|
20
20
|
| `create(spec)` | Creates one card: parent validation → sequence-number allocation (continuing past archived cards, so an id is never reissued) → exclusive directory creation → the journal's first `created` entry (the only commit point) → projection write → `devflow/card-created`. Domain rejections resolve `ok: false` with a stable code (`empty-title`, `invalid-slug`, `exists`, `unknown-parent`, `nested-parent`, `parent-settled`); only infrastructure failures reject. |
|
|
21
21
|
| `resolve(request)` | Explicit defaulting: turns a caller `TransitionRequest` into the fully specified `TransitionSpec` with its resolved root and commit timestamp. |
|
|
22
|
-
| `transition(spec)` | Commits one move: revision CAS → edge check → `devflow/transition` waterfall → journal append (the only commit point) → projection rewrite → `devflow/stage-changed`. Domain rejections resolve `ok: false` with a stable code (`revision-mismatch`, `illegal-edge`, `reason-required`, `vetoed`); only infrastructure failures reject. |
|
|
23
|
-
| `claim(id, owner, options?)` | Takes the card's exclusive lease; a held lease resolves with the current holder, unless `options.staleAfterMs` marks its heartbeat lapsed — then the lease
|
|
24
|
-
| `attachArtifact(request)` | Registers a stage deliverable in the journal against the current stage
|
|
22
|
+
| `transition(spec)` | Commits one move: revision CAS → edge check → `devflow/transition` waterfall → cross-process commit lock and journal append (the only commit point) → projection rewrite → `devflow/stage-changed`. Domain rejections resolve `ok: false` with a stable code (`revision-mismatch`, `illegal-edge`, `reason-required`, `vetoed`, `write-contended`); only infrastructure failures reject. |
|
|
23
|
+
| `claim(id, owner, options?)` | Takes the card's exclusive lease; a held lease resolves with the current holder, unless `options.staleAfterMs` marks its heartbeat lapsed — then one concurrent caller journals `claim-expired` and replaces the lease under the commit lock. Lock contention leaves the observed holder in place. |
|
|
24
|
+
| `attachArtifact(request)` | Registers a stage deliverable in the journal against the current stage, in one of two mutually exclusive forms: the reference form records a `path` the caller already wrote under the card directory, and the store-written form hands over `kind` plus `content` for the implementation to write `artifacts/<rev>-<kind>.md` itself before the journal append — still the only commit point, so a lost commit registers nothing and its unreferenced file is overwritten by a same-revision retry. Registrations are immutable: the newest record of one kind is that kind's current content. Rejected while `blocked` or `done`, with `revision-mismatch`, `illegal-edge`, `invalid-kind`, or `write-contended`; the outcome carries the registered `ArtifactRecord`. |
|
|
25
25
|
| `archiveDone(root?)` | Moves every archivable `done` card of one root out of the active set into that root's archive, keyed by the month of its last journal entry; a decomposed requirement archives as one family (a done child waits for its parent, then joins the parent's month bucket). Archived cards leave `list` but keep their complete journal. Returns the archived ids in id order. |
|
|
26
26
|
|
|
27
|
-
Current state always comes from journal replay; a card file's frontmatter is a rebuildable projection. Implementations must fail a read loudly on a structurally invalid journal (naming file and line), warn-and-override on projection drift, and publish state and notifications only after the journal committed. Legal edges (`isLegalTransition`): the pipeline order, rework from `reviewing`/`testing` back to `developing
|
|
27
|
+
Current state always comes from journal replay; a card file's frontmatter is a rebuildable projection. Implementations must fail a read loudly on a structurally invalid journal (naming file and line), warn-and-override on projection drift, and publish state and notifications only after the journal committed. Legal edges (`isLegalTransition`): the pipeline order, rework from `reviewing`/`testing` back to whichever stage owns the fault — `developing` for the implementation, `designing` for the design — `blocked` entry from any non-terminal location, and recovery only to the exact interrupted stage. `done` is terminal in both directions: a delivered card is not reopened, because its history may already have been archived and the seam has no operation that reads the archive. A rework edge (`isReworkEdge`) without a `reason` is rejected `reason-required`, so the next holder always learns what to fix.
|
|
28
28
|
|
|
29
29
|
## Stages and journal
|
|
30
30
|
|
|
31
31
|
`DevStage` is the closed union `draft | designing | ready | developing | reviewing | testing | done`; `blocked` is a bypass location that remembers the stage it interrupted (`CardLocation = DevStage | 'blocked'`). The journal entry union is `created | transition | artifact | claim-expired`, decoded by `decodeJournalEntry` (the durable-boundary validator) and folded by `foldJournal`, which enforces: contiguous revisions from 1, `created` first and only first, transitions departing the current location, and blocked recovery returning exactly to the remembered stage.
|
|
32
32
|
|
|
33
|
+
An `artifact` entry may carry a `kind` naming the deliverable of a store-written registration; entries without one decode and fold exactly as before. `foldArtifactRecords` derives every registration as an `ArtifactRecord` (path, optional kind, journal revision, registering stage), surfaced as `DevCard.artifactRecords` with `DevCard.artifacts` remaining its path projection. A `transition` entry's `gate` records what permitted the move: the human approval signature (`approvedBy`) and/or the gate verdicts (`checks`) a policy listener attached to its permitting waterfall decision — both fields optional, so existing `{ approvedBy }` entries decode unchanged.
|
|
34
|
+
|
|
33
35
|
A requirement too big for one card becomes a **parent card plus one child card per slice**. The edge is the `created` entry's `parent`, so it is fixed at creation, replayable, and never re-pointed; `foldJournal` surfaces it as `DevCard.parent` and the frontmatter `parent:` is its projection. The breakdown is one level deep — a card carrying `parent` is never itself a parent — and parent and children always share a root. Which cards may take children is the provider's creation-time decision (`unknown-parent`, `nested-parent`, `parent-settled`); the seam holds no rule about how a parent's own stage relates to its children's.
|
|
34
36
|
|
|
35
37
|
## Events
|
|
36
38
|
|
|
37
39
|
| Event | Mode | Meaning |
|
|
38
40
|
|---|---|---|
|
|
39
|
-
| `devflow/transition` | `waterfall` | Single-decision pipeline before the commit, dispatched with the complete `TransitionAttempt` (spec plus departure); a policy listener that owns the decision returns `{ allowed: false, reason }` without calling `next()`. [`dsh-devflow-gates`](../devflow-gates/README.md) runs command policies here. |
|
|
41
|
+
| `devflow/transition` | `waterfall` | Single-decision pipeline before the commit, dispatched with the complete `TransitionAttempt` (spec plus departure); a policy listener that owns the decision returns `{ allowed: false, reason }` without calling `next()`, and a permitting decision may carry `approvedBy` and `checks`, recorded as the committed entry's `gate`. [`dsh-devflow-gates`](../devflow-gates/README.md) runs command policies here. |
|
|
40
42
|
| `devflow/card-created` | `emit` | A new card entered the active set: its journal committed the first `created` entry. |
|
|
41
43
|
| `devflow/stage-changed` | `emit` | A card settled at a new location after a committed transition. |
|
|
42
44
|
|
package/README.zh.md
CHANGED
|
@@ -19,24 +19,26 @@
|
|
|
19
19
|
| `resolveCreate(request)` | 显式默认值补全:把调用方的 `CreateRequest`(标题、Markdown 正文、可选 slug、actor、可选 parent、可选 root)变成完全确定的 `CreateSpec`——slug 省略时由标题推导,root 解析定型,并盖上创建时间戳。 |
|
|
20
20
|
| `create(spec)` | 创建一张卡:父卡校验 → 顺序号分配(越过归档卡续排,id 永不复用)→ 独占目录创建 → journal 首条 `created`(唯一提交点)→ 投影写入 → `devflow/card-created`。领域拒绝以稳定 code(`empty-title`、`invalid-slug`、`exists`、`unknown-parent`、`nested-parent`、`parent-settled`)解析为 `ok: false`;仅基础设施故障才 reject。 |
|
|
21
21
|
| `resolve(request)` | 显式默认值补全:把调用方的 `TransitionRequest` 变成完全确定的 `TransitionSpec`,带已解析的 root 与提交时间戳。 |
|
|
22
|
-
| `transition(spec)` | 提交一次移动:revision CAS → 边合法性 → `devflow/transition` waterfall → journal 追加(唯一提交点)→ 投影重写 → `devflow/stage-changed`。领域拒绝以稳定 code(`revision-mismatch`、`illegal-edge`、`reason-required`、`vetoed`)解析为 `ok: false`;仅基础设施故障才 reject。 |
|
|
23
|
-
| `claim(id, owner, options?)` | 取得卡片的独占租约;租约已被持有时解析出当前持有者——除非 `options.staleAfterMs`
|
|
24
|
-
| `attachArtifact(request)` | 按当前阶段在 journal
|
|
22
|
+
| `transition(spec)` | 提交一次移动:revision CAS → 边合法性 → `devflow/transition` waterfall → 跨进程 commit lock 与 journal 追加(唯一提交点)→ 投影重写 → `devflow/stage-changed`。领域拒绝以稳定 code(`revision-mismatch`、`illegal-edge`、`reason-required`、`vetoed`、`write-contended`)解析为 `ok: false`;仅基础设施故障才 reject。 |
|
|
23
|
+
| `claim(id, owner, options?)` | 取得卡片的独占租约;租约已被持有时解析出当前持有者——除非 `options.staleAfterMs` 判定其心跳已过期,此时一个并发调用者在 commit lock 下写入 `claim-expired` 并替换租约。锁竞争让观察到的持有者保持原位。 |
|
|
24
|
+
| `attachArtifact(request)` | 按当前阶段在 journal 登记一个阶段产物,两种互斥形式二选一:引用形式登记调用方已写在卡目录下的 `path`;代写形式交出 `kind` 与 `content`,由实现在 journal 追加之前自行写入 `artifacts/<rev>-<kind>.md`——追加仍是唯一提交点,输掉提交则什么也没登记,其无引用文件会被同 revision 的重试覆盖。登记不可变:同一 kind 最新的记录就是该 kind 的当前内容。`blocked` 或 `done` 时拒绝,并可能返回 `revision-mismatch`、`illegal-edge`、`invalid-kind` 或 `write-contended`;结果携带登记的 `ArtifactRecord`。 |
|
|
25
25
|
| `archiveDone(root?)` | 把一个根中每张可归档的 `done` 卡按其最后一条 journal 的月份移出活跃集合、归入该根的档案;拆分需求以族为单位归档(已完成的子卡等待父卡,随后并入父卡的月份桶)。归档卡从 `list` 消失但保留完整 journal。按 id 顺序返回归档的 id。 |
|
|
26
26
|
|
|
27
|
-
当前状态永远来自 journal 回放;卡片文件的 frontmatter 是可重建的投影。实现必须在 journal 结构非法时读取即失败(指明文件与行号),在投影漂移时告警并覆盖,且只在 journal 提交之后发布状态与通知。合法边(`isLegalTransition`):流水线顺序、`reviewing`/`testing`
|
|
27
|
+
当前状态永远来自 journal 回放;卡片文件的 frontmatter 是可重建的投影。实现必须在 journal 结构非法时读取即失败(指明文件与行号),在投影漂移时告警并覆盖,且只在 journal 提交之后发布状态与通知。合法边(`isLegalTransition`):流水线顺序、`reviewing`/`testing` 打回实际拥有缺陷的阶段——实现问题回 `developing`,设计问题回 `designing`——任意非终态进入 `blocked`、且只能恢复到被打断的那个阶段。`done` 双向都是终态:已交付卡片可能已经归档,而缝没有读取归档的操作。无 `reason` 的打回边(`isReworkEdge`)以 `reason-required` 拒绝,下一个持有者永远知道要修什么。
|
|
28
28
|
|
|
29
29
|
## 阶段与 journal
|
|
30
30
|
|
|
31
31
|
`DevStage` 是闭合联合 `draft | designing | ready | developing | reviewing | testing | done`;`blocked` 是记住被打断阶段的旁路位置(`CardLocation = DevStage | 'blocked'`)。journal 条目联合为 `created | transition | artifact | claim-expired`,由 `decodeJournalEntry`(持久化边界校验器)解码、`foldJournal` 折叠,后者强制:revision 从 1 连续、`created` 必须且只能是首条、transition 必须从当前位置出发、blocked 恢复必须回到被记住的阶段。
|
|
32
32
|
|
|
33
|
+
`artifact` 条目可以携带 `kind`,指名一次代写登记的产物种类;不带 kind 的条目解码与折叠与从前完全一致。`foldArtifactRecords` 把每次登记折出为一条 `ArtifactRecord`(路径、可选 kind、journal revision、登记阶段),以 `DevCard.artifactRecords` 呈现,`DevCard.artifacts` 仍是其路径投影。`transition` 条目的 `gate` 记录放行这次移动的事实:策略监听器附在放行的 waterfall 决策上的人工审批签名(`approvedBy`)与/或门禁裁决(`checks`)——两个字段皆可选,既有的 `{ approvedBy }` 条目解码不变。
|
|
34
|
+
|
|
33
35
|
一张卡装不下的大需求拆成**一张父卡加每个切片一张子卡**。这条边就是 `created` 条目的 `parent`,因此创建时即固定、可回放、永不改指;`foldJournal` 把它折出为 `DevCard.parent`,frontmatter 的 `parent:` 是其投影。拆分只有一层——带 `parent` 的卡自身永远不会成为父卡——父卡与子卡始终同根。哪些卡可以接子卡是 provider 的创建期决策(`unknown-parent`、`nested-parent`、`parent-settled`);缝本身不持有"父卡阶段与子卡阶段如何关联"的任何规则。
|
|
34
36
|
|
|
35
37
|
## 事件
|
|
36
38
|
|
|
37
39
|
| 事件 | 模式 | 含义 |
|
|
38
40
|
|---|---|---|
|
|
39
|
-
| `devflow/transition` | `waterfall` | 提交前的单决策管线,以完整 `TransitionAttempt`(spec 加出发位置)分发;拥有决策的策略监听器不调 `next()` 直接返回 `{ allowed: false, reason }`。[`dsh-devflow-gates`](../devflow-gates/README.zh.md) 在此运行命令策略。 |
|
|
41
|
+
| `devflow/transition` | `waterfall` | 提交前的单决策管线,以完整 `TransitionAttempt`(spec 加出发位置)分发;拥有决策的策略监听器不调 `next()` 直接返回 `{ allowed: false, reason }`,放行的决策可携带 `approvedBy` 与 `checks`,记入已提交条目的 `gate`。[`dsh-devflow-gates`](../devflow-gates/README.zh.md) 在此运行命令策略。 |
|
|
40
42
|
| `devflow/card-created` | `emit` | 一张新卡进入活跃集合:其 journal 提交了首条 `created`。 |
|
|
41
43
|
| `devflow/stage-changed` | `emit` | 一次已提交的流转后,卡片落在新位置。 |
|
|
42
44
|
|
package/lib/index.js
CHANGED
|
@@ -28,6 +28,31 @@ function isCardLocation(value) {
|
|
|
28
28
|
return value === "blocked" || isDevStage(value);
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
|
+
* The service classes, in ascending order of what they skip.
|
|
32
|
+
*
|
|
33
|
+
* Closed vocabulary rather than a plugin `Config` field, on the same grounds as
|
|
34
|
+
* {@link DEV_STAGES}: the board, both language documents, and the agent's
|
|
35
|
+
* prompts all reference these names, and a deployment-defined class would make
|
|
36
|
+
* every one of those references local. Letting a deployment mint its own
|
|
37
|
+
* shorter class is also precisely the failure mode this vocabulary exists to
|
|
38
|
+
* prevent — see the service-class Agent Note.
|
|
39
|
+
*/
|
|
40
|
+
const SERVICE_CLASSES = [
|
|
41
|
+
"standard",
|
|
42
|
+
"express",
|
|
43
|
+
"emergency"
|
|
44
|
+
];
|
|
45
|
+
/** The class of a card that declares none, on disk and in memory. */
|
|
46
|
+
const DEFAULT_SERVICE_CLASS = "standard";
|
|
47
|
+
/**
|
|
48
|
+
* Narrow an unknown value to a service class.
|
|
49
|
+
* @param value - the candidate value.
|
|
50
|
+
* @returns `true` when `value` is one of {@link SERVICE_CLASSES}.
|
|
51
|
+
*/
|
|
52
|
+
function isServiceClass(value) {
|
|
53
|
+
return typeof value === "string" && SERVICE_CLASSES.includes(value);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
31
56
|
* Brand a raw string as a {@link DevflowCardId}. The id equals the card's
|
|
32
57
|
* directory name; construction lives here because this package owns the brand.
|
|
33
58
|
* @param value - the card directory name.
|
|
@@ -36,41 +61,93 @@ function isCardLocation(value) {
|
|
|
36
61
|
function DevflowCardId(value) {
|
|
37
62
|
return value;
|
|
38
63
|
}
|
|
39
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Forward and rework edges of the pipeline; `blocked` legality lives in
|
|
66
|
+
* {@link isLegalTransition}.
|
|
67
|
+
*
|
|
68
|
+
* Review and verification send a card back to whichever stage owns the fault:
|
|
69
|
+
* `developing` when the implementation is wrong, `designing` when the design
|
|
70
|
+
* is. Without the second, design rework happens on a card labelled
|
|
71
|
+
* `developing`, and the board stops answering the one question it exists to
|
|
72
|
+
* answer.
|
|
73
|
+
*
|
|
74
|
+
* `developing` reaches `designing` for the same reason, from the stage that
|
|
75
|
+
* finds such faults most often. Its absence left `developing → reviewing →
|
|
76
|
+
* designing` as the only route back, which records a review that never
|
|
77
|
+
* happened in the authoritative journal to reach the stage owning the fault.
|
|
78
|
+
*/
|
|
40
79
|
const FLOW = {
|
|
41
80
|
draft: ["designing"],
|
|
42
81
|
designing: ["ready"],
|
|
43
82
|
ready: ["developing"],
|
|
44
|
-
developing: ["reviewing"],
|
|
45
|
-
reviewing: [
|
|
46
|
-
|
|
83
|
+
developing: ["reviewing", "designing"],
|
|
84
|
+
reviewing: [
|
|
85
|
+
"testing",
|
|
86
|
+
"developing",
|
|
87
|
+
"designing"
|
|
88
|
+
],
|
|
89
|
+
testing: [
|
|
90
|
+
"done",
|
|
91
|
+
"developing",
|
|
92
|
+
"designing"
|
|
93
|
+
],
|
|
47
94
|
done: []
|
|
48
95
|
};
|
|
49
96
|
/**
|
|
97
|
+
* Edges each service class adds to {@link FLOW}, and the only place a class
|
|
98
|
+
* differs from another. Stated as additions rather than as one whole graph per
|
|
99
|
+
* class so "every class is a superset of `standard`" is a property of the code
|
|
100
|
+
* instead of a convention: a class cannot remove an edge, and therefore cannot
|
|
101
|
+
* make a journal that replays today stop replaying.
|
|
102
|
+
*/
|
|
103
|
+
const CLASS_EXTRA = {
|
|
104
|
+
standard: {},
|
|
105
|
+
express: {
|
|
106
|
+
draft: ["developing"],
|
|
107
|
+
reviewing: ["done"]
|
|
108
|
+
},
|
|
109
|
+
emergency: {
|
|
110
|
+
draft: ["developing"],
|
|
111
|
+
developing: ["done"]
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
50
115
|
* Whether one stage move is a legal edge of the state machine.
|
|
51
116
|
*
|
|
52
117
|
* Main flow follows the pipeline order; `reviewing` and `testing` may rework
|
|
53
|
-
* to `developing
|
|
54
|
-
*
|
|
118
|
+
* to `developing` or `designing` and `developing` may rework to `designing`;
|
|
119
|
+
* any non-terminal location may enter `blocked`; a blocked card may only
|
|
120
|
+
* recover to the exact stage it interrupted. A card's service class adds the
|
|
121
|
+
* shortcuts in {@link CLASS_EXTRA} and takes nothing away.
|
|
122
|
+
*
|
|
123
|
+
* `blocked` legality does not vary by class: a shortcut is about which stages
|
|
124
|
+
* a card may skip, not about how it pauses.
|
|
55
125
|
* @param from - the card's current location.
|
|
56
126
|
* @param to - the requested target location.
|
|
57
|
-
* @param
|
|
127
|
+
* @param card - the moving card's own context; omitted reads as a `standard`
|
|
128
|
+
* card that is not blocked.
|
|
58
129
|
* @returns `true` when the move is a legal edge.
|
|
59
130
|
*/
|
|
60
|
-
function isLegalTransition(from, to,
|
|
131
|
+
function isLegalTransition(from, to, card) {
|
|
61
132
|
if (from === to) return false;
|
|
62
|
-
if (from === "blocked") return to === blockedFrom;
|
|
133
|
+
if (from === "blocked") return to === card?.blockedFrom;
|
|
63
134
|
if (to === "blocked") return from !== "done";
|
|
64
|
-
|
|
135
|
+
if (FLOW[from].includes(to)) return true;
|
|
136
|
+
return (CLASS_EXTRA[card?.serviceClass ?? "standard"][from] ?? []).includes(to);
|
|
65
137
|
}
|
|
66
138
|
/**
|
|
67
139
|
* Whether a legal edge moves the card backwards (a rework). Rework edges
|
|
68
|
-
* require a recorded `reason` so the next holder knows what to fix
|
|
140
|
+
* require a recorded `reason` so the next holder knows what to fix — on
|
|
141
|
+
* `developing -> designing` that reason is what implementing the design
|
|
142
|
+
* revealed about it, which is the whole point of routing the card back rather
|
|
143
|
+
* than redesigning in place.
|
|
69
144
|
* @param from - the departing location.
|
|
70
145
|
* @param to - the target location.
|
|
71
|
-
* @returns `true` for
|
|
146
|
+
* @returns `true` for a move from `reviewing` or `testing` back to
|
|
147
|
+
* `developing` or `designing`, and for `developing` back to `designing`.
|
|
72
148
|
*/
|
|
73
149
|
function isReworkEdge(from, to) {
|
|
150
|
+
if (to === "designing") return from === "developing" || from === "reviewing" || from === "testing";
|
|
74
151
|
return to === "developing" && (from === "reviewing" || from === "testing");
|
|
75
152
|
}
|
|
76
153
|
//#endregion
|
|
@@ -104,7 +181,8 @@ function decodeJournalEntry(value) {
|
|
|
104
181
|
at: entry.at,
|
|
105
182
|
type: "created",
|
|
106
183
|
by: decodeActor(entry.by),
|
|
107
|
-
...decodeOptionalCardId(entry, "parent")
|
|
184
|
+
...decodeOptionalCardId(entry, "parent"),
|
|
185
|
+
...decodeOptionalServiceClass(entry)
|
|
108
186
|
};
|
|
109
187
|
case "transition":
|
|
110
188
|
if (!isCardLocation(entry.from)) throw new Error("transition field \"from\" must be a stage or \"blocked\"");
|
|
@@ -128,7 +206,17 @@ function decodeJournalEntry(value) {
|
|
|
128
206
|
type: "artifact",
|
|
129
207
|
path: entry.path,
|
|
130
208
|
stage: entry.stage,
|
|
131
|
-
...entry.by !== void 0 ? { by: decodeActor(entry.by) } : {}
|
|
209
|
+
...entry.by !== void 0 ? { by: decodeActor(entry.by) } : {},
|
|
210
|
+
...decodeOptionalString(entry, "kind")
|
|
211
|
+
};
|
|
212
|
+
case "abandoned":
|
|
213
|
+
if (typeof entry.reason !== "string" || entry.reason.trim().length === 0) throw new Error("abandoned field \"reason\" must be a non-empty string; a card leaving the board without one loses the decision");
|
|
214
|
+
return {
|
|
215
|
+
rev,
|
|
216
|
+
at: entry.at,
|
|
217
|
+
type: "abandoned",
|
|
218
|
+
by: decodeActor(entry.by),
|
|
219
|
+
reason: entry.reason
|
|
132
220
|
};
|
|
133
221
|
case "claim-expired":
|
|
134
222
|
if (entry.previousOwner === void 0) throw new Error("claim-expired field \"previousOwner\" is required");
|
|
@@ -139,7 +227,7 @@ function decodeJournalEntry(value) {
|
|
|
139
227
|
previousOwner: decodeActor(entry.previousOwner),
|
|
140
228
|
by: decodeActor(entry.by)
|
|
141
229
|
};
|
|
142
|
-
default: throw new Error(`journal entry field "type" must be created, transition, artifact, or claim-expired (got ${JSON.stringify(entry.type)})`);
|
|
230
|
+
default: throw new Error(`journal entry field "type" must be created, transition, artifact, abandoned, or claim-expired (got ${JSON.stringify(entry.type)})`);
|
|
143
231
|
}
|
|
144
232
|
}
|
|
145
233
|
/**
|
|
@@ -148,7 +236,8 @@ function decodeJournalEntry(value) {
|
|
|
148
236
|
* Validates the structural invariants of the durable stream: revisions are the
|
|
149
237
|
* contiguous sequence 1..n, the first entry is `created`, every transition
|
|
150
238
|
* departs from the current location, a move to `blocked` remembers its origin,
|
|
151
|
-
*
|
|
239
|
+
* the matching recovery returns exactly there, and nothing follows an
|
|
240
|
+
* `abandoned` entry.
|
|
152
241
|
* @param entries - decoded entries in file order.
|
|
153
242
|
* @returns the folded card state.
|
|
154
243
|
* @throws {Error} naming the first violated invariant and its entry revision.
|
|
@@ -158,6 +247,7 @@ function foldJournal(entries) {
|
|
|
158
247
|
const state = {
|
|
159
248
|
stage: "draft",
|
|
160
249
|
revision: 0,
|
|
250
|
+
serviceClass: DEFAULT_SERVICE_CLASS,
|
|
161
251
|
artifacts: []
|
|
162
252
|
};
|
|
163
253
|
for (const [index, entry] of entries.entries()) {
|
|
@@ -165,9 +255,11 @@ function foldJournal(entries) {
|
|
|
165
255
|
if (index === 0) {
|
|
166
256
|
if (entry.type !== "created") throw new Error("journal entry 1 must be \"created\"");
|
|
167
257
|
if (entry.parent !== void 0) state.parent = entry.parent;
|
|
258
|
+
if (entry.serviceClass !== void 0) state.serviceClass = entry.serviceClass;
|
|
168
259
|
state.revision = entry.rev;
|
|
169
260
|
continue;
|
|
170
261
|
}
|
|
262
|
+
if (state.abandoned === true) throw new Error(`journal entry rev ${entry.rev} follows an abandoned card; abandoning is terminal`);
|
|
171
263
|
switch (entry.type) {
|
|
172
264
|
case "created": throw new Error(`journal entry rev ${entry.rev} repeats "created"`);
|
|
173
265
|
case "transition":
|
|
@@ -185,16 +277,57 @@ function foldJournal(entries) {
|
|
|
185
277
|
state.artifacts.push(entry.path);
|
|
186
278
|
state.revision = entry.rev;
|
|
187
279
|
break;
|
|
280
|
+
case "abandoned":
|
|
281
|
+
state.abandoned = true;
|
|
282
|
+
state.revision = entry.rev;
|
|
283
|
+
break;
|
|
188
284
|
case "claim-expired": state.revision = entry.rev;
|
|
189
285
|
}
|
|
190
286
|
}
|
|
191
287
|
return state;
|
|
192
288
|
}
|
|
289
|
+
/**
|
|
290
|
+
* Derive the artifact registrations of a decoded journal, in registration
|
|
291
|
+
* order. Kept beside {@link foldJournal} — whose `artifacts` is this list's
|
|
292
|
+
* path projection — so every consumer derives identical records; an entry
|
|
293
|
+
* without a `kind` yields a record without one.
|
|
294
|
+
* @param entries - decoded entries in file order.
|
|
295
|
+
* @returns the artifact records, oldest first.
|
|
296
|
+
*/
|
|
297
|
+
function foldArtifactRecords(entries) {
|
|
298
|
+
const records = [];
|
|
299
|
+
for (const entry of entries) {
|
|
300
|
+
if (entry.type !== "artifact") continue;
|
|
301
|
+
records.push({
|
|
302
|
+
path: entry.path,
|
|
303
|
+
...entry.kind !== void 0 ? { kind: entry.kind } : {},
|
|
304
|
+
rev: entry.rev,
|
|
305
|
+
stage: entry.stage
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
return records;
|
|
309
|
+
}
|
|
193
310
|
function decodeGate(value) {
|
|
194
311
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("transition field \"gate\" must be a JSON object");
|
|
195
312
|
const gate = value;
|
|
196
|
-
if (gate.approvedBy === void 0) throw new Error("transition field \"gate\" requires \"approvedBy\"");
|
|
197
|
-
return {
|
|
313
|
+
if (gate.approvedBy === void 0 && gate.checks === void 0) throw new Error("transition field \"gate\" requires \"approvedBy\" or \"checks\"");
|
|
314
|
+
return {
|
|
315
|
+
...gate.approvedBy !== void 0 ? { approvedBy: decodeActor(gate.approvedBy) } : {},
|
|
316
|
+
...gate.checks !== void 0 ? { checks: decodeGateChecks(gate.checks) } : {}
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
function decodeGateChecks(value) {
|
|
320
|
+
if (!Array.isArray(value)) throw new Error("transition field \"gate.checks\" must be an array");
|
|
321
|
+
return value.map((check) => {
|
|
322
|
+
if (typeof check !== "object" || check === null || Array.isArray(check)) throw new Error("gate check must be a JSON object");
|
|
323
|
+
const record = check;
|
|
324
|
+
if (record.verdict !== "allowed") throw new Error(`gate check field "verdict" must be "allowed" (got ${JSON.stringify(record.verdict)})`);
|
|
325
|
+
return {
|
|
326
|
+
by: decodeActor(record.by),
|
|
327
|
+
verdict: "allowed",
|
|
328
|
+
...decodeOptionalString(record, "summary")
|
|
329
|
+
};
|
|
330
|
+
});
|
|
198
331
|
}
|
|
199
332
|
function decodeActor(value) {
|
|
200
333
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("actor must be a JSON object");
|
|
@@ -215,6 +348,12 @@ function decodeActor(value) {
|
|
|
215
348
|
default: throw new Error(`actor field "kind" must be human, agent, or command (got ${JSON.stringify(actor.kind)})`);
|
|
216
349
|
}
|
|
217
350
|
}
|
|
351
|
+
function decodeOptionalServiceClass(record) {
|
|
352
|
+
const value = record.serviceClass;
|
|
353
|
+
if (value === void 0) return {};
|
|
354
|
+
if (!isServiceClass(value)) throw new Error(`created field "serviceClass" must be one of ${SERVICE_CLASSES.join(", ")} when present`);
|
|
355
|
+
return { serviceClass: value };
|
|
356
|
+
}
|
|
218
357
|
function decodeOptionalCardId(record, key) {
|
|
219
358
|
const value = record[key];
|
|
220
359
|
if (value === void 0) return {};
|
|
@@ -237,6 +376,88 @@ function decodeOptionalString(record, key) {
|
|
|
237
376
|
* model-facing tools belong to `@zhchxiao123/dsh-devflow-tool`.
|
|
238
377
|
* @module @zhchxiao123/dsh-devflow
|
|
239
378
|
*/
|
|
379
|
+
/** JSON Schema for the Definition-owned artifact registration record. */
|
|
380
|
+
const ARTIFACT_RECORD_SCHEMA = {
|
|
381
|
+
type: "object",
|
|
382
|
+
additionalProperties: false,
|
|
383
|
+
properties: {
|
|
384
|
+
path: {
|
|
385
|
+
type: "string",
|
|
386
|
+
required: true
|
|
387
|
+
},
|
|
388
|
+
kind: { type: "string" },
|
|
389
|
+
rev: {
|
|
390
|
+
type: "integer",
|
|
391
|
+
required: true
|
|
392
|
+
},
|
|
393
|
+
stage: {
|
|
394
|
+
type: "string",
|
|
395
|
+
required: true,
|
|
396
|
+
enum: [...DEV_STAGES]
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
/** JSON Schema for one public artifact transition inspection. */
|
|
401
|
+
const ARTIFACT_TRANSITION_INSPECTION_SCHEMA = {
|
|
402
|
+
type: "object",
|
|
403
|
+
additionalProperties: false,
|
|
404
|
+
properties: {
|
|
405
|
+
from: {
|
|
406
|
+
type: "string",
|
|
407
|
+
required: true,
|
|
408
|
+
enum: [...DEV_STAGES, "blocked"]
|
|
409
|
+
},
|
|
410
|
+
to: {
|
|
411
|
+
type: "string",
|
|
412
|
+
required: true,
|
|
413
|
+
enum: [...DEV_STAGES, "blocked"]
|
|
414
|
+
},
|
|
415
|
+
requirements: {
|
|
416
|
+
type: "array",
|
|
417
|
+
required: true,
|
|
418
|
+
items: {
|
|
419
|
+
type: "object",
|
|
420
|
+
additionalProperties: false,
|
|
421
|
+
properties: {
|
|
422
|
+
kind: {
|
|
423
|
+
type: "string",
|
|
424
|
+
required: true
|
|
425
|
+
},
|
|
426
|
+
status: {
|
|
427
|
+
type: "string",
|
|
428
|
+
required: true,
|
|
429
|
+
enum: [
|
|
430
|
+
"missing",
|
|
431
|
+
"malformed",
|
|
432
|
+
"satisfied"
|
|
433
|
+
]
|
|
434
|
+
},
|
|
435
|
+
spec: {
|
|
436
|
+
type: "object",
|
|
437
|
+
required: true,
|
|
438
|
+
additionalProperties: false,
|
|
439
|
+
properties: {
|
|
440
|
+
frontmatter: {
|
|
441
|
+
type: "array",
|
|
442
|
+
items: { type: "string" }
|
|
443
|
+
},
|
|
444
|
+
sections: {
|
|
445
|
+
type: "array",
|
|
446
|
+
items: { type: "string" }
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
artifact: ARTIFACT_RECORD_SCHEMA,
|
|
451
|
+
defects: {
|
|
452
|
+
type: "array",
|
|
453
|
+
required: true,
|
|
454
|
+
items: { type: "string" }
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
};
|
|
240
461
|
/**
|
|
241
462
|
* Abstract task-card store registered as `ctx.devflow` (one implementation per
|
|
242
463
|
* context; loading a second throws, cordis' standard duplicate-service
|
|
@@ -320,6 +541,6 @@ function rootOfCwd(cwd) {
|
|
|
320
541
|
return cwd === void 0 ? void 0 : join(cwd, ".devflow");
|
|
321
542
|
}
|
|
322
543
|
//#endregion
|
|
323
|
-
export { DEV_STAGES, DevflowCardId, DevflowStore, DevflowStore as default, decodeJournalEntry, foldJournal, isCardLocation, isDevStage, isLegalTransition, isReworkEdge };
|
|
544
|
+
export { ARTIFACT_RECORD_SCHEMA, ARTIFACT_TRANSITION_INSPECTION_SCHEMA, DEFAULT_SERVICE_CLASS, DEV_STAGES, DevflowCardId, DevflowStore, DevflowStore as default, SERVICE_CLASSES, decodeJournalEntry, foldArtifactRecords, foldJournal, isCardLocation, isDevStage, isLegalTransition, isReworkEdge, isServiceClass };
|
|
324
545
|
|
|
325
546
|
//# sourceMappingURL=index.js.map
|
package/lib/types/index.d.ts
CHANGED
|
@@ -7,11 +7,119 @@
|
|
|
7
7
|
* @module @zhchxiao123/dsh-devflow
|
|
8
8
|
*/
|
|
9
9
|
import { Context, Service } from '@deepseek-ai/cordis';
|
|
10
|
-
import type { ArtifactRequest, ArtifactResult, CardFilter, ClaimHolder, ClaimOptions, ClaimResult, CreateRequest, CreateResult, CreateSpec, DevActor, DevCard, DevCardDetail, DevflowCardId, DevflowJournalEntry, TransitionRequest, TransitionResult, TransitionSpec } from './types.ts';
|
|
10
|
+
import type { AbandonRequest, AbandonResult, ArtifactRequest, ArtifactResult, CardFilter, ClaimHolder, ClaimOptions, ClaimResult, CreateRequest, CreateResult, CreateSpec, DevActor, DevCard, DevCardDetail, DevflowCardId, DevflowJournalEntry, TransitionRequest, TransitionResult, TransitionSpec } from './types.ts';
|
|
11
11
|
export type * from './types.ts';
|
|
12
|
-
export { DEV_STAGES, DevflowCardId, isCardLocation, isDevStage, isLegalTransition, isReworkEdge } from './stages.ts';
|
|
13
|
-
export {
|
|
12
|
+
export { DEFAULT_SERVICE_CLASS, DEV_STAGES, DevflowCardId, SERVICE_CLASSES, isCardLocation, isDevStage, isLegalTransition, isReworkEdge, isServiceClass } from './stages.ts';
|
|
13
|
+
export type { TransitionContext } from './stages.ts';
|
|
14
|
+
export { decodeJournalEntry, foldArtifactRecords, foldJournal } from './journal.ts';
|
|
14
15
|
export type { JournalFoldState } from './journal.ts';
|
|
16
|
+
/** JSON Schema for the Definition-owned artifact registration record. */
|
|
17
|
+
export declare const ARTIFACT_RECORD_SCHEMA: {
|
|
18
|
+
readonly type: "object";
|
|
19
|
+
readonly additionalProperties: false;
|
|
20
|
+
readonly properties: {
|
|
21
|
+
readonly path: {
|
|
22
|
+
readonly type: "string";
|
|
23
|
+
readonly required: true;
|
|
24
|
+
};
|
|
25
|
+
readonly kind: {
|
|
26
|
+
readonly type: "string";
|
|
27
|
+
};
|
|
28
|
+
readonly rev: {
|
|
29
|
+
readonly type: "integer";
|
|
30
|
+
readonly required: true;
|
|
31
|
+
};
|
|
32
|
+
readonly stage: {
|
|
33
|
+
readonly type: "string";
|
|
34
|
+
readonly required: true;
|
|
35
|
+
readonly enum: readonly ["draft", "designing", "ready", "developing", "reviewing", "testing", "done"];
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
/** JSON Schema for one public artifact transition inspection. */
|
|
40
|
+
export declare const ARTIFACT_TRANSITION_INSPECTION_SCHEMA: {
|
|
41
|
+
readonly type: "object";
|
|
42
|
+
readonly additionalProperties: false;
|
|
43
|
+
readonly properties: {
|
|
44
|
+
readonly from: {
|
|
45
|
+
readonly type: "string";
|
|
46
|
+
readonly required: true;
|
|
47
|
+
readonly enum: readonly ["draft", "designing", "ready", "developing", "reviewing", "testing", "done", "blocked"];
|
|
48
|
+
};
|
|
49
|
+
readonly to: {
|
|
50
|
+
readonly type: "string";
|
|
51
|
+
readonly required: true;
|
|
52
|
+
readonly enum: readonly ["draft", "designing", "ready", "developing", "reviewing", "testing", "done", "blocked"];
|
|
53
|
+
};
|
|
54
|
+
readonly requirements: {
|
|
55
|
+
readonly type: "array";
|
|
56
|
+
readonly required: true;
|
|
57
|
+
readonly items: {
|
|
58
|
+
readonly type: "object";
|
|
59
|
+
readonly additionalProperties: false;
|
|
60
|
+
readonly properties: {
|
|
61
|
+
readonly kind: {
|
|
62
|
+
readonly type: "string";
|
|
63
|
+
readonly required: true;
|
|
64
|
+
};
|
|
65
|
+
readonly status: {
|
|
66
|
+
readonly type: "string";
|
|
67
|
+
readonly required: true;
|
|
68
|
+
readonly enum: readonly ["missing", "malformed", "satisfied"];
|
|
69
|
+
};
|
|
70
|
+
readonly spec: {
|
|
71
|
+
readonly type: "object";
|
|
72
|
+
readonly required: true;
|
|
73
|
+
readonly additionalProperties: false;
|
|
74
|
+
readonly properties: {
|
|
75
|
+
readonly frontmatter: {
|
|
76
|
+
readonly type: "array";
|
|
77
|
+
readonly items: {
|
|
78
|
+
readonly type: "string";
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
readonly sections: {
|
|
82
|
+
readonly type: "array";
|
|
83
|
+
readonly items: {
|
|
84
|
+
readonly type: "string";
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
readonly artifact: {
|
|
90
|
+
readonly type: "object";
|
|
91
|
+
readonly additionalProperties: false;
|
|
92
|
+
readonly properties: {
|
|
93
|
+
readonly path: {
|
|
94
|
+
readonly type: "string";
|
|
95
|
+
readonly required: true;
|
|
96
|
+
};
|
|
97
|
+
readonly kind: {
|
|
98
|
+
readonly type: "string";
|
|
99
|
+
};
|
|
100
|
+
readonly rev: {
|
|
101
|
+
readonly type: "integer";
|
|
102
|
+
readonly required: true;
|
|
103
|
+
};
|
|
104
|
+
readonly stage: {
|
|
105
|
+
readonly type: "string";
|
|
106
|
+
readonly required: true;
|
|
107
|
+
readonly enum: readonly ["draft", "designing", "ready", "developing", "reviewing", "testing", "done"];
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
readonly defects: {
|
|
112
|
+
readonly type: "array";
|
|
113
|
+
readonly required: true;
|
|
114
|
+
readonly items: {
|
|
115
|
+
readonly type: "string";
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
};
|
|
15
123
|
declare module '@deepseek-ai/cordis' {
|
|
16
124
|
interface Context {
|
|
17
125
|
devflow: DevflowStore;
|
|
@@ -91,27 +199,61 @@ export declare abstract class DevflowStore extends Service {
|
|
|
91
199
|
* `devflow/transition` waterfall, the journal append (the only commit
|
|
92
200
|
* point), the projection rewrite, then `devflow/stage-changed`. State and
|
|
93
201
|
* notifications publish only after the journal committed.
|
|
202
|
+
*
|
|
203
|
+
* The waterfall's gate commands put real time between those checks and the
|
|
204
|
+
* append, so implementations must re-establish the checked revision at the
|
|
205
|
+
* append itself, under an exclusion another process observes. A card that
|
|
206
|
+
* moved in that window resolves `revision-mismatch`; a card whose commit
|
|
207
|
+
* stayed excluded resolves `write-contended` with nothing written.
|
|
94
208
|
* @param spec - a resolved spec from {@link resolve}, never a raw request.
|
|
95
209
|
* @returns the outcome; domain rejections resolve with `ok: false`.
|
|
96
210
|
*/
|
|
97
211
|
abstract transition(spec: TransitionSpec): Promise<TransitionResult>;
|
|
98
212
|
/**
|
|
99
|
-
* Take the card's exclusive lease.
|
|
213
|
+
* Take the card's exclusive lease. A stale takeover journals the eviction
|
|
214
|
+
* under the same cross-process commit exclusion as transitions and artifact
|
|
215
|
+
* registration, so concurrent takeover attempts grant at most one holder.
|
|
100
216
|
* @param id - the card to claim.
|
|
101
217
|
* @param owner - the prospective holder, recorded in the lease.
|
|
102
218
|
* @param options - staleness takeover policy and root; omitted never takes
|
|
103
219
|
* over and uses the implementation's default root.
|
|
104
|
-
* @returns the live handle, or
|
|
220
|
+
* @returns the live handle, or a holder read from the lease. On journal-commit
|
|
221
|
+
* contention that holder was observed before trying the lock, not freshly
|
|
222
|
+
* established as the current owner.
|
|
105
223
|
*/
|
|
106
224
|
abstract claim(id: DevflowCardId, owner: DevActor, options?: ClaimOptions): Promise<ClaimResult>;
|
|
107
225
|
/**
|
|
108
226
|
* Register a stage deliverable in the card's journal against its current
|
|
109
|
-
* stage
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
227
|
+
* stage, in one of two mutually exclusive forms: the reference form records
|
|
228
|
+
* a `path` the caller already wrote under the card directory, and the
|
|
229
|
+
* store-written form hands over `kind` plus `content` for the
|
|
230
|
+
* implementation to write `artifacts/<rev>-<kind>.md` itself before the
|
|
231
|
+
* journal append — which stays the only commit point, so a registration
|
|
232
|
+
* that loses the commit registers nothing and its unreferenced file is
|
|
233
|
+
* overwritten by a same-revision retry. Registrations are immutable: the
|
|
234
|
+
* newest record of one kind is that kind's current content. A blocked or
|
|
235
|
+
* done card cannot register artifacts, the revision check mirrors
|
|
236
|
+
* {@link transition}, and an ill-formed kind resolves `invalid-kind`.
|
|
237
|
+
* @param request - card, expected revision, actor, and the artifact reference or content.
|
|
238
|
+
* @returns the outcome carrying the registered record; domain rejections resolve with `ok: false`.
|
|
113
239
|
*/
|
|
114
240
|
abstract attachArtifact(request: ArtifactRequest): Promise<ArtifactResult>;
|
|
241
|
+
/**
|
|
242
|
+
* Record that a card's work stops and take it off the active board.
|
|
243
|
+
*
|
|
244
|
+
* The journal append is the commit point; the card's directory then joins
|
|
245
|
+
* the archive. A crash between the two leaves an abandoned card under
|
|
246
|
+
* `tasks/`, so {@link list} must exclude it by its folded state rather than
|
|
247
|
+
* by where its directory sits.
|
|
248
|
+
*
|
|
249
|
+
* The reason is required — a card that disappears without one loses the
|
|
250
|
+
* decision it exists to record — and a `done` card is refused: a delivered
|
|
251
|
+
* outcome is settled by {@link archiveDone}, not overwritten by a decision
|
|
252
|
+
* not to deliver it.
|
|
253
|
+
* @param request - card, expected revision, actor, and the reason.
|
|
254
|
+
* @returns the outcome; domain rejections resolve with `ok: false`.
|
|
255
|
+
*/
|
|
256
|
+
abstract abandon(request: AbandonRequest): Promise<AbandonResult>;
|
|
115
257
|
/**
|
|
116
258
|
* Move every `done` card of one root out of the active set into that root's
|
|
117
259
|
* archive, keyed by the month of its last journal entry. Archived cards
|