@zhchxiao123/dsh-devflow 0.2.0 → 0.4.0-dev.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/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.
@@ -45,12 +70,17 @@ function DevflowCardId(value) {
45
70
  * is. Without the second, design rework happens on a card labelled
46
71
  * `developing`, and the board stops answering the one question it exists to
47
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.
48
78
  */
49
79
  const FLOW = {
50
80
  draft: ["designing"],
51
81
  designing: ["ready"],
52
82
  ready: ["developing"],
53
- developing: ["reviewing"],
83
+ developing: ["reviewing", "designing"],
54
84
  reviewing: [
55
85
  "testing",
56
86
  "developing",
@@ -64,32 +94,61 @@ const FLOW = {
64
94
  done: []
65
95
  };
66
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
+ /**
67
115
  * Whether one stage move is a legal edge of the state machine.
68
116
  *
69
117
  * Main flow follows the pipeline order; `reviewing` and `testing` may rework
70
- * to `developing`; any non-terminal location may enter `blocked`; a blocked
71
- * card may only recover to the exact stage it interrupted.
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.
72
125
  * @param from - the card's current location.
73
126
  * @param to - the requested target location.
74
- * @param blockedFrom - the remembered origin stage while `from` is `blocked`.
127
+ * @param card - the moving card's own context; omitted reads as a `standard`
128
+ * card that is not blocked.
75
129
  * @returns `true` when the move is a legal edge.
76
130
  */
77
- function isLegalTransition(from, to, blockedFrom) {
131
+ function isLegalTransition(from, to, card) {
78
132
  if (from === to) return false;
79
- if (from === "blocked") return to === blockedFrom;
133
+ if (from === "blocked") return to === card?.blockedFrom;
80
134
  if (to === "blocked") return from !== "done";
81
- return FLOW[from].includes(to);
135
+ if (FLOW[from].includes(to)) return true;
136
+ return (CLASS_EXTRA[card?.serviceClass ?? "standard"][from] ?? []).includes(to);
82
137
  }
83
138
  /**
84
139
  * Whether a legal edge moves the card backwards (a rework). Rework edges
85
- * 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.
86
144
  * @param from - the departing location.
87
145
  * @param to - the target location.
88
146
  * @returns `true` for a move from `reviewing` or `testing` back to
89
- * `developing` or `designing`.
147
+ * `developing` or `designing`, and for `developing` back to `designing`.
90
148
  */
91
149
  function isReworkEdge(from, to) {
92
- return (to === "developing" || to === "designing") && (from === "reviewing" || from === "testing");
150
+ if (to === "designing") return from === "developing" || from === "reviewing" || from === "testing";
151
+ return to === "developing" && (from === "reviewing" || from === "testing");
93
152
  }
94
153
  //#endregion
95
154
  //#region packages/devflow/src/journal.ts
@@ -122,7 +181,8 @@ function decodeJournalEntry(value) {
122
181
  at: entry.at,
123
182
  type: "created",
124
183
  by: decodeActor(entry.by),
125
- ...decodeOptionalCardId(entry, "parent")
184
+ ...decodeOptionalCardId(entry, "parent"),
185
+ ...decodeOptionalServiceClass(entry)
126
186
  };
127
187
  case "transition":
128
188
  if (!isCardLocation(entry.from)) throw new Error("transition field \"from\" must be a stage or \"blocked\"");
@@ -149,6 +209,15 @@ function decodeJournalEntry(value) {
149
209
  ...entry.by !== void 0 ? { by: decodeActor(entry.by) } : {},
150
210
  ...decodeOptionalString(entry, "kind")
151
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
220
+ };
152
221
  case "claim-expired":
153
222
  if (entry.previousOwner === void 0) throw new Error("claim-expired field \"previousOwner\" is required");
154
223
  return {
@@ -158,7 +227,7 @@ function decodeJournalEntry(value) {
158
227
  previousOwner: decodeActor(entry.previousOwner),
159
228
  by: decodeActor(entry.by)
160
229
  };
161
- 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)})`);
162
231
  }
163
232
  }
164
233
  /**
@@ -167,7 +236,8 @@ function decodeJournalEntry(value) {
167
236
  * Validates the structural invariants of the durable stream: revisions are the
168
237
  * contiguous sequence 1..n, the first entry is `created`, every transition
169
238
  * departs from the current location, a move to `blocked` remembers its origin,
170
- * and the matching recovery returns exactly there.
239
+ * the matching recovery returns exactly there, and nothing follows an
240
+ * `abandoned` entry.
171
241
  * @param entries - decoded entries in file order.
172
242
  * @returns the folded card state.
173
243
  * @throws {Error} naming the first violated invariant and its entry revision.
@@ -177,6 +247,7 @@ function foldJournal(entries) {
177
247
  const state = {
178
248
  stage: "draft",
179
249
  revision: 0,
250
+ serviceClass: DEFAULT_SERVICE_CLASS,
180
251
  artifacts: []
181
252
  };
182
253
  for (const [index, entry] of entries.entries()) {
@@ -184,9 +255,11 @@ function foldJournal(entries) {
184
255
  if (index === 0) {
185
256
  if (entry.type !== "created") throw new Error("journal entry 1 must be \"created\"");
186
257
  if (entry.parent !== void 0) state.parent = entry.parent;
258
+ if (entry.serviceClass !== void 0) state.serviceClass = entry.serviceClass;
187
259
  state.revision = entry.rev;
188
260
  continue;
189
261
  }
262
+ if (state.abandoned === true) throw new Error(`journal entry rev ${entry.rev} follows an abandoned card; abandoning is terminal`);
190
263
  switch (entry.type) {
191
264
  case "created": throw new Error(`journal entry rev ${entry.rev} repeats "created"`);
192
265
  case "transition":
@@ -204,6 +277,10 @@ function foldJournal(entries) {
204
277
  state.artifacts.push(entry.path);
205
278
  state.revision = entry.rev;
206
279
  break;
280
+ case "abandoned":
281
+ state.abandoned = true;
282
+ state.revision = entry.rev;
283
+ break;
207
284
  case "claim-expired": state.revision = entry.rev;
208
285
  }
209
286
  }
@@ -271,6 +348,12 @@ function decodeActor(value) {
271
348
  default: throw new Error(`actor field "kind" must be human, agent, or command (got ${JSON.stringify(actor.kind)})`);
272
349
  }
273
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
+ }
274
357
  function decodeOptionalCardId(record, key) {
275
358
  const value = record[key];
276
359
  if (value === void 0) return {};
@@ -458,6 +541,6 @@ function rootOfCwd(cwd) {
458
541
  return cwd === void 0 ? void 0 : join(cwd, ".devflow");
459
542
  }
460
543
  //#endregion
461
- export { ARTIFACT_RECORD_SCHEMA, ARTIFACT_TRANSITION_INSPECTION_SCHEMA, DEV_STAGES, DevflowCardId, DevflowStore, DevflowStore as default, decodeJournalEntry, foldArtifactRecords, 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 };
462
545
 
463
546
  //# sourceMappingURL=index.js.map
@@ -7,9 +7,10 @@
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';
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';
13
14
  export { decodeJournalEntry, foldArtifactRecords, foldJournal } from './journal.ts';
14
15
  export type { JournalFoldState } from './journal.ts';
15
16
  /** JSON Schema for the Definition-owned artifact registration record. */
@@ -237,6 +238,22 @@ export declare abstract class DevflowStore extends Service {
237
238
  * @returns the outcome carrying the registered record; domain rejections resolve with `ok: false`.
238
239
  */
239
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>;
240
257
  /**
241
258
  * Move every `done` card of one root out of the active set into that root's
242
259
  * archive, keyed by the month of its last journal entry. Archived cards
@@ -9,7 +9,7 @@
9
9
  import { join } from 'node:path';
10
10
  import { Service } from '@deepseek-ai/cordis';
11
11
  import { DEV_STAGES } from "./stages.js";
12
- export { DEV_STAGES, DevflowCardId, isCardLocation, isDevStage, isLegalTransition, isReworkEdge } from "./stages.js";
12
+ export { DEFAULT_SERVICE_CLASS, DEV_STAGES, DevflowCardId, SERVICE_CLASSES, isCardLocation, isDevStage, isLegalTransition, isReworkEdge, isServiceClass } from "./stages.js";
13
13
  export { decodeJournalEntry, foldArtifactRecords, foldJournal } from "./journal.js";
14
14
  /** JSON Schema for the Definition-owned artifact registration record. */
15
15
  export const ARTIFACT_RECORD_SCHEMA = {
@@ -6,7 +6,7 @@
6
6
  * @module @zhchxiao123/dsh-devflow/src/journal
7
7
  */
8
8
  import { DevflowCardId } from './stages.ts';
9
- import type { ArtifactRecord, CardLocation, DevStage, DevflowJournalEntry } from './types.ts';
9
+ import type { ArtifactRecord, CardLocation, DevStage, DevflowJournalEntry, ServiceClass } 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. */
@@ -17,6 +17,17 @@ export interface JournalFoldState {
17
17
  blockedFrom?: DevStage;
18
18
  /** The card this one decomposes, from the `created` entry; absent for a top-level card. */
19
19
  parent?: DevflowCardId;
20
+ /**
21
+ * The card's service class, from the `created` entry. Always set: an entry
22
+ * that states none is a {@link DEFAULT_SERVICE_CLASS} card, so no read-side
23
+ * consumer branches on absence.
24
+ */
25
+ serviceClass: ServiceClass;
26
+ /**
27
+ * Set once the card was abandoned. Terminal: no entry may follow, and the
28
+ * card is off the active board even while its directory awaits archiving.
29
+ */
30
+ abandoned?: true;
20
31
  /** Artifact paths in registration order. */
21
32
  artifacts: string[];
22
33
  }
@@ -37,7 +48,8 @@ export declare function decodeJournalEntry(value: unknown): DevflowJournalEntry;
37
48
  * Validates the structural invariants of the durable stream: revisions are the
38
49
  * contiguous sequence 1..n, the first entry is `created`, every transition
39
50
  * departs from the current location, a move to `blocked` remembers its origin,
40
- * and the matching recovery returns exactly there.
51
+ * the matching recovery returns exactly there, and nothing follows an
52
+ * `abandoned` entry.
41
53
  * @param entries - decoded entries in file order.
42
54
  * @returns the folded card state.
43
55
  * @throws {Error} naming the first violated invariant and its entry revision.
@@ -5,7 +5,7 @@
5
5
  * entries through this module so every consumer derives identical state.
6
6
  * @module @zhchxiao123/dsh-devflow/src/journal
7
7
  */
8
- import { DEV_STAGES, DevflowCardId, isCardLocation, isDevStage } from "./stages.js";
8
+ import { DEFAULT_SERVICE_CLASS, DEV_STAGES, DevflowCardId, SERVICE_CLASSES, isCardLocation, isDevStage, isServiceClass } from "./stages.js";
9
9
  /**
10
10
  * Decode one parsed journal value into a {@link DevflowJournalEntry}.
11
11
  *
@@ -36,6 +36,7 @@ export function decodeJournalEntry(value) {
36
36
  type: 'created',
37
37
  by: decodeActor(entry.by),
38
38
  ...decodeOptionalCardId(entry, 'parent'),
39
+ ...decodeOptionalServiceClass(entry),
39
40
  };
40
41
  case 'transition': {
41
42
  if (!isCardLocation(entry.from))
@@ -70,6 +71,18 @@ export function decodeJournalEntry(value) {
70
71
  ...decodeOptionalString(entry, 'kind'),
71
72
  };
72
73
  }
74
+ case 'abandoned': {
75
+ if (typeof entry.reason !== 'string' || entry.reason.trim().length === 0) {
76
+ throw new Error('abandoned field "reason" must be a non-empty string; a card leaving the board without one loses the decision');
77
+ }
78
+ return {
79
+ rev,
80
+ at: entry.at,
81
+ type: 'abandoned',
82
+ by: decodeActor(entry.by),
83
+ reason: entry.reason,
84
+ };
85
+ }
73
86
  case 'claim-expired': {
74
87
  if (entry.previousOwner === undefined) {
75
88
  throw new Error('claim-expired field "previousOwner" is required');
@@ -83,7 +96,7 @@ export function decodeJournalEntry(value) {
83
96
  };
84
97
  }
85
98
  default:
86
- throw new Error(`journal entry field "type" must be created, transition, artifact, or claim-expired (got ${JSON.stringify(entry.type)})`);
99
+ throw new Error(`journal entry field "type" must be created, transition, artifact, abandoned, or claim-expired (got ${JSON.stringify(entry.type)})`);
87
100
  }
88
101
  }
89
102
  /**
@@ -92,7 +105,8 @@ export function decodeJournalEntry(value) {
92
105
  * Validates the structural invariants of the durable stream: revisions are the
93
106
  * contiguous sequence 1..n, the first entry is `created`, every transition
94
107
  * departs from the current location, a move to `blocked` remembers its origin,
95
- * and the matching recovery returns exactly there.
108
+ * the matching recovery returns exactly there, and nothing follows an
109
+ * `abandoned` entry.
96
110
  * @param entries - decoded entries in file order.
97
111
  * @returns the folded card state.
98
112
  * @throws {Error} naming the first violated invariant and its entry revision.
@@ -100,7 +114,7 @@ export function decodeJournalEntry(value) {
100
114
  export function foldJournal(entries) {
101
115
  if (entries.length === 0)
102
116
  throw new Error('journal is empty; every card starts with a "created" entry');
103
- const state = { stage: 'draft', revision: 0, artifacts: [] };
117
+ const state = { stage: 'draft', revision: 0, serviceClass: DEFAULT_SERVICE_CLASS, artifacts: [] };
104
118
  for (const [index, entry] of entries.entries()) {
105
119
  if (entry.rev !== index + 1) {
106
120
  throw new Error(`journal entry ${index + 1} carries rev ${entry.rev}; revisions must be contiguous from 1`);
@@ -110,9 +124,14 @@ export function foldJournal(entries) {
110
124
  throw new Error('journal entry 1 must be "created"');
111
125
  if (entry.parent !== undefined)
112
126
  state.parent = entry.parent;
127
+ if (entry.serviceClass !== undefined)
128
+ state.serviceClass = entry.serviceClass;
113
129
  state.revision = entry.rev;
114
130
  continue;
115
131
  }
132
+ if (state.abandoned === true) {
133
+ throw new Error(`journal entry rev ${entry.rev} follows an abandoned card; abandoning is terminal`);
134
+ }
116
135
  switch (entry.type) {
117
136
  case 'created':
118
137
  throw new Error(`journal entry rev ${entry.rev} repeats "created"`);
@@ -142,6 +161,10 @@ export function foldJournal(entries) {
142
161
  state.artifacts.push(entry.path);
143
162
  state.revision = entry.rev;
144
163
  break;
164
+ case 'abandoned':
165
+ state.abandoned = true;
166
+ state.revision = entry.rev;
167
+ break;
145
168
  case 'claim-expired':
146
169
  state.revision = entry.rev;
147
170
  break;
@@ -215,6 +238,15 @@ function decodeActor(value) {
215
238
  throw new Error(`actor field "kind" must be human, agent, or command (got ${JSON.stringify(actor.kind)})`);
216
239
  }
217
240
  }
241
+ function decodeOptionalServiceClass(record) {
242
+ const value = record.serviceClass;
243
+ if (value === undefined)
244
+ return {};
245
+ if (!isServiceClass(value)) {
246
+ throw new Error(`created field "serviceClass" must be one of ${SERVICE_CLASSES.join(', ')} when present`);
247
+ }
248
+ return { serviceClass: value };
249
+ }
218
250
  function decodeOptionalCardId(record, key) {
219
251
  const value = record[key];
220
252
  if (value === undefined)
@@ -5,7 +5,7 @@
5
5
  * @module @zhchxiao123/dsh-devflow/src/stages
6
6
  */
7
7
  import type { Branded } from '@deepseek-ai/dsh-brand';
8
- import type { CardLocation, DevStage } from './types.ts';
8
+ import type { CardLocation, DevStage, ServiceClass } from './types.ts';
9
9
  /** Opaque id of one task card; equals the card's directory name and never changes. */
10
10
  export type DevflowCardId = Branded<'DevflowCardId'>;
11
11
  /** The pipeline stages in flow order; `blocked` is a bypass, not a member. */
@@ -22,6 +22,25 @@ export declare function isDevStage(value: unknown): value is DevStage;
22
22
  * @returns `true` when `value` is a stage or the `blocked` bypass.
23
23
  */
24
24
  export declare function isCardLocation(value: unknown): value is CardLocation;
25
+ /**
26
+ * The service classes, in ascending order of what they skip.
27
+ *
28
+ * Closed vocabulary rather than a plugin `Config` field, on the same grounds as
29
+ * {@link DEV_STAGES}: the board, both language documents, and the agent's
30
+ * prompts all reference these names, and a deployment-defined class would make
31
+ * every one of those references local. Letting a deployment mint its own
32
+ * shorter class is also precisely the failure mode this vocabulary exists to
33
+ * prevent — see the service-class Agent Note.
34
+ */
35
+ export declare const SERVICE_CLASSES: readonly ["standard", "express", "emergency"];
36
+ /** The class of a card that declares none, on disk and in memory. */
37
+ export declare const DEFAULT_SERVICE_CLASS: ServiceClass;
38
+ /**
39
+ * Narrow an unknown value to a service class.
40
+ * @param value - the candidate value.
41
+ * @returns `true` when `value` is one of {@link SERVICE_CLASSES}.
42
+ */
43
+ export declare function isServiceClass(value: unknown): value is ServiceClass;
25
44
  /**
26
45
  * Brand a raw string as a {@link DevflowCardId}. The id equals the card's
27
46
  * directory name; construction lives here because this package owns the brand.
@@ -29,25 +48,41 @@ export declare function isCardLocation(value: unknown): value is CardLocation;
29
48
  * @returns the branded id.
30
49
  */
31
50
  export declare function DevflowCardId(value: string): DevflowCardId;
51
+ /** A card's own contribution to edge legality; {@link DevCard} satisfies it. */
52
+ export interface TransitionContext {
53
+ /** The remembered origin stage while the card sits at `blocked`. */
54
+ blockedFrom?: DevStage;
55
+ /** The card's service class; omitted is {@link DEFAULT_SERVICE_CLASS}. */
56
+ serviceClass?: ServiceClass;
57
+ }
32
58
  /**
33
59
  * Whether one stage move is a legal edge of the state machine.
34
60
  *
35
61
  * Main flow follows the pipeline order; `reviewing` and `testing` may rework
36
- * to `developing`; any non-terminal location may enter `blocked`; a blocked
37
- * card may only recover to the exact stage it interrupted.
62
+ * to `developing` or `designing` and `developing` may rework to `designing`;
63
+ * any non-terminal location may enter `blocked`; a blocked card may only
64
+ * recover to the exact stage it interrupted. A card's service class adds the
65
+ * shortcuts in {@link CLASS_EXTRA} and takes nothing away.
66
+ *
67
+ * `blocked` legality does not vary by class: a shortcut is about which stages
68
+ * a card may skip, not about how it pauses.
38
69
  * @param from - the card's current location.
39
70
  * @param to - the requested target location.
40
- * @param blockedFrom - the remembered origin stage while `from` is `blocked`.
71
+ * @param card - the moving card's own context; omitted reads as a `standard`
72
+ * card that is not blocked.
41
73
  * @returns `true` when the move is a legal edge.
42
74
  */
43
- export declare function isLegalTransition(from: CardLocation, to: CardLocation, blockedFrom?: DevStage): boolean;
75
+ export declare function isLegalTransition(from: CardLocation, to: CardLocation, card?: TransitionContext): boolean;
44
76
  /**
45
77
  * Whether a legal edge moves the card backwards (a rework). Rework edges
46
- * require a recorded `reason` so the next holder knows what to fix.
78
+ * require a recorded `reason` so the next holder knows what to fix — on
79
+ * `developing -> designing` that reason is what implementing the design
80
+ * revealed about it, which is the whole point of routing the card back rather
81
+ * than redesigning in place.
47
82
  * @param from - the departing location.
48
83
  * @param to - the target location.
49
84
  * @returns `true` for a move from `reviewing` or `testing` back to
50
- * `developing` or `designing`.
85
+ * `developing` or `designing`, and for `developing` back to `designing`.
51
86
  */
52
87
  export declare function isReworkEdge(from: CardLocation, to: CardLocation): boolean;
53
88
  //# sourceMappingURL=stages.d.ts.map
@@ -30,6 +30,27 @@ export function isDevStage(value) {
30
30
  export function isCardLocation(value) {
31
31
  return value === 'blocked' || isDevStage(value);
32
32
  }
33
+ /**
34
+ * The service classes, in ascending order of what they skip.
35
+ *
36
+ * Closed vocabulary rather than a plugin `Config` field, on the same grounds as
37
+ * {@link DEV_STAGES}: the board, both language documents, and the agent's
38
+ * prompts all reference these names, and a deployment-defined class would make
39
+ * every one of those references local. Letting a deployment mint its own
40
+ * shorter class is also precisely the failure mode this vocabulary exists to
41
+ * prevent — see the service-class Agent Note.
42
+ */
43
+ export const SERVICE_CLASSES = ['standard', 'express', 'emergency'];
44
+ /** The class of a card that declares none, on disk and in memory. */
45
+ export const DEFAULT_SERVICE_CLASS = 'standard';
46
+ /**
47
+ * Narrow an unknown value to a service class.
48
+ * @param value - the candidate value.
49
+ * @returns `true` when `value` is one of {@link SERVICE_CLASSES}.
50
+ */
51
+ export function isServiceClass(value) {
52
+ return typeof value === 'string' && SERVICE_CLASSES.includes(value);
53
+ }
33
54
  /**
34
55
  * Brand a raw string as a {@link DevflowCardId}. The id equals the card's
35
56
  * directory name; construction lives here because this package owns the brand.
@@ -48,45 +69,75 @@ export function DevflowCardId(value) {
48
69
  * is. Without the second, design rework happens on a card labelled
49
70
  * `developing`, and the board stops answering the one question it exists to
50
71
  * answer.
72
+ *
73
+ * `developing` reaches `designing` for the same reason, from the stage that
74
+ * finds such faults most often. Its absence left `developing → reviewing →
75
+ * designing` as the only route back, which records a review that never
76
+ * happened in the authoritative journal to reach the stage owning the fault.
51
77
  */
52
78
  const FLOW = {
53
79
  draft: ['designing'],
54
80
  designing: ['ready'],
55
81
  ready: ['developing'],
56
- developing: ['reviewing'],
82
+ developing: ['reviewing', 'designing'],
57
83
  reviewing: ['testing', 'developing', 'designing'],
58
84
  testing: ['done', 'developing', 'designing'],
59
85
  done: [],
60
86
  };
87
+ /**
88
+ * Edges each service class adds to {@link FLOW}, and the only place a class
89
+ * differs from another. Stated as additions rather than as one whole graph per
90
+ * class so "every class is a superset of `standard`" is a property of the code
91
+ * instead of a convention: a class cannot remove an edge, and therefore cannot
92
+ * make a journal that replays today stop replaying.
93
+ */
94
+ const CLASS_EXTRA = {
95
+ standard: {},
96
+ express: { draft: ['developing'], reviewing: ['done'] },
97
+ emergency: { draft: ['developing'], developing: ['done'] },
98
+ };
61
99
  /**
62
100
  * Whether one stage move is a legal edge of the state machine.
63
101
  *
64
102
  * Main flow follows the pipeline order; `reviewing` and `testing` may rework
65
- * to `developing`; any non-terminal location may enter `blocked`; a blocked
66
- * card may only recover to the exact stage it interrupted.
103
+ * to `developing` or `designing` and `developing` may rework to `designing`;
104
+ * any non-terminal location may enter `blocked`; a blocked card may only
105
+ * recover to the exact stage it interrupted. A card's service class adds the
106
+ * shortcuts in {@link CLASS_EXTRA} and takes nothing away.
107
+ *
108
+ * `blocked` legality does not vary by class: a shortcut is about which stages
109
+ * a card may skip, not about how it pauses.
67
110
  * @param from - the card's current location.
68
111
  * @param to - the requested target location.
69
- * @param blockedFrom - the remembered origin stage while `from` is `blocked`.
112
+ * @param card - the moving card's own context; omitted reads as a `standard`
113
+ * card that is not blocked.
70
114
  * @returns `true` when the move is a legal edge.
71
115
  */
72
- export function isLegalTransition(from, to, blockedFrom) {
116
+ export function isLegalTransition(from, to, card) {
73
117
  if (from === to)
74
118
  return false;
75
119
  if (from === 'blocked')
76
- return to === blockedFrom;
120
+ return to === card?.blockedFrom;
77
121
  if (to === 'blocked')
78
122
  return from !== 'done';
79
- return FLOW[from].includes(to);
123
+ if (FLOW[from].includes(to))
124
+ return true;
125
+ return (CLASS_EXTRA[card?.serviceClass ?? DEFAULT_SERVICE_CLASS][from] ?? []).includes(to);
80
126
  }
81
127
  /**
82
128
  * Whether a legal edge moves the card backwards (a rework). Rework edges
83
- * require a recorded `reason` so the next holder knows what to fix.
129
+ * require a recorded `reason` so the next holder knows what to fix — on
130
+ * `developing -> designing` that reason is what implementing the design
131
+ * revealed about it, which is the whole point of routing the card back rather
132
+ * than redesigning in place.
84
133
  * @param from - the departing location.
85
134
  * @param to - the target location.
86
135
  * @returns `true` for a move from `reviewing` or `testing` back to
87
- * `developing` or `designing`.
136
+ * `developing` or `designing`, and for `developing` back to `designing`.
88
137
  */
89
138
  export function isReworkEdge(from, to) {
90
- return (to === 'developing' || to === 'designing') && (from === 'reviewing' || from === 'testing');
139
+ if (to === 'designing')
140
+ return from === 'developing' || from === 'reviewing' || from === 'testing';
141
+ return to === 'developing' && (from === 'reviewing' || from === 'testing');
91
142
  }
92
143
  //# sourceMappingURL=stages.js.map
@@ -47,6 +47,20 @@ declare module '@deepseek-ai/cordis' {
47
47
  export type DevStage = 'draft' | 'designing' | 'ready' | 'developing' | 'reviewing' | 'testing' | 'done';
48
48
  /** Where a card currently sits: a pipeline stage, or the `blocked` bypass. */
49
49
  export type CardLocation = DevStage | 'blocked';
50
+ /**
51
+ * The closed set of service classes a card is created under, each selecting
52
+ * which edges of the pipeline that card may take. Every class is a superset of
53
+ * `standard`, so a class only ever adds a shortcut.
54
+ *
55
+ * - `standard` — the full pipeline; the class of a card that declares none.
56
+ * - `express` — reaches `developing` from `draft` and `done` from
57
+ * `reviewing`, skipping design, readiness, and independent verification.
58
+ * Peer review stays: it is the control worth keeping on cheap work.
59
+ * - `emergency` — reaches `developing` from `draft` and `done` from
60
+ * `developing`. It gives up review too, and the follow-up is an ordinary
61
+ * card rather than an obligation encoded in the state machine.
62
+ */
63
+ export type ServiceClass = 'standard' | 'express' | 'emergency';
50
64
  /** Who performed a journal action; `command` marks the human-command intervention plane. */
51
65
  export type DevActor = {
52
66
  kind: 'human';
@@ -66,6 +80,12 @@ export interface JournalCreated {
66
80
  by: DevActor;
67
81
  /** The card this one decomposes, fixed here at creation and never changed. */
68
82
  parent?: DevflowCardId;
83
+ /**
84
+ * The card's service class, fixed here at creation and never changed.
85
+ * Omitted is `standard`, so a journal written before classes existed reads
86
+ * as one and a `standard` card's first entry keeps its original bytes.
87
+ */
88
+ serviceClass?: ServiceClass;
69
89
  }
70
90
  /**
71
91
  * One recorded gate verdict on a committed transition: which actor allowed the
@@ -115,6 +135,23 @@ export interface JournalArtifact {
115
135
  */
116
136
  kind?: string;
117
137
  }
138
+ /**
139
+ * The decision to stop: this card will never be finished. Terminal — no entry
140
+ * may follow it — and the card leaves the active board rather than occupying
141
+ * a stage nobody is working in.
142
+ */
143
+ export interface JournalAbandoned {
144
+ rev: number;
145
+ at: string;
146
+ type: 'abandoned';
147
+ by: DevActor;
148
+ /**
149
+ * Why the work stopped. Required, unlike a transition's reason: a transition
150
+ * leaves the card visible and explicable from where it sits, while this
151
+ * removes it from the board, so the reason is all that is left of it.
152
+ */
153
+ reason: string;
154
+ }
118
155
  /** Takeover of a stale lease: the previous holder's heartbeat lapsed. */
119
156
  export interface JournalClaimExpired {
120
157
  rev: number;
@@ -124,7 +161,7 @@ export interface JournalClaimExpired {
124
161
  by: DevActor;
125
162
  }
126
163
  /** The journal entry union; the discriminant is `type`. */
127
- export type DevflowJournalEntry = JournalCreated | JournalTransition | JournalArtifact | JournalClaimExpired;
164
+ export type DevflowJournalEntry = JournalCreated | JournalTransition | JournalArtifact | JournalAbandoned | JournalClaimExpired;
128
165
  /**
129
166
  * Read-side value of one artifact registration: the journal entry's facts
130
167
  * without its envelope. Registrations are immutable — the newest record of one
@@ -158,6 +195,16 @@ export interface DevCard {
158
195
  * exists, so a card carrying `parent` is never itself a parent.
159
196
  */
160
197
  parent?: DevflowCardId;
198
+ /**
199
+ * The card's service class, selecting which pipeline edges it may take.
200
+ * Always present: a card whose journal states none is `standard`.
201
+ */
202
+ serviceClass: ServiceClass;
203
+ /**
204
+ * Set once the card was abandoned: the work stopped and will not resume.
205
+ * Such a card is off the active board, so `list` never reports one.
206
+ */
207
+ abandoned?: true;
161
208
  /** Markdown body of the card file below its frontmatter. */
162
209
  body: string;
163
210
  /** Display path of the card file. */
@@ -215,6 +262,12 @@ export interface CreateRequest {
215
262
  * must be an active top-level card of the same root — only one level exists.
216
263
  */
217
264
  parent?: DevflowCardId;
265
+ /**
266
+ * Which pipeline edges the card may take, fixed here and never changed —
267
+ * escalating live work means a new card, not a mutated one. Omitted is
268
+ * `standard`, the full pipeline.
269
+ */
270
+ serviceClass?: ServiceClass;
218
271
  /** Devflow root receiving the card; omitted uses the implementation's default root. */
219
272
  root?: string;
220
273
  }
@@ -307,6 +360,35 @@ export type TransitionResult = {
307
360
  code: TransitionRejectionCode;
308
361
  message: string;
309
362
  };
363
+ /** Caller view of one abandonment: the decision that this card stops here. */
364
+ export interface AbandonRequest {
365
+ id: DevflowCardId;
366
+ /** Optimistic-concurrency token: the `stageRevision` the caller last observed. */
367
+ expectedRevision: number;
368
+ by: DevActor;
369
+ /** Why the work stopped; blank is rejected rather than recorded as nothing. */
370
+ reason: string;
371
+ /** Devflow root holding the card; omitted uses the implementation's default root. */
372
+ root?: string;
373
+ }
374
+ /**
375
+ * Stable rejection codes of {@link AbandonResult}. `already-done` names the one
376
+ * card that must not be abandoned: a delivered outcome is not a decision to
377
+ * stop, and `archiveDone` is what settles it.
378
+ */
379
+ export type AbandonRejectionCode = 'empty-reason' | 'already-done' | 'revision-mismatch' | 'write-contended';
380
+ /**
381
+ * Outcome of one abandonment. Domain rejections resolve with a stable code;
382
+ * only infrastructure failures reject the promise.
383
+ */
384
+ export type AbandonResult = {
385
+ ok: true;
386
+ card: DevCard;
387
+ } | {
388
+ ok: false;
389
+ code: AbandonRejectionCode;
390
+ message: string;
391
+ };
310
392
  /** Fields shared by both {@link ArtifactRequest} forms. */
311
393
  interface ArtifactRequestBase {
312
394
  id: DevflowCardId;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zhchxiao123/dsh-devflow",
3
3
  "description": "Service Definition for the ctx.devflow task-card capability seam of the DeepSeek Harness",
4
- "version": "0.2.0",
4
+ "version": "0.4.0-dev.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -46,16 +46,16 @@
46
46
  "zod": "^4.4.3"
47
47
  },
48
48
  "peerDependencies": {
49
- "@deepseek-ai/cordis": "4.0.1",
50
- "@deepseek-ai/dsh-brand": "0.1.1-rc.2",
51
- "@deepseek-ai/dsh-invariants": "0.1.1-rc.2"
49
+ "@deepseek-ai/cordis": "4.0.2",
50
+ "@deepseek-ai/dsh-brand": "0.1.2-alpha.3",
51
+ "@deepseek-ai/dsh-invariants": "0.1.2-alpha.3"
52
52
  },
53
53
  "devDependencies": {
54
- "@deepseek-ai/cordis": "4.0.1",
55
- "@deepseek-ai/dsh-brand": "0.1.1-rc.2",
56
- "@deepseek-ai/dsh-invariants": "0.1.1-rc.2",
57
- "@deepseek-ai/dsh-session": "0.1.1-rc.2",
58
- "@deepseek-ai/dsh-session-persistence": "0.1.1-rc.2"
54
+ "@deepseek-ai/cordis": "4.0.2",
55
+ "@deepseek-ai/dsh-brand": "0.1.2-alpha.3",
56
+ "@deepseek-ai/dsh-invariants": "0.1.2-alpha.3",
57
+ "@deepseek-ai/dsh-session": "0.1.2-alpha.3",
58
+ "@deepseek-ai/dsh-session-persistence": "0.1.2-alpha.3"
59
59
  },
60
60
  "scripts": {}
61
61
  }