@crustjs/store 0.0.1 → 0.0.3

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 CHANGED
@@ -1,8 +1,8 @@
1
1
  # @crustjs/store
2
2
 
3
- Minimal, DX-first, type-safe config persistence for CLI apps.
3
+ DX-first, typed persistence for CLI apps with clear config/data/state/cache separation.
4
4
 
5
- `@crustjs/store` gives your CLI a production-ready config store with near-zero setup. Declare your fields, get full type inference, and let the store handle path resolution, file IO, defaults, and error mapping.
5
+ `@crustjs/store` gives your CLI production-ready local persistence with near-zero setup. Provide a defaults object, pick a storage intent, and get a typed store with read/write/update/patch/reset — no manual type annotations needed.
6
6
 
7
7
  ## Install
8
8
 
@@ -26,156 +26,213 @@ import { createStore, configDir } from "@crustjs/store";
26
26
 
27
27
  const store = createStore({
28
28
  dirPath: configDir("my-cli"),
29
- fields: {
30
- theme: { type: "string", default: "light" },
31
- verbose: { type: "boolean", default: false },
32
- token: { type: "string" }, // optional — no default
29
+ defaults: {
30
+ ui: { theme: "light" as string, fontSize: 14 },
31
+ verbose: false,
33
32
  },
34
33
  });
35
34
 
36
- // Read config (returns defaults when no persisted file exists)
37
- const config = await store.read();
38
- // → { theme: "light", verbose: false, token: undefined }
35
+ // Read state (returns defaults when no persisted file exists)
36
+ const state = await store.read();
37
+ // → { ui: { theme: "light", fontSize: 14 }, verbose: false }
39
38
 
40
- // Write a full config object
41
- await store.write({ theme: "dark", verbose: true, token: "abc123" });
39
+ // Write a full state object
40
+ await store.write({ ui: { theme: "dark", fontSize: 16 }, verbose: true });
42
41
 
43
- // Update a single field
42
+ // Update with a function
44
43
  await store.update((current) => ({ ...current, verbose: false }));
45
44
 
45
+ // Patch only specific nested keys
46
+ await store.patch({ ui: { theme: "solarized" } });
47
+
46
48
  // Reset to defaults (removes persisted file)
47
49
  await store.reset();
48
50
  ```
49
51
 
50
- No explicit generics needed — types are inferred from your field definitions.
52
+ No explicit generics needed — types are inferred from your `defaults` object.
53
+
54
+ ## Storage Intent: Choosing a Directory
55
+
56
+ `@crustjs/store` provides four path helpers that resolve platform-standard directories for different storage intents. Use the one that matches what you're persisting:
57
+
58
+ | Helper | Intent | Example Use Case |
59
+ | ----------- | -------------------------- | --------------------------------------- |
60
+ | `configDir` | User preferences & config | Theme, editor settings, API keys |
61
+ | `dataDir` | Important app data | Local databases, downloaded resources |
62
+ | `stateDir` | Runtime state | Window positions, scroll offsets, undo |
63
+ | `cacheDir` | Regenerable cached data | HTTP caches, compiled assets |
64
+
65
+ ```ts
66
+ import { createStore, configDir, dataDir, stateDir, cacheDir } from "@crustjs/store";
67
+
68
+ // User preferences → ~/.config/my-cli/config.json
69
+ const config = createStore({
70
+ dirPath: configDir("my-cli"),
71
+ defaults: { theme: "light" as string, verbose: false },
72
+ });
73
+
74
+ // App data → ~/.local/share/my-cli/config.json
75
+ const data = createStore({
76
+ dirPath: dataDir("my-cli"),
77
+ defaults: { bookmarks: [] as string[] },
78
+ });
79
+
80
+ // Runtime state → ~/.local/state/my-cli/config.json
81
+ const state = createStore({
82
+ dirPath: stateDir("my-cli"),
83
+ defaults: { lastOpened: "" as string, scrollY: 0 },
84
+ });
85
+
86
+ // Cache → ~/.cache/my-cli/config.json
87
+ const cache = createStore({
88
+ dirPath: cacheDir("my-cli"),
89
+ defaults: { etag: "" as string, payload: "" as string },
90
+ });
91
+ ```
92
+
93
+ ### Platform Paths
94
+
95
+ All four helpers follow XDG conventions on Linux and macOS, and Windows-native conventions on Windows:
96
+
97
+ | Helper | Linux / macOS | Windows | Env Override |
98
+ | ----------- | ----------------------------------- | ------------------------------------------- | -------------------- |
99
+ | `configDir` | `~/.config/<app>` | `%APPDATA%\<app>` | `$XDG_CONFIG_HOME` |
100
+ | `dataDir` | `~/.local/share/<app>` | `%LOCALAPPDATA%\<app>\Data` | `$XDG_DATA_HOME` |
101
+ | `stateDir` | `~/.local/state/<app>` | `%LOCALAPPDATA%\<app>\State` | `$XDG_STATE_HOME` |
102
+ | `cacheDir` | `~/.cache/<app>` | `%LOCALAPPDATA%\<app>\Cache` | `$XDG_CACHE_HOME` |
103
+
104
+ macOS uses XDG conventions (same as Linux) for a unified Unix mental model.
51
105
 
52
106
  ## API
53
107
 
54
108
  ### `createStore(options)`
55
109
 
56
- Creates a typed async config store backed by a local JSON file. The file path is resolved once at creation time.
110
+ Creates a typed async store backed by a local JSON file. The file path is resolved once at creation time.
57
111
 
58
112
  ```ts
59
113
  const store = createStore(options);
60
114
  ```
61
115
 
62
- **Returns:** A `Store<TConfig>` with `read()`, `write()`, `update()`, and `reset()` methods.
116
+ **Returns:** A `Store<T>` with `read()`, `write()`, `update()`, `patch()`, and `reset()` methods.
63
117
 
64
118
  **Throws:** `CrustStoreError` with `PATH` code if `dirPath` or `name` is invalid.
65
119
 
66
120
  #### Options
67
121
 
68
- | Option | Type | Required | Description |
69
- | --------- | ---------- | -------- | ------------------------------------------------------------------ |
70
- | `dirPath` | `string` | Yes | Absolute directory path where the JSON file is stored. |
71
- | `name` | `string` | No | Store name used as filename (default `"config"` → `config.json`). |
72
- | `fields` | `FieldsDef`| Yes | Field definitions that declare the config schema. |
122
+ | Option | Type | Required | Description |
123
+ | -------------- | -------------------------- | -------- | ------------------------------------------------------------------ |
124
+ | `dirPath` | `string` | Yes | Absolute directory path where the JSON file is stored. |
125
+ | `name` | `string` | No | Store name used as filename (default `"config"` → `config.json`). |
126
+ | `defaults` | `T` | Yes | Default values defining the store's data shape and fallbacks. |
127
+ | `validator` | `StoreValidator<T>` | No | Validation function applied on read, write, update, and patch. |
128
+ | `pruneUnknown` | `boolean` | No | Drop unknown persisted keys on read (default `true`). |
73
129
 
74
- ### `configDir(appName, env?)`
130
+ ### `store.read()`
75
131
 
76
- Resolves the platform-standard config directory for the given app name.
132
+ Reads the persisted state file. Missing keys are filled from `defaults` via deep merge. When no file exists, returns `defaults`.
77
133
 
78
134
  ```ts
79
- import { configDir } from "@crustjs/store";
80
-
81
- const dir = configDir("my-cli");
82
- // → "/home/user/.config/my-cli" (Linux)
83
- // → "/Users/user/Library/Application Support/my-cli" (macOS)
84
- // → "C:\\Users\\user\\AppData\\Roaming\\my-cli" (Windows)
135
+ const state = await store.read();
85
136
  ```
86
137
 
87
- Use it with `dirPath`:
138
+ Always returns a value — never `undefined`. Does not write merged defaults back to disk.
139
+
140
+ ### `store.write(state)`
141
+
142
+ Atomically persists a full state object. The entire previous state is replaced.
88
143
 
89
144
  ```ts
90
- const store = createStore({
91
- dirPath: configDir("my-cli"),
92
- fields: { /* ... */ },
93
- });
145
+ await store.write({ ui: { theme: "dark", fontSize: 14 }, verbose: true });
94
146
  ```
95
147
 
96
- ### Field Definitions
148
+ Calls `validator` (if provided) before writing. Parent directories are created if missing.
97
149
 
98
- Each field in the `fields` record is a `FieldDef` with a `type` discriminant:
150
+ ### `store.update(updater)`
99
151
 
100
- | Property | Type | Required | Description |
101
- | ------------- | --------------------------------------- | -------- | --------------------------------------------------- |
102
- | `type` | `"string" \| "number" \| "boolean"` | Yes | The value type. |
103
- | `default` | Matches `type` | No | Default value when the field is not persisted. |
104
- | `array` | `true` | No | Collect values into an array. |
105
- | `description` | `string` | No | Human-readable description for tooling. |
152
+ Reads the current effective state, applies the updater function, and atomically persists the result.
106
153
 
107
- Type inference rules:
154
+ ```ts
155
+ await store.update((current) => ({
156
+ ...current,
157
+ ui: { ...current.ui, theme: "dark" },
158
+ }));
159
+ ```
108
160
 
109
- - **Has `default`** → field is guaranteed present (e.g., `string`)
110
- - **No `default`** → field is optional (e.g., `string | undefined`)
111
- - **`array: true`** → wraps the type in an array (e.g., `string[]`)
161
+ When no persisted file exists, the updater receives `defaults` as the current value. Calls `validator` before writing.
162
+
163
+ ### `store.patch(partial)`
164
+
165
+ Applies a deep partial update to the current state and persists. Only the provided keys are updated; everything else is preserved.
112
166
 
113
167
  ```ts
114
- const store = createStore({
115
- dirPath: configDir("my-cli"),
116
- fields: {
117
- theme: { type: "string", default: "light" }, // → string
118
- verbose: { type: "boolean", default: false }, // → boolean
119
- retries: { type: "number", default: 3 }, // → number
120
- token: { type: "string" }, // → string | undefined
121
- tags: { type: "string", array: true, default: [] },// → string[]
122
- ids: { type: "number", array: true }, // → number[] | undefined
123
- },
124
- });
168
+ await store.patch({ ui: { theme: "solarized" } });
125
169
  ```
126
170
 
127
- ### `store.read()`
171
+ Arrays are replaced wholesale (not merged element-by-element). Calls `validator` before writing.
128
172
 
129
- Reads the persisted config file. Missing fields are filled from their `default` values. Fields without defaults are `undefined` when not persisted.
173
+ ### `store.reset()`
174
+
175
+ Removes the persisted state file. After reset, `read()` returns `defaults`.
130
176
 
131
177
  ```ts
132
- const config = await store.read();
178
+ await store.reset();
133
179
  ```
134
180
 
135
- Always returns a value — never `undefined`.
181
+ Reset is idempotent — calling it when no file exists is a no-op.
182
+
183
+ ### Path Helpers
136
184
 
137
- ### `store.write(config)`
185
+ #### `configDir(appName, env?)`
138
186
 
139
- Atomically persists a full config object. The entire previous config is replaced.
187
+ Resolves the platform-standard config directory for the given app name.
140
188
 
141
189
  ```ts
142
- await store.write({ theme: "dark", verbose: true, token: "abc123" });
190
+ import { configDir } from "@crustjs/store";
191
+
192
+ const dir = configDir("my-cli");
193
+ // → "/home/user/.config/my-cli" (Linux / macOS)
194
+ // → "C:\\Users\\user\\AppData\\Roaming\\my-cli" (Windows)
143
195
  ```
144
196
 
145
- ### `store.update(updater)`
197
+ #### `dataDir(appName, env?)`
146
198
 
147
- Reads the current effective config, applies the updater function, and atomically persists the result.
199
+ Resolves the platform-standard data directory.
148
200
 
149
201
  ```ts
150
- await store.update((current) => ({
151
- ...current,
152
- theme: "dark",
153
- }));
154
- ```
202
+ import { dataDir } from "@crustjs/store";
155
203
 
156
- When no persisted file exists, the updater receives field defaults as the current value.
204
+ const dir = dataDir("my-cli");
205
+ // → "/home/user/.local/share/my-cli" (Linux / macOS)
206
+ // → "C:\\Users\\user\\AppData\\Local\\my-cli\\Data" (Windows)
207
+ ```
157
208
 
158
- ### `store.reset()`
209
+ #### `stateDir(appName, env?)`
159
210
 
160
- Removes the persisted config file. After reset, `read()` returns field defaults.
211
+ Resolves the platform-standard state directory.
161
212
 
162
213
  ```ts
163
- await store.reset();
214
+ import { stateDir } from "@crustjs/store";
215
+
216
+ const dir = stateDir("my-cli");
217
+ // → "/home/user/.local/state/my-cli" (Linux / macOS)
218
+ // → "C:\\Users\\user\\AppData\\Local\\my-cli\\State" (Windows)
164
219
  ```
165
220
 
166
- Reset is idempotent — calling it when no file exists is a no-op.
221
+ #### `cacheDir(appName, env?)`
167
222
 
168
- ## Config File Path
223
+ Resolves the platform-standard cache directory.
169
224
 
170
- `configDir()` resolves the platform-standard config directory. The `name` option defaults to `"config"`, producing `config.json`:
225
+ ```ts
226
+ import { cacheDir } from "@crustjs/store";
171
227
 
172
- | Platform | Default Path | Env Override |
173
- | -------- | ------------------------------------------------------- | ------------------ |
174
- | Linux | `~/.config/<appName>/<name>.json` | `$XDG_CONFIG_HOME` |
175
- | macOS | `~/Library/Application Support/<appName>/<name>.json` | — |
176
- | Windows | `%APPDATA%\<appName>\<name>.json` | `%APPDATA%` |
228
+ const dir = cacheDir("my-cli");
229
+ // → "/home/user/.cache/my-cli" (Linux / macOS)
230
+ // → "C:\\Users\\user\\AppData\\Local\\my-cli\\Cache" (Windows)
231
+ ```
232
+
233
+ All path helpers accept an optional `PlatformEnv` parameter for deterministic testing without mutating `process.env`.
177
234
 
178
- ### Multiple Stores
235
+ ## Multiple Stores
179
236
 
180
237
  Use the `name` option to create multiple stores under the same directory:
181
238
 
@@ -185,11 +242,11 @@ import { createStore, configDir } from "@crustjs/store";
185
242
  const dir = configDir("my-cli");
186
243
 
187
244
  // Default store → ~/.config/my-cli/config.json
188
- const configStore = createStore({
245
+ const settingsStore = createStore({
189
246
  dirPath: dir,
190
- fields: {
191
- theme: { type: "string", default: "light" },
192
- verbose: { type: "boolean", default: false },
247
+ defaults: {
248
+ ui: { theme: "light" as string, fontSize: 14 },
249
+ verbose: false,
193
250
  },
194
251
  });
195
252
 
@@ -197,9 +254,7 @@ const configStore = createStore({
197
254
  const authStore = createStore({
198
255
  dirPath: dir,
199
256
  name: "auth",
200
- fields: {
201
- token: { type: "string" },
202
- },
257
+ defaults: { token: "" as string },
203
258
  });
204
259
  ```
205
260
 
@@ -209,45 +264,144 @@ The `name` must not contain path separators or the `.json` extension.
209
264
 
210
265
  ## Defaults & Merge
211
266
 
212
- When a persisted file exists, `read()` applies field defaults for any missing keys:
267
+ When a persisted file exists, `read()` deep-merges defaults for any missing keys:
213
268
 
214
269
  ```ts
215
270
  const store = createStore({
216
271
  dirPath: configDir("my-cli"),
217
- fields: {
218
- theme: { type: "string", default: "light" },
219
- verbose: { type: "boolean", default: false },
220
- retries: { type: "number", default: 3 },
272
+ defaults: {
273
+ ui: { theme: "light", fontSize: 14 },
274
+ verbose: false,
221
275
  },
222
276
  });
223
277
 
224
- // Persisted file contains: { "theme": "dark" }
225
- const config = await store.read();
226
- // → { theme: "dark", verbose: false, retries: 3 }
278
+ // Persisted file contains: { "ui": { "theme": "dark" }, "verbose": true }
279
+ const state = await store.read();
280
+ // → { ui: { theme: "dark", fontSize: 14 }, verbose: true }
227
281
  ```
228
282
 
229
- ### Merge rules
283
+ ### Merge Rules
230
284
 
231
- - Fields present in the persisted file use the persisted value.
232
- - Missing fields with a `default` use the default value.
233
- - Missing fields without a `default` are omitted (typed as `T | undefined`).
234
- - Extra keys in the persisted file that are not defined in `fields` are dropped.
235
- - Array defaults are shallow-copied to prevent shared mutation.
285
+ - Persisted values override defaults at every nesting level.
286
+ - Missing keys fall back to defaults (recursively for nested objects).
287
+ - Arrays are replaced wholesale — not merged element-by-element.
288
+ - Unknown persisted keys are dropped by default (`pruneUnknown: true`). Set `pruneUnknown: false` to preserve them.
289
+ - All values are deep-cloned to prevent shared-reference mutation from defaults.
236
290
  - Falsy values (`null`, `0`, `""`, `false`) in the persisted file are preserved — only truly missing keys trigger defaults.
237
291
 
238
292
  ### Important: defaults are not auto-persisted
239
293
 
240
- Merged defaults exist only in memory. The merged result is **not** written back to disk. The persisted file remains unchanged until you explicitly call `write()` or `update()`.
294
+ Merged defaults exist only in memory. The persisted file remains unchanged until you explicitly call `write()`, `update()`, or `patch()`.
295
+
296
+ ## Validation
297
+
298
+ Add schema validation to enforce config integrity on every read, write, update, and patch. When a `validator` is configured, validation is **strict by default** — invalid config fails loudly.
299
+
300
+ ### Using `@crustjs/validate`
301
+
302
+ The easiest way to add validation is with store adapters from `@crustjs/validate`:
303
+
304
+ ```ts
305
+ import { z } from "zod";
306
+ import { storeValidator } from "@crustjs/validate/zod";
307
+ import { createStore, configDir } from "@crustjs/store";
308
+
309
+ const store = createStore({
310
+ dirPath: configDir("my-cli"),
311
+ defaults: { theme: "light" as string, verbose: false },
312
+ validator: storeValidator(
313
+ z.object({
314
+ theme: z.enum(["light", "dark"]),
315
+ verbose: z.boolean(),
316
+ }),
317
+ ),
318
+ });
319
+
320
+ // write() validates before persisting
321
+ await store.write({ theme: "neon", verbose: true });
322
+ // → throws CrustStoreError("VALIDATION")
323
+
324
+ // read() validates after applying defaults
325
+ const config = await store.read();
326
+ // → throws if persisted config is invalid
327
+ ```
328
+
329
+ `storeValidatorSync()` is also available for synchronous schemas.
330
+
331
+ For Effect schemas, wrap with `Schema.standardSchemaV1()`:
332
+
333
+ ```ts
334
+ import * as Schema from "effect/Schema";
335
+ import { storeValidator } from "@crustjs/validate/effect";
336
+
337
+ const store = createStore({
338
+ dirPath: configDir("my-cli"),
339
+ defaults: { theme: "light" as string },
340
+ validator: storeValidator(
341
+ Schema.standardSchemaV1(
342
+ Schema.Struct({ theme: Schema.Literal("light", "dark") }),
343
+ ),
344
+ ),
345
+ });
346
+ ```
347
+
348
+ ### Custom validators
349
+
350
+ You can also provide a validator function directly without `@crustjs/validate`:
351
+
352
+ ```ts
353
+ import type { StoreValidator } from "@crustjs/store";
354
+
355
+ const validator: StoreValidator<{ theme: string }> = (value) => {
356
+ const config = value as { theme: string };
357
+ if (config.theme !== "light" && config.theme !== "dark") {
358
+ return {
359
+ ok: false,
360
+ issues: [{ message: 'Must be "light" or "dark"', path: "theme" }],
361
+ };
362
+ }
363
+ return { ok: true, value: config };
364
+ };
365
+ ```
366
+
367
+ The `StoreValidator<T>` contract is `(value: unknown) => StoreValidatorResult<T> | Promise<StoreValidatorResult<T>>`, where `StoreValidatorResult<T>` is `{ ok: true, value: T } | { ok: false, issues: StoreValidatorIssue[] }`.
368
+
369
+ ### Validation behavior
370
+
371
+ - **Write**: Validates before persisting. If the validator transforms the value, the transformed result is persisted.
372
+ - **Read**: Validates after applying defaults. Invalid persisted config fails loudly — no silent fallback to defaults.
373
+ - **Update**: Reads raw config (no validation), applies updater, validates the result, then persists.
374
+ - **Patch**: Reads raw config, applies deep partial merge, validates the result, then persists.
375
+ - **Reset**: No validation — just removes the persisted file.
376
+
377
+ ### Catching validation errors
378
+
379
+ ```ts
380
+ import { CrustStoreError } from "@crustjs/store";
381
+
382
+ try {
383
+ await store.read();
384
+ } catch (err) {
385
+ if (err instanceof CrustStoreError && err.is("VALIDATION")) {
386
+ // err.details is { operation: "read" | "write" | "update" | "patch", issues: StoreValidationIssue[] }
387
+ console.error(`Validation failed during ${err.details.operation}:`);
388
+ for (const issue of err.details.issues) {
389
+ console.error(` ${issue.path || "(root)"}: ${issue.message}`);
390
+ }
391
+ }
392
+ }
393
+ ```
241
394
 
242
395
  ## Error Handling
243
396
 
244
397
  All errors thrown by `@crustjs/store` are instances of `CrustStoreError` with a typed `code` property:
245
398
 
246
- | Code | When | Details |
247
- | ------- | ----------------------------------------------------------- | --------------------------- |
248
- | `PATH` | Invalid `dirPath`, invalid `name`, unsupported platform | `{ path: string }` |
249
- | `PARSE` | Malformed JSON in persisted config file | `{ path: string }` |
250
- | `IO` | Filesystem read, write, or delete failure | `{ path, operation }` |
399
+ | Code | When | Details |
400
+ | ------------ | --------------------------------------------------------------- | --------------------------- |
401
+ | `PATH` | Invalid `dirPath`, invalid `name`, unsupported platform | `{ path: string }` |
402
+ | `PARSE` | Malformed JSON in persisted config file | `{ path: string }` |
403
+ | `IO` | Filesystem read, write, or delete failure | `{ path, operation }` |
404
+ | `VALIDATION` | Config fails validator on read, write, update, or patch | `{ operation, issues }` |
251
405
 
252
406
  ### Catching errors by code
253
407
 
@@ -255,12 +409,12 @@ All errors thrown by `@crustjs/store` are instances of `CrustStoreError` with a
255
409
  import { CrustStoreError } from "@crustjs/store";
256
410
 
257
411
  try {
258
- const config = await store.read();
412
+ const state = await store.read();
259
413
  } catch (err) {
260
414
  if (err instanceof CrustStoreError) {
261
415
  switch (err.code) {
262
416
  case "PARSE":
263
- console.error(`Corrupt config at ${err.details.path}`);
417
+ console.error(`Corrupt file at ${err.details.path}`);
264
418
  console.error("Delete the file and retry, or fix the JSON manually.");
265
419
  break;
266
420
  case "IO":
@@ -287,14 +441,14 @@ if (err instanceof CrustStoreError && err.is("IO")) {
287
441
  `CrustStoreError` preserves the original error as `cause` for debugging:
288
442
 
289
443
  ```ts
290
- if (err instanceof CrustStoreError && err.is("PARSE")) {
291
- console.error("Original parse error:", err.cause);
444
+ if (err instanceof CrustStoreError && err.is("VALIDATION")) {
445
+ console.error("Validation error:", err.cause);
292
446
  }
293
447
  ```
294
448
 
295
449
  ### Malformed JSON
296
450
 
297
- `@crustjs/store` does **not** silently recover from malformed JSON. If the persisted config file contains invalid JSON, `read()` throws a `CrustStoreError` with `PARSE` code immediately — no fallback to defaults.
451
+ `@crustjs/store` does **not** silently recover from malformed JSON. If the persisted file contains invalid JSON, `read()` throws a `CrustStoreError` with `PARSE` code immediately — no fallback to defaults.
298
452
 
299
453
  ## Types
300
454
 
@@ -303,38 +457,47 @@ All types are exported for use in your application:
303
457
  ```ts
304
458
  import type {
305
459
  CreateStoreOptions,
306
- FieldDef,
307
- FieldsDef,
308
- InferStoreConfig,
309
460
  Store,
310
461
  StoreUpdater,
311
- ValueType,
462
+ StoreValidator,
463
+ StoreValidatorResult,
464
+ StoreValidatorSuccess,
465
+ StoreValidatorFailure,
466
+ StoreValidatorIssue,
312
467
  StoreErrorCode,
468
+ StoreValidationIssue,
469
+ ValidationErrorDetails,
470
+ PlatformEnv,
313
471
  } from "@crustjs/store";
314
472
  ```
315
473
 
316
- | Type | Description |
317
- | -------------------- | ----------------------------------------------------------------------- |
318
- | `CreateStoreOptions` | Options object for `createStore()`. |
319
- | `FieldDef` | Single field definition (discriminated by `type` and `array`). |
320
- | `FieldsDef` | Record mapping field names to `FieldDef`. |
321
- | `InferStoreConfig` | Utility type: infers the config shape from a `FieldsDef`. |
322
- | `Store` | Store instance with `read`, `write`, `update`, `reset`. |
323
- | `StoreUpdater` | Updater function type `(current: TConfig) => TConfig`. |
324
- | `ValueType` | Supported type literals: `"string" \| "number" \| "boolean"`. |
325
- | `StoreErrorCode` | Union of error codes: `"PATH" \| "PARSE" \| "IO"`. |
326
-
327
- ## v1 Scope & Non-Goals
328
-
329
- `@crustjs/store` v1 is intentionally minimal. The following are **not** included:
330
-
331
- - **Generic state/cache abstractions** — v1 is config-only (single typed object per store).
474
+ | Type | Description |
475
+ | ------------------------- | ----------------------------------------------------------------------- |
476
+ | `CreateStoreOptions` | Options object for `createStore()`. |
477
+ | `Store` | Store instance with `read`, `write`, `update`, `patch`, `reset`. |
478
+ | `StoreUpdater` | Updater function type `(current: T) => T`. |
479
+ | `StoreValidator` | Validator function contract `(value: unknown) => StoreValidatorResult`. |
480
+ | `StoreValidatorResult` | Discriminated union: `StoreValidatorSuccess \| StoreValidatorFailure`. |
481
+ | `StoreValidatorSuccess` | `{ ok: true, value: T }`. |
482
+ | `StoreValidatorFailure` | `{ ok: false, issues: StoreValidatorIssue[] }`. |
483
+ | `StoreValidatorIssue` | `{ message: string, path: string }`. |
484
+ | `StoreValidationIssue` | Validation issue in error details payload. |
485
+ | `ValidationErrorDetails` | Error details for `VALIDATION` code: `{ operation, issues }`. |
486
+ | `StoreErrorCode` | Union of error codes: `"PATH" \| "PARSE" \| "IO" \| "VALIDATION"`. |
487
+ | `PlatformEnv` | Injectable platform environment for testing path helpers. |
488
+ | `CrustStoreError` | Typed error class with `code`, `details`, and `cause`. |
489
+
490
+ ## Scope & Non-Goals
491
+
492
+ `@crustjs/store` is intentionally focused. The following are **not** included:
493
+
494
+ - **Cross-process locking** — assumes single-process usage for write coordination.
332
495
  - **Built-in encryption or keychain integration.**
333
- - **Cross-process locking** — v1 assumes single-process usage for write coordination.
334
496
  - **Sync API variants** (`readSync`, `writeSync`, etc.).
335
- - **Built-in validation** — will be added later via `@crustjs/validate` integration.
497
+ - **Automatic migration of invalid persisted config** — validation fails loudly; callers handle recovery.
336
498
  - **Alternative formats** (YAML, TOML, JSON5) — JSON is the only on-disk format.
337
- - **Deep `@crustjs/core` integration** — the store is a standalone package.
499
+ - **Remote/cloud synchronization** or multi-device sync.
500
+ - **Domain-specific migration frameworks** beyond simple store-level upgrade hooks.
338
501
 
339
502
  ## License
340
503