@smthrs/plugin 0.0.0-stage → 1.0.0-rc.13

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.
Files changed (118) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/LICENSE +21 -0
  3. package/README.md +88 -2
  4. package/dist/cjs/Config.d.ts +129 -0
  5. package/dist/cjs/Config.d.ts.map +1 -0
  6. package/dist/cjs/Config.js +115 -0
  7. package/dist/cjs/Config.js.map +7 -0
  8. package/dist/cjs/Hooks.d.ts +176 -0
  9. package/dist/cjs/Hooks.d.ts.map +1 -0
  10. package/dist/cjs/Hooks.js +34 -0
  11. package/dist/cjs/Hooks.js.map +7 -0
  12. package/dist/cjs/Kernel.d.ts +81 -0
  13. package/dist/cjs/Kernel.d.ts.map +1 -0
  14. package/dist/cjs/Kernel.js +57 -0
  15. package/dist/cjs/Kernel.js.map +7 -0
  16. package/dist/cjs/Plugin.d.ts +71 -0
  17. package/dist/cjs/Plugin.d.ts.map +1 -0
  18. package/dist/cjs/Plugin.js +25 -0
  19. package/dist/cjs/Plugin.js.map +7 -0
  20. package/dist/cjs/PluginError.d.ts +50 -0
  21. package/dist/cjs/PluginError.d.ts.map +1 -0
  22. package/dist/cjs/PluginError.js +58 -0
  23. package/dist/cjs/PluginError.js.map +7 -0
  24. package/dist/cjs/Plugins.d.ts +97 -0
  25. package/dist/cjs/Plugins.d.ts.map +1 -0
  26. package/dist/cjs/Plugins.js +169 -0
  27. package/dist/cjs/Plugins.js.map +7 -0
  28. package/dist/cjs/Resolve.d.ts +128 -0
  29. package/dist/cjs/Resolve.d.ts.map +1 -0
  30. package/dist/cjs/Resolve.js +346 -0
  31. package/dist/cjs/Resolve.js.map +7 -0
  32. package/dist/cjs/index.d.ts +83 -0
  33. package/dist/cjs/index.d.ts.map +1 -0
  34. package/dist/cjs/index.js +45 -0
  35. package/dist/cjs/index.js.map +7 -0
  36. package/dist/cjs/internal/Boundary.d.ts +96 -0
  37. package/dist/cjs/internal/Boundary.d.ts.map +1 -0
  38. package/dist/cjs/internal/Boundary.js +129 -0
  39. package/dist/cjs/internal/Boundary.js.map +7 -0
  40. package/dist/cjs/internal/mergePluginLayers.d.ts +21 -0
  41. package/dist/cjs/internal/mergePluginLayers.d.ts.map +1 -0
  42. package/dist/cjs/internal/mergePluginLayers.js +57 -0
  43. package/dist/cjs/internal/mergePluginLayers.js.map +7 -0
  44. package/dist/cjs/internal/snapshotCacheEnvironment.d.ts +23 -0
  45. package/dist/cjs/internal/snapshotCacheEnvironment.d.ts.map +1 -0
  46. package/dist/cjs/internal/snapshotCacheEnvironment.js +92 -0
  47. package/dist/cjs/internal/snapshotCacheEnvironment.js.map +7 -0
  48. package/dist/cjs/package.json +1 -0
  49. package/dist/esm/Config.d.ts +129 -0
  50. package/dist/esm/Config.d.ts.map +1 -0
  51. package/dist/esm/Config.js +160 -0
  52. package/dist/esm/Config.js.map +1 -0
  53. package/dist/esm/Hooks.d.ts +176 -0
  54. package/dist/esm/Hooks.d.ts.map +1 -0
  55. package/dist/esm/Hooks.js +56 -0
  56. package/dist/esm/Hooks.js.map +1 -0
  57. package/dist/esm/Kernel.d.ts +81 -0
  58. package/dist/esm/Kernel.d.ts.map +1 -0
  59. package/dist/esm/Kernel.js +58 -0
  60. package/dist/esm/Kernel.js.map +1 -0
  61. package/dist/esm/Plugin.d.ts +71 -0
  62. package/dist/esm/Plugin.d.ts.map +1 -0
  63. package/dist/esm/Plugin.js +20 -0
  64. package/dist/esm/Plugin.js.map +1 -0
  65. package/dist/esm/PluginError.d.ts +50 -0
  66. package/dist/esm/PluginError.d.ts.map +1 -0
  67. package/dist/esm/PluginError.js +53 -0
  68. package/dist/esm/PluginError.js.map +1 -0
  69. package/dist/esm/Plugins.d.ts +97 -0
  70. package/dist/esm/Plugins.d.ts.map +1 -0
  71. package/dist/esm/Plugins.js +190 -0
  72. package/dist/esm/Plugins.js.map +1 -0
  73. package/dist/esm/Resolve.d.ts +128 -0
  74. package/dist/esm/Resolve.d.ts.map +1 -0
  75. package/dist/esm/Resolve.js +369 -0
  76. package/dist/esm/Resolve.js.map +1 -0
  77. package/dist/esm/index.d.ts +83 -0
  78. package/dist/esm/index.d.ts.map +1 -0
  79. package/dist/esm/index.js +48 -0
  80. package/dist/esm/index.js.map +1 -0
  81. package/dist/esm/internal/Boundary.d.ts +96 -0
  82. package/dist/esm/internal/Boundary.d.ts.map +1 -0
  83. package/dist/esm/internal/Boundary.js +162 -0
  84. package/dist/esm/internal/Boundary.js.map +1 -0
  85. package/dist/esm/internal/mergePluginLayers.d.ts +21 -0
  86. package/dist/esm/internal/mergePluginLayers.d.ts.map +1 -0
  87. package/dist/esm/internal/mergePluginLayers.js +41 -0
  88. package/dist/esm/internal/mergePluginLayers.js.map +1 -0
  89. package/dist/esm/internal/snapshotCacheEnvironment.d.ts +23 -0
  90. package/dist/esm/internal/snapshotCacheEnvironment.d.ts.map +1 -0
  91. package/dist/esm/internal/snapshotCacheEnvironment.js +64 -0
  92. package/dist/esm/internal/snapshotCacheEnvironment.js.map +1 -0
  93. package/docs/README.md +133 -0
  94. package/docs/api.md +524 -0
  95. package/docs/concepts/configuration.md +111 -0
  96. package/docs/concepts/hook-kinds.md +107 -0
  97. package/docs/concepts/resolution.md +132 -0
  98. package/docs/guides/cache-identity.md +106 -0
  99. package/docs/guides/contribute-services.md +84 -0
  100. package/docs/guides/host-your-own-hooks.md +129 -0
  101. package/docs/guides/order-handlers.md +76 -0
  102. package/docs/guides/select-plugins.md +72 -0
  103. package/docs/guides/testing.md +149 -0
  104. package/docs/installation.md +75 -0
  105. package/docs/quickstart.md +152 -0
  106. package/docs/troubleshooting.md +180 -0
  107. package/package.json +179 -3
  108. package/src/Config.ts +207 -0
  109. package/src/Hooks.ts +212 -0
  110. package/src/Kernel.ts +119 -0
  111. package/src/Plugin.ts +80 -0
  112. package/src/PluginError.ts +62 -0
  113. package/src/Plugins.ts +271 -0
  114. package/src/Resolve.ts +539 -0
  115. package/src/index.ts +91 -0
  116. package/src/internal/Boundary.ts +252 -0
  117. package/src/internal/mergePluginLayers.ts +57 -0
  118. package/src/internal/snapshotCacheEnvironment.ts +78 -0
@@ -0,0 +1,180 @@
1
+ ---
2
+ title: "Troubleshooting"
3
+ description: "Every PluginError code the kernel reports, what causes it, and what to change, plus the failures that are silent by design."
4
+ ---
5
+
6
+ Every refusal this package makes is a `PluginError` with a `code` from a closed
7
+ set. Find the code and read its section. The full error schema is in the
8
+ [API reference](./api.md).
9
+
10
+ Most errors carry `plugin`, `hook`, and `path` alongside the message. Read
11
+ `path` first: it is a diagnostic dollar-path into the value the kernel refused,
12
+ not an RFC 6901 JSON Pointer. `$` is the input, `.config` an identifier
13
+ property, `[2]` an array index, and `["not an identifier"]` any other key,
14
+ JSON-quoted. So `$[2].hooks.config` is the third plugin's `config` entry and
15
+ `$options.parallelConcurrency` is an option.
16
+
17
+ ## duplicate_name
18
+
19
+ **What happened.** Two selected plugins have the same name. Names compare as
20
+ exact Unicode strings with no normalization, so this is a real collision, not a
21
+ spelling variant.
22
+
23
+ **What to change.** Rename one, or exclude one with
24
+ [`apply`](./guides/select-plugins.md). The kernel refuses rather than letting
25
+ the last one win, because last-wins turns a duplicated preset into a silently
26
+ dropped plugin.
27
+
28
+ ## unknown_hook
29
+
30
+ **What happened.** A selected plugin declares a hook name that is not in the
31
+ host's runtime catalog. The error names the plugin and the hook.
32
+
33
+ **What to change.** If the hook is yours to add, list it in the catalog you pass
34
+ as `Resolve.Options.hooks` and augment `FlowsHooks` to match. See
35
+ [Declare hooks for your host](./guides/host-your-own-hooks.md). If the hook
36
+ belongs to a different host, give the plugin an `apply` selector so this host
37
+ does not select it.
38
+
39
+ Passing no catalog means the shared one: `config` and `configResolved` only.
40
+
41
+ ## hook_kind_mismatch
42
+
43
+ **What happened.** The runtime catalog and the kind actually run disagree.
44
+ At startup, `Options.hooks` labelled `config` with a kind other than
45
+ `waterfall`, or `configResolved` with a kind other than `parallel`; the path
46
+ names the entry, such as `$options.hooks.config`. At dispatch, a method was
47
+ called for a hook the catalog declares with a different kind, which only a cast
48
+ around the typed dispatcher can reach, or for a hook a hand-built `Resolved`
49
+ gave handlers without declaring it in `kinds`. In every case no handler ran.
50
+
51
+ **What to change.** Make the catalog agree with the hook interface: spread
52
+ `engineHooks` for the shared hooks and write each of your own hooks with the
53
+ kind its type declares. `HookCatalog<H>` refuses a wrong kind for a declared
54
+ name at compile time.
55
+
56
+ ## invalid_plugin
57
+
58
+ **What happened.** A plugin, preset, option, or hook entry has a shape the
59
+ kernel will not accept. Common causes, each reported at its own path:
60
+
61
+ - A hook entry that is `undefined` or `null`. Declarations must be callable, so
62
+ a conditionally built hook is refused rather than skipped.
63
+ - An unknown property on a plugin record or on the options object.
64
+ - A name or version that is empty, whitespace only, longer than 256 UTF-16 code
65
+ units, or not well-formed text.
66
+ - `enforce` or `order` that is not `"pre"` or `"post"`.
67
+ - `apply` that is not `"engine"`, `"harness"`, or a function.
68
+ - `layer` that is not an Effect `Layer`.
69
+ - A preset array that is sparse, cyclic, repeated, or carries extra properties.
70
+ - An accessor, a symbol key, or a non-enumerable property anywhere in the
71
+ record.
72
+
73
+ **What to change.** Read `path`, fix that value. To omit an optional hook, leave
74
+ the key out of the `hooks` record instead of setting it to `undefined`.
75
+
76
+ ## apply_failed
77
+
78
+ **What happened.** An `apply` predicate threw. The raw failure is not retained,
79
+ because a predicate runs before the kernel has anywhere safe to put an arbitrary
80
+ value.
81
+
82
+ **What to change.** Make the predicate total. It receives the pre-resolution
83
+ configuration and must return a boolean without reading anything it cannot
84
+ guarantee is there. A predicate that returns a non-boolean fails with
85
+ `invalid_plugin` instead.
86
+
87
+ ## config_invalid
88
+
89
+ **What happened.** A configuration value is not bounded strict JSON, or it uses
90
+ a reserved root key. This covers the pre-resolution config, any patch a `config`
91
+ handler returned, and the merged result.
92
+
93
+ **What to change.** Read `path`. The frequent causes:
94
+
95
+ - `$.engine`, `$.retry`, `$.store`, or `$.plugins`: rename the namespace. The
96
+ kernel does not apply those policies and refuses to look like it does.
97
+ - An `undefined` member: omit the key, or write `null`. `{ endpoint: undefined }`
98
+ fails at `$.endpoint` rather than resolving to `{}`.
99
+ - A `Date`, `Map`, class instance, or non-finite number: encode it as JSON, or
100
+ keep it in a service instead of in configuration.
101
+ - `__proto__`, `constructor`, or `prototype` as a key at any depth.
102
+ - A tree over 1 MiB, 64 deep, 4,096 members, or 8,192 values. See
103
+ [Configuration](./concepts/configuration.md) for the whole list.
104
+
105
+ When a `config` handler returned the offending patch, the error also carries
106
+ that plugin and hook.
107
+
108
+ ## cache_environment_invalid
109
+
110
+ **What happened.** You declared `cacheEnvironment` and the kernel could not
111
+ build a complete identity from it. Either a selected plugin has no `version`, or
112
+ the environment does not match the `Action.CacheEnvironment` schema.
113
+
114
+ **What to change.** Add a `version` to every selected plugin, or drop
115
+ `cacheEnvironment` and let sealed keys stay run-local. Both are correct; a bare
116
+ plugin name folded into a cross-run cache key is not. See
117
+ [Declare a cache identity](./guides/cache-identity.md).
118
+
119
+ ## invalid_hook_result
120
+
121
+ **What happened.** A `first` hook handler returned something other than an
122
+ `Option`. The error names the plugin and the hook.
123
+
124
+ **What to change.** Return `Option.some(value)` to answer and `Option.none()` to
125
+ pass. A `first` hook stops at the first `some`, so returning a bare value would
126
+ make "no opinion" indistinguishable from "the answer is undefined".
127
+
128
+ ## resource_limit
129
+
130
+ **What happened.** The plugin input, the handler count, or a concurrency option
131
+ exceeded a published bound: 256 plugins, 1,024 handlers, 4,096 input nodes, 64
132
+ nested preset arrays, or a `parallelConcurrency` outside 1 through 256.
133
+
134
+ **What to change.** Split the composition, or raise nothing: these bounds are
135
+ fixed. A list this large is usually a preset included twice or an array building
136
+ itself in a loop. The [limits table](./concepts/resolution.md) has each bound
137
+ and the export that names it.
138
+
139
+ ## hook_failed
140
+
141
+ **What happened.** A handler failed, defected, threw synchronously, or returned
142
+ something that is not an Effect. The error names the plugin and the hook, and
143
+ the original failure is on `cause`.
144
+
145
+ **What to change.** Fix the handler. Where it surfaces depends on the kind: a
146
+ `sequential`, `first`, or `waterfall` hook fails the caller and stops dispatch
147
+ at that handler, while a `parallel` hook returns the error and keeps going. See
148
+ [Hook kinds](./concepts/hook-kinds.md).
149
+
150
+ ## layer_failed
151
+
152
+ **What happened.** A plugin's layer failed while it was being built. The error
153
+ names the plugin, with the layer's own failure on `cause`.
154
+
155
+ **What to change.** Fix the layer's acquisition, or make the plugin conditional
156
+ so it is not selected in compositions where its dependencies are absent. The
157
+ failure arrives when something provides `kernel.layer`, not when `Kernel.make`
158
+ returns.
159
+
160
+ ## Failures that are silent by design
161
+
162
+ **A `configResolved` handler failed and nothing happened.** Parallel observers
163
+ are lossy on purpose: their failures are collected, not raised. Read
164
+ `kernel.observerErrors` and log them. A composition that ignores that array
165
+ cannot tell a working observer from a broken one.
166
+
167
+ **A plugin was dropped and no error was reported.** Exclusion is not a failure.
168
+ `apply: "harness"` under the default `target: "engine"` drops the plugin
169
+ silently, which is exactly what a shared preset needs. Check
170
+ `kernel.plugins.resolved.plugins` for what was actually selected, and pass
171
+ `{ target: "harness" }` if your host is a harness.
172
+
173
+ **A hook never runs even though it resolved.** The kernel dispatches only the
174
+ config lifecycle. Every other hook runs when the host calls the dispatcher for
175
+ it, so a hook in the catalog that no host code dispatches is a handler nobody
176
+ calls.
177
+
178
+ **Mutating a plugin or a config object after startup changed nothing.**
179
+ Resolution copies everything it accepts. Dispatch reads the kernel's own frozen
180
+ records, never yours. Change the input and resolve again.
package/package.json CHANGED
@@ -1,6 +1,182 @@
1
1
  {
2
2
  "name": "@smthrs/plugin",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "type": "module",
4
+ "version": "1.0.0-rc.13",
5
+ "license": "MIT",
6
+ "smthrs": {
7
+ "group": "agent"
8
+ },
9
+ "description": "Vite-style typed plugin kernel for flows: hooks, resolution, and config execution",
10
+ "homepage": "https://github.com/smithersai/smithers",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/smithersai/smithers.git",
14
+ "directory": "packages/smithers/agent/plugin"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/smithersai/smithers/issues"
18
+ },
19
+ "tags": [
20
+ "typescript",
21
+ "effect",
22
+ "plugin",
23
+ "hooks"
24
+ ],
25
+ "keywords": [
26
+ "typescript",
27
+ "effect",
28
+ "plugin",
29
+ "hooks"
30
+ ],
31
+ "engines": {
32
+ "node": ">=26.4.0"
33
+ },
34
+ "sideEffects": [],
35
+ "exports": {
36
+ "./package.json": "./package.json",
37
+ ".": {
38
+ "import": {
39
+ "types": "./dist/esm/index.d.ts",
40
+ "default": "./dist/esm/index.js"
41
+ },
42
+ "require": {
43
+ "types": "./dist/cjs/index.d.ts",
44
+ "default": "./dist/cjs/index.js"
45
+ }
46
+ },
47
+ "./internal/*": null,
48
+ "./*/index": null,
49
+ "./Config": {
50
+ "import": {
51
+ "types": "./dist/esm/Config.d.ts",
52
+ "default": "./dist/esm/Config.js"
53
+ },
54
+ "require": {
55
+ "types": "./dist/cjs/Config.d.ts",
56
+ "default": "./dist/cjs/Config.js"
57
+ }
58
+ },
59
+ "./Hooks": {
60
+ "import": {
61
+ "types": "./dist/esm/Hooks.d.ts",
62
+ "default": "./dist/esm/Hooks.js"
63
+ },
64
+ "require": {
65
+ "types": "./dist/cjs/Hooks.d.ts",
66
+ "default": "./dist/cjs/Hooks.js"
67
+ }
68
+ },
69
+ "./Kernel": {
70
+ "import": {
71
+ "types": "./dist/esm/Kernel.d.ts",
72
+ "default": "./dist/esm/Kernel.js"
73
+ },
74
+ "require": {
75
+ "types": "./dist/cjs/Kernel.d.ts",
76
+ "default": "./dist/cjs/Kernel.js"
77
+ }
78
+ },
79
+ "./Plugin": {
80
+ "import": {
81
+ "types": "./dist/esm/Plugin.d.ts",
82
+ "default": "./dist/esm/Plugin.js"
83
+ },
84
+ "require": {
85
+ "types": "./dist/cjs/Plugin.d.ts",
86
+ "default": "./dist/cjs/Plugin.js"
87
+ }
88
+ },
89
+ "./PluginError": {
90
+ "import": {
91
+ "types": "./dist/esm/PluginError.d.ts",
92
+ "default": "./dist/esm/PluginError.js"
93
+ },
94
+ "require": {
95
+ "types": "./dist/cjs/PluginError.d.ts",
96
+ "default": "./dist/cjs/PluginError.js"
97
+ }
98
+ },
99
+ "./Plugins": {
100
+ "import": {
101
+ "types": "./dist/esm/Plugins.d.ts",
102
+ "default": "./dist/esm/Plugins.js"
103
+ },
104
+ "require": {
105
+ "types": "./dist/cjs/Plugins.d.ts",
106
+ "default": "./dist/cjs/Plugins.js"
107
+ }
108
+ },
109
+ "./Resolve": {
110
+ "import": {
111
+ "types": "./dist/esm/Resolve.d.ts",
112
+ "default": "./dist/esm/Resolve.js"
113
+ },
114
+ "require": {
115
+ "types": "./dist/cjs/Resolve.d.ts",
116
+ "default": "./dist/cjs/Resolve.js"
117
+ }
118
+ },
119
+ "./index": {
120
+ "import": {
121
+ "types": "./dist/esm/index.d.ts",
122
+ "default": "./dist/esm/index.js"
123
+ },
124
+ "require": {
125
+ "types": "./dist/cjs/index.d.ts",
126
+ "default": "./dist/cjs/index.js"
127
+ }
128
+ }
129
+ },
130
+ "files": [
131
+ "src/**/*.ts",
132
+ "dist/**/*.js",
133
+ "dist/**/*.js.map",
134
+ "dist/**/*.d.ts",
135
+ "dist/**/*.d.ts.map",
136
+ "dist/**/package.json",
137
+ "LICENSE",
138
+ "README.md",
139
+ "CHANGELOG.md",
140
+ "docs/"
141
+ ],
142
+ "publishConfig": {
143
+ "access": "public",
144
+ "provenance": true,
145
+ "tag": "next"
146
+ },
147
+ "dependencies": {
148
+ "@smthrs/canonical": "1.0.0-rc.13",
149
+ "@smthrs/flow": "1.0.0-rc.13"
150
+ },
151
+ "devDependencies": {
152
+ "@effect/language-service": "0.87.2",
153
+ "@effect/vitest": "4.0.5",
154
+ "@eslint/js": "9.39.5",
155
+ "@types/node": "26.4.1",
156
+ "@vitest/coverage-v8": "5.0.0",
157
+ "dprint": "0.57.1",
158
+ "effect": "4.0.5",
159
+ "esbuild": "0.28.2",
160
+ "eslint": "9.39.5",
161
+ "eslint-import-resolver-typescript": "4.4.5",
162
+ "eslint-plugin-import": "2.32.0",
163
+ "eslint-plugin-jsdoc": "64.3.4",
164
+ "eslint-plugin-unicorn": "65.0.0",
165
+ "madge": "8.0.0",
166
+ "typescript": "5.9.3",
167
+ "typescript-eslint": "8.69.0",
168
+ "vitest": "5.0.0"
169
+ },
170
+ "peerDependencies": {
171
+ "effect": "4.0.5"
172
+ },
173
+ "scripts": {
174
+ "lint": "eslint src examples --max-warnings=0 && dprint check",
175
+ "format": "dprint fmt",
176
+ "build": "node scripts/build.mjs",
177
+ "check": "tsc -b tsconfig.json && tsc -p tsconfig.test.json --noEmit",
178
+ "circular": "node scripts/circular.mjs",
179
+ "test": "vitest",
180
+ "coverage": "vitest --coverage"
181
+ }
6
182
  }
package/src/Config.ts ADDED
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Bounded JSON configuration consumed and produced by the plugin kernel.
3
+ *
4
+ * Configuration is an extension namespace, not an engine-policy surface.
5
+ * Durable retry, concurrency, and storage policy stay on their owning Effect
6
+ * services and constructor options. Every accepted value is copied before any
7
+ * plugin observes it, then frozen recursively.
8
+ *
9
+ * @since 1.0.0-rc.0
10
+ */
11
+
12
+ import * as Effect from "effect/Effect"
13
+ import * as Schema from "effect/Schema"
14
+ import * as SchemaGetter from "effect/SchemaGetter"
15
+ import * as SchemaIssue from "effect/SchemaIssue"
16
+ import * as Boundary from "./internal/Boundary.ts"
17
+ import { PluginError } from "./PluginError.ts"
18
+
19
+ /**
20
+ * Maximum encoded bytes accepted for one plugin configuration.
21
+ *
22
+ * @category limits
23
+ * @since 1.0.0-rc.0
24
+ */
25
+ export const maximumConfigBytes = Boundary.defaultLimits.maxBytes
26
+
27
+ /**
28
+ * Maximum nesting accepted for one plugin configuration.
29
+ *
30
+ * @category limits
31
+ * @since 1.0.0-rc.0
32
+ */
33
+ export const maximumConfigDepth = Boundary.defaultLimits.maxDepth
34
+
35
+ /**
36
+ * Maximum aggregate members accepted for one plugin configuration.
37
+ *
38
+ * @category limits
39
+ * @since 1.0.0-rc.0
40
+ */
41
+ export const maximumConfigMembers = Boundary.defaultLimits.maxMembers
42
+
43
+ /**
44
+ * Maximum aggregate values accepted for one plugin configuration.
45
+ *
46
+ * @category limits
47
+ * @since 1.0.0-rc.0
48
+ */
49
+ export const maximumConfigNodes = Boundary.defaultLimits.maxNodes
50
+
51
+ /**
52
+ * One value accepted in a plugin-owned configuration namespace.
53
+ *
54
+ * @category models
55
+ * @since 1.0.0-rc.0
56
+ */
57
+ export const ConfigValue = Schema.Json
58
+
59
+ /**
60
+ * One value accepted in a plugin-owned configuration namespace.
61
+ *
62
+ * @category models
63
+ * @since 1.0.0-rc.0
64
+ */
65
+ export type ConfigValue = typeof ConfigValue.Type
66
+
67
+ const admittedConfigs = new WeakSet<object>()
68
+
69
+ const configurationSchema = Schema.Unknown.pipe(
70
+ Schema.decodeTo(
71
+ Schema.declare<Readonly<Record<string, ConfigValue>>>((value): value is Readonly<Record<string, ConfigValue>> =>
72
+ typeof value === "object" && value !== null && admittedConfigs.has(value)
73
+ ),
74
+ {
75
+ decode: SchemaGetter.transformEffect((value) =>
76
+ Effect.try({
77
+ try: () => snapshotRecord(value),
78
+ catch: (cause) => {
79
+ const error = cause as PluginError
80
+ return new SchemaIssue.InvalidValue({ message: `${error.path}: ${error.message}` })
81
+ }
82
+ })
83
+ ),
84
+ encode: SchemaGetter.transform((value) => value)
85
+ }
86
+ )
87
+ )
88
+
89
+ /**
90
+ * Admits pre-resolution configuration as a detached, recursively frozen snapshot.
91
+ *
92
+ * Keys name plugin-owned namespaces. `engine`, `retry`, `store`, and
93
+ * `plugins` are refused because accepting those names would imply policy the
94
+ * plugin kernel does not own or apply.
95
+ *
96
+ * @category models
97
+ * @since 1.0.0-rc.0
98
+ */
99
+ export const FlowsConfig = configurationSchema
100
+
101
+ /**
102
+ * The decoded form of {@link FlowsConfig}.
103
+ *
104
+ * @category models
105
+ * @since 1.0.0-rc.0
106
+ */
107
+ export type FlowsConfig = typeof FlowsConfig.Type
108
+
109
+ /**
110
+ * Admits the detached, recursively frozen configuration handed to plugins.
111
+ *
112
+ * @category models
113
+ * @since 1.0.0-rc.0
114
+ */
115
+ export const ResolvedConfig = configurationSchema
116
+
117
+ /**
118
+ * The decoded form of {@link ResolvedConfig}.
119
+ *
120
+ * @category models
121
+ * @since 1.0.0-rc.0
122
+ */
123
+ export type ResolvedConfig = typeof ResolvedConfig.Type
124
+
125
+ const reservedRootKeys = new Set(["engine", "retry", "store", "plugins"])
126
+
127
+ const invalid = (path: string, complaint: string): PluginError =>
128
+ new PluginError({
129
+ code: "config_invalid",
130
+ message: `plugin configuration ${complaint}`,
131
+ path
132
+ })
133
+
134
+ const snapshotRecord = (input: unknown, reuse = true): Readonly<Record<string, ConfigValue>> => {
135
+ if (reuse && typeof input === "object" && input !== null && admittedConfigs.has(input)) return input as ResolvedConfig
136
+ const admitted = Boundary.record(input)
137
+ if (!admitted.ok) throw invalid(admitted.path, admitted.complaint)
138
+ const record = admitted.value as Readonly<Record<string, ConfigValue>>
139
+ for (const key of Object.keys(record)) {
140
+ if (reservedRootKeys.has(key)) {
141
+ throw invalid(`$.${key}`, "uses a runtime-policy key that the plugin kernel does not own")
142
+ }
143
+ }
144
+ admittedConfigs.add(record)
145
+ return record
146
+ }
147
+
148
+ /**
149
+ * Empty defaults. Engine policy is deliberately not defaulted here.
150
+ *
151
+ * @category constructors
152
+ * @since 1.0.0-rc.0
153
+ */
154
+ export const defaults: ResolvedConfig = snapshotRecord({})
155
+
156
+ /**
157
+ * Copies and deep-merges a configuration patch over a base configuration.
158
+ *
159
+ * Records merge key-by-key and every other JSON value replaces wholesale.
160
+ * Raw operands are admitted; known snapshots retain unchanged subtrees.
161
+ * Cached subtree totals enforce merged output bounds. Unsafe property names,
162
+ * accessors, cycles, exotic prototypes, and aggregate values outside the
163
+ * resource limits fail with `config_invalid`.
164
+ *
165
+ * @category combinators
166
+ * @since 1.0.0-rc.0
167
+ */
168
+ export const merge = (base: FlowsConfig, patch: unknown): FlowsConfig => {
169
+ const safeBase = snapshotRecord(base)
170
+ if (patch === undefined) return safeBase
171
+ // Even a known patch is copied so its tree cannot alias the retained base.
172
+ const safePatch = snapshotRecord(patch, false)
173
+ const merged = Boundary.mergeRecords(safeBase, safePatch)
174
+ if (!merged.ok) throw invalid(merged.path, merged.complaint)
175
+ const record = merged.value as ResolvedConfig
176
+ admittedConfigs.add(record)
177
+ return record
178
+ }
179
+
180
+ /**
181
+ * Copies and freezes one JSON value without retaining caller-owned objects.
182
+ *
183
+ * @category utils
184
+ * @since 1.0.0-rc.0
185
+ */
186
+ export const deepFreeze = <A extends ConfigValue>(value: A): A => {
187
+ const admitted = Boundary.admit(value)
188
+ if (!admitted.ok) throw invalid(admitted.path, admitted.complaint)
189
+ return admitted.value as A
190
+ }
191
+
192
+ /**
193
+ * Admits a raw pre-resolution configuration as an immutable snapshot.
194
+ *
195
+ * @category constructors
196
+ * @since 1.0.0-rc.0
197
+ */
198
+ export const snapshot = (config: unknown): Effect.Effect<FlowsConfig, PluginError> =>
199
+ Effect.try({ try: () => snapshotRecord(config), catch: (cause) => cause as PluginError })
200
+
201
+ /**
202
+ * Decodes the post-waterfall configuration into its final immutable form.
203
+ *
204
+ * @category constructors
205
+ * @since 1.0.0-rc.0
206
+ */
207
+ export const resolve = (config: unknown): Effect.Effect<ResolvedConfig, PluginError> => snapshot(config)