@zodal/dials-core 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,457 @@
1
+ import { z } from 'zod';
2
+ import { CollectionConfig, CollectionDefinition } from '@zodal/core';
3
+
4
+ /**
5
+ * Core model types for zodal-dials: settings, layers, scopes, the cascade result, and provenance.
6
+ *
7
+ * A SETTING is a typed named parameter identified by a stable dotted-path KEY. A LAYER is a partial
8
+ * map of key -> value (or the UNSET sentinel) from one source. A SCOPE names an ordered source of
9
+ * layers; the CASCADE merges an ordered stack of scoped layers into an EFFECTIVE value per key,
10
+ * paired with PROVENANCE (which scope won, what it shadowed, whether it is policy-managed).
11
+ *
12
+ * UNSET is the explicit deletion/reset sentinel — never raw `null` — so a layer can declare "I do
13
+ * not contribute this key" (re-exposing a lower scope) without colliding with a legitimate `null`
14
+ * value. See `docs/zodal-dials-concept.md` and `docs/dev-plan.md` §4.
15
+ */
16
+ /** A setting's stable, serialization-independent identity (a dotted path, e.g. "editor.fontSize"). */
17
+ type SettingKey = string;
18
+ /** A setting value: a scalar, an array, or an irreducible nested object. */
19
+ type SettingValue = unknown;
20
+ /**
21
+ * The deletion/reset sentinel. In a layer, `UNSET` marks a key as explicitly not contributed by
22
+ * that scope, re-exposing the value from a lower-precedence scope (or its absence). Distinct from
23
+ * `null`/`undefined`, which are legitimate setting values. Uses a registered symbol so it survives
24
+ * realm boundaries and bundler duplication.
25
+ */
26
+ declare const UNSET: unique symbol;
27
+ type Unset = typeof UNSET;
28
+ /** Type guard for the UNSET sentinel. */
29
+ declare function isUnset(v: unknown): v is Unset;
30
+ /** A partial/sparse set of setting values from one source — the unit that gets merged. */
31
+ type Layer = Record<SettingKey, SettingValue | Unset>;
32
+ /**
33
+ * A named layer in the cascade. Scopes are DATA, not constants: the resolver is handed an ordered
34
+ * stack (lowest precedence first) and never hardcodes the ladder. `managed: true` elevates a layer
35
+ * into the policy band — it wins over every non-managed layer regardless of position and marks the
36
+ * resulting value as non-overridable (the UI should lock the control).
37
+ */
38
+ interface ScopedLayer {
39
+ /** The scope id this layer comes from (e.g. "default", "preset", "profile", "user", "policy"). */
40
+ scope: string;
41
+ /** The (sparse) layer of values. */
42
+ layer: Layer;
43
+ /** Policy/managed band: wins over all non-managed layers and marks the value non-overridable. */
44
+ managed?: boolean;
45
+ }
46
+ /** Per-key merge strategy. Type-directed by default; `.meta({ mergeStrategy })`-overridable. */
47
+ type MergeStrategy = 'replace' | 'deep-merge' | 'append';
48
+ /** A lower layer that set a key but did not win (or that explicitly UNSET it). */
49
+ interface ShadowedLayer {
50
+ scope: string;
51
+ /** The shadowed value, or 'UNSET' if that layer reset the key. */
52
+ value: SettingValue | 'UNSET';
53
+ managed: boolean;
54
+ }
55
+ /**
56
+ * The cascade's first-class explanation of an effective value: which scope won, how the value was
57
+ * produced, what it shadowed, whether it is policy-managed, and (for deep-merged objects) which
58
+ * scopes contributed. This is the deliberate differentiator — provenance is never a debug
59
+ * afterthought.
60
+ */
61
+ interface KeyProvenance {
62
+ key: SettingKey;
63
+ /** The scope id that supplied the effective value. */
64
+ winningScope: string;
65
+ /** The resolved value (also present in `effective`). */
66
+ value: SettingValue;
67
+ /** True when the winning scope is in the managed/policy band (control should be locked). */
68
+ managed: boolean;
69
+ /** How the value was produced from the contributing layers. */
70
+ mergeStrategy: MergeStrategy;
71
+ /** Other layers that set this key, highest precedence first (each value or 'UNSET'). */
72
+ shadowed: ShadowedLayer[];
73
+ /** For deep-merged object values: the scope ids whose objects were merged, low -> high. */
74
+ mergedFrom?: string[];
75
+ }
76
+ /** A key set to differing values by more than one layer (surfaced for the UI). */
77
+ interface Conflict {
78
+ key: SettingKey;
79
+ /** Contributing scopes (highest precedence first) with their values. */
80
+ contributors: Array<{
81
+ scope: string;
82
+ value: SettingValue | 'UNSET';
83
+ managed: boolean;
84
+ }>;
85
+ /** True when a managed/policy scope overrode a differing non-managed value. */
86
+ overriddenByPolicy: boolean;
87
+ }
88
+ /** The complete result of resolving a cascade. */
89
+ interface EffectiveResult {
90
+ /** The resolved value per key (keys that resolve to UNSET/absent are omitted). */
91
+ effective: Record<SettingKey, SettingValue>;
92
+ /** Provenance per resolved key. */
93
+ provenance: Record<SettingKey, KeyProvenance>;
94
+ /** Keys set by multiple layers to differing values (informational). */
95
+ conflicts: Conflict[];
96
+ }
97
+ /** Sensitivity classification of a setting. */
98
+ type Sensitivity = 'public' | 'sensitive' | 'secret';
99
+ /**
100
+ * A masked reference to a secret value — mirrors zodal's `ContentRef`. Reads of a secret setting
101
+ * return a `SecretRef`, never plaintext; the plaintext is fetched via an explicit reveal call.
102
+ */
103
+ interface SecretRef {
104
+ readonly _tag: 'SecretRef';
105
+ /** The setting key this secret belongs to. */
106
+ key: SettingKey;
107
+ /** Whether a value is set (without revealing it). */
108
+ isSet: boolean;
109
+ /** A display mask, e.g. "•••• (set)" or "not set". */
110
+ masked: string;
111
+ }
112
+ /** Type guard for SecretRef. */
113
+ declare function isSecretRef(v: unknown): v is SecretRef;
114
+
115
+ /**
116
+ * The cascade resolver: merge an ordered stack of scoped layers (lowest precedence first) into an
117
+ * effective value per key, paired with provenance.
118
+ *
119
+ * Precedence is the stack order, with managed/policy layers elevated into a top band that wins over
120
+ * every non-managed layer regardless of position (and marks the value non-overridable). The UNSET
121
+ * sentinel is a fall-through: a layer that UNSETs a key ABSTAINS (contributes nothing for it),
122
+ * re-exposing the next lower scope — at the top of a stack this is exactly "reset to default". Under
123
+ * `deep-merge`/`append`, an abstaining (UNSET) layer simply does not participate in the merge; it
124
+ * does NOT sever the contributors below it — to override a lower object wholesale, use the `replace`
125
+ * strategy or set the full value. Provenance reports the winning scope, the shadowed layers
126
+ * (including higher resets), the merge strategy used, and — for merges — only the scopes that
127
+ * actually changed the result (`mergedFrom`, computed by leave-one-out, so fully-shadowed scopes are
128
+ * not falsely claimed).
129
+ */
130
+
131
+ interface ResolveOptions {
132
+ /** Per-key merge strategy resolver. Default: 'replace' for every key. */
133
+ strategyFor?: (key: SettingKey) => MergeStrategy;
134
+ }
135
+ /** Resolve a stack of scoped layers (lowest precedence first) into effective values + provenance. */
136
+ declare function resolve(stack: ScopedLayer[], options?: ResolveOptions): EffectiveResult;
137
+
138
+ /**
139
+ * Value combination under a merge strategy. Given an ordered (low -> high precedence) list of
140
+ * contributing values for one key, produce the merged value: `replace` (highest wins), `deep-merge`
141
+ * (structural merge of object contributors), or `append` (concatenate array contributors). Pure.
142
+ */
143
+
144
+ /** Combine an ordered (low -> high precedence) list of contributing values under a strategy. */
145
+ declare function mergeValues(values: SettingValue[], strategy: MergeStrategy): SettingValue;
146
+
147
+ /**
148
+ * Patch & serialization utilities for layers.
149
+ *
150
+ * - RFC 7386 JSON Merge Patch (`applyMergePatch`) — the ergonomic, mirror-shaped delta format.
151
+ * - Lossless layer serialization (`serializeLayer`/`deserializeLayer`) — encodes the UNSET sentinel
152
+ * separately from values so a layer round-trips with zero drift (UNSET is NOT conflated with a
153
+ * literal `null`, the standard RFC 7386 footgun).
154
+ * - `layerToMergePatch` — a layer AS a standard RFC 7386 patch (UNSET -> null) for interop (lossy
155
+ * only for the rare literal-`null` value).
156
+ * - RFC 6902 JSON Patch (`applyJsonPatch`) + `diffJsonPatch` + `invertJsonPatch` — for history/undo.
157
+ *
158
+ * All functions are pure; inputs are never mutated. Pointer traversal rejects the prototype-polluting
159
+ * keys `__proto__`/`constructor`/`prototype`, and array indices are validated per RFC 6902 §4.
160
+ */
161
+
162
+ /** Apply an RFC 7386 JSON Merge Patch to a target. `null` in the patch deletes a member. Pure. */
163
+ declare function applyMergePatch(target: unknown, patch: unknown): unknown;
164
+ /** The on-disk/wire shape of a layer: values plus an explicit list of reset (UNSET) keys. */
165
+ interface SerializedLayer {
166
+ values: Record<string, unknown>;
167
+ unset: string[];
168
+ }
169
+ /** Serialize a layer losslessly (UNSET keys recorded separately from values). */
170
+ declare function serializeLayer(layer: Layer): SerializedLayer;
171
+ /** Deserialize a layer produced by `serializeLayer` (the inverse; round-trips with zero drift). */
172
+ declare function deserializeLayer(s: SerializedLayer): Layer;
173
+ /**
174
+ * Express a layer as a standard RFC 7386 merge patch (UNSET -> null). Lossy ONLY for the rare case
175
+ * of a setting whose legitimate value is `null` (indistinguishable from a reset under RFC 7386); use
176
+ * `serializeLayer` when that distinction must be preserved.
177
+ */
178
+ declare function layerToMergePatch(layer: Layer): Record<string, unknown>;
179
+ /** An RFC 6902 JSON Patch operation. */
180
+ type JsonPatchOp = {
181
+ op: 'add';
182
+ path: string;
183
+ value: unknown;
184
+ } | {
185
+ op: 'remove';
186
+ path: string;
187
+ } | {
188
+ op: 'replace';
189
+ path: string;
190
+ value: unknown;
191
+ } | {
192
+ op: 'move';
193
+ from: string;
194
+ path: string;
195
+ } | {
196
+ op: 'copy';
197
+ from: string;
198
+ path: string;
199
+ } | {
200
+ op: 'test';
201
+ path: string;
202
+ value: unknown;
203
+ };
204
+ /** Apply an ordered RFC 6902 JSON Patch to a document. Pure; throws on a failed `test` or bad path. */
205
+ declare function applyJsonPatch(doc: unknown, ops: JsonPatchOp[]): unknown;
206
+ /**
207
+ * Compute an RFC 6902 patch turning `before` into `after`, at object-member granularity (recursing
208
+ * into nested plain objects; arrays and scalars are replaced wholesale). The patches we generate are
209
+ * always add/remove/replace, which `invertJsonPatch` inverts exactly — ideal for settings history.
210
+ */
211
+ declare function diffJsonPatch(before: unknown, after: unknown, basePath?: string): JsonPatchOp[];
212
+ /**
213
+ * Produce the inverse of a patch (relative to the document it was applied to), so applying the
214
+ * inverse undoes it. Exact for add/remove/replace/test/copy; `move` is inverted as a reverse move.
215
+ */
216
+ declare function invertJsonPatch(ops: JsonPatchOp[], before: unknown): JsonPatchOp[];
217
+
218
+ /**
219
+ * Zod v4 introspection helpers for settings schemas: reading the object shape, per-field `.meta()`
220
+ * (unwrapping wrappers so wrapped metadata survives), extracting declared defaults (robustly, by
221
+ * parsing `undefined` — no reliance on private internals), the type-directed merge strategy, and the
222
+ * sensitivity classification (name heuristics + `.meta()` override). Uses `@zodal/core` helpers
223
+ * where they apply and stable `instanceof` checks for base typing.
224
+ */
225
+
226
+ /** Get the field map of a Zod object schema. Prefers `_zod.def.shape` (the workspace Zod-v4 rule),
227
+ * falling back to the public `.shape`. */
228
+ declare function getObjectShape(schema: z.ZodObject<z.ZodRawShape>): Record<string, z.ZodType>;
229
+ /** Read a field's `.meta()` metadata, unwrapping wrappers so metadata on the inner schema is found. */
230
+ declare function readMeta(schema: z.ZodType): Record<string, unknown>;
231
+ /** The stable base type of a field (after unwrapping optional/default/nullable). */
232
+ declare function baseType(field: z.ZodType): string;
233
+ /** Extract the per-key default values declared in the schema (the lowest cascade layer). */
234
+ declare function extractDefaults(schema: z.ZodObject<z.ZodRawShape>): Record<string, unknown>;
235
+ /** The type-directed default merge strategy for a field, overridable via `.meta({ mergeStrategy })`. */
236
+ declare function keyMergeStrategy(field: z.ZodType): MergeStrategy;
237
+ /**
238
+ * Classify a setting's sensitivity: `.meta()` override first, then field-name heuristics, then — for
239
+ * a container-valued setting (object/array/record/tuple/union) — a fail-safe recursion: if any
240
+ * nested field is secret, the WHOLE setting is classified `secret`, so masking/redaction protects
241
+ * the nested value (an irreducible nested value cannot be masked field-by-field). `field` is optional
242
+ * so out-of-schema (ad-hoc) layer keys are still name-classified rather than defaulting to `public`.
243
+ */
244
+ declare function classifySensitivity(key: string, field?: z.ZodType): Sensitivity;
245
+
246
+ /**
247
+ * Secret handling — the model-side of zodal's content/metadata bifurcation applied to settings.
248
+ *
249
+ * A setting classified `secret` (by name heuristic or `.meta({ secret: true })`) must never appear
250
+ * in the queryable config store, an exported layer/patch, or an audit log as plaintext. These pure
251
+ * helpers enforce that: `splitBySensitivity` routes secret values out of the config layer to a
252
+ * secret backend; `redactSecretsFromLayer` strips them from anything serialized for export/audit;
253
+ * `maskSecrets` replaces secret effective values with a masked `SecretRef`. The concrete secret
254
+ * backend (OS keychain / Vault / encrypted store) is a satellite store package; here we define the
255
+ * seam and the guarantees.
256
+ */
257
+
258
+ /** Build a masked reference to a secret value (never carries the plaintext). */
259
+ declare function makeSecretRef(key: SettingKey, isSet: boolean): SecretRef;
260
+ /**
261
+ * Replace the effective values of secret settings with a masked `SecretRef`. Pure. The plaintext is
262
+ * never copied into the output — reveal is an explicit, separate operation against the secret backend.
263
+ */
264
+ declare function maskSecrets(effective: Record<SettingKey, SettingValue>, sensitivityFor: (key: SettingKey) => Sensitivity): Record<SettingKey, SettingValue>;
265
+ /**
266
+ * Mask EVERY surface of a resolved result that could carry a secret as plaintext: `effective`,
267
+ * `provenance[key].value`, every `provenance[key].shadowed[].value`, and `conflicts[].contributors[].value`.
268
+ * This is the function `defineDials.resolve({ maskSecrets: true })` and `explain()` go through — so
269
+ * the audit/provenance path can never leak. Pure.
270
+ */
271
+ declare function maskEffectiveResult(result: EffectiveResult, sensitivityFor: (key: SettingKey) => Sensitivity): EffectiveResult;
272
+ /**
273
+ * Partition a layer into the config values (safe to persist/query) and the secret values (to route
274
+ * to a secret backend). Secret keys never appear in `config`. Pure.
275
+ */
276
+ declare function splitBySensitivity(layer: Layer, sensitivityFor: (key: SettingKey) => Sensitivity): {
277
+ config: Layer;
278
+ secrets: Layer;
279
+ };
280
+ /** Strip secret keys from a serialized layer before export/audit (they must never be written). */
281
+ declare function redactSecretsFromLayer(serialized: SerializedLayer, sensitivityFor: (key: SettingKey) => Sensitivity): SerializedLayer;
282
+ /**
283
+ * The contract a secret backend implements (OS keychain, Vault, encrypted store). Mirrors zodal's
284
+ * bifurcation "content provider". Reads return a masked ref; the plaintext is fetched only via an
285
+ * explicit `reveal`. Implemented by satellite `@zodal/dials-store-*` packages.
286
+ */
287
+ interface SecretBackend {
288
+ has(key: SettingKey): Promise<boolean>;
289
+ get(key: SettingKey): Promise<SecretRef>;
290
+ /** Explicit, audited plaintext reveal — separate from ordinary reads. */
291
+ reveal(key: SettingKey): Promise<string | undefined>;
292
+ set(key: SettingKey, value: string): Promise<SecretRef>;
293
+ delete(key: SettingKey): Promise<void>;
294
+ /** The keys this backend holds (so a bifurcation provider can surface masked refs on load). */
295
+ list(): Promise<SettingKey[]>;
296
+ }
297
+
298
+ /**
299
+ * Linked validation — one model of relations over setting keys, evaluated to VALIDATE. Hard
300
+ * constraints are authored in Zod (`.refine`/`.superRefine`, validated over the effective values)
301
+ * and/or as a serializable `{ message, keys, check }` list (the NixOS `assertions`/`warnings` shape,
302
+ * a mirror that could be exported to a CSP/SAT solver). Soft `warnings` are advisory and never fail.
303
+ * Evaluation is fail-fast: it returns all errors so the UI can show them at resolve time.
304
+ */
305
+
306
+ /** A serializable hard constraint: a predicate over the effective values plus a message + the keys it concerns. */
307
+ interface Assertion {
308
+ id?: string;
309
+ message: string;
310
+ /** The keys this constraint relates (for highlighting + solver export). */
311
+ keys?: SettingKey[];
312
+ /** The predicate: returns true when satisfied. A throw counts as unsatisfied. */
313
+ check: (values: Record<string, unknown>) => boolean;
314
+ }
315
+ /** A soft warning: advisory guidance shown when `when` holds. Never fails validation. */
316
+ interface Warning {
317
+ message: string;
318
+ keys?: SettingKey[];
319
+ when: (values: Record<string, unknown>) => boolean;
320
+ }
321
+ interface ConstraintsConfig {
322
+ /** A Zod schema validated against the effective values (cross-field via `.superRefine`). */
323
+ schema?: z.ZodType;
324
+ /** Declarative hard constraints (serializable mirror; solver-exportable). */
325
+ assertions?: Assertion[];
326
+ /** Soft, advisory warnings. */
327
+ warnings?: Warning[];
328
+ }
329
+ interface ConstraintError {
330
+ message: string;
331
+ keys: SettingKey[];
332
+ }
333
+ interface ConstraintResult {
334
+ ok: boolean;
335
+ errors: ConstraintError[];
336
+ warnings: string[];
337
+ }
338
+ /** Evaluate constraints + warnings over a resolved values map. Pure; collects all errors. */
339
+ declare function evaluateConstraints(values: Record<string, unknown>, config?: ConstraintsConfig): ConstraintResult;
340
+
341
+ /**
342
+ * Dependent (smart) defaults — the SUGGEST mode of the same field-relation model that constraints
343
+ * use to VALIDATE. A dependent default computes an advisory value for one key from the current
344
+ * values of others ("C is usually near f(A,B), but any C is valid"). It honors OVERRIDE-STICKINESS:
345
+ * once the user has set the target key (it is dirty), its dependent default is never recomputed, so
346
+ * a user's choice is never silently clobbered. Pure.
347
+ */
348
+
349
+ interface DependentDefault {
350
+ /** The key this default applies to. */
351
+ key: SettingKey;
352
+ /** Keys this default depends on (documented; drives recompute scheduling in reactive consumers). */
353
+ dependsOn: SettingKey[];
354
+ /** Compute the suggested value from the current values. Return `undefined` to suggest nothing. */
355
+ derive: (values: Record<string, unknown>) => unknown;
356
+ }
357
+ interface DeriveOptions {
358
+ /** Keys the user has explicitly set (dirty) — their dependent defaults are NOT recomputed. */
359
+ dirtyKeys?: Iterable<SettingKey>;
360
+ }
361
+ interface DeriveResult {
362
+ values: Record<string, unknown>;
363
+ /** Keys whose dependent default was applied this pass. */
364
+ applied: SettingKey[];
365
+ }
366
+ /**
367
+ * Fill dependent defaults into a values map, returning a new map. A dirty target key is never
368
+ * overwritten (stickiness). Defaults are applied in declaration order, each seeing the results of
369
+ * earlier ones.
370
+ */
371
+ declare function applyDependentDefaults(values: Record<string, unknown>, defaults: DependentDefault[], options?: DeriveOptions): DeriveResult;
372
+
373
+ /**
374
+ * `defineDials` — the entry point. A settings document is modeled as a degenerate one-item zodal
375
+ * collection, so the Zod schema is the single source of truth and zodal's `defineCollection` supplies
376
+ * per-field affordances (for the UI layer). On top of that this binds the cascade engine: declared
377
+ * defaults become the lowest scope, the type-directed merge strategy and sensitivity classification
378
+ * are precomputed per key, and `resolve`/`explain`/`validate`/`withDependentDefaults` operate over an
379
+ * ordered stack of scoped layers.
380
+ */
381
+
382
+ interface DefineDialsConfig<TSchema extends z.ZodObject<z.ZodRawShape> = z.ZodObject<z.ZodRawShape>> {
383
+ /** Hard/soft constraints evaluated over the effective values. */
384
+ constraints?: ConstraintsConfig;
385
+ /** Dependent (smart) defaults. */
386
+ dependentDefaults?: DependentDefault[];
387
+ /** Passed through to `defineCollection` (escape hatch for affordance config). */
388
+ collection?: CollectionConfig<z.infer<TSchema>>;
389
+ }
390
+ interface DialsResolveOptions {
391
+ /** Prepend the schema defaults as the lowest scope. Default: true. */
392
+ includeDefaults?: boolean;
393
+ /** Replace secret effective values with a masked `SecretRef`. Default: false. */
394
+ maskSecrets?: boolean;
395
+ }
396
+ interface DialsCapabilities {
397
+ keyCount: number;
398
+ hasSecrets: boolean;
399
+ hasConstraints: boolean;
400
+ hasDependentDefaults: boolean;
401
+ mergeStrategies: Record<string, MergeStrategy>;
402
+ }
403
+ interface DialsDefinition<TSchema extends z.ZodObject<z.ZodRawShape>> {
404
+ schema: TSchema;
405
+ /** The underlying zodal collection (affordance source for the UI). `undefined` if the schema is
406
+ * not a shape `defineCollection` accepts — the cascade does not depend on it. */
407
+ collection: CollectionDefinition<TSchema> | undefined;
408
+ /** The declared defaults (the lowest cascade layer). */
409
+ defaults: Record<string, unknown>;
410
+ keys: SettingKey[];
411
+ mergeStrategyFor(key: SettingKey): MergeStrategy;
412
+ sensitivityFor(key: SettingKey): Sensitivity;
413
+ /** Resolve an ordered stack of scoped layers (lowest precedence first). */
414
+ resolve(stack: ScopedLayer[], options?: DialsResolveOptions): EffectiveResult;
415
+ /** Explain how one key resolved (its provenance), or undefined if no scope set it. */
416
+ explain(key: SettingKey, stack: ScopedLayer[], options?: DialsResolveOptions): KeyProvenance | undefined;
417
+ /** Evaluate constraints over a resolved values map. */
418
+ validate(values: Record<string, unknown>): ConstraintResult;
419
+ /** Fill dependent defaults into a values map, honoring dirty stickiness. */
420
+ withDependentDefaults(values: Record<string, unknown>, dirtyKeys?: SettingKey[]): Record<string, unknown>;
421
+ getCapabilities(): DialsCapabilities;
422
+ }
423
+ /** Define a settings surface from a Zod object schema. */
424
+ declare function defineDials<TSchema extends z.ZodObject<z.ZodRawShape>>(schema: TSchema, config?: DefineDialsConfig<TSchema>): DialsDefinition<TSchema>;
425
+
426
+ /**
427
+ * The `LayerStore` contract — how a scope sources (and optionally persists) its layer. The cascade
428
+ * resolves an ordered stack of `{ scope, layer }`; a `LayerStore` is what produces one scope's layer
429
+ * from a backing source (env vars, a JSONC/TOML/YAML file, a remote store, a secret backend, …).
430
+ * Concrete adapters live in satellite `@zodal/dials-store-*` packages; this is just the interface +
431
+ * an honest capability report. Pure types — no runtime/Node dependency.
432
+ */
433
+
434
+ /** What a store can do, reported honestly (mirrors the spirit of zodal `ProviderCapabilities`). */
435
+ interface LayerStoreCapabilities {
436
+ /** Can produce a layer via `load()`. */
437
+ readable: boolean;
438
+ /** Can persist a layer via `save()`. */
439
+ writable: boolean;
440
+ /** Can notify on external change via `subscribe()`. */
441
+ watchable: boolean;
442
+ }
443
+ /** A source/sink for a single scope's layer. */
444
+ interface LayerStore {
445
+ /** The scope id this store provides a layer for (e.g. 'env', 'user', 'workspace'). */
446
+ readonly scope: string;
447
+ /** Honest capability report. */
448
+ getCapabilities(): LayerStoreCapabilities;
449
+ /** Load the current layer from the backing source. */
450
+ load(): Promise<Layer>;
451
+ /** Persist a layer to the backing source (present only when `writable`). */
452
+ save?(layer: Layer): Promise<void>;
453
+ /** Subscribe to external changes (present only when `watchable`); returns an unsubscribe function. */
454
+ subscribe?(onChange: (layer: Layer) => void): () => void;
455
+ }
456
+
457
+ export { type Assertion, type Conflict, type ConstraintError, type ConstraintResult, type ConstraintsConfig, type DefineDialsConfig, type DependentDefault, type DeriveOptions, type DeriveResult, type DialsCapabilities, type DialsDefinition, type DialsResolveOptions, type EffectiveResult, type JsonPatchOp, type KeyProvenance, type Layer, type LayerStore, type LayerStoreCapabilities, type MergeStrategy, type ResolveOptions, type ScopedLayer, type SecretBackend, type SecretRef, type Sensitivity, type SerializedLayer, type SettingKey, type SettingValue, type ShadowedLayer, UNSET, type Unset, type Warning, applyDependentDefaults, applyJsonPatch, applyMergePatch, baseType, classifySensitivity, defineDials, deserializeLayer, diffJsonPatch, evaluateConstraints, extractDefaults, getObjectShape, invertJsonPatch, isSecretRef, isUnset, keyMergeStrategy, layerToMergePatch, makeSecretRef, maskEffectiveResult, maskSecrets, mergeValues, readMeta, redactSecretsFromLayer, resolve, serializeLayer, splitBySensitivity };