ihsm 0.0.19 → 0.0.20
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.md +273 -132
- package/lib/cjs/index.d.ts +692 -40
- package/lib/cjs/index.js +193 -31
- package/lib/cjs/index.js.map +1 -1
- package/lib/esm/index.d.ts +692 -40
- package/lib/esm/index.js +193 -31
- package/lib/esm/index.js.map +1 -1
- package/package.json +13 -11
package/README.md
CHANGED
|
@@ -1,192 +1,333 @@
|
|
|
1
1
|
[](https://github.com/filasieno/ihsm/actions/workflows/ci.yml)
|
|
2
2
|
[](https://github.com/filasieno/ihsm/actions/workflows/docs.yml)
|
|
3
|
-
[](https://coveralls.io/github/filasieno/ihsm)
|
|
4
3
|
[](https://github.com/filasieno/ihsm/blob/HEAD/LICENSE)
|
|
5
4
|
[](https://www.npmjs.com/package/ihsm)
|
|
6
5
|
[](https://github.com/filasieno/ihsm/blob/HEAD/package.json)
|
|
7
6
|
|
|
8
7
|
# ihsm
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
**cached LCA transitions**, **zero production dependencies**, and **100% code coverage** on the runtime.
|
|
12
|
-
|
|
13
|
-
## Quality
|
|
14
|
-
|
|
15
|
-
| Metric | Value |
|
|
16
|
-
|--------|-------|
|
|
17
|
-
| **Statements** | 100% |
|
|
18
|
-
| **Branches** | 100% |
|
|
19
|
-
| **Functions** | 100% |
|
|
20
|
-
| **Lines** | 100% |
|
|
21
|
-
|
|
22
|
-
CI enforces full coverage on every push (`nix flake check`).
|
|
23
|
-
|
|
24
|
-
## Features
|
|
25
|
-
|
|
26
|
-
- User-defined event payloads (typed `Protocol`)
|
|
27
|
-
- User-defined state context (`ctx`)
|
|
28
|
-
- Hierarchically nested states (class inheritance)
|
|
29
|
-
- Orthogonal regions (nest multiple machines; compose via `post`/`call`)
|
|
30
|
-
- Internal transitions (handle event without calling `transition()`)
|
|
31
|
-
- Explicit transitions with cached entry/exit sequences
|
|
32
|
-
- Guards (inline `if` in handlers)
|
|
33
|
-
- History (`ctx` + `restore()`)
|
|
34
|
-
- Entry / exit actions (`onEntry`, `onExit`)
|
|
35
|
-
- Async and sync handlers
|
|
36
|
-
- **`call()` — typed request/response through the actor mailbox** (unique)
|
|
37
|
-
- **`then()` — decision pseudo-states with automatic follow-up transitions**
|
|
38
|
-
- **`postNow()` — hi-priority extended transitions within the same dispatch**
|
|
39
|
-
- Actor-style messaging (`post`, `deferredPost`, serialized queue)
|
|
40
|
-
- Structured errors and trace levels
|
|
9
|
+
**Class-based hierarchical state machines and actor mailboxes for TypeScript** — typed `post`/`call`, **zero** production dependencies, **~4.6 KB gzip** in the browser. → [Documentation](https://filasieno.github.io/ihsm/)
|
|
41
10
|
|
|
42
|
-
|
|
11
|
+
ihsm is state management and orchestration for backends, session actors, protocol handlers, and embedded tooling: states are **classes**, events are **methods**, hierarchy is **inheritance**, and each machine is an **actor** with a serialized mailbox and run-to-completion dispatch.
|
|
43
12
|
|
|
44
|
-
|
|
45
|
-
|----------|------|
|
|
46
|
-
| **Documentation site** | [filasieno.github.io/ihsm](https://filasieno.github.io/ihsm/) — reference manual + interactive tutorials |
|
|
47
|
-
| Reference (source) | [reference/REFERENCE.md](./reference/REFERENCE.md) |
|
|
48
|
-
| Tutorials (source) | [tutorials/](./tutorials/) |
|
|
49
|
-
| Examples | [`src/spec/`](./src/spec/) |
|
|
50
|
-
| Code of conduct | [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) |
|
|
51
|
-
| Security | [SECURITY.md](./SECURITY.md) |
|
|
52
|
-
|
|
53
|
-
Each tutorial page on the documentation site combines prose, code samples, and an embedded playground
|
|
54
|
-
(sender/message forms, live trace, reset). The same machines are verified headlessly by Mocha specs under
|
|
55
|
-
`tutorials/*/tutorial.spec.ts`.
|
|
13
|
+
Requires **Node.js 22+** (or a modern browser). Class names in traces and errors come from `Class.name` — no extra registration step in a typical npm/Node project.
|
|
56
14
|
|
|
57
|
-
|
|
15
|
+
It uses event-driven programming, class-based hierarchical statecharts, and the actor model to handle complex logic in predictable, robust ways. States are **classes**, events are **methods**, hierarchy is **inheritance**, and each machine is an **actor** with a serialized mailbox with RTC guarantees.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
📖 [Read the documentation](https://filasieno.github.io/ihsm/)
|
|
20
|
+
|
|
21
|
+
📑 [API reference](https://filasieno.github.io/ihsm/api)
|
|
22
|
+
|
|
23
|
+
📖 [Reference](https://filasieno.github.io/ihsm/reference)
|
|
24
|
+
|
|
25
|
+
💬 [Open an issue](https://github.com/filasieno/ihsm/issues)
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Super quick start
|
|
58
30
|
|
|
59
|
-
```
|
|
60
|
-
npm install ihsm
|
|
31
|
+
```bash
|
|
32
|
+
npm install ihsm
|
|
61
33
|
```
|
|
62
34
|
|
|
63
|
-
|
|
35
|
+
```ts
|
|
36
|
+
import { InitialState, makeHsm, TopState } from 'ihsm';
|
|
64
37
|
|
|
65
|
-
|
|
66
|
-
|
|
38
|
+
interface DoorCtx {
|
|
39
|
+
openCount: number;
|
|
40
|
+
}
|
|
67
41
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
42
|
+
// All possible signals are enumerated in a formal protocol
|
|
43
|
+
interface DoorProtocol {
|
|
44
|
+
open(): void;
|
|
45
|
+
close(): void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class DoorTop extends TopState<DoorCtx, DoorProtocol> {}
|
|
74
49
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
50
|
+
@InitialState
|
|
51
|
+
class Closed extends DoorTop {
|
|
52
|
+
open(): void {
|
|
53
|
+
this.ctx.openCount += 1;
|
|
54
|
+
this.transition(Open);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
78
57
|
|
|
79
|
-
|
|
58
|
+
class Open extends DoorTop {
|
|
59
|
+
close(): void {
|
|
60
|
+
this.transition(Closed);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
80
63
|
|
|
81
|
-
|
|
82
|
-
|
|
64
|
+
const door = makeHsm(DoorTop, { openCount: 0 });
|
|
65
|
+
await door.sync(); // wait for initialization
|
|
83
66
|
|
|
84
|
-
|
|
67
|
+
door.post('open');
|
|
68
|
+
await door.sync();
|
|
85
69
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
cd ihsm
|
|
70
|
+
console.log(door.currentStateName); // 'Open'
|
|
71
|
+
console.log(door.ctx.openCount); // 1
|
|
89
72
|
```
|
|
90
73
|
|
|
91
|
-
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Typed services with `call()`
|
|
77
|
+
|
|
78
|
+
Most state-machine libraries make you reach for snapshots, child actors, or ad hoc callbacks to ask the machine a question. ihsm treats **services** as ordinary protocol methods — the runtime injects `resolve` / `reject`, and the client gets a typed `Promise`.
|
|
92
79
|
|
|
93
|
-
|
|
94
|
-
PlantUML, Graphviz, and a store-pinned `node_modules` symlink (same lockfile as CI).
|
|
80
|
+
Define the service once on your `Protocol`. Implement it on a state class. Call it from anywhere that holds the `Hsm` handle.
|
|
95
81
|
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
|
|
82
|
+
```ts
|
|
83
|
+
import {
|
|
84
|
+
InitialState,
|
|
85
|
+
makeHsm,
|
|
86
|
+
RejectCallback,
|
|
87
|
+
ResolveCallback,
|
|
88
|
+
TopState,
|
|
89
|
+
} from 'ihsm';
|
|
90
|
+
|
|
91
|
+
interface WalletCtx {
|
|
92
|
+
balance: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// note the `getBalance` and `withdraw`.
|
|
96
|
+
// since they have a *resolve* and *reject* the are services allowing State Machines to serve requests **AND** transition at the same time if required.
|
|
97
|
+
interface WalletProtocol {
|
|
98
|
+
deposit(amount: number): void;
|
|
99
|
+
getBalance(resolve: ResolveCallback<number>, reject: RejectCallback): void;
|
|
100
|
+
withdraw(resolve: ResolveCallback<number>, reject: RejectCallback, amount: number): void;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
class WalletTop extends TopState<WalletCtx, WalletProtocol> implements WalletProtocol {
|
|
104
|
+
deposit(amount: number): void {
|
|
105
|
+
this.ctx.balance += amount;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
getBalance(resolve: ResolveCallback<number>): void {
|
|
109
|
+
resolve(this.ctx.balance);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
withdraw(resolve: ResolveCallback<number>, reject: RejectCallback, amount: number): void {
|
|
113
|
+
if (amount > this.ctx.balance) {
|
|
114
|
+
reject(new Error('insufficient funds'));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
this.ctx.balance -= amount;
|
|
118
|
+
resolve(this.ctx.balance);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@InitialState
|
|
123
|
+
class Open extends WalletTop {}
|
|
124
|
+
|
|
125
|
+
const wallet = makeHsm(WalletTop, { balance: 100 });
|
|
126
|
+
await wallet.sync();
|
|
127
|
+
|
|
128
|
+
wallet.post('deposit', 50);
|
|
129
|
+
|
|
130
|
+
const balance = await wallet.call('getBalance'); // Promise<number> — no extra sync()
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
await wallet.call('withdraw', 200);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
// reject() from the handler becomes a thrown Error here
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const left = await wallet.call('getBalance'); // 150
|
|
99
139
|
```
|
|
100
140
|
|
|
101
|
-
|
|
141
|
+
**Events** (`void` handlers) → `post('deposit', 50)`. **Services** (`resolve` / `reject` handlers) → `await call('getBalance')`. Same mailbox, same serialization guarantees, full TypeScript inference on names, payloads, and return types.
|
|
142
|
+
|
|
143
|
+
See [Call services](https://filasieno.github.io/ihsm/reference#_4-messaging-post-call-sync) in the reference.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Hierarchical (nested) state machines
|
|
148
|
+
|
|
149
|
+
Child states extend parent states. The prototype chain is the state tree; entering a composite runs `onEntry` from outer to inner initial leaf, exiting walks the lowest common ancestor path.
|
|
150
|
+
Hierarchical state machines are extreamly easy to write just a extend a class.
|
|
151
|
+
Also not that all states are stateless classes.
|
|
152
|
+
All state is stored in the actor context available at `this.ctx`.
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
import { InitialState, makeHsm, TopState } from 'ihsm';
|
|
156
|
+
|
|
157
|
+
interface PlayerCtx {
|
|
158
|
+
track: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
interface PlayerProtocol {
|
|
162
|
+
play(): void;
|
|
163
|
+
pause(): void;
|
|
164
|
+
stop(): void;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
class PlayerTop extends TopState<PlayerCtx, PlayerProtocol> {}
|
|
168
|
+
|
|
169
|
+
class Active extends PlayerTop {
|
|
170
|
+
stop(): void {
|
|
171
|
+
this.transition(Stopped);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
102
174
|
|
|
103
|
-
|
|
104
|
-
|
|
175
|
+
@InitialState
|
|
176
|
+
class Playing extends Active {
|
|
177
|
+
pause(): void {
|
|
178
|
+
this.transition(Paused);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
class Paused extends Active {
|
|
183
|
+
play(): void {
|
|
184
|
+
this.transition(Playing);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
@InitialState
|
|
189
|
+
class Stopped extends PlayerTop {
|
|
190
|
+
play(): void {
|
|
191
|
+
this.ctx.track = 'demo.mp3';
|
|
192
|
+
this.transition(Playing);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const player = makeHsm(PlayerTop, { track: '' });
|
|
197
|
+
await player.sync();
|
|
198
|
+
|
|
199
|
+
player.post('play');
|
|
200
|
+
await player.sync();
|
|
201
|
+
// active leaf: Playing — inherits stop() from Active
|
|
105
202
|
```
|
|
106
203
|
|
|
107
|
-
|
|
108
|
-
`npm install` tree: `rm -rf node_modules` and enter `nix develop` again.
|
|
204
|
+
See [Hierarchy & transitions](https://filasieno.github.io/ihsm/reference#_5-transitions) in the reference.
|
|
109
205
|
|
|
110
|
-
|
|
206
|
+
---
|
|
111
207
|
|
|
112
|
-
|
|
113
|
-
| ------- | ------- |
|
|
114
|
-
| `nix flake check` | Full CI gate: library compile, unit + tutorial tests, lint, docs site |
|
|
115
|
-
| `nix build` | Compile library and run tests → `result/lib/` |
|
|
116
|
-
| `nix build .#lint` | TypeScript (full solution), ESLint, Prettier |
|
|
117
|
-
| `nix build .#docs` | Production documentation site → `result/share/doc/ihsm/` |
|
|
208
|
+
## Messaging: `post`, `sync`, and `call`
|
|
118
209
|
|
|
119
|
-
|
|
210
|
+
Every machine is an actor with a **single-threaded mailbox**. While a handler runs, new messages queue — no re-entrancy.
|
|
120
211
|
|
|
121
|
-
|
|
122
|
-
|
|
212
|
+
| API | Role | Returns |
|
|
213
|
+
| --- | ---- | ------- |
|
|
214
|
+
| `post(event, …args)` | Fire-and-forget event | `void` (use `sync()` to wait) |
|
|
215
|
+
| `call(service, …args)` | Typed request/response | `Promise<T>` |
|
|
216
|
+
| `deferredPost(ms, event, …args)` | Timer then `post` | `void` |
|
|
217
|
+
| `sync()` | Drain queue up to marker | `Promise<void>` |
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
door.post('open');
|
|
221
|
+
await door.sync(); // handler + transition finished
|
|
222
|
+
|
|
223
|
+
const id = await account.call('lookup', 'user-42'); // await the service directly
|
|
123
224
|
```
|
|
124
225
|
|
|
125
|
-
|
|
126
|
-
`bash scripts/verify-docs-site.sh result/share/doc/ihsm`.
|
|
226
|
+
Inside handlers you also get `transition()`, `sleep()`, and `postNow()` for hi-priority follow-up steps within the same dispatch turn.
|
|
127
227
|
|
|
128
|
-
|
|
228
|
+
See [Post & sync](https://filasieno.github.io/ihsm/reference#_4-messaging-post-call-sync) in the reference.
|
|
129
229
|
|
|
130
|
-
|
|
131
|
-
**`nix develop --command …`** (one-shot). See [website/README.md](./website/README.md)
|
|
132
|
-
for docs-site layout and generated output.
|
|
230
|
+
---
|
|
133
231
|
|
|
134
|
-
|
|
232
|
+
## Async handlers
|
|
135
233
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
| `npm run build` | Compile the publishable library → `lib/cjs/` (CommonJS) and `lib/esm/` (ESM), then finalize |
|
|
139
|
-
| `npm run build-cjs` | Compile the CommonJS tree only → `lib/cjs/` |
|
|
140
|
-
| `npm run build-esm` | Compile the ESM tree only → `lib/esm/` |
|
|
141
|
-
| `npm run clean` | Remove generated artifacts (`lib/`, `.tsc/`, coverage, `docs-build/`, `website/docs/`, …) |
|
|
142
|
-
| `npm run dist` | Clean, then build library and documentation site (maintainer bundle) |
|
|
234
|
+
Handlers may be `async`. The runtime awaits the returned `Promise` before applying a scheduled `transition()` — so you can run an entire I/O pipeline inside one handler while staying in the same state.
|
|
235
|
+
This is important to minimize states and exploit RTC semantics.
|
|
143
236
|
|
|
144
|
-
|
|
237
|
+
```ts
|
|
238
|
+
@InitialState
|
|
239
|
+
class Idle extends FileTop {
|
|
240
|
+
async transfer(from: string, to: string): Promise<void> {
|
|
241
|
+
const data = await readFile(from);
|
|
242
|
+
await writeFile(to, data);
|
|
243
|
+
this.transition(Done);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
```
|
|
145
247
|
|
|
146
|
-
|
|
147
|
-
| ------- | ------- |
|
|
148
|
-
| `npm test` | Unit tests in Node (`src/spec/`) with NYC coverage, then the same specs minified in headless Chromium |
|
|
149
|
-
| `npm run test:node` | Node-only unit tests (with coverage) |
|
|
150
|
-
| `npm run test:browser` | Minified browser bundles for unit + tutorial specs (Playwright + esbuild) |
|
|
151
|
-
| `npm run test:tutorials` | Tutorial specs in Node, then minified in the browser |
|
|
152
|
-
| `npm run test:all` | `npm test` + `npm run test:tutorials` (both environments) |
|
|
153
|
-
| `npm run coverage` | Print an LCOV coverage report from the last `npm run test:node` run |
|
|
248
|
+
See [Async handlers](https://filasieno.github.io/ihsm/reference#_9-async-handlers) in the reference.
|
|
154
249
|
|
|
155
|
-
|
|
250
|
+
---
|
|
156
251
|
|
|
157
|
-
|
|
252
|
+
## Install
|
|
158
253
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
254
|
+
Requires [Node.js](https://nodejs.org/) **22+**.
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
npm install ihsm
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Runtime support
|
|
166
261
|
|
|
167
|
-
|
|
262
|
+
ihsm ships modern **ES2022** ESM and CommonJS. Supported runtimes:
|
|
168
263
|
|
|
169
|
-
|
|
|
264
|
+
| Runtime | Minimum |
|
|
170
265
|
| ------- | ------- |
|
|
171
|
-
|
|
|
172
|
-
|
|
|
173
|
-
|
|
|
174
|
-
|
|
|
175
|
-
|
|
266
|
+
| Node.js | **22+** |
|
|
267
|
+
| Chrome / Edge | **94+** |
|
|
268
|
+
| Firefox | **93+** |
|
|
269
|
+
| Safari (macOS / iOS) | **15.4+** |
|
|
270
|
+
|
|
271
|
+
### Size and dependencies
|
|
272
|
+
|
|
273
|
+
Measured with `esbuild` bundling `lib/esm/index.js` for the browser (full runtime — mailbox, transitions, tracing, typed `call`):
|
|
274
|
+
|
|
275
|
+
| | |
|
|
276
|
+
| --- | --- |
|
|
277
|
+
| **Production dependencies** | **0** |
|
|
278
|
+
| **Published package** | `lib/` only (~46 KB npm tarball) |
|
|
279
|
+
| **Minified bundle** | **~22 KB** (21.7 KiB; single-file ESM/IIFE) |
|
|
280
|
+
| **Gzip** | **~4.6 KB** (typical CDN / HTTP transfer size) |
|
|
281
|
+
| **Tree-shaking** | `"sideEffects": false` — runtime is one cohesive module (~22 KB even when importing only `makeHsm`) |
|
|
282
|
+
|
|
283
|
+
Node loads the unminified `lib/` files directly (~18 KB entry, ~62 KB total); minify numbers apply to browser bundles.
|
|
284
|
+
|
|
285
|
+
No React, no RxJS, no interpreter plugins — just the runtime you import.
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Why?
|
|
290
|
+
|
|
291
|
+
Hierarchical statecharts are a formalism for modeling stateful, reactive systems. ihsm encodes them the **Samek/QP way**: class hierarchy, explicit transitions, cached LCA paths, and actor mailboxes — with compile-time safety from a single `Protocol` interface.
|
|
292
|
+
|
|
293
|
+
Good fit when you want:
|
|
294
|
+
|
|
295
|
+
- Typed events and services from one protocol definition
|
|
296
|
+
- Backend / session actors without a heavy framework
|
|
297
|
+
- Zero-dependency supply chain and a small browser bundle
|
|
298
|
+
- Class-based states that read like ordinary TypeScript
|
|
299
|
+
|
|
300
|
+
For visual editors and declarative chart JSON, libraries like [XState](https://github.com/statelyai/xstate) may fit better. See [Comparison with XState](https://filasieno.github.io/ihsm/reference#_13-comparison-with-xstate) in the reference.
|
|
301
|
+
|
|
302
|
+
Inspired by Harel statecharts and the SCXML family of notations.
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## Documentation
|
|
307
|
+
|
|
308
|
+
| Resource | Link |
|
|
309
|
+
| -------- | ---- |
|
|
310
|
+
| **Documentation site** | [filasieno.github.io/ihsm](https://filasieno.github.io/ihsm/) |
|
|
311
|
+
| Reference (concepts + interactive examples) | [/reference](https://filasieno.github.io/ihsm/reference) |
|
|
312
|
+
| API reference (TSDoc) | [/api](https://filasieno.github.io/ihsm/api) |
|
|
313
|
+
| Source: reference | [reference/REFERENCE.md](./reference/REFERENCE.md) |
|
|
314
|
+
| Source: example machines | [examples/](./examples/) |
|
|
315
|
+
|
|
316
|
+
The reference page combines the full manual with embedded playgrounds; the API is generated from TSDoc.
|
|
176
317
|
|
|
177
|
-
|
|
318
|
+
---
|
|
178
319
|
|
|
179
320
|
## Contributing
|
|
180
321
|
|
|
181
|
-
Contributions are welcome — bug reports, docs, and code.
|
|
322
|
+
Contributions are welcome — bug reports, docs, and code. See **[CONTRIBUTING.md](./CONTRIBUTING.md)** for the development environment, build commands, and PR guidelines.
|
|
182
323
|
|
|
183
324
|
- Bug reports → [issue template](https://github.com/filasieno/ihsm/issues/new?template=bug_report.yml)
|
|
184
325
|
- Features → [issue template](https://github.com/filasieno/ihsm/issues/new?template=feature_request.yml)
|
|
185
|
-
- Security → [GitHub Security Advisories](https://github.com/filasieno/ihsm/security/advisories/new)
|
|
326
|
+
- Security → [GitHub Security Advisories](https://github.com/filasieno/ihsm/security/advisories/new)
|
|
186
327
|
|
|
187
|
-
Please follow [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
|
|
328
|
+
Please follow [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
|
|
188
329
|
|
|
189
|
-
|
|
330
|
+
---
|
|
190
331
|
|
|
191
332
|
## License
|
|
192
333
|
|