@zhchxiao123/dsh-devflow 0.1.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.
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Service Definition of the `ctx.devflow` capability seam: file-backed task
3
+ * cards whose stage moves through a fixed pipeline. This package owns the card
4
+ * vocabulary and the journal decode/replay used by every consumer. Storage
5
+ * mechanics belong to a provider such as `@zhchxiao123/dsh-devflow-filesystem`;
6
+ * model-facing tools belong to `@zhchxiao123/dsh-devflow-tool`.
7
+ * @module @zhchxiao123/dsh-devflow
8
+ */
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';
11
+ export type * from './types.ts';
12
+ export { DEV_STAGES, DevflowCardId, isCardLocation, isDevStage, isLegalTransition, isReworkEdge } from './stages.ts';
13
+ export { decodeJournalEntry, foldJournal } from './journal.ts';
14
+ export type { JournalFoldState } from './journal.ts';
15
+ declare module '@deepseek-ai/cordis' {
16
+ interface Context {
17
+ devflow: DevflowStore;
18
+ }
19
+ }
20
+ /**
21
+ * Abstract task-card store registered as `ctx.devflow` (one implementation per
22
+ * context; loading a second throws, cordis' standard duplicate-service
23
+ * behavior). Subclass, implement the abstract methods, and load the subclass
24
+ * as a plugin.
25
+ *
26
+ * Implementations must honor these read-side semantics:
27
+ * - Current state comes from journal replay ({@link foldJournal}); the card
28
+ * file's frontmatter is a projection. On disagreement the journal wins and
29
+ * the drift is warned, never silently adopted.
30
+ * - A structurally invalid journal fails the read loudly, naming the file and
31
+ * line; a card is never silently skipped.
32
+ */
33
+ export declare abstract class DevflowStore extends Service {
34
+ constructor(ctx: Context);
35
+ /**
36
+ * List the cards in the active set of one root.
37
+ * @param filter - optional narrowing; omitted lists every card.
38
+ * @param root - devflow root to list; omitted uses the implementation's default root.
39
+ * @returns cards ordered by id.
40
+ */
41
+ abstract list(filter?: CardFilter, root?: string): Promise<DevCard[]>;
42
+ /**
43
+ * Read one card.
44
+ * @param id - the card id (its directory name).
45
+ * @param root - devflow root holding the card; omitted uses the implementation's default root.
46
+ * @returns the card with journal-derived current state.
47
+ */
48
+ abstract read(id: DevflowCardId, root?: string): Promise<DevCard>;
49
+ /**
50
+ * Read one card's complete decoded journal, in revision order. The stream
51
+ * is validated like a read: a structurally invalid journal fails loudly,
52
+ * naming the file and line.
53
+ * @param id - the card id (its directory name).
54
+ * @param root - devflow root holding the card; omitted uses the implementation's default root.
55
+ * @returns the decoded entries, oldest first.
56
+ */
57
+ abstract history(id: DevflowCardId, root?: string): Promise<DevflowJournalEntry[]>;
58
+ /**
59
+ * Read the card's current lease holder.
60
+ * @param id - the card id (its directory name).
61
+ * @param root - devflow root holding the card; omitted uses the implementation's default root.
62
+ * @returns the holder facts, or `undefined` while the card is unclaimed; a
63
+ * corrupt claim record fails loudly.
64
+ */
65
+ abstract holder(id: DevflowCardId, root?: string): Promise<ClaimHolder | undefined>;
66
+ /**
67
+ * Apply implementation-owned defaults to a creation request: the slug when
68
+ * omitted, the devflow root when omitted, and the creation timestamp.
69
+ * @param request - the caller's request.
70
+ * @returns the fully specified spec to hand to {@link create}.
71
+ */
72
+ abstract resolveCreate(request: CreateRequest): CreateSpec;
73
+ /**
74
+ * Create one card in the active set: sequence-number allocation, the
75
+ * exclusive card-directory creation, the journal's first `created` entry
76
+ * (the only commit point), the projection write, then `devflow/card-created`.
77
+ * Sequence numbers continue past archived cards, so an id is never reissued.
78
+ * @param spec - a resolved spec from {@link resolveCreate}, never a raw request.
79
+ * @returns the outcome; domain rejections resolve with `ok: false`.
80
+ */
81
+ abstract create(spec: CreateSpec): Promise<CreateResult>;
82
+ /**
83
+ * Apply implementation-owned defaults to a transition request: the devflow
84
+ * root when omitted and the commit timestamp.
85
+ * @param request - the caller's request.
86
+ * @returns the fully specified spec to hand to {@link transition}.
87
+ */
88
+ abstract resolve(request: TransitionRequest): TransitionSpec;
89
+ /**
90
+ * Commit one stage move: revision check, edge check, the
91
+ * `devflow/transition` waterfall, the journal append (the only commit
92
+ * point), the projection rewrite, then `devflow/stage-changed`. State and
93
+ * notifications publish only after the journal committed.
94
+ * @param spec - a resolved spec from {@link resolve}, never a raw request.
95
+ * @returns the outcome; domain rejections resolve with `ok: false`.
96
+ */
97
+ abstract transition(spec: TransitionSpec): Promise<TransitionResult>;
98
+ /**
99
+ * Take the card's exclusive lease.
100
+ * @param id - the card to claim.
101
+ * @param owner - the prospective holder, recorded in the lease.
102
+ * @param options - staleness takeover policy and root; omitted never takes
103
+ * over and uses the implementation's default root.
104
+ * @returns the live handle, or the current holder when the lease is taken.
105
+ */
106
+ abstract claim(id: DevflowCardId, owner: DevActor, options?: ClaimOptions): Promise<ClaimResult>;
107
+ /**
108
+ * Register a stage deliverable in the card's journal against its current
109
+ * stage. A blocked card cannot register artifacts, and the revision check
110
+ * mirrors {@link transition}.
111
+ * @param request - card, artifact path, expected revision, and actor.
112
+ * @returns the outcome; domain rejections resolve with `ok: false`.
113
+ */
114
+ abstract attachArtifact(request: ArtifactRequest): Promise<ArtifactResult>;
115
+ /**
116
+ * Move every `done` card of one root out of the active set into that root's
117
+ * archive, keyed by the month of its last journal entry. Archived cards
118
+ * leave {@link list} but keep their complete journal.
119
+ * @param root - devflow root to archive; omitted uses the implementation's default root.
120
+ * @returns the archived card ids, in id order.
121
+ */
122
+ abstract archiveDone(root?: string): Promise<DevflowCardId[]>;
123
+ /**
124
+ * {@link list} scoped to a viewing session's workspace, the face every
125
+ * browser channel reads through.
126
+ * @param filter - optional narrowing; omitted lists every card.
127
+ * @param sessionId - the viewing session; its workspace resolves host-side
128
+ * to the devflow root, so the wire never carries a file path. Omitted
129
+ * lists the default root.
130
+ * @returns cards ordered by id.
131
+ */
132
+ listForSession(filter?: CardFilter, sessionId?: string): Promise<DevCard[]>;
133
+ /**
134
+ * One card's detail scoped to a viewing session's workspace: the read value,
135
+ * its complete decoded journal, and the current lease holder in one round
136
+ * trip.
137
+ * @param id - the card id (its directory name).
138
+ * @param sessionId - the viewing session; resolved like {@link listForSession}.
139
+ * @returns the aggregated detail; `holder` is absent while the card is unclaimed.
140
+ */
141
+ detailForSession(id: DevflowCardId, sessionId?: string): Promise<DevCardDetail>;
142
+ /**
143
+ * Resolve a viewing session into its workspace devflow root: the live or
144
+ * persisted session's header cwd maps to `<cwd>/.devflow`, and a session
145
+ * without a cwd derives no root (the implementation default applies). The
146
+ * browser sends only the session id — this host-side step is what keeps
147
+ * a root off the wire the browser can choose.
148
+ * @param sessionId - the viewing session, or `undefined` for the default root.
149
+ * @returns the derived root, or `undefined` when none derives.
150
+ * @throws {Error} for an unknown session, or when no session service is composed.
151
+ */
152
+ protected sessionRoot(sessionId: string | undefined): Promise<string | undefined>;
153
+ }
154
+ export default DevflowStore;
155
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Service Definition of the `ctx.devflow` capability seam: file-backed task
3
+ * cards whose stage moves through a fixed pipeline. This package owns the card
4
+ * vocabulary and the journal decode/replay used by every consumer. Storage
5
+ * mechanics belong to a provider such as `@zhchxiao123/dsh-devflow-filesystem`;
6
+ * model-facing tools belong to `@zhchxiao123/dsh-devflow-tool`.
7
+ * @module @zhchxiao123/dsh-devflow
8
+ */
9
+ import { join } from 'node:path';
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";
13
+ /**
14
+ * Abstract task-card store registered as `ctx.devflow` (one implementation per
15
+ * context; loading a second throws, cordis' standard duplicate-service
16
+ * behavior). Subclass, implement the abstract methods, and load the subclass
17
+ * as a plugin.
18
+ *
19
+ * Implementations must honor these read-side semantics:
20
+ * - Current state comes from journal replay ({@link foldJournal}); the card
21
+ * file's frontmatter is a projection. On disagreement the journal wins and
22
+ * the drift is warned, never silently adopted.
23
+ * - A structurally invalid journal fails the read loudly, naming the file and
24
+ * line; a card is never silently skipped.
25
+ */
26
+ export class DevflowStore extends Service {
27
+ constructor(ctx) {
28
+ super(ctx, 'devflow');
29
+ }
30
+ /**
31
+ * {@link list} scoped to a viewing session's workspace, the face every
32
+ * browser channel reads through.
33
+ * @param filter - optional narrowing; omitted lists every card.
34
+ * @param sessionId - the viewing session; its workspace resolves host-side
35
+ * to the devflow root, so the wire never carries a file path. Omitted
36
+ * lists the default root.
37
+ * @returns cards ordered by id.
38
+ */
39
+ async listForSession(filter, sessionId) {
40
+ return this.list(filter, await this.sessionRoot(sessionId));
41
+ }
42
+ /**
43
+ * One card's detail scoped to a viewing session's workspace: the read value,
44
+ * its complete decoded journal, and the current lease holder in one round
45
+ * trip.
46
+ * @param id - the card id (its directory name).
47
+ * @param sessionId - the viewing session; resolved like {@link listForSession}.
48
+ * @returns the aggregated detail; `holder` is absent while the card is unclaimed.
49
+ */
50
+ async detailForSession(id, sessionId) {
51
+ const root = await this.sessionRoot(sessionId);
52
+ // The card and its journal are two reads of one file; a transition landing
53
+ // between them would tear the aggregate (revisions are contiguous from 1,
54
+ // so the last entry's rev must equal the card's). One re-read absorbs the
55
+ // race; a still-moving card ships its newest pair and the next forwarded
56
+ // event refetches anyway.
57
+ let card = await this.read(id, root);
58
+ let entries = await this.history(id, root);
59
+ if (entries.at(-1)?.rev !== card.stageRevision) {
60
+ card = await this.read(id, root);
61
+ entries = await this.history(id, root);
62
+ }
63
+ const holder = await this.holder(id, root);
64
+ return { card, entries, ...holder === undefined ? {} : { holder } };
65
+ }
66
+ /**
67
+ * Resolve a viewing session into its workspace devflow root: the live or
68
+ * persisted session's header cwd maps to `<cwd>/.devflow`, and a session
69
+ * without a cwd derives no root (the implementation default applies). The
70
+ * browser sends only the session id — this host-side step is what keeps
71
+ * a root off the wire the browser can choose.
72
+ * @param sessionId - the viewing session, or `undefined` for the default root.
73
+ * @returns the derived root, or `undefined` when none derives.
74
+ * @throws {Error} for an unknown session, or when no session service is composed.
75
+ */
76
+ async sessionRoot(sessionId) {
77
+ if (sessionId === undefined)
78
+ return undefined;
79
+ // The wire delivers the id as a validated string; the brand is this
80
+ // process's own session vocabulary.
81
+ const id = sessionId;
82
+ const live = this.ctx.get('sessions')?.get(id);
83
+ if (live !== undefined)
84
+ return rootOfCwd(live.header.cwd);
85
+ const persistence = this.ctx.get('sessionPersistence');
86
+ if (persistence === undefined) {
87
+ throw new Error(`devflow: cannot resolve session ${sessionId}: no session service is composed`);
88
+ }
89
+ let cwd;
90
+ try {
91
+ cwd = (await persistence.inspect(id)).meta.cwd;
92
+ }
93
+ catch (error) {
94
+ throw new Error(`devflow: unknown session ${sessionId}`, { cause: error });
95
+ }
96
+ return rootOfCwd(cwd);
97
+ }
98
+ }
99
+ /** The workspace's devflow root for a session cwd; no cwd derives no root. */
100
+ function rootOfCwd(cwd) {
101
+ return cwd === undefined ? undefined : join(cwd, '.devflow');
102
+ }
103
+ export default DevflowStore;
104
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,13 @@
1
+ /** Package-owned devflow event-stream invariants. @module @zhchxiao123/dsh-devflow/invariant */
2
+ import type { Context } from '@deepseek-ai/cordis';
3
+ /** Cordis companion plugin name. */
4
+ export declare const name = "devflow-invariant";
5
+ /** Service required before the companion can reserve package ownership. */
6
+ export declare const inject: string[];
7
+ /**
8
+ * Register this package's invariant companion.
9
+ * @param ctx - Cordis context carrying the invariant service.
10
+ * @returns the installed registration's disposer after setup succeeds.
11
+ */
12
+ export declare const apply: (ctx: Context) => Promise<() => void>;
13
+ //# sourceMappingURL=invariant.d.ts.map
@@ -0,0 +1,51 @@
1
+ /** Package-owned devflow event-stream invariants. @module @zhchxiao123/dsh-devflow/invariant */
2
+ const PACKAGE_NAME = '@zhchxiao123/dsh-devflow';
3
+ /** Cordis companion plugin name. */
4
+ export const name = 'devflow-invariant';
5
+ /** Service required before the companion can reserve package ownership. */
6
+ export const inject = ['invariants'];
7
+ /**
8
+ * Validate the devflow notification streams: `devflow/card-created` announces
9
+ * only fresh drafts at revision 1 for ids the stream has never seen and never
10
+ * nests a breakdown two levels deep, and per card, `devflow/stage-changed`
11
+ * revisions strictly increase while every notification reports an actual move.
12
+ */
13
+ const install = (ctx, fail) => {
14
+ const lastRevision = new Map();
15
+ const children = new Set();
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.
18
+ const key = (card) => `${card.root} ${card.id}`;
19
+ ctx.on('devflow/card-created', (card) => {
20
+ if (card.stage !== 'draft' || card.stageRevision !== 1) {
21
+ fail(`devflow/card-created for card ${card.id} reports "${card.stage}" at rev ${card.stageRevision}; a card must enter the board as "draft" at revision 1`);
22
+ }
23
+ if (lastRevision.has(key(card))) {
24
+ fail(`devflow/card-created repeats card ${card.id} of root ${card.root}; an id is never reissued`);
25
+ }
26
+ if (card.parent !== undefined) {
27
+ if (children.has(`${card.root} ${card.parent}`)) {
28
+ fail(`devflow/card-created hangs card ${card.id} under ${card.parent}, which is itself a child; the breakdown is one level deep`);
29
+ }
30
+ children.add(key(card));
31
+ }
32
+ lastRevision.set(key(card), card.stageRevision);
33
+ }, { global: true });
34
+ ctx.on('devflow/stage-changed', (card, from) => {
35
+ if (card.stage === from) {
36
+ fail(`devflow/stage-changed for card ${card.id} reports no move (still at "${from}")`);
37
+ }
38
+ const previous = lastRevision.get(key(card));
39
+ if (previous !== undefined && card.stageRevision <= previous) {
40
+ fail(`devflow/stage-changed for card ${card.id} carries rev ${card.stageRevision} after rev ${previous}; revisions must strictly increase`);
41
+ }
42
+ lastRevision.set(key(card), card.stageRevision);
43
+ }, { global: true });
44
+ };
45
+ /**
46
+ * Register this package's invariant companion.
47
+ * @param ctx - Cordis context carrying the invariant service.
48
+ * @returns the installed registration's disposer after setup succeeds.
49
+ */
50
+ export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
51
+ //# sourceMappingURL=invariant.js.map
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Journal decoding and replay for the devflow seam. The journal is the
3
+ * authoritative card history; the card file's frontmatter is a rebuildable
4
+ * projection. Both the filesystem provider and the invariant companion fold
5
+ * entries through this module so every consumer derives identical state.
6
+ * @module @zhchxiao123/dsh-devflow/src/journal
7
+ */
8
+ import { DevflowCardId } from './stages.ts';
9
+ import type { CardLocation, DevStage, DevflowJournalEntry } from './types.ts';
10
+ /** Card state derived by {@link foldJournal}; the read-side authority. */
11
+ export interface JournalFoldState {
12
+ /** Current location after the last entry. */
13
+ stage: CardLocation;
14
+ /** Revision of the last entry; the optimistic-concurrency token. */
15
+ revision: number;
16
+ /** The stage a blocked card returns to; present exactly while `stage` is `blocked`. */
17
+ blockedFrom?: DevStage;
18
+ /** The card this one decomposes, from the `created` entry; absent for a top-level card. */
19
+ parent?: DevflowCardId;
20
+ /** Artifact paths in registration order. */
21
+ artifacts: string[];
22
+ }
23
+ /**
24
+ * Decode one parsed journal value into a {@link DevflowJournalEntry}.
25
+ *
26
+ * This is the durable-boundary validator: journal lines come from a file that
27
+ * humans and other processes may write, so every field is checked and a bad
28
+ * entry throws instead of being skipped.
29
+ * @param value - one JSON-parsed journal line.
30
+ * @returns the validated entry.
31
+ * @throws {Error} naming the first violated field.
32
+ */
33
+ export declare function decodeJournalEntry(value: unknown): DevflowJournalEntry;
34
+ /**
35
+ * Replay a complete journal into the card's current state.
36
+ *
37
+ * Validates the structural invariants of the durable stream: revisions are the
38
+ * contiguous sequence 1..n, the first entry is `created`, every transition
39
+ * departs from the current location, a move to `blocked` remembers its origin,
40
+ * and the matching recovery returns exactly there.
41
+ * @param entries - decoded entries in file order.
42
+ * @returns the folded card state.
43
+ * @throws {Error} naming the first violated invariant and its entry revision.
44
+ */
45
+ export declare function foldJournal(entries: readonly DevflowJournalEntry[]): JournalFoldState;
46
+ //# sourceMappingURL=journal.d.ts.map
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Journal decoding and replay for the devflow seam. The journal is the
3
+ * authoritative card history; the card file's frontmatter is a rebuildable
4
+ * projection. Both the filesystem provider and the invariant companion fold
5
+ * entries through this module so every consumer derives identical state.
6
+ * @module @zhchxiao123/dsh-devflow/src/journal
7
+ */
8
+ import { DEV_STAGES, DevflowCardId, isCardLocation, isDevStage } from "./stages.js";
9
+ /**
10
+ * Decode one parsed journal value into a {@link DevflowJournalEntry}.
11
+ *
12
+ * This is the durable-boundary validator: journal lines come from a file that
13
+ * humans and other processes may write, so every field is checked and a bad
14
+ * entry throws instead of being skipped.
15
+ * @param value - one JSON-parsed journal line.
16
+ * @returns the validated entry.
17
+ * @throws {Error} naming the first violated field.
18
+ */
19
+ export function decodeJournalEntry(value) {
20
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
21
+ throw new Error('journal entry must be a JSON object');
22
+ }
23
+ const entry = value;
24
+ const rev = entry.rev;
25
+ if (typeof rev !== 'number' || !Number.isInteger(rev) || rev < 1) {
26
+ throw new Error('journal entry field "rev" must be a positive integer');
27
+ }
28
+ if (typeof entry.at !== 'string' || entry.at.length === 0) {
29
+ throw new Error('journal entry field "at" must be a non-empty string');
30
+ }
31
+ switch (entry.type) {
32
+ case 'created':
33
+ return {
34
+ rev,
35
+ at: entry.at,
36
+ type: 'created',
37
+ by: decodeActor(entry.by),
38
+ ...decodeOptionalCardId(entry, 'parent'),
39
+ };
40
+ case 'transition': {
41
+ if (!isCardLocation(entry.from))
42
+ throw new Error('transition field "from" must be a stage or "blocked"');
43
+ if (!isCardLocation(entry.to))
44
+ throw new Error('transition field "to" must be a stage or "blocked"');
45
+ return {
46
+ rev,
47
+ at: entry.at,
48
+ type: 'transition',
49
+ from: entry.from,
50
+ to: entry.to,
51
+ ...entry.by !== undefined ? { by: decodeActor(entry.by) } : {},
52
+ ...decodeOptionalString(entry, 'reason'),
53
+ ...entry.gate !== undefined ? { gate: decodeGate(entry.gate) } : {},
54
+ };
55
+ }
56
+ case 'artifact': {
57
+ if (typeof entry.path !== 'string' || entry.path.length === 0) {
58
+ throw new Error('artifact field "path" must be a non-empty string');
59
+ }
60
+ if (!isDevStage(entry.stage)) {
61
+ throw new Error(`artifact field "stage" must be one of ${DEV_STAGES.join(', ')}`);
62
+ }
63
+ return {
64
+ rev,
65
+ at: entry.at,
66
+ type: 'artifact',
67
+ path: entry.path,
68
+ stage: entry.stage,
69
+ ...entry.by !== undefined ? { by: decodeActor(entry.by) } : {},
70
+ };
71
+ }
72
+ case 'claim-expired': {
73
+ if (entry.previousOwner === undefined) {
74
+ throw new Error('claim-expired field "previousOwner" is required');
75
+ }
76
+ return {
77
+ rev,
78
+ at: entry.at,
79
+ type: 'claim-expired',
80
+ previousOwner: decodeActor(entry.previousOwner),
81
+ by: decodeActor(entry.by),
82
+ };
83
+ }
84
+ default:
85
+ throw new Error(`journal entry field "type" must be created, transition, artifact, or claim-expired (got ${JSON.stringify(entry.type)})`);
86
+ }
87
+ }
88
+ /**
89
+ * Replay a complete journal into the card's current state.
90
+ *
91
+ * Validates the structural invariants of the durable stream: revisions are the
92
+ * contiguous sequence 1..n, the first entry is `created`, every transition
93
+ * departs from the current location, a move to `blocked` remembers its origin,
94
+ * and the matching recovery returns exactly there.
95
+ * @param entries - decoded entries in file order.
96
+ * @returns the folded card state.
97
+ * @throws {Error} naming the first violated invariant and its entry revision.
98
+ */
99
+ export function foldJournal(entries) {
100
+ if (entries.length === 0)
101
+ throw new Error('journal is empty; every card starts with a "created" entry');
102
+ const state = { stage: 'draft', revision: 0, artifacts: [] };
103
+ for (const [index, entry] of entries.entries()) {
104
+ if (entry.rev !== index + 1) {
105
+ throw new Error(`journal entry ${index + 1} carries rev ${entry.rev}; revisions must be contiguous from 1`);
106
+ }
107
+ if (index === 0) {
108
+ if (entry.type !== 'created')
109
+ throw new Error('journal entry 1 must be "created"');
110
+ if (entry.parent !== undefined)
111
+ state.parent = entry.parent;
112
+ state.revision = entry.rev;
113
+ continue;
114
+ }
115
+ switch (entry.type) {
116
+ case 'created':
117
+ throw new Error(`journal entry rev ${entry.rev} repeats "created"`);
118
+ case 'transition': {
119
+ if (entry.from !== state.stage) {
120
+ throw new Error(`transition rev ${entry.rev} departs from "${entry.from}" but the card is at "${state.stage}"`);
121
+ }
122
+ if (entry.to === state.stage) {
123
+ throw new Error(`transition rev ${entry.rev} does not move the card (already at "${entry.to}")`);
124
+ }
125
+ if (entry.to === 'blocked') {
126
+ // `from` is a stage here: the departure check above matched the
127
+ // current location, and a blocked card cannot block again.
128
+ state.blockedFrom = entry.from;
129
+ }
130
+ else if (state.stage === 'blocked') {
131
+ if (entry.to !== state.blockedFrom) {
132
+ throw new Error(`transition rev ${entry.rev} recovers to "${entry.to}" but the card blocked from "${state.blockedFrom}"`);
133
+ }
134
+ delete state.blockedFrom;
135
+ }
136
+ state.stage = entry.to;
137
+ state.revision = entry.rev;
138
+ break;
139
+ }
140
+ case 'artifact':
141
+ state.artifacts.push(entry.path);
142
+ state.revision = entry.rev;
143
+ break;
144
+ case 'claim-expired':
145
+ state.revision = entry.rev;
146
+ break;
147
+ }
148
+ }
149
+ return state;
150
+ }
151
+ function decodeGate(value) {
152
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
153
+ throw new Error('transition field "gate" must be a JSON object');
154
+ }
155
+ const gate = value;
156
+ if (gate.approvedBy === undefined) {
157
+ throw new Error('transition field "gate" requires "approvedBy"');
158
+ }
159
+ return { approvedBy: decodeActor(gate.approvedBy) };
160
+ }
161
+ function decodeActor(value) {
162
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
163
+ throw new Error('actor must be a JSON object');
164
+ }
165
+ const actor = value;
166
+ switch (actor.kind) {
167
+ case 'human':
168
+ return { kind: 'human', ...decodeOptionalString(actor, 'name') };
169
+ case 'agent':
170
+ return { kind: 'agent', ...decodeOptionalString(actor, 'session') };
171
+ case 'command':
172
+ return { kind: 'command', ...decodeOptionalString(actor, 'name') };
173
+ default:
174
+ throw new Error(`actor field "kind" must be human, agent, or command (got ${JSON.stringify(actor.kind)})`);
175
+ }
176
+ }
177
+ function decodeOptionalCardId(record, key) {
178
+ const value = record[key];
179
+ if (value === undefined)
180
+ return {};
181
+ if (typeof value !== 'string' || value.length === 0) {
182
+ throw new Error(`field "${key}" must be a non-empty card id when present`);
183
+ }
184
+ return { [key]: DevflowCardId(value) };
185
+ }
186
+ function decodeOptionalString(record, key) {
187
+ const value = record[key];
188
+ if (value === undefined)
189
+ return {};
190
+ if (typeof value !== 'string' || value.length === 0) {
191
+ throw new Error(`field "${key}" must be a non-empty string when present`);
192
+ }
193
+ return { [key]: value };
194
+ }
195
+ //# sourceMappingURL=journal.js.map
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Runtime stage vocabulary: the ordered stage list, location narrowing, and the
3
+ * card-id factory. Kept beside the type-only module so `types.ts` stays free of
4
+ * runtime code.
5
+ * @module @zhchxiao123/dsh-devflow/src/stages
6
+ */
7
+ import type { Branded } from '@deepseek-ai/dsh-brand';
8
+ import type { CardLocation, DevStage } from './types.ts';
9
+ /** Opaque id of one task card; equals the card's directory name and never changes. */
10
+ export type DevflowCardId = Branded<'DevflowCardId'>;
11
+ /** The pipeline stages in flow order; `blocked` is a bypass, not a member. */
12
+ export declare const DEV_STAGES: readonly ["draft", "designing", "ready", "developing", "reviewing", "testing", "done"];
13
+ /**
14
+ * Narrow an unknown value to a pipeline stage.
15
+ * @param value - the candidate value.
16
+ * @returns `true` when `value` is one of {@link DEV_STAGES}.
17
+ */
18
+ export declare function isDevStage(value: unknown): value is DevStage;
19
+ /**
20
+ * Narrow an unknown value to a card location (a stage or `blocked`).
21
+ * @param value - the candidate value.
22
+ * @returns `true` when `value` is a stage or the `blocked` bypass.
23
+ */
24
+ export declare function isCardLocation(value: unknown): value is CardLocation;
25
+ /**
26
+ * Brand a raw string as a {@link DevflowCardId}. The id equals the card's
27
+ * directory name; construction lives here because this package owns the brand.
28
+ * @param value - the card directory name.
29
+ * @returns the branded id.
30
+ */
31
+ export declare function DevflowCardId(value: string): DevflowCardId;
32
+ /**
33
+ * Whether one stage move is a legal edge of the state machine.
34
+ *
35
+ * 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.
38
+ * @param from - the card's current location.
39
+ * @param to - the requested target location.
40
+ * @param blockedFrom - the remembered origin stage while `from` is `blocked`.
41
+ * @returns `true` when the move is a legal edge.
42
+ */
43
+ export declare function isLegalTransition(from: CardLocation, to: CardLocation, blockedFrom?: DevStage): boolean;
44
+ /**
45
+ * 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.
47
+ * @param from - the departing location.
48
+ * @param to - the target location.
49
+ * @returns `true` for `reviewing -> developing` and `testing -> developing`.
50
+ */
51
+ export declare function isReworkEdge(from: CardLocation, to: CardLocation): boolean;
52
+ //# sourceMappingURL=stages.d.ts.map