@zhchxiao123/dsh-devflow 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +7 -5
- package/README.zh.md +7 -5
- package/lib/index.js +240 -19
- package/lib/types/index.d.ts +151 -9
- package/lib/types/index.js +46 -2
- package/lib/types/invariant.js +1 -1
- package/lib/types/journal.d.ts +23 -2
- package/lib/types/journal.js +80 -7
- package/lib/types/stages.d.ts +43 -7
- package/lib/types/stages.js +73 -12
- package/lib/types/types.d.ts +190 -11
- package/package.json +1 -1
package/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
|
-
|
|
12
|
-
export {
|
|
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
|
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, 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
|
-
*
|
|
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
|
package/lib/types/journal.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
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)
|
package/lib/types/stages.d.ts
CHANGED
|
@@ -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
|
|
37
|
-
*
|
|
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
|
|
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,
|
|
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
|
|
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
|
package/lib/types/stages.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
57
|
-
*
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|