@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.
@@ -8,8 +8,52 @@
8
8
  */
9
9
  import { join } from 'node:path';
10
10
  import { Service } from '@deepseek-ai/cordis';
11
- export { DEV_STAGES, DevflowCardId, isCardLocation, isDevStage, isLegalTransition, isReworkEdge } from "./stages.js";
12
- export { decodeJournalEntry, foldJournal } from "./journal.js";
11
+ import { DEV_STAGES } from "./stages.js";
12
+ export { DEFAULT_SERVICE_CLASS, DEV_STAGES, DevflowCardId, SERVICE_CLASSES, isCardLocation, isDevStage, isLegalTransition, isReworkEdge, isServiceClass } from "./stages.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
@@ -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 and driver book-keeping uses.
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) {
@@ -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, 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,10 +48,20 @@ 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.
44
56
  */
45
57
  export declare function foldJournal(entries: readonly DevflowJournalEntry[]): JournalFoldState;
58
+ /**
59
+ * Derive the artifact registrations of a decoded journal, in registration
60
+ * order. Kept beside {@link foldJournal} — whose `artifacts` is this list's
61
+ * path projection — so every consumer derives identical records; an entry
62
+ * without a `kind` yields a record without one.
63
+ * @param entries - decoded entries in file order.
64
+ * @returns the artifact records, oldest first.
65
+ */
66
+ export declare function foldArtifactRecords(entries: readonly DevflowJournalEntry[]): ArtifactRecord[];
46
67
  //# sourceMappingURL=journal.d.ts.map
@@ -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))
@@ -67,6 +68,19 @@ export function decodeJournalEntry(value) {
67
68
  path: entry.path,
68
69
  stage: entry.stage,
69
70
  ...entry.by !== undefined ? { by: decodeActor(entry.by) } : {},
71
+ ...decodeOptionalString(entry, 'kind'),
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,
70
84
  };
71
85
  }
72
86
  case 'claim-expired': {
@@ -82,7 +96,7 @@ export function decodeJournalEntry(value) {
82
96
  };
83
97
  }
84
98
  default:
85
- 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)})`);
86
100
  }
87
101
  }
88
102
  /**
@@ -91,7 +105,8 @@ export function decodeJournalEntry(value) {
91
105
  * Validates the structural invariants of the durable stream: revisions are the
92
106
  * contiguous sequence 1..n, the first entry is `created`, every transition
93
107
  * departs from the current location, a move to `blocked` remembers its origin,
94
- * and the matching recovery returns exactly there.
108
+ * the matching recovery returns exactly there, and nothing follows an
109
+ * `abandoned` entry.
95
110
  * @param entries - decoded entries in file order.
96
111
  * @returns the folded card state.
97
112
  * @throws {Error} naming the first violated invariant and its entry revision.
@@ -99,7 +114,7 @@ export function decodeJournalEntry(value) {
99
114
  export function foldJournal(entries) {
100
115
  if (entries.length === 0)
101
116
  throw new Error('journal is empty; every card starts with a "created" entry');
102
- const state = { stage: 'draft', revision: 0, artifacts: [] };
117
+ const state = { stage: 'draft', revision: 0, serviceClass: DEFAULT_SERVICE_CLASS, artifacts: [] };
103
118
  for (const [index, entry] of entries.entries()) {
104
119
  if (entry.rev !== index + 1) {
105
120
  throw new Error(`journal entry ${index + 1} carries rev ${entry.rev}; revisions must be contiguous from 1`);
@@ -109,9 +124,14 @@ export function foldJournal(entries) {
109
124
  throw new Error('journal entry 1 must be "created"');
110
125
  if (entry.parent !== undefined)
111
126
  state.parent = entry.parent;
127
+ if (entry.serviceClass !== undefined)
128
+ state.serviceClass = entry.serviceClass;
112
129
  state.revision = entry.rev;
113
130
  continue;
114
131
  }
132
+ if (state.abandoned === true) {
133
+ throw new Error(`journal entry rev ${entry.rev} follows an abandoned card; abandoning is terminal`);
134
+ }
115
135
  switch (entry.type) {
116
136
  case 'created':
117
137
  throw new Error(`journal entry rev ${entry.rev} repeats "created"`);
@@ -141,6 +161,10 @@ export function foldJournal(entries) {
141
161
  state.artifacts.push(entry.path);
142
162
  state.revision = entry.rev;
143
163
  break;
164
+ case 'abandoned':
165
+ state.abandoned = true;
166
+ state.revision = entry.rev;
167
+ break;
144
168
  case 'claim-expired':
145
169
  state.revision = entry.rev;
146
170
  break;
@@ -148,15 +172,55 @@ export function foldJournal(entries) {
148
172
  }
149
173
  return state;
150
174
  }
175
+ /**
176
+ * Derive the artifact registrations of a decoded journal, in registration
177
+ * order. Kept beside {@link foldJournal} — whose `artifacts` is this list's
178
+ * path projection — so every consumer derives identical records; an entry
179
+ * without a `kind` yields a record without one.
180
+ * @param entries - decoded entries in file order.
181
+ * @returns the artifact records, oldest first.
182
+ */
183
+ export function foldArtifactRecords(entries) {
184
+ const records = [];
185
+ for (const entry of entries) {
186
+ if (entry.type !== 'artifact')
187
+ continue;
188
+ records.push({
189
+ path: entry.path,
190
+ ...entry.kind !== undefined ? { kind: entry.kind } : {},
191
+ rev: entry.rev,
192
+ stage: entry.stage,
193
+ });
194
+ }
195
+ return records;
196
+ }
151
197
  function decodeGate(value) {
152
198
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
153
199
  throw new Error('transition field "gate" must be a JSON object');
154
200
  }
155
201
  const gate = value;
156
- if (gate.approvedBy === undefined) {
157
- throw new Error('transition field "gate" requires "approvedBy"');
202
+ if (gate.approvedBy === undefined && gate.checks === undefined) {
203
+ throw new Error('transition field "gate" requires "approvedBy" or "checks"');
204
+ }
205
+ return {
206
+ ...gate.approvedBy !== undefined ? { approvedBy: decodeActor(gate.approvedBy) } : {},
207
+ ...gate.checks !== undefined ? { checks: decodeGateChecks(gate.checks) } : {},
208
+ };
209
+ }
210
+ function decodeGateChecks(value) {
211
+ if (!Array.isArray(value)) {
212
+ throw new Error('transition field "gate.checks" must be an array');
158
213
  }
159
- return { approvedBy: decodeActor(gate.approvedBy) };
214
+ return value.map((check) => {
215
+ if (typeof check !== 'object' || check === null || Array.isArray(check)) {
216
+ throw new Error('gate check must be a JSON object');
217
+ }
218
+ const record = check;
219
+ if (record.verdict !== 'allowed') {
220
+ throw new Error(`gate check field "verdict" must be "allowed" (got ${JSON.stringify(record.verdict)})`);
221
+ }
222
+ return { by: decodeActor(record.by), verdict: 'allowed', ...decodeOptionalString(record, 'summary') };
223
+ });
160
224
  }
161
225
  function decodeActor(value) {
162
226
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
@@ -174,6 +238,15 @@ function decodeActor(value) {
174
238
  throw new Error(`actor field "kind" must be human, agent, or command (got ${JSON.stringify(actor.kind)})`);
175
239
  }
176
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
+ }
177
250
  function decodeOptionalCardId(record, key) {
178
251
  const value = record[key];
179
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,24 +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
- * @returns `true` for `reviewing -> developing` and `testing -> developing`.
84
+ * @returns `true` for a move from `reviewing` or `testing` back to
85
+ * `developing` or `designing`, and for `developing` back to `designing`.
50
86
  */
51
87
  export declare function isReworkEdge(from: CardLocation, to: CardLocation): boolean;
52
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.
@@ -39,44 +60,84 @@ export function isCardLocation(value) {
39
60
  export function DevflowCardId(value) {
40
61
  return value;
41
62
  }
42
- /** Forward and rework edges of the pipeline; `blocked` legality lives in {@link isLegalTransition}. */
63
+ /**
64
+ * Forward and rework edges of the pipeline; `blocked` legality lives in
65
+ * {@link isLegalTransition}.
66
+ *
67
+ * Review and verification send a card back to whichever stage owns the fault:
68
+ * `developing` when the implementation is wrong, `designing` when the design
69
+ * is. Without the second, design rework happens on a card labelled
70
+ * `developing`, and the board stops answering the one question it exists to
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.
77
+ */
43
78
  const FLOW = {
44
79
  draft: ['designing'],
45
80
  designing: ['ready'],
46
81
  ready: ['developing'],
47
- developing: ['reviewing'],
48
- reviewing: ['testing', 'developing'],
49
- testing: ['done', 'developing'],
82
+ developing: ['reviewing', 'designing'],
83
+ reviewing: ['testing', 'developing', 'designing'],
84
+ testing: ['done', 'developing', 'designing'],
50
85
  done: [],
51
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
+ };
52
99
  /**
53
100
  * Whether one stage move is a legal edge of the state machine.
54
101
  *
55
102
  * Main flow follows the pipeline order; `reviewing` and `testing` may rework
56
- * to `developing`; any non-terminal location may enter `blocked`; a blocked
57
- * 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.
58
110
  * @param from - the card's current location.
59
111
  * @param to - the requested target location.
60
- * @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.
61
114
  * @returns `true` when the move is a legal edge.
62
115
  */
63
- export function isLegalTransition(from, to, blockedFrom) {
116
+ export function isLegalTransition(from, to, card) {
64
117
  if (from === to)
65
118
  return false;
66
119
  if (from === 'blocked')
67
- return to === blockedFrom;
120
+ return to === card?.blockedFrom;
68
121
  if (to === 'blocked')
69
122
  return from !== 'done';
70
- 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);
71
126
  }
72
127
  /**
73
128
  * Whether a legal edge moves the card backwards (a rework). Rework edges
74
- * 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.
75
133
  * @param from - the departing location.
76
134
  * @param to - the target location.
77
- * @returns `true` for `reviewing -> developing` and `testing -> developing`.
135
+ * @returns `true` for a move from `reviewing` or `testing` back to
136
+ * `developing` or `designing`, and for `developing` back to `designing`.
78
137
  */
79
138
  export function isReworkEdge(from, to) {
139
+ if (to === 'designing')
140
+ return from === 'developing' || from === 'reviewing' || from === 'testing';
80
141
  return to === 'developing' && (from === 'reviewing' || from === 'testing');
81
142
  }
82
143
  //# sourceMappingURL=stages.js.map