@zhchxiao123/dsh-devflow 0.1.0 → 0.2.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 +147 -9
- package/lib/types/index.d.ts +132 -7
- package/lib/types/index.js +45 -1
- package/lib/types/invariant.js +1 -1
- package/lib/types/journal.d.ts +10 -1
- package/lib/types/journal.js +44 -3
- package/lib/types/stages.d.ts +2 -1
- package/lib/types/stages.js +15 -5
- package/lib/types/types.d.ts +107 -10
- 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
|
@@ -36,14 +36,31 @@ function isCardLocation(value) {
|
|
|
36
36
|
function DevflowCardId(value) {
|
|
37
37
|
return value;
|
|
38
38
|
}
|
|
39
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Forward and rework edges of the pipeline; `blocked` legality lives in
|
|
41
|
+
* {@link isLegalTransition}.
|
|
42
|
+
*
|
|
43
|
+
* Review and verification send a card back to whichever stage owns the fault:
|
|
44
|
+
* `developing` when the implementation is wrong, `designing` when the design
|
|
45
|
+
* is. Without the second, design rework happens on a card labelled
|
|
46
|
+
* `developing`, and the board stops answering the one question it exists to
|
|
47
|
+
* answer.
|
|
48
|
+
*/
|
|
40
49
|
const FLOW = {
|
|
41
50
|
draft: ["designing"],
|
|
42
51
|
designing: ["ready"],
|
|
43
52
|
ready: ["developing"],
|
|
44
53
|
developing: ["reviewing"],
|
|
45
|
-
reviewing: [
|
|
46
|
-
|
|
54
|
+
reviewing: [
|
|
55
|
+
"testing",
|
|
56
|
+
"developing",
|
|
57
|
+
"designing"
|
|
58
|
+
],
|
|
59
|
+
testing: [
|
|
60
|
+
"done",
|
|
61
|
+
"developing",
|
|
62
|
+
"designing"
|
|
63
|
+
],
|
|
47
64
|
done: []
|
|
48
65
|
};
|
|
49
66
|
/**
|
|
@@ -68,10 +85,11 @@ function isLegalTransition(from, to, blockedFrom) {
|
|
|
68
85
|
* require a recorded `reason` so the next holder knows what to fix.
|
|
69
86
|
* @param from - the departing location.
|
|
70
87
|
* @param to - the target location.
|
|
71
|
-
* @returns `true` for
|
|
88
|
+
* @returns `true` for a move from `reviewing` or `testing` back to
|
|
89
|
+
* `developing` or `designing`.
|
|
72
90
|
*/
|
|
73
91
|
function isReworkEdge(from, to) {
|
|
74
|
-
return to === "developing" && (from === "reviewing" || from === "testing");
|
|
92
|
+
return (to === "developing" || to === "designing") && (from === "reviewing" || from === "testing");
|
|
75
93
|
}
|
|
76
94
|
//#endregion
|
|
77
95
|
//#region packages/devflow/src/journal.ts
|
|
@@ -128,7 +146,8 @@ function decodeJournalEntry(value) {
|
|
|
128
146
|
type: "artifact",
|
|
129
147
|
path: entry.path,
|
|
130
148
|
stage: entry.stage,
|
|
131
|
-
...entry.by !== void 0 ? { by: decodeActor(entry.by) } : {}
|
|
149
|
+
...entry.by !== void 0 ? { by: decodeActor(entry.by) } : {},
|
|
150
|
+
...decodeOptionalString(entry, "kind")
|
|
132
151
|
};
|
|
133
152
|
case "claim-expired":
|
|
134
153
|
if (entry.previousOwner === void 0) throw new Error("claim-expired field \"previousOwner\" is required");
|
|
@@ -190,11 +209,48 @@ function foldJournal(entries) {
|
|
|
190
209
|
}
|
|
191
210
|
return state;
|
|
192
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Derive the artifact registrations of a decoded journal, in registration
|
|
214
|
+
* order. Kept beside {@link foldJournal} — whose `artifacts` is this list's
|
|
215
|
+
* path projection — so every consumer derives identical records; an entry
|
|
216
|
+
* without a `kind` yields a record without one.
|
|
217
|
+
* @param entries - decoded entries in file order.
|
|
218
|
+
* @returns the artifact records, oldest first.
|
|
219
|
+
*/
|
|
220
|
+
function foldArtifactRecords(entries) {
|
|
221
|
+
const records = [];
|
|
222
|
+
for (const entry of entries) {
|
|
223
|
+
if (entry.type !== "artifact") continue;
|
|
224
|
+
records.push({
|
|
225
|
+
path: entry.path,
|
|
226
|
+
...entry.kind !== void 0 ? { kind: entry.kind } : {},
|
|
227
|
+
rev: entry.rev,
|
|
228
|
+
stage: entry.stage
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
return records;
|
|
232
|
+
}
|
|
193
233
|
function decodeGate(value) {
|
|
194
234
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("transition field \"gate\" must be a JSON object");
|
|
195
235
|
const gate = value;
|
|
196
|
-
if (gate.approvedBy === void 0) throw new Error("transition field \"gate\" requires \"approvedBy\"");
|
|
197
|
-
return {
|
|
236
|
+
if (gate.approvedBy === void 0 && gate.checks === void 0) throw new Error("transition field \"gate\" requires \"approvedBy\" or \"checks\"");
|
|
237
|
+
return {
|
|
238
|
+
...gate.approvedBy !== void 0 ? { approvedBy: decodeActor(gate.approvedBy) } : {},
|
|
239
|
+
...gate.checks !== void 0 ? { checks: decodeGateChecks(gate.checks) } : {}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function decodeGateChecks(value) {
|
|
243
|
+
if (!Array.isArray(value)) throw new Error("transition field \"gate.checks\" must be an array");
|
|
244
|
+
return value.map((check) => {
|
|
245
|
+
if (typeof check !== "object" || check === null || Array.isArray(check)) throw new Error("gate check must be a JSON object");
|
|
246
|
+
const record = check;
|
|
247
|
+
if (record.verdict !== "allowed") throw new Error(`gate check field "verdict" must be "allowed" (got ${JSON.stringify(record.verdict)})`);
|
|
248
|
+
return {
|
|
249
|
+
by: decodeActor(record.by),
|
|
250
|
+
verdict: "allowed",
|
|
251
|
+
...decodeOptionalString(record, "summary")
|
|
252
|
+
};
|
|
253
|
+
});
|
|
198
254
|
}
|
|
199
255
|
function decodeActor(value) {
|
|
200
256
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("actor must be a JSON object");
|
|
@@ -237,6 +293,88 @@ function decodeOptionalString(record, key) {
|
|
|
237
293
|
* model-facing tools belong to `@zhchxiao123/dsh-devflow-tool`.
|
|
238
294
|
* @module @zhchxiao123/dsh-devflow
|
|
239
295
|
*/
|
|
296
|
+
/** JSON Schema for the Definition-owned artifact registration record. */
|
|
297
|
+
const ARTIFACT_RECORD_SCHEMA = {
|
|
298
|
+
type: "object",
|
|
299
|
+
additionalProperties: false,
|
|
300
|
+
properties: {
|
|
301
|
+
path: {
|
|
302
|
+
type: "string",
|
|
303
|
+
required: true
|
|
304
|
+
},
|
|
305
|
+
kind: { type: "string" },
|
|
306
|
+
rev: {
|
|
307
|
+
type: "integer",
|
|
308
|
+
required: true
|
|
309
|
+
},
|
|
310
|
+
stage: {
|
|
311
|
+
type: "string",
|
|
312
|
+
required: true,
|
|
313
|
+
enum: [...DEV_STAGES]
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
/** JSON Schema for one public artifact transition inspection. */
|
|
318
|
+
const ARTIFACT_TRANSITION_INSPECTION_SCHEMA = {
|
|
319
|
+
type: "object",
|
|
320
|
+
additionalProperties: false,
|
|
321
|
+
properties: {
|
|
322
|
+
from: {
|
|
323
|
+
type: "string",
|
|
324
|
+
required: true,
|
|
325
|
+
enum: [...DEV_STAGES, "blocked"]
|
|
326
|
+
},
|
|
327
|
+
to: {
|
|
328
|
+
type: "string",
|
|
329
|
+
required: true,
|
|
330
|
+
enum: [...DEV_STAGES, "blocked"]
|
|
331
|
+
},
|
|
332
|
+
requirements: {
|
|
333
|
+
type: "array",
|
|
334
|
+
required: true,
|
|
335
|
+
items: {
|
|
336
|
+
type: "object",
|
|
337
|
+
additionalProperties: false,
|
|
338
|
+
properties: {
|
|
339
|
+
kind: {
|
|
340
|
+
type: "string",
|
|
341
|
+
required: true
|
|
342
|
+
},
|
|
343
|
+
status: {
|
|
344
|
+
type: "string",
|
|
345
|
+
required: true,
|
|
346
|
+
enum: [
|
|
347
|
+
"missing",
|
|
348
|
+
"malformed",
|
|
349
|
+
"satisfied"
|
|
350
|
+
]
|
|
351
|
+
},
|
|
352
|
+
spec: {
|
|
353
|
+
type: "object",
|
|
354
|
+
required: true,
|
|
355
|
+
additionalProperties: false,
|
|
356
|
+
properties: {
|
|
357
|
+
frontmatter: {
|
|
358
|
+
type: "array",
|
|
359
|
+
items: { type: "string" }
|
|
360
|
+
},
|
|
361
|
+
sections: {
|
|
362
|
+
type: "array",
|
|
363
|
+
items: { type: "string" }
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
artifact: ARTIFACT_RECORD_SCHEMA,
|
|
368
|
+
defects: {
|
|
369
|
+
type: "array",
|
|
370
|
+
required: true,
|
|
371
|
+
items: { type: "string" }
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
};
|
|
240
378
|
/**
|
|
241
379
|
* Abstract task-card store registered as `ctx.devflow` (one implementation per
|
|
242
380
|
* context; loading a second throws, cordis' standard duplicate-service
|
|
@@ -320,6 +458,6 @@ function rootOfCwd(cwd) {
|
|
|
320
458
|
return cwd === void 0 ? void 0 : join(cwd, ".devflow");
|
|
321
459
|
}
|
|
322
460
|
//#endregion
|
|
323
|
-
export { DEV_STAGES, DevflowCardId, DevflowStore, DevflowStore as default, decodeJournalEntry, foldJournal, isCardLocation, isDevStage, isLegalTransition, isReworkEdge };
|
|
461
|
+
export { ARTIFACT_RECORD_SCHEMA, ARTIFACT_TRANSITION_INSPECTION_SCHEMA, DEV_STAGES, DevflowCardId, DevflowStore, DevflowStore as default, decodeJournalEntry, foldArtifactRecords, foldJournal, isCardLocation, isDevStage, isLegalTransition, isReworkEdge };
|
|
324
462
|
|
|
325
463
|
//# sourceMappingURL=index.js.map
|
package/lib/types/index.d.ts
CHANGED
|
@@ -10,8 +10,115 @@ import { Context, Service } from '@deepseek-ai/cordis';
|
|
|
10
10
|
import type { 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
12
|
export { DEV_STAGES, DevflowCardId, isCardLocation, isDevStage, isLegalTransition, isReworkEdge } from './stages.ts';
|
|
13
|
-
export { decodeJournalEntry, foldJournal } from './journal.ts';
|
|
13
|
+
export { decodeJournalEntry, foldArtifactRecords, foldJournal } from './journal.ts';
|
|
14
14
|
export type { JournalFoldState } from './journal.ts';
|
|
15
|
+
/** JSON Schema for the Definition-owned artifact registration record. */
|
|
16
|
+
export declare const ARTIFACT_RECORD_SCHEMA: {
|
|
17
|
+
readonly type: "object";
|
|
18
|
+
readonly additionalProperties: false;
|
|
19
|
+
readonly properties: {
|
|
20
|
+
readonly path: {
|
|
21
|
+
readonly type: "string";
|
|
22
|
+
readonly required: true;
|
|
23
|
+
};
|
|
24
|
+
readonly kind: {
|
|
25
|
+
readonly type: "string";
|
|
26
|
+
};
|
|
27
|
+
readonly rev: {
|
|
28
|
+
readonly type: "integer";
|
|
29
|
+
readonly required: true;
|
|
30
|
+
};
|
|
31
|
+
readonly stage: {
|
|
32
|
+
readonly type: "string";
|
|
33
|
+
readonly required: true;
|
|
34
|
+
readonly enum: readonly ["draft", "designing", "ready", "developing", "reviewing", "testing", "done"];
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
/** JSON Schema for one public artifact transition inspection. */
|
|
39
|
+
export declare const ARTIFACT_TRANSITION_INSPECTION_SCHEMA: {
|
|
40
|
+
readonly type: "object";
|
|
41
|
+
readonly additionalProperties: false;
|
|
42
|
+
readonly properties: {
|
|
43
|
+
readonly from: {
|
|
44
|
+
readonly type: "string";
|
|
45
|
+
readonly required: true;
|
|
46
|
+
readonly enum: readonly ["draft", "designing", "ready", "developing", "reviewing", "testing", "done", "blocked"];
|
|
47
|
+
};
|
|
48
|
+
readonly to: {
|
|
49
|
+
readonly type: "string";
|
|
50
|
+
readonly required: true;
|
|
51
|
+
readonly enum: readonly ["draft", "designing", "ready", "developing", "reviewing", "testing", "done", "blocked"];
|
|
52
|
+
};
|
|
53
|
+
readonly requirements: {
|
|
54
|
+
readonly type: "array";
|
|
55
|
+
readonly required: true;
|
|
56
|
+
readonly items: {
|
|
57
|
+
readonly type: "object";
|
|
58
|
+
readonly additionalProperties: false;
|
|
59
|
+
readonly properties: {
|
|
60
|
+
readonly kind: {
|
|
61
|
+
readonly type: "string";
|
|
62
|
+
readonly required: true;
|
|
63
|
+
};
|
|
64
|
+
readonly status: {
|
|
65
|
+
readonly type: "string";
|
|
66
|
+
readonly required: true;
|
|
67
|
+
readonly enum: readonly ["missing", "malformed", "satisfied"];
|
|
68
|
+
};
|
|
69
|
+
readonly spec: {
|
|
70
|
+
readonly type: "object";
|
|
71
|
+
readonly required: true;
|
|
72
|
+
readonly additionalProperties: false;
|
|
73
|
+
readonly properties: {
|
|
74
|
+
readonly frontmatter: {
|
|
75
|
+
readonly type: "array";
|
|
76
|
+
readonly items: {
|
|
77
|
+
readonly type: "string";
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
readonly sections: {
|
|
81
|
+
readonly type: "array";
|
|
82
|
+
readonly items: {
|
|
83
|
+
readonly type: "string";
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
readonly artifact: {
|
|
89
|
+
readonly type: "object";
|
|
90
|
+
readonly additionalProperties: false;
|
|
91
|
+
readonly properties: {
|
|
92
|
+
readonly path: {
|
|
93
|
+
readonly type: "string";
|
|
94
|
+
readonly required: true;
|
|
95
|
+
};
|
|
96
|
+
readonly kind: {
|
|
97
|
+
readonly type: "string";
|
|
98
|
+
};
|
|
99
|
+
readonly rev: {
|
|
100
|
+
readonly type: "integer";
|
|
101
|
+
readonly required: true;
|
|
102
|
+
};
|
|
103
|
+
readonly stage: {
|
|
104
|
+
readonly type: "string";
|
|
105
|
+
readonly required: true;
|
|
106
|
+
readonly enum: readonly ["draft", "designing", "ready", "developing", "reviewing", "testing", "done"];
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
readonly defects: {
|
|
111
|
+
readonly type: "array";
|
|
112
|
+
readonly required: true;
|
|
113
|
+
readonly items: {
|
|
114
|
+
readonly type: "string";
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
};
|
|
15
122
|
declare module '@deepseek-ai/cordis' {
|
|
16
123
|
interface Context {
|
|
17
124
|
devflow: DevflowStore;
|
|
@@ -91,25 +198,43 @@ export declare abstract class DevflowStore extends Service {
|
|
|
91
198
|
* `devflow/transition` waterfall, the journal append (the only commit
|
|
92
199
|
* point), the projection rewrite, then `devflow/stage-changed`. State and
|
|
93
200
|
* notifications publish only after the journal committed.
|
|
201
|
+
*
|
|
202
|
+
* The waterfall's gate commands put real time between those checks and the
|
|
203
|
+
* append, so implementations must re-establish the checked revision at the
|
|
204
|
+
* append itself, under an exclusion another process observes. A card that
|
|
205
|
+
* moved in that window resolves `revision-mismatch`; a card whose commit
|
|
206
|
+
* stayed excluded resolves `write-contended` with nothing written.
|
|
94
207
|
* @param spec - a resolved spec from {@link resolve}, never a raw request.
|
|
95
208
|
* @returns the outcome; domain rejections resolve with `ok: false`.
|
|
96
209
|
*/
|
|
97
210
|
abstract transition(spec: TransitionSpec): Promise<TransitionResult>;
|
|
98
211
|
/**
|
|
99
|
-
* Take the card's exclusive lease.
|
|
212
|
+
* Take the card's exclusive lease. A stale takeover journals the eviction
|
|
213
|
+
* under the same cross-process commit exclusion as transitions and artifact
|
|
214
|
+
* registration, so concurrent takeover attempts grant at most one holder.
|
|
100
215
|
* @param id - the card to claim.
|
|
101
216
|
* @param owner - the prospective holder, recorded in the lease.
|
|
102
217
|
* @param options - staleness takeover policy and root; omitted never takes
|
|
103
218
|
* over and uses the implementation's default root.
|
|
104
|
-
* @returns the live handle, or
|
|
219
|
+
* @returns the live handle, or a holder read from the lease. On journal-commit
|
|
220
|
+
* contention that holder was observed before trying the lock, not freshly
|
|
221
|
+
* established as the current owner.
|
|
105
222
|
*/
|
|
106
223
|
abstract claim(id: DevflowCardId, owner: DevActor, options?: ClaimOptions): Promise<ClaimResult>;
|
|
107
224
|
/**
|
|
108
225
|
* Register a stage deliverable in the card's journal against its current
|
|
109
|
-
* stage
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
226
|
+
* stage, in one of two mutually exclusive forms: the reference form records
|
|
227
|
+
* a `path` the caller already wrote under the card directory, and the
|
|
228
|
+
* store-written form hands over `kind` plus `content` for the
|
|
229
|
+
* implementation to write `artifacts/<rev>-<kind>.md` itself before the
|
|
230
|
+
* journal append — which stays the only commit point, so a registration
|
|
231
|
+
* that loses the commit registers nothing and its unreferenced file is
|
|
232
|
+
* overwritten by a same-revision retry. Registrations are immutable: the
|
|
233
|
+
* newest record of one kind is that kind's current content. A blocked or
|
|
234
|
+
* done card cannot register artifacts, the revision check mirrors
|
|
235
|
+
* {@link transition}, and an ill-formed kind resolves `invalid-kind`.
|
|
236
|
+
* @param request - card, expected revision, actor, and the artifact reference or content.
|
|
237
|
+
* @returns the outcome carrying the registered record; domain rejections resolve with `ok: false`.
|
|
113
238
|
*/
|
|
114
239
|
abstract attachArtifact(request: ArtifactRequest): Promise<ArtifactResult>;
|
|
115
240
|
/**
|
package/lib/types/index.js
CHANGED
|
@@ -8,8 +8,52 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import { Service } from '@deepseek-ai/cordis';
|
|
11
|
+
import { DEV_STAGES } from "./stages.js";
|
|
11
12
|
export { DEV_STAGES, DevflowCardId, isCardLocation, isDevStage, isLegalTransition, isReworkEdge } from "./stages.js";
|
|
12
|
-
export { decodeJournalEntry, foldJournal } from "./journal.js";
|
|
13
|
+
export { decodeJournalEntry, foldArtifactRecords, foldJournal } from "./journal.js";
|
|
14
|
+
/** JSON Schema for the Definition-owned artifact registration record. */
|
|
15
|
+
export const ARTIFACT_RECORD_SCHEMA = {
|
|
16
|
+
type: 'object',
|
|
17
|
+
additionalProperties: false,
|
|
18
|
+
properties: {
|
|
19
|
+
path: { type: 'string', required: true },
|
|
20
|
+
kind: { type: 'string' },
|
|
21
|
+
rev: { type: 'integer', required: true },
|
|
22
|
+
stage: { type: 'string', required: true, enum: [...DEV_STAGES] },
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
/** JSON Schema for one public artifact transition inspection. */
|
|
26
|
+
export const ARTIFACT_TRANSITION_INSPECTION_SCHEMA = {
|
|
27
|
+
type: 'object',
|
|
28
|
+
additionalProperties: false,
|
|
29
|
+
properties: {
|
|
30
|
+
from: { type: 'string', required: true, enum: [...DEV_STAGES, 'blocked'] },
|
|
31
|
+
to: { type: 'string', required: true, enum: [...DEV_STAGES, 'blocked'] },
|
|
32
|
+
requirements: {
|
|
33
|
+
type: 'array',
|
|
34
|
+
required: true,
|
|
35
|
+
items: {
|
|
36
|
+
type: 'object',
|
|
37
|
+
additionalProperties: false,
|
|
38
|
+
properties: {
|
|
39
|
+
kind: { type: 'string', required: true },
|
|
40
|
+
status: { type: 'string', required: true, enum: ['missing', 'malformed', 'satisfied'] },
|
|
41
|
+
spec: {
|
|
42
|
+
type: 'object',
|
|
43
|
+
required: true,
|
|
44
|
+
additionalProperties: false,
|
|
45
|
+
properties: {
|
|
46
|
+
frontmatter: { type: 'array', items: { type: 'string' } },
|
|
47
|
+
sections: { type: 'array', items: { type: 'string' } },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
artifact: ARTIFACT_RECORD_SCHEMA,
|
|
51
|
+
defects: { type: 'array', required: true, items: { type: 'string' } },
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
};
|
|
13
57
|
/**
|
|
14
58
|
* Abstract task-card store registered as `ctx.devflow` (one implementation per
|
|
15
59
|
* context; loading a second throws, cordis' standard duplicate-service
|
package/lib/types/invariant.js
CHANGED
|
@@ -14,7 +14,7 @@ const install = (ctx, fail) => {
|
|
|
14
14
|
const lastRevision = new Map();
|
|
15
15
|
const children = new Set();
|
|
16
16
|
// Cards from different roots may share an id; the stream relations hold
|
|
17
|
-
// per root + id, the same key every store
|
|
17
|
+
// per root + id, the same key every store bookkeeping path uses.
|
|
18
18
|
const key = (card) => `${card.root} ${card.id}`;
|
|
19
19
|
ctx.on('devflow/card-created', (card) => {
|
|
20
20
|
if (card.stage !== 'draft' || card.stageRevision !== 1) {
|
package/lib/types/journal.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @module @zhchxiao123/dsh-devflow/src/journal
|
|
7
7
|
*/
|
|
8
8
|
import { DevflowCardId } from './stages.ts';
|
|
9
|
-
import type { CardLocation, DevStage, DevflowJournalEntry } from './types.ts';
|
|
9
|
+
import type { ArtifactRecord, CardLocation, DevStage, DevflowJournalEntry } from './types.ts';
|
|
10
10
|
/** Card state derived by {@link foldJournal}; the read-side authority. */
|
|
11
11
|
export interface JournalFoldState {
|
|
12
12
|
/** Current location after the last entry. */
|
|
@@ -43,4 +43,13 @@ export declare function decodeJournalEntry(value: unknown): DevflowJournalEntry;
|
|
|
43
43
|
* @throws {Error} naming the first violated invariant and its entry revision.
|
|
44
44
|
*/
|
|
45
45
|
export declare function foldJournal(entries: readonly DevflowJournalEntry[]): JournalFoldState;
|
|
46
|
+
/**
|
|
47
|
+
* Derive the artifact registrations of a decoded journal, in registration
|
|
48
|
+
* order. Kept beside {@link foldJournal} — whose `artifacts` is this list's
|
|
49
|
+
* path projection — so every consumer derives identical records; an entry
|
|
50
|
+
* without a `kind` yields a record without one.
|
|
51
|
+
* @param entries - decoded entries in file order.
|
|
52
|
+
* @returns the artifact records, oldest first.
|
|
53
|
+
*/
|
|
54
|
+
export declare function foldArtifactRecords(entries: readonly DevflowJournalEntry[]): ArtifactRecord[];
|
|
46
55
|
//# sourceMappingURL=journal.d.ts.map
|
package/lib/types/journal.js
CHANGED
|
@@ -67,6 +67,7 @@ export function decodeJournalEntry(value) {
|
|
|
67
67
|
path: entry.path,
|
|
68
68
|
stage: entry.stage,
|
|
69
69
|
...entry.by !== undefined ? { by: decodeActor(entry.by) } : {},
|
|
70
|
+
...decodeOptionalString(entry, 'kind'),
|
|
70
71
|
};
|
|
71
72
|
}
|
|
72
73
|
case 'claim-expired': {
|
|
@@ -148,15 +149,55 @@ export function foldJournal(entries) {
|
|
|
148
149
|
}
|
|
149
150
|
return state;
|
|
150
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Derive the artifact registrations of a decoded journal, in registration
|
|
154
|
+
* order. Kept beside {@link foldJournal} — whose `artifacts` is this list's
|
|
155
|
+
* path projection — so every consumer derives identical records; an entry
|
|
156
|
+
* without a `kind` yields a record without one.
|
|
157
|
+
* @param entries - decoded entries in file order.
|
|
158
|
+
* @returns the artifact records, oldest first.
|
|
159
|
+
*/
|
|
160
|
+
export function foldArtifactRecords(entries) {
|
|
161
|
+
const records = [];
|
|
162
|
+
for (const entry of entries) {
|
|
163
|
+
if (entry.type !== 'artifact')
|
|
164
|
+
continue;
|
|
165
|
+
records.push({
|
|
166
|
+
path: entry.path,
|
|
167
|
+
...entry.kind !== undefined ? { kind: entry.kind } : {},
|
|
168
|
+
rev: entry.rev,
|
|
169
|
+
stage: entry.stage,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return records;
|
|
173
|
+
}
|
|
151
174
|
function decodeGate(value) {
|
|
152
175
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
153
176
|
throw new Error('transition field "gate" must be a JSON object');
|
|
154
177
|
}
|
|
155
178
|
const gate = value;
|
|
156
|
-
if (gate.approvedBy === undefined) {
|
|
157
|
-
throw new Error('transition field "gate" requires "approvedBy"');
|
|
179
|
+
if (gate.approvedBy === undefined && gate.checks === undefined) {
|
|
180
|
+
throw new Error('transition field "gate" requires "approvedBy" or "checks"');
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
...gate.approvedBy !== undefined ? { approvedBy: decodeActor(gate.approvedBy) } : {},
|
|
184
|
+
...gate.checks !== undefined ? { checks: decodeGateChecks(gate.checks) } : {},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function decodeGateChecks(value) {
|
|
188
|
+
if (!Array.isArray(value)) {
|
|
189
|
+
throw new Error('transition field "gate.checks" must be an array');
|
|
158
190
|
}
|
|
159
|
-
return
|
|
191
|
+
return value.map((check) => {
|
|
192
|
+
if (typeof check !== 'object' || check === null || Array.isArray(check)) {
|
|
193
|
+
throw new Error('gate check must be a JSON object');
|
|
194
|
+
}
|
|
195
|
+
const record = check;
|
|
196
|
+
if (record.verdict !== 'allowed') {
|
|
197
|
+
throw new Error(`gate check field "verdict" must be "allowed" (got ${JSON.stringify(record.verdict)})`);
|
|
198
|
+
}
|
|
199
|
+
return { by: decodeActor(record.by), verdict: 'allowed', ...decodeOptionalString(record, 'summary') };
|
|
200
|
+
});
|
|
160
201
|
}
|
|
161
202
|
function decodeActor(value) {
|
|
162
203
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
package/lib/types/stages.d.ts
CHANGED
|
@@ -46,7 +46,8 @@ export declare function isLegalTransition(from: CardLocation, to: CardLocation,
|
|
|
46
46
|
* require a recorded `reason` so the next holder knows what to fix.
|
|
47
47
|
* @param from - the departing location.
|
|
48
48
|
* @param to - the target location.
|
|
49
|
-
* @returns `true` for
|
|
49
|
+
* @returns `true` for a move from `reviewing` or `testing` back to
|
|
50
|
+
* `developing` or `designing`.
|
|
50
51
|
*/
|
|
51
52
|
export declare function isReworkEdge(from: CardLocation, to: CardLocation): boolean;
|
|
52
53
|
//# sourceMappingURL=stages.d.ts.map
|
package/lib/types/stages.js
CHANGED
|
@@ -39,14 +39,23 @@ export function isCardLocation(value) {
|
|
|
39
39
|
export function DevflowCardId(value) {
|
|
40
40
|
return value;
|
|
41
41
|
}
|
|
42
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Forward and rework edges of the pipeline; `blocked` legality lives in
|
|
44
|
+
* {@link isLegalTransition}.
|
|
45
|
+
*
|
|
46
|
+
* Review and verification send a card back to whichever stage owns the fault:
|
|
47
|
+
* `developing` when the implementation is wrong, `designing` when the design
|
|
48
|
+
* is. Without the second, design rework happens on a card labelled
|
|
49
|
+
* `developing`, and the board stops answering the one question it exists to
|
|
50
|
+
* answer.
|
|
51
|
+
*/
|
|
43
52
|
const FLOW = {
|
|
44
53
|
draft: ['designing'],
|
|
45
54
|
designing: ['ready'],
|
|
46
55
|
ready: ['developing'],
|
|
47
56
|
developing: ['reviewing'],
|
|
48
|
-
reviewing: ['testing', 'developing'],
|
|
49
|
-
testing: ['done', 'developing'],
|
|
57
|
+
reviewing: ['testing', 'developing', 'designing'],
|
|
58
|
+
testing: ['done', 'developing', 'designing'],
|
|
50
59
|
done: [],
|
|
51
60
|
};
|
|
52
61
|
/**
|
|
@@ -74,9 +83,10 @@ export function isLegalTransition(from, to, blockedFrom) {
|
|
|
74
83
|
* require a recorded `reason` so the next holder knows what to fix.
|
|
75
84
|
* @param from - the departing location.
|
|
76
85
|
* @param to - the target location.
|
|
77
|
-
* @returns `true` for
|
|
86
|
+
* @returns `true` for a move from `reviewing` or `testing` back to
|
|
87
|
+
* `developing` or `designing`.
|
|
78
88
|
*/
|
|
79
89
|
export function isReworkEdge(from, to) {
|
|
80
|
-
return to === 'developing' && (from === 'reviewing' || from === 'testing');
|
|
90
|
+
return (to === 'developing' || to === 'designing') && (from === 'reviewing' || from === 'testing');
|
|
81
91
|
}
|
|
82
92
|
//# sourceMappingURL=stages.js.map
|
package/lib/types/types.d.ts
CHANGED
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
import type { DevflowCardId } from './stages.ts';
|
|
8
8
|
export type { DevflowCardId } from './stages.ts';
|
|
9
9
|
declare module '@deepseek-ai/cordis' {
|
|
10
|
+
interface Context {
|
|
11
|
+
/** Optional dynamic artifact-contract inspection published by a policy provider. */
|
|
12
|
+
devflowArtifactContract: ArtifactContract;
|
|
13
|
+
}
|
|
10
14
|
interface Events {
|
|
11
15
|
/**
|
|
12
16
|
* Single-decision transition pipeline. The store dispatches this after the
|
|
@@ -63,6 +67,18 @@ export interface JournalCreated {
|
|
|
63
67
|
/** The card this one decomposes, fixed here at creation and never changed. */
|
|
64
68
|
parent?: DevflowCardId;
|
|
65
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* One recorded gate verdict on a committed transition: which actor allowed the
|
|
72
|
+
* move and, optionally, what the check covered. Only permitting verdicts
|
|
73
|
+
* exist — a refusal vetoes the transition instead of being recorded.
|
|
74
|
+
*/
|
|
75
|
+
export interface GateCheck {
|
|
76
|
+
/** The actor that allowed the move. */
|
|
77
|
+
by: DevActor;
|
|
78
|
+
verdict: 'allowed';
|
|
79
|
+
/** One-line account of what the check covered. */
|
|
80
|
+
summary?: string;
|
|
81
|
+
}
|
|
66
82
|
/**
|
|
67
83
|
* One stage move. A move to `blocked` remembers `from`; the matching recovery
|
|
68
84
|
* must return to exactly that stage.
|
|
@@ -75,9 +91,14 @@ export interface JournalTransition {
|
|
|
75
91
|
to: CardLocation;
|
|
76
92
|
by?: DevActor;
|
|
77
93
|
reason?: string;
|
|
78
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* Gate facts attached by the transition waterfall: the human approval
|
|
96
|
+
* signature and/or the recorded gate verdicts. At least one is present —
|
|
97
|
+
* a move nothing gated carries no `gate` at all.
|
|
98
|
+
*/
|
|
79
99
|
gate?: {
|
|
80
|
-
approvedBy
|
|
100
|
+
approvedBy?: DevActor;
|
|
101
|
+
checks?: GateCheck[];
|
|
81
102
|
};
|
|
82
103
|
}
|
|
83
104
|
/** Registration of a stage deliverable produced under `artifacts/`. */
|
|
@@ -88,6 +109,11 @@ export interface JournalArtifact {
|
|
|
88
109
|
path: string;
|
|
89
110
|
stage: DevStage;
|
|
90
111
|
by?: DevActor;
|
|
112
|
+
/**
|
|
113
|
+
* Deliverable kind of a store-written artifact; absent for a path-only
|
|
114
|
+
* registration and for entries predating kinds.
|
|
115
|
+
*/
|
|
116
|
+
kind?: string;
|
|
91
117
|
}
|
|
92
118
|
/** Takeover of a stale lease: the previous holder's heartbeat lapsed. */
|
|
93
119
|
export interface JournalClaimExpired {
|
|
@@ -99,6 +125,21 @@ export interface JournalClaimExpired {
|
|
|
99
125
|
}
|
|
100
126
|
/** The journal entry union; the discriminant is `type`. */
|
|
101
127
|
export type DevflowJournalEntry = JournalCreated | JournalTransition | JournalArtifact | JournalClaimExpired;
|
|
128
|
+
/**
|
|
129
|
+
* Read-side value of one artifact registration: the journal entry's facts
|
|
130
|
+
* without its envelope. Registrations are immutable — the newest record of one
|
|
131
|
+
* `kind` (the highest `rev`) is that kind's current content.
|
|
132
|
+
*/
|
|
133
|
+
export interface ArtifactRecord {
|
|
134
|
+
/** Artifact path relative to the card directory. */
|
|
135
|
+
path: string;
|
|
136
|
+
/** Deliverable kind; absent for a path-only registration. */
|
|
137
|
+
kind?: string;
|
|
138
|
+
/** Journal revision of the registration; orders records of one kind. */
|
|
139
|
+
rev: number;
|
|
140
|
+
/** The stage the deliverable was registered against. */
|
|
141
|
+
stage: DevStage;
|
|
142
|
+
}
|
|
102
143
|
/** Read-side value of one card, current state derived by journal replay. */
|
|
103
144
|
export interface DevCard {
|
|
104
145
|
id: DevflowCardId;
|
|
@@ -121,8 +162,36 @@ export interface DevCard {
|
|
|
121
162
|
body: string;
|
|
122
163
|
/** Display path of the card file. */
|
|
123
164
|
path: string;
|
|
124
|
-
/** Artifact paths registered in the journal, in registration order. */
|
|
165
|
+
/** Artifact paths registered in the journal, in registration order; the path projection of {@link artifactRecords}. */
|
|
125
166
|
artifacts: string[];
|
|
167
|
+
/** Artifact registrations in registration order, each carrying its journal revision, registering stage, and optional kind. */
|
|
168
|
+
artifactRecords: ArtifactRecord[];
|
|
169
|
+
}
|
|
170
|
+
/** Immutable normalized artifact shape published through the inspection seam. */
|
|
171
|
+
export interface PublishedArtifactKindSpec {
|
|
172
|
+
readonly frontmatter?: readonly string[];
|
|
173
|
+
readonly sections?: readonly string[];
|
|
174
|
+
}
|
|
175
|
+
/** Mechanical state of one required artifact at the inspected card revision. */
|
|
176
|
+
export type ArtifactRequirementStatus = 'missing' | 'malformed' | 'satisfied';
|
|
177
|
+
/** One required kind and the exact evidence the transition policy will judge. */
|
|
178
|
+
export interface ArtifactRequirementInspection {
|
|
179
|
+
readonly kind: string;
|
|
180
|
+
readonly status: ArtifactRequirementStatus;
|
|
181
|
+
readonly spec: PublishedArtifactKindSpec;
|
|
182
|
+
readonly artifact?: Readonly<ArtifactRecord>;
|
|
183
|
+
readonly defects: readonly string[];
|
|
184
|
+
}
|
|
185
|
+
/** Artifact requirements of one configured, currently legal outgoing edge. */
|
|
186
|
+
export interface ArtifactTransitionInspection {
|
|
187
|
+
readonly from: CardLocation;
|
|
188
|
+
readonly to: CardLocation;
|
|
189
|
+
readonly requirements: readonly ArtifactRequirementInspection[];
|
|
190
|
+
}
|
|
191
|
+
/** Optional read-only policy seam consumed by model-facing card tools. */
|
|
192
|
+
export interface ArtifactContract {
|
|
193
|
+
/** Inspect every configured legal edge leaving the card's current location. */
|
|
194
|
+
inspectOutgoing(card: DevCard): Promise<readonly ArtifactTransitionInspection[]>;
|
|
126
195
|
}
|
|
127
196
|
/** Read filter accepted by {@link import('./index.ts').DevflowStore.list}. */
|
|
128
197
|
export interface CardFilter {
|
|
@@ -211,12 +280,19 @@ export type TransitionDecision = {
|
|
|
211
280
|
allowed: true;
|
|
212
281
|
/** The human signature a policy listener collected; recorded as the journal entry's `gate.approvedBy`. */
|
|
213
282
|
approvedBy?: DevActor;
|
|
283
|
+
/** Gate verdicts policy listeners collected; recorded as the journal entry's `gate.checks` when non-empty. */
|
|
284
|
+
checks?: GateCheck[];
|
|
214
285
|
} | {
|
|
215
286
|
allowed: false;
|
|
216
287
|
reason: string;
|
|
217
288
|
};
|
|
218
|
-
/**
|
|
219
|
-
|
|
289
|
+
/**
|
|
290
|
+
* Stable rejection codes of {@link TransitionResult}; the discriminant is
|
|
291
|
+
* `code`. `write-contended` is the only one a caller can retry unchanged: it
|
|
292
|
+
* says another process held the card's commit long enough that this one gave
|
|
293
|
+
* up, and that nothing was written.
|
|
294
|
+
*/
|
|
295
|
+
export type TransitionRejectionCode = 'revision-mismatch' | 'illegal-edge' | 'reason-required' | 'vetoed' | 'write-contended';
|
|
220
296
|
/**
|
|
221
297
|
* Transition outcome. Domain rejections resolve with `ok: false` and a stable
|
|
222
298
|
* code; only infrastructure failures (unwritable journal, unreadable card)
|
|
@@ -231,24 +307,45 @@ export type TransitionResult = {
|
|
|
231
307
|
code: TransitionRejectionCode;
|
|
232
308
|
message: string;
|
|
233
309
|
};
|
|
234
|
-
/**
|
|
235
|
-
|
|
310
|
+
/** Fields shared by both {@link ArtifactRequest} forms. */
|
|
311
|
+
interface ArtifactRequestBase {
|
|
236
312
|
id: DevflowCardId;
|
|
237
|
-
/** Artifact path relative to the card directory, e.g. `artifacts/design.md`. */
|
|
238
|
-
path: string;
|
|
239
313
|
/** Optimistic-concurrency token: the `stageRevision` the caller last observed. */
|
|
240
314
|
expectedRevision: number;
|
|
241
315
|
by: DevActor;
|
|
242
316
|
/** Devflow root holding the card; omitted uses the implementation's default root. */
|
|
243
317
|
root?: string;
|
|
244
318
|
}
|
|
319
|
+
/** Reference form: the caller already wrote the file and registers its path. */
|
|
320
|
+
export interface ArtifactPathRequest extends ArtifactRequestBase {
|
|
321
|
+
/** Artifact path relative to the card directory, e.g. `artifacts/design.md`. */
|
|
322
|
+
path: string;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Store-written form: the implementation writes `artifacts/<rev>-<kind>.md`
|
|
326
|
+
* itself, before the journal append, and registers that path.
|
|
327
|
+
*/
|
|
328
|
+
export interface ArtifactContentRequest extends ArtifactRequestBase {
|
|
329
|
+
/** Deliverable kind; the slug grammar, rejected `invalid-kind` otherwise. */
|
|
330
|
+
kind: string;
|
|
331
|
+
/** Complete Markdown content the implementation writes. */
|
|
332
|
+
content: string;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Caller view of one artifact registration against the card's current stage:
|
|
336
|
+
* the reference form or the store-written form. The two are mutually
|
|
337
|
+
* exclusive — the model-facing tool rejects a call carrying both before the
|
|
338
|
+
* seam is reached.
|
|
339
|
+
*/
|
|
340
|
+
export type ArtifactRequest = ArtifactPathRequest | ArtifactContentRequest;
|
|
245
341
|
/** Artifact-registration outcome; domain rejections resolve like {@link TransitionResult}. */
|
|
246
342
|
export type ArtifactResult = {
|
|
247
343
|
ok: true;
|
|
248
344
|
card: DevCard;
|
|
345
|
+
record: ArtifactRecord;
|
|
249
346
|
} | {
|
|
250
347
|
ok: false;
|
|
251
|
-
code: 'revision-mismatch' | 'illegal-edge';
|
|
348
|
+
code: 'revision-mismatch' | 'illegal-edge' | 'invalid-kind' | 'write-contended';
|
|
252
349
|
message: string;
|
|
253
350
|
};
|
|
254
351
|
/** Current lease facts of one card, read from its claim record. */
|
package/package.json
CHANGED