@mostajs/statemachine 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ @mostajs/statemachine
2
+ Copyright (C) 2026 Dr Hamid MADANI <drmdh@msn.com>
3
+
4
+ This program is free software: you can redistribute it and/or modify it under
5
+ the terms of the GNU Affero General Public License as published by the Free
6
+ Software Foundation, either version 3 of the License, or (at your option) any
7
+ later version.
8
+
9
+ This program is distributed in the hope that it will be useful, but WITHOUT
10
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
+ FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
12
+ details.
13
+
14
+ You should have received a copy of the GNU Affero General Public License
15
+ along with this program. If not, see <https://www.gnu.org/licenses/agpl-3.0.html>.
package/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # @mostajs/statemachine
2
+
3
+ > Machine à états finie **pure** — transitions de configuration (`from[]→to` + permission + **garde injectée**) et **historique immuable** des transitions exécutées (via `AuditSink` injecté). La brique **STATE** du triptyque **State · Rule · Trigger** (modèle Drupal *Workflow*).
4
+
5
+ **Auteur** : Dr Hamid MADANI <drmdh@msn.com> · **Licence** : AGPL-3.0-or-later
6
+
7
+ Primitive de l'écosystème `@mostajs/*`. Reste **pure** : la garde (→ `@mostajs/rules`), la permission (→ `@mostajs/rbac`) et le journal (→ `@mostajs/audit`) sont **injectés**. La stack `@mostajs/workflow` les assemble.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm i @mostajs/statemachine
13
+ ```
14
+
15
+ Le cœur n'a **aucune dépendance**. Les adaptateurs réels (`@mostajs/audit`, `@mostajs/rbac`) arrivent en v0.1.
16
+
17
+ ## Exemple (cycle de commande)
18
+
19
+ ```ts
20
+ import { defineMachine } from "@mostajs/statemachine";
21
+
22
+ const m = defineMachine({
23
+ states: [
24
+ { id: "nouvelle", initial: true },
25
+ { id: "en_cours" },
26
+ { id: "validee" },
27
+ { id: "terminee", terminal: true },
28
+ { id: "annulee", terminal: true },
29
+ ],
30
+ transitions: [
31
+ { id: "start", from: ["nouvelle"], to: "en_cours" },
32
+ { id: "valid", from: ["en_cours"], to: "validee", guard: (c) => c.data?.ok === true },
33
+ { id: "finish", from: ["validee"], to: "terminee" },
34
+ { id: "cancel", from: ["nouvelle", "en_cours", "validee"], to: "annulee", permission: "crm.order.transition.cancel" },
35
+ ],
36
+ });
37
+
38
+ const ref = { entityType: "order", entityId: "42" };
39
+ await m.transition(ref, "nouvelle", "en_cours", { actorId: "u1", comment: "prise en charge" });
40
+ await m.canTransition("en_cours", "validee", { data: { ok: false } }); // { allowed:false, reason:"guard" }
41
+ await m.history(ref); // historique immuable (qui / quand / from→to / comment)
42
+ ```
43
+
44
+ ## Modèle (distinction Drupal config / exécution)
45
+
46
+ | Concept | Type |
47
+ |---|---|
48
+ | chemin autorisé | `ConfigTransition { id, from: string[], to, permission?, guard? }` |
49
+ | transition exécutée (immuable) | `ExecutedTransition { entityType, entityId, fromState, toState, actorId?, timestamp, comment? }` |
50
+ | journal append-only | `AuditSink { record, list }` |
51
+
52
+ ## Injections (composition)
53
+
54
+ | Capacité | Injection | Module cible |
55
+ |---|---|---|
56
+ | historique immuable | `audit` | `@mostajs/audit` |
57
+ | permission par transition | `checkPermission` | `@mostajs/rbac` |
58
+ | garde déclarative | `guard` (par transition) | `@mostajs/rules` |
59
+ | date | `now` | testabilité |
60
+
61
+ ## API
62
+
63
+ `defineMachine(def, opts?)` → `Machine` · `m.availableTransitions(current, input?)` · `m.canTransition(from, to, input?)` → `{ allowed, reason? }` · `m.transition(ref, from, to, input?)` → `ExecutedTransition` · `m.history(ref)` · `m.onPre/onPost`. `input = { actorId?, data?, comment? }`.
64
+
65
+ ## Statut
66
+
67
+ **v0.0.1** — cœur **implémenté et testé** (`defineMachine`, `can/transition`, gardes, `MemoryAuditSink`, hooks `pre/post`). Feuille de route : `docs/03-PLAN-DEV-STATEMACHINE.md` (0.1 adaptateurs `audit`/`rbac`, 1.0 = 14 livrables). Étude : `docs/01-ETUDE-ETAT-ART-STATEMACHINE-07062026.md`.
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @mostajs/statemachine — journal d'exécution en mémoire (défaut / tests).
3
+ *
4
+ * Append-only et immuable : aucune API d'édition/suppression n'est exposée ;
5
+ * `list` retourne des copies. En production, brancher un adaptateur sur
6
+ * @mostajs/audit (et **ne jamais** câbler sa purge `deleteOlderThan`).
7
+ *
8
+ * @author Dr Hamid MADANI <drmdh@msn.com>
9
+ * @license AGPL-3.0-or-later
10
+ */
11
+ import { type AuditSink, type ExecutedTransition, type EntityRef } from "./types.js";
12
+ export declare class MemoryAuditSink implements AuditSink {
13
+ private readonly log;
14
+ record(transition: ExecutedTransition): Promise<void>;
15
+ list(ref: EntityRef): Promise<ExecutedTransition[]>;
16
+ }
17
+ //# sourceMappingURL=audit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,kBAAkB,EAAE,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AAErF,qBAAa,eAAgB,YAAW,SAAS;IAC/C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA4B;IAE1C,MAAM,CAAC,UAAU,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAIrD,IAAI,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC;CAK1D"}
package/dist/audit.js ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @mostajs/statemachine — journal d'exécution en mémoire (défaut / tests).
3
+ *
4
+ * Append-only et immuable : aucune API d'édition/suppression n'est exposée ;
5
+ * `list` retourne des copies. En production, brancher un adaptateur sur
6
+ * @mostajs/audit (et **ne jamais** câbler sa purge `deleteOlderThan`).
7
+ *
8
+ * @author Dr Hamid MADANI <drmdh@msn.com>
9
+ * @license AGPL-3.0-or-later
10
+ */
11
+ export class MemoryAuditSink {
12
+ log = [];
13
+ async record(transition) {
14
+ this.log.push({ ...transition });
15
+ }
16
+ async list(ref) {
17
+ return this.log
18
+ .filter((e) => e.entityType === ref.entityType && e.entityId === ref.entityId)
19
+ .map((e) => ({ ...e }));
20
+ }
21
+ }
22
+ //# sourceMappingURL=audit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,MAAM,OAAO,eAAe;IACT,GAAG,GAAyB,EAAE,CAAC;IAEhD,KAAK,CAAC,MAAM,CAAC,UAA8B;QACzC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,UAAU,EAAE,CAAC,CAAC;IACnC,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,GAAc;QACvB,OAAO,IAAI,CAAC,GAAG;aACZ,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,GAAG,CAAC,UAAU,IAAI,CAAC,CAAC,QAAQ,KAAK,GAAG,CAAC,QAAQ,CAAC;aAC7E,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IAC5B,CAAC;CACF"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @mostajs/statemachine — point d'entrée public.
3
+ *
4
+ * Machine à états finie pure (STATE du triptyque State·Rule·Trigger). Transitions
5
+ * de config (from[]→to + permission + garde injectée), historique immuable délégué
6
+ * à un AuditSink. Assemblée par la stack @mostajs/workflow.
7
+ *
8
+ * @author Dr Hamid MADANI <drmdh@msn.com>
9
+ * @license AGPL-3.0-or-later
10
+ */
11
+ export { type State, type EntityRef, type GuardContext, type Guard, type ConfigTransition, type ExecutedTransition, type AuditSink, type PermissionCheck, type MachineDef, type DenyReason, type CanResult, StateMachineError, UnknownStateError, InvalidTransitionError, TerminalStateError, GuardDeniedError, PermissionDeniedError, } from "./types.js";
12
+ export { MemoryAuditSink } from "./audit.js";
13
+ export { Machine, defineMachine, type MachineOptions, type TransitionInput, } from "./machine.js";
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EACL,KAAK,KAAK,EACV,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,KAAK,KAAK,EACV,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,SAAS,EACd,iBAAiB,EACjB,iBAAiB,EACjB,sBAAsB,EACtB,kBAAkB,EAClB,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EACL,OAAO,EACP,aAAa,EACb,KAAK,cAAc,EACnB,KAAK,eAAe,GACrB,MAAM,cAAc,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @mostajs/statemachine — point d'entrée public.
3
+ *
4
+ * Machine à états finie pure (STATE du triptyque State·Rule·Trigger). Transitions
5
+ * de config (from[]→to + permission + garde injectée), historique immuable délégué
6
+ * à un AuditSink. Assemblée par la stack @mostajs/workflow.
7
+ *
8
+ * @author Dr Hamid MADANI <drmdh@msn.com>
9
+ * @license AGPL-3.0-or-later
10
+ */
11
+ export { StateMachineError, UnknownStateError, InvalidTransitionError, TerminalStateError, GuardDeniedError, PermissionDeniedError, } from "./types.js";
12
+ export { MemoryAuditSink } from "./audit.js";
13
+ export { Machine, defineMachine, } from "./machine.js";
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAYL,iBAAiB,EACjB,iBAAiB,EACjB,sBAAsB,EACtB,kBAAkB,EAClB,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EACL,OAAO,EACP,aAAa,GAGd,MAAM,cAAc,CAAC"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @mostajs/statemachine — façade `Machine` + `defineMachine`.
3
+ *
4
+ * FSM pure : garde et permission **injectées**, historique **délégué** à un
5
+ * `AuditSink`. Aucune dépendance dure à @mostajs/rules ni @mostajs/trigger —
6
+ * c'est la stack @mostajs/workflow qui les branche.
7
+ *
8
+ * @author Dr Hamid MADANI <drmdh@msn.com>
9
+ * @license AGPL-3.0-or-later
10
+ */
11
+ import { type MachineDef, type State, type ConfigTransition, type ExecutedTransition, type EntityRef, type AuditSink, type PermissionCheck, type CanResult } from "./types.js";
12
+ export interface MachineOptions {
13
+ /** Journal d'exécution (défaut : `MemoryAuditSink`). */
14
+ audit?: AuditSink;
15
+ /** Vérificateur de permission (optionnel ; sans lui, `permission` reste déclaratif). */
16
+ checkPermission?: PermissionCheck;
17
+ /** Horloge injectée (testabilité ; défaut : `() => new Date()`). */
18
+ now?: () => Date;
19
+ }
20
+ export interface TransitionInput {
21
+ actorId?: string;
22
+ data?: Record<string, unknown>;
23
+ comment?: string;
24
+ }
25
+ type Hook = (t: ExecutedTransition) => void | Promise<void>;
26
+ export declare class Machine {
27
+ readonly states: ReadonlyMap<string, State>;
28
+ readonly initial: string;
29
+ private readonly transitions;
30
+ private readonly audit;
31
+ private readonly checkPermission?;
32
+ private readonly now;
33
+ private readonly preHooks;
34
+ private readonly postHooks;
35
+ constructor(def: MachineDef, opts?: MachineOptions);
36
+ private isTerminal;
37
+ private find;
38
+ /** Transitions praticables depuis `current` (chemin + permission + garde évalués). */
39
+ availableTransitions(current: string, input?: TransitionInput): Promise<ConfigTransition[]>;
40
+ /** Indique si une transition `from→to` est autorisée et, sinon, pourquoi. */
41
+ canTransition(from: string, to: string, input?: TransitionInput): Promise<CanResult>;
42
+ /** Exécute une transition : valide, émet `pre`, journalise (immuable), émet `post`. */
43
+ transition(ref: EntityRef, from: string, to: string, input?: TransitionInput): Promise<ExecutedTransition>;
44
+ /** Historique chronologique immuable de l'entité (ordre d'insertion). */
45
+ history(ref: EntityRef): Promise<ExecutedTransition[]>;
46
+ onPre(fn: Hook): this;
47
+ onPost(fn: Hook): this;
48
+ private denyError;
49
+ }
50
+ /** Définit (et valide) une machine à états. */
51
+ export declare function defineMachine(def: MachineDef, opts?: MachineOptions): Machine;
52
+ export {};
53
+ //# sourceMappingURL=machine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"machine.d.ts","sourceRoot":"","sources":["../src/machine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EACL,KAAK,UAAU,EACf,KAAK,KAAK,EACV,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,SAAS,EAEd,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,KAAK,SAAS,EAOf,MAAM,YAAY,CAAC;AAGpB,MAAM,WAAW,cAAc;IAC7B,wDAAwD;IACxD,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,wFAAwF;IACxF,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,oEAAoE;IACpE,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,KAAK,IAAI,GAAG,CAAC,CAAC,EAAE,kBAAkB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE5D,qBAAa,OAAO;IAClB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC5C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqB;IACjD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAY;IAClC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAkB;IACnD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAa;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAc;IACvC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAc;gBAE5B,GAAG,EAAE,UAAU,EAAE,IAAI,GAAE,cAAmB;IAgCtD,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,IAAI;IAIZ,sFAAsF;IAChF,oBAAoB,CACxB,OAAO,EAAE,MAAM,EACf,KAAK,GAAE,eAAoB,GAC1B,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAY9B,6EAA6E;IACvE,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,GAAE,eAAoB,GAAG,OAAO,CAAC,SAAS,CAAC;IAoB9F,uFAAuF;IACjF,UAAU,CACd,GAAG,EAAE,SAAS,EACd,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EACV,KAAK,GAAE,eAAoB,GAC1B,OAAO,CAAC,kBAAkB,CAAC;IAsB9B,yEAAyE;IACzE,OAAO,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC;IAItD,KAAK,CAAC,EAAE,EAAE,IAAI,GAAG,IAAI;IAIrB,MAAM,CAAC,EAAE,EAAE,IAAI,GAAG,IAAI;IAKtB,OAAO,CAAC,SAAS;CAgBlB;AAED,+CAA+C;AAC/C,wBAAgB,aAAa,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,cAAc,GAAG,OAAO,CAE7E"}
@@ -0,0 +1,157 @@
1
+ /**
2
+ * @mostajs/statemachine — façade `Machine` + `defineMachine`.
3
+ *
4
+ * FSM pure : garde et permission **injectées**, historique **délégué** à un
5
+ * `AuditSink`. Aucune dépendance dure à @mostajs/rules ni @mostajs/trigger —
6
+ * c'est la stack @mostajs/workflow qui les branche.
7
+ *
8
+ * @author Dr Hamid MADANI <drmdh@msn.com>
9
+ * @license AGPL-3.0-or-later
10
+ */
11
+ import { StateMachineError, UnknownStateError, InvalidTransitionError, TerminalStateError, GuardDeniedError, PermissionDeniedError, } from "./types.js";
12
+ import { MemoryAuditSink } from "./audit.js";
13
+ export class Machine {
14
+ states;
15
+ initial;
16
+ transitions;
17
+ audit;
18
+ checkPermission;
19
+ now;
20
+ preHooks = [];
21
+ postHooks = [];
22
+ constructor(def, opts = {}) {
23
+ if (!def.states?.length)
24
+ throw new StateMachineError("Machine sans état.");
25
+ const states = new Map();
26
+ for (const s of def.states) {
27
+ if (states.has(s.id))
28
+ throw new StateMachineError(`État dupliqué : "${s.id}"`);
29
+ states.set(s.id, s);
30
+ }
31
+ const initials = def.states.filter((s) => s.initial);
32
+ if (initials.length !== 1) {
33
+ throw new StateMachineError(`Exactement un état initial requis (trouvé : ${initials.length}).`);
34
+ }
35
+ // Validation des transitions : états connus + dédup (from,to).
36
+ const seen = new Set();
37
+ for (const t of def.transitions) {
38
+ if (!states.has(t.to))
39
+ throw new UnknownStateError(`Transition "${t.id}" : état cible inconnu "${t.to}".`);
40
+ for (const f of t.from) {
41
+ if (!states.has(f))
42
+ throw new UnknownStateError(`Transition "${t.id}" : état source inconnu "${f}".`);
43
+ const key = `${f}->${t.to}`;
44
+ if (seen.has(key))
45
+ throw new InvalidTransitionError(`Transition dupliquée : ${key}`);
46
+ seen.add(key);
47
+ }
48
+ }
49
+ this.states = states;
50
+ this.initial = initials[0].id;
51
+ this.transitions = def.transitions;
52
+ this.audit = opts.audit ?? new MemoryAuditSink();
53
+ this.checkPermission = opts.checkPermission;
54
+ this.now = opts.now ?? (() => new Date());
55
+ }
56
+ isTerminal(id) {
57
+ return this.states.get(id)?.terminal === true;
58
+ }
59
+ find(from, to) {
60
+ return this.transitions.find((t) => t.to === to && t.from.includes(from));
61
+ }
62
+ /** Transitions praticables depuis `current` (chemin + permission + garde évalués). */
63
+ async availableTransitions(current, input = {}) {
64
+ if (!this.states.has(current))
65
+ throw new UnknownStateError(`État inconnu : "${current}".`);
66
+ if (this.isTerminal(current))
67
+ return [];
68
+ const out = [];
69
+ for (const t of this.transitions) {
70
+ if (!t.from.includes(current))
71
+ continue;
72
+ const res = await this.canTransition(current, t.to, input);
73
+ if (res.allowed)
74
+ out.push(t);
75
+ }
76
+ return out;
77
+ }
78
+ /** Indique si une transition `from→to` est autorisée et, sinon, pourquoi. */
79
+ async canTransition(from, to, input = {}) {
80
+ if (!this.states.has(from) || !this.states.has(to)) {
81
+ return { allowed: false, reason: "unknown-state" };
82
+ }
83
+ if (this.isTerminal(from))
84
+ return { allowed: false, reason: "terminal" };
85
+ const t = this.find(from, to);
86
+ if (!t)
87
+ return { allowed: false, reason: "no-path" };
88
+ const ctx = { from, to, actorId: input.actorId, data: input.data };
89
+ if (t.permission && this.checkPermission) {
90
+ const ok = await this.checkPermission(input.actorId, t.permission, ctx);
91
+ if (!ok)
92
+ return { allowed: false, reason: "permission" };
93
+ }
94
+ if (t.guard) {
95
+ const ok = await t.guard(ctx); // deny-by-default : falsy => refus
96
+ if (!ok)
97
+ return { allowed: false, reason: "guard" };
98
+ }
99
+ return { allowed: true };
100
+ }
101
+ /** Exécute une transition : valide, émet `pre`, journalise (immuable), émet `post`. */
102
+ async transition(ref, from, to, input = {}) {
103
+ const res = await this.canTransition(from, to, input);
104
+ if (!res.allowed)
105
+ throw this.denyError(res.reason, from, to);
106
+ const t = this.find(from, to);
107
+ const executed = {
108
+ entityType: ref.entityType,
109
+ entityId: ref.entityId,
110
+ fromState: from,
111
+ toState: to,
112
+ actorId: input.actorId,
113
+ timestamp: this.now().toISOString(),
114
+ comment: input.comment,
115
+ transitionId: t.id,
116
+ };
117
+ for (const h of this.preHooks)
118
+ await h(executed); // un hook peut lever pour vétoer
119
+ await this.audit.record(executed);
120
+ for (const h of this.postHooks)
121
+ await h(executed);
122
+ return executed;
123
+ }
124
+ /** Historique chronologique immuable de l'entité (ordre d'insertion). */
125
+ history(ref) {
126
+ return this.audit.list(ref);
127
+ }
128
+ onPre(fn) {
129
+ this.preHooks.push(fn);
130
+ return this;
131
+ }
132
+ onPost(fn) {
133
+ this.postHooks.push(fn);
134
+ return this;
135
+ }
136
+ denyError(reason, from, to) {
137
+ const path = `${from} → ${to}`;
138
+ switch (reason) {
139
+ case "terminal":
140
+ return new TerminalStateError(`État terminal : aucune transition depuis "${from}".`);
141
+ case "permission":
142
+ return new PermissionDeniedError(`Permission refusée pour ${path}.`);
143
+ case "guard":
144
+ return new GuardDeniedError(`Garde refusée pour ${path}.`);
145
+ case "unknown-state":
146
+ return new UnknownStateError(`État inconnu dans ${path}.`);
147
+ case "no-path":
148
+ default:
149
+ return new InvalidTransitionError(`Transition non autorisée : ${path}.`);
150
+ }
151
+ }
152
+ }
153
+ /** Définit (et valide) une machine à états. */
154
+ export function defineMachine(def, opts) {
155
+ return new Machine(def, opts);
156
+ }
157
+ //# sourceMappingURL=machine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"machine.js","sourceRoot":"","sources":["../src/machine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * @mostajs/statemachine — types publics (modèle Drupal Workflow : config vs exécution).
3
+ *
4
+ * @author Dr Hamid MADANI <drmdh@msn.com>
5
+ * @license AGPL-3.0-or-later
6
+ */
7
+ /** Un état de la machine. `initial`/`terminal` marquent l'entrée et les puits ; `weight` ordonne ; `status=false` = désactivé. */
8
+ export interface State {
9
+ id: string;
10
+ label?: string;
11
+ weight?: number;
12
+ initial?: boolean;
13
+ terminal?: boolean;
14
+ status?: boolean;
15
+ /** Code couleur de présentation (badge/kanban) — métadonnée d'UI, ex. `"#3B82F6"`. */
16
+ color?: string;
17
+ /** Métadonnées de domaine libres (ex. `{ party: "sender" | "receiver", initiators: [...] }`). */
18
+ meta?: Record<string, unknown>;
19
+ }
20
+ /** Référence d'une entité métier suivie (ex. une commande). */
21
+ export interface EntityRef {
22
+ entityType: string;
23
+ entityId: string;
24
+ }
25
+ /** Contexte fourni aux gardes et au checker de permission. */
26
+ export interface GuardContext {
27
+ from: string;
28
+ to: string;
29
+ actorId?: string;
30
+ data?: Record<string, unknown>;
31
+ }
32
+ /** Garde de transition (prédicat **injecté** — typiquement délégué à @mostajs/rules). Falsy = refus. */
33
+ export type Guard = (ctx: GuardContext) => boolean | Promise<boolean>;
34
+ /**
35
+ * Transition de **configuration** (chemin autorisé). Modèle Drupal : plusieurs `from` → un `to`.
36
+ * `permission` = clé RBAC ; `guard` = prédicat injecté.
37
+ */
38
+ export interface ConfigTransition {
39
+ id: string;
40
+ from: string[];
41
+ to: string;
42
+ label?: string;
43
+ permission?: string;
44
+ guard?: Guard;
45
+ }
46
+ /** Transition **exécutée** (entrée d'historique immuable : qui / quand / ancien→nouveau / commentaire). */
47
+ export interface ExecutedTransition {
48
+ entityType: string;
49
+ entityId: string;
50
+ fromState: string;
51
+ toState: string;
52
+ actorId?: string;
53
+ timestamp: string;
54
+ comment?: string;
55
+ transitionId?: string;
56
+ }
57
+ /** Journal d'exécution **append-only** (immuable). En prod : adaptateur @mostajs/audit. */
58
+ export interface AuditSink {
59
+ record(transition: ExecutedTransition): Promise<void>;
60
+ list(ref: EntityRef): Promise<ExecutedTransition[]>;
61
+ }
62
+ /** Vérificateur de permission **injecté** (typiquement @mostajs/rbac). */
63
+ export type PermissionCheck = (actorId: string | undefined, permission: string, ctx: GuardContext) => boolean | Promise<boolean>;
64
+ /** Définition d'une machine : ses états et ses transitions de configuration. */
65
+ export interface MachineDef {
66
+ states: State[];
67
+ transitions: ConfigTransition[];
68
+ }
69
+ /** Raison d'un refus de transition. */
70
+ export type DenyReason = "unknown-state" | "no-path" | "terminal" | "permission" | "guard";
71
+ export interface CanResult {
72
+ allowed: boolean;
73
+ reason?: DenyReason;
74
+ }
75
+ export declare class StateMachineError extends Error {
76
+ constructor(message: string);
77
+ }
78
+ export declare class UnknownStateError extends StateMachineError {
79
+ }
80
+ export declare class InvalidTransitionError extends StateMachineError {
81
+ }
82
+ export declare class TerminalStateError extends StateMachineError {
83
+ }
84
+ export declare class GuardDeniedError extends StateMachineError {
85
+ }
86
+ export declare class PermissionDeniedError extends StateMachineError {
87
+ }
88
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,kIAAkI;AAClI,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,sFAAsF;IACtF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iGAAiG;IACjG,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,+DAA+D;AAC/D,MAAM,WAAW,SAAS;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,8DAA8D;AAC9D,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,wGAAwG;AACxG,MAAM,MAAM,KAAK,GAAG,CAAC,GAAG,EAAE,YAAY,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEtE;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAED,2GAA2G;AAC3G,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,2FAA2F;AAC3F,MAAM,WAAW,SAAS;IACxB,MAAM,CAAC,UAAU,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtD,IAAI,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAAC;CACrD;AAED,0EAA0E;AAC1E,MAAM,MAAM,eAAe,GAAG,CAC5B,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,YAAY,KACd,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEhC,gFAAgF;AAChF,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,WAAW,EAAE,gBAAgB,EAAE,CAAC;CACjC;AAED,uCAAuC;AACvC,MAAM,MAAM,UAAU,GAClB,eAAe,GACf,SAAS,GACT,UAAU,GACV,YAAY,GACZ,OAAO,CAAC;AAEZ,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB;AAGD,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,OAAO,EAAE,MAAM;CAI5B;AACD,qBAAa,iBAAkB,SAAQ,iBAAiB;CAAG;AAC3D,qBAAa,sBAAuB,SAAQ,iBAAiB;CAAG;AAChE,qBAAa,kBAAmB,SAAQ,iBAAiB;CAAG;AAC5D,qBAAa,gBAAiB,SAAQ,iBAAiB;CAAG;AAC1D,qBAAa,qBAAsB,SAAQ,iBAAiB;CAAG"}
package/dist/types.js ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @mostajs/statemachine — types publics (modèle Drupal Workflow : config vs exécution).
3
+ *
4
+ * @author Dr Hamid MADANI <drmdh@msn.com>
5
+ * @license AGPL-3.0-or-later
6
+ */
7
+ // ─── Erreurs typées ──────────────────────────────────────────────────────────
8
+ export class StateMachineError extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = new.target.name;
12
+ }
13
+ }
14
+ export class UnknownStateError extends StateMachineError {
15
+ }
16
+ export class InvalidTransitionError extends StateMachineError {
17
+ }
18
+ export class TerminalStateError extends StateMachineError {
19
+ }
20
+ export class GuardDeniedError extends StateMachineError {
21
+ }
22
+ export class PermissionDeniedError extends StateMachineError {
23
+ }
24
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA0FH,gFAAgF;AAChF,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IAC1C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;IAC9B,CAAC;CACF;AACD,MAAM,OAAO,iBAAkB,SAAQ,iBAAiB;CAAG;AAC3D,MAAM,OAAO,sBAAuB,SAAQ,iBAAiB;CAAG;AAChE,MAAM,OAAO,kBAAmB,SAAQ,iBAAiB;CAAG;AAC5D,MAAM,OAAO,gBAAiB,SAAQ,iBAAiB;CAAG;AAC1D,MAAM,OAAO,qBAAsB,SAAQ,iBAAiB;CAAG"}
package/llms.txt ADDED
@@ -0,0 +1,77 @@
1
+ # @mostajs/statemachine — fiche LLM
2
+
3
+ RÔLE
4
+ Machine à états finie PURE = la brique STATE du triptyque State·Rule·Trigger
5
+ (inspiré du module Drupal Workflow). Définit états + transitions de configuration
6
+ (from[]→to + permission + garde INJECTÉE), exécute une transition et JOURNALISE
7
+ immuablement (qui/quand/ancien→nouveau/commentaire) via un AuditSink INJECTÉ.
8
+ NE FAIT PAS : évaluer les règles (→ @mostajs/rules, garde injectée), événements/actions
9
+ (→ @mostajs/trigger), assemblage (→ stack @mostajs/workflow), persistance de l'état métier
10
+ (→ consommateur). Pure : aucune dépendance dure à rules/trigger. Responsabilité unique.
11
+
12
+ MODÈLE (Drupal Workflow : config vs exécution)
13
+ State { id, label?, weight?, initial?, terminal?, status? }
14
+ ConfigTransition { id, from: string[], to, label?, permission?, guard? } # plusieurs from -> un to
15
+ ExecutedTransition { entityType, entityId, fromState, toState, actorId?, timestamp, comment?, transitionId? } # immuable
16
+ AuditSink { record(t): Promise<void>; list(ref): Promise<ExecutedTransition[]> } # append-only
17
+ PermissionCheck(actorId, permission, ctx) -> boolean|Promise<boolean> # injecté (rbac)
18
+ Guard(ctx) -> boolean|Promise<boolean> # injecté (rules) ; falsy = refus
19
+ EntityRef { entityType, entityId }
20
+
21
+ INSTALL
22
+ npm i @mostajs/statemachine # cœur sans dépendance ; audit/permission/guard injectés
23
+
24
+ EXPORTS
25
+ defineMachine(def, opts?) -> Machine # opts: { audit?, checkPermission?, now? }
26
+ Machine :
27
+ states: ReadonlyMap<string,State> ; initial: string
28
+ availableTransitions(current, input?) -> Promise<ConfigTransition[]>
29
+ canTransition(from, to, input?) -> Promise<{ allowed, reason? }>
30
+ transition(ref, from, to, input?) -> Promise<ExecutedTransition> # émet pre -> record -> post
31
+ history(ref) -> Promise<ExecutedTransition[]>
32
+ onPre(fn) / onPost(fn)
33
+ MemoryAuditSink # défaut / tests (append-only, immuable)
34
+ Types + erreurs : StateMachineError, UnknownStateError, InvalidTransitionError,
35
+ TerminalStateError, GuardDeniedError, PermissionDeniedError
36
+ input (TransitionInput) = { actorId?, data?, comment? }
37
+ reason (DenyReason) = "unknown-state" | "no-path" | "terminal" | "permission" | "guard"
38
+
39
+ EXEMPLE (commande CRM, 8 états)
40
+ import { defineMachine } from "@mostajs/statemachine";
41
+ const m = defineMachine({
42
+ states: [
43
+ { id: "nouvelle", initial: true }, { id: "en_cours" }, { id: "validee" },
44
+ { id: "terminee", terminal: true }, { id: "annulee", terminal: true },
45
+ ],
46
+ transitions: [
47
+ { id: "start", from: ["nouvelle"], to: "en_cours" },
48
+ { id: "valid", from: ["en_cours"], to: "validee", guard: (c) => c.data?.ok === true },
49
+ { id: "finish", from: ["validee"], to: "terminee" },
50
+ { id: "cancel", from: ["nouvelle","en_cours","validee"], to: "annulee", permission: "crm.order.transition.cancel" },
51
+ ],
52
+ });
53
+ const ref = { entityType: "order", entityId: "42" };
54
+ await m.canTransition("nouvelle", "en_cours"); // { allowed: true }
55
+ await m.transition(ref, "nouvelle", "en_cours", { actorId: "u1" });
56
+ await m.canTransition("en_cours", "validee", { data: { ok: false } }); // { allowed:false, reason:"guard" }
57
+ await m.history(ref); // [ ExecutedTransition... ] immuable
58
+
59
+ COMPOSITION
60
+ - Historique : @mostajs/audit (adaptateur AuditSink en prod ; ne JAMAIS câbler deleteOlderThan).
61
+ - Permission par transition : @mostajs/rbac (PermissionCheck injecté).
62
+ - Garde déclarative : @mostajs/rules (guard injectée, par la stack workflow).
63
+ - Assemblage : @mostajs/workflow branche garde->rules, pre/post->trigger, historique->audit.
64
+
65
+ PIÈGES
66
+ - Toutes les opérations sont ASYNCHRONES.
67
+ - Garde deny-by-default : une garde renvoyant falsy refuse la transition (GuardDeniedError).
68
+ - États terminaux : aucune transition sortante (TerminalStateError).
69
+ - Exactement UN état initial requis ; transitions dédupliquées sur (from,to) à la définition.
70
+ - `permission` déclarée mais sans checkPermission injecté => NON enforcée (métadonnée) ; brancher rbac pour l'appliquer.
71
+ - Historique IMMUABLE : MemoryAuditSink est append-only, list() renvoie des copies ; ne pas exposer d'édition.
72
+ - Date injectée (now) pour la testabilité.
73
+
74
+ STATUT
75
+ v0.0.1 — cœur IMPLÉMENTÉ (defineMachine, can/transition, gardes, MemoryAuditSink, hooks), testé.
76
+ v0.1 = adaptateurs réels @mostajs/audit + @mostajs/rbac. Voir docs/03-PLAN-DEV-STATEMACHINE.md.
77
+ Licence AGPL-3.0-or-later. Auteur Dr Hamid MADANI.
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@mostajs/statemachine",
3
+ "version": "0.0.1",
4
+ "description": "Pure finite state machine — config transitions (from[]→to + permission + injected guard) and immutable executed-transition history (via injected AuditSink). The STATE primitive of the State·Rule·Trigger triptych, assembled by @mostajs/workflow.",
5
+ "author": "Dr Hamid MADANI <drmdh@msn.com>",
6
+ "license": "AGPL-3.0-or-later",
7
+ "type": "module",
8
+ "main": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "require": "./dist/index.js",
15
+ "default": "./dist/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "LICENSE",
21
+ "README.md",
22
+ "llms.txt"
23
+ ],
24
+ "keywords": [
25
+ "state-machine",
26
+ "fsm",
27
+ "workflow",
28
+ "transition",
29
+ "guard",
30
+ "audit-trail",
31
+ "immutable-history",
32
+ "mosta",
33
+ "mostajs"
34
+ ],
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/apolocine/mosta-statemachine"
38
+ },
39
+ "homepage": "https://mostajs.dev/packages/statemachine",
40
+ "bugs": {
41
+ "url": "https://github.com/apolocine/mosta-statemachine/issues"
42
+ },
43
+ "engines": {
44
+ "node": ">=18.0.0"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^22.0.0",
48
+ "typescript": "^5.6.0"
49
+ },
50
+ "scripts": {
51
+ "build": "tsc",
52
+ "dev": "tsc --watch",
53
+ "test": "node --test test-scripts/*.test.mjs"
54
+ }
55
+ }