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

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 +86 -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 +160 -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 +343 -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 +19 -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 +178 -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 +367 -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 +19 -0
  90. package/dist/esm/internal/snapshotCacheEnvironment.d.ts.map +1 -0
  91. package/dist/esm/internal/snapshotCacheEnvironment.js +63 -0
  92. package/dist/esm/internal/snapshotCacheEnvironment.js.map +1 -0
  93. package/docs/README.md +131 -0
  94. package/docs/api.md +522 -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 +71 -0
  105. package/docs/quickstart.md +151 -0
  106. package/docs/troubleshooting.md +179 -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 +254 -0
  114. package/src/Resolve.ts +535 -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 +77 -0
@@ -0,0 +1,72 @@
1
+ ---
2
+ title: "Include a plugin conditionally"
3
+ description: "Use apply to select a plugin by host target or by a predicate over the pre-resolution configuration, and know what selection does not excuse."
4
+ sidebar:
5
+ order: 3
6
+ ---
7
+
8
+ A plugin can decide it does not belong in this composition. Declare `apply`,
9
+ and the kernel drops the plugin before it contributes a handler, a layer, or a
10
+ cache identity.
11
+
12
+ ## Select by host target
13
+
14
+ `apply: "engine"` and `apply: "harness"` name the host the plugin is for. The
15
+ kernel compares the literal with the `target` option, which defaults to
16
+ `"engine"`:
17
+
18
+ ```ts
19
+ import { Kernel, make } from "@smthrs/plugin"
20
+
21
+ const list = [
22
+ make({ name: "flows-plugin-shared" }),
23
+ make({ name: "flows-plugin-harness-only", apply: "harness" })
24
+ ]
25
+
26
+ // Engine kernel: only flows-plugin-shared is selected.
27
+ const engine = Kernel.make(list)
28
+
29
+ // Harness kernel: both are selected.
30
+ const harness = Kernel.make(list, {}, { target: "harness" })
31
+ ```
32
+
33
+ A plugin with no `apply` is always selected. This is how one shared preset
34
+ serves two hosts: list every plugin once, and let each host take its half.
35
+
36
+ ## Select by a predicate over the configuration
37
+
38
+ A predicate receives the pre-resolution configuration, the same value you passed
39
+ to `Kernel.make`, and returns a boolean:
40
+
41
+ ```ts
42
+ const fast = make({
43
+ name: "flows-plugin-fast",
44
+ apply: (config) => config["mode"] === "fast"
45
+ })
46
+
47
+ const kernel = Kernel.make([fast], { mode: "fast" })
48
+ ```
49
+
50
+ The configuration a predicate sees is a snapshot taken before the first
51
+ predicate runs, so a predicate cannot observe another predicate's side effects,
52
+ and a caller cannot mutate the object between two predicate calls.
53
+
54
+ It is the _pre-resolution_ configuration: the `config` waterfall has not run
55
+ yet, so a predicate cannot see a namespace another plugin is about to
56
+ contribute. Selection has to settle before the waterfall, because the waterfall
57
+ runs the plugins selection chose.
58
+
59
+ A predicate that throws fails resolution with `apply_failed`, and its raw
60
+ failure is not retained. A predicate that returns anything other than a boolean
61
+ fails with `invalid_plugin`. Keep predicates pure and total.
62
+
63
+ ## What selection does not excuse
64
+
65
+ Every plugin record is validated before `apply` runs, so a malformed plugin
66
+ fails startup even when the predicate would have excluded it. Selection cannot
67
+ hide a typo.
68
+
69
+ Hook names are checked after selection, and that asymmetry is deliberate: a
70
+ shared preset may carry `apply: "harness"` plugins whose hooks only the harness
71
+ catalog declares, and it still resolves under a bare engine kernel because those
72
+ plugins were never selected.
@@ -0,0 +1,149 @@
1
+ ---
2
+ title: "Test a plugin"
3
+ description: "Exercise a plugin without booting a host: resolve a list, dispatch a hook, assert on ordering and typed failures, and stub the kernel out of a composition under test."
4
+ sidebar:
5
+ order: 6
6
+ ---
7
+
8
+ A plugin is a plain record and the dispatcher is a plain service, so a plugin
9
+ test needs no host, no engine, and no I/O. Resolve the list, dispatch the hook,
10
+ assert on the result.
11
+
12
+ ## Dispatch a hook without a kernel
13
+
14
+ `Resolve.resolve` gives you the catalog and `Plugins.make` gives you the
15
+ dispatcher. Use them directly when the test is about one hook, with the hook
16
+ interface as an explicit type argument:
17
+
18
+ ```ts
19
+ import { type FlowsPlugin, Plugins, Resolve, type WaterfallHook } from "@smthrs/plugin"
20
+ import * as Effect from "effect/Effect"
21
+ import { expect, it } from "vitest"
22
+
23
+ interface HostHooks {
24
+ readonly tools: WaterfallHook<
25
+ (tools: ReadonlyArray<string>) => Effect.Effect<ReadonlyArray<string> | void>
26
+ >
27
+ }
28
+
29
+ const subject: FlowsPlugin<HostHooks> = {
30
+ name: "flows-plugin-subject",
31
+ hooks: { tools: (tools) => Effect.succeed([...tools, "read", "write"]) }
32
+ }
33
+
34
+ it("contributes its tools", async () => {
35
+ const resolved = await Effect.runPromise(
36
+ Resolve.resolve<HostHooks>([subject], { hooks: { tools: "waterfall" } })
37
+ )
38
+ const tools = await Effect.runPromise(
39
+ Plugins.make<HostHooks>(resolved).waterfall("tools", [] as ReadonlyArray<string>, (_previous, next) => next)
40
+ )
41
+ expect(tools).toEqual(["read", "write"])
42
+ })
43
+ ```
44
+
45
+ Use `Kernel.make` instead when the test is about the whole startup path: the
46
+ waterfall, the freeze, the observers, and the merged layer together.
47
+
48
+ ## Startup hooks see no services
49
+
50
+ The shared `config` and `configResolved` hooks are context-free: `Kernel.make`
51
+ runs them before any plugin layer exists and supplies nothing itself, so a
52
+ handler that reads a service does not compile against `FlowsHooks`, and one
53
+ cast past the type fails at runtime with `hook_failed` on that hook. Provide
54
+ the service inside the hook, or, when a host's startup hooks genuinely require
55
+ services, declare a separate hook interface and provide them to the startup
56
+ Effect. `Kernel.make<H>` carries the requirement as `Kernel.StartupContext<H>`:
57
+
58
+ ```ts
59
+ interface HostHooks {
60
+ readonly config: WaterfallHook<
61
+ (config: FlowsConfig) => Effect.Effect<Partial<FlowsConfig> | void, never, Settings>
62
+ >
63
+ readonly configResolved: ParallelHook<(config: ResolvedConfig) => Effect.Effect<void, never, Settings>>
64
+ }
65
+
66
+ const kernel = await Effect.runPromise(
67
+ Kernel.make<HostHooks>([reader], {}, { hooks: { config: "waterfall", configResolved: "parallel" } }).pipe(
68
+ Effect.provideService(Settings, { enabled: true })
69
+ )
70
+ )
71
+ ```
72
+
73
+ Without the `provideService`, the program does not compile.
74
+
75
+ ## Assert the order, not just the membership
76
+
77
+ `resolved.plugins` is the resolved plugin order and
78
+ `resolved.handlers.get(hook)` is the ordered handler list for one hook, each
79
+ record carrying the plugin that declared it:
80
+
81
+ ```ts
82
+ const names = (resolved: Resolve.Resolved, hook: string) =>
83
+ (resolved.handlers.get(hook) ?? []).map((record) => record.plugin)
84
+
85
+ expect(names(resolved, "configResolved")).toEqual(["early", "normal", "late"])
86
+ ```
87
+
88
+ Assert both when a plugin declares a per-hook `order`: the two lists differ on
89
+ purpose, and only the handler list proves the hook is positioned the way you
90
+ meant.
91
+
92
+ ## Assert typed failures by code
93
+
94
+ Every refusal is a `PluginError` with a `code`, and most carry the plugin, the
95
+ hook, and the value path. Flip the Effect and match the fields you care about:
96
+
97
+ ```ts
98
+ import { Kernel } from "@smthrs/plugin"
99
+
100
+ const error = await Effect.runPromise(
101
+ Kernel.make([{ name: "duplicate" }, { name: "duplicate" }]).pipe(Effect.flip)
102
+ )
103
+ expect(error).toMatchObject({ code: "duplicate_name", plugin: "duplicate" })
104
+ ```
105
+
106
+ Match on `code`, `plugin`, `hook`, and `path`, not on `message`. The codes are a
107
+ closed set, listed in [Troubleshooting](../troubleshooting.md); the messages are
108
+ prose and may be reworded.
109
+
110
+ ## Observer failures are a return value
111
+
112
+ A failing `configResolved` handler does not fail startup, so an assertion that
113
+ the kernel succeeded proves nothing about it. Read `observerErrors`:
114
+
115
+ ```ts
116
+ const kernel = await Effect.runPromise(
117
+ Kernel.make([{ name: "noisy", hooks: { configResolved: () => Effect.fail("boom") } }])
118
+ )
119
+ expect(kernel.observerErrors.map((error) => error.plugin)).toEqual(["noisy"])
120
+ ```
121
+
122
+ ## Stub the kernel out of a composition under test
123
+
124
+ When the subject is the host rather than the plugins, give it a dispatcher with
125
+ no plugins at all:
126
+
127
+ ```ts
128
+ const dispatcher = Plugins.makeNoop()
129
+ ```
130
+
131
+ `Plugins.layerNoop` is the same thing as a layer, for a composition that resolves
132
+ the shared `Plugins` service tag. Both dispatch nothing: a parallel hook returns
133
+ an empty error list, a first hook answers `Option.none()`, and a waterfall
134
+ returns the value it was given.
135
+
136
+ ## Test the pure pieces directly
137
+
138
+ `Config.merge` and `Config.deepFreeze` are ordinary functions that throw a
139
+ `PluginError` rather than returning an Effect, so a merge-semantics test needs no
140
+ runtime:
141
+
142
+ ```ts
143
+ import { Config } from "@smthrs/plugin"
144
+
145
+ expect(Config.merge({ a: { b: 1 } }, { a: { c: 2 } })).toEqual({ a: { b: 1, c: 2 } })
146
+ ```
147
+
148
+ `Hooks.handlerOf` and `Hooks.orderOf` read either hook entry form and are the
149
+ same shape of test.
@@ -0,0 +1,71 @@
1
+ ---
2
+ title: "Installation"
3
+ description: "Install @smthrs/plugin, its runtime requirements, its import forms, and the subpaths its export map blocks."
4
+ sidebar:
5
+ order: 1
6
+ ---
7
+
8
+ ## Install the package
9
+
10
+ Not on npm yet; see [Installation](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/flow/docs/installation.md#use-the-libraries).
11
+
12
+ Install `effect` explicitly at `4.0.0-rc.115`. The kernel depends on it, but your
13
+ plugins import `Effect`, `Layer`, and `Option` directly, so `effect` belongs in
14
+ your own dependency list at the release the kernel is built against. A different
15
+ `effect` major gives you two copies of the runtime and two sets of service tags.
16
+
17
+ The package requires Node.js 26.4.0 or later and ships as both ESM and
18
+ CommonJS with TypeScript declarations. Its runtime dependencies install with it:
19
+ [`@smthrs/flow`](/api/flow) supplies `Action.CacheEnvironment` for
20
+ [cache identity](./guides/cache-identity.md), and
21
+ [`@smthrs/canonical`](/api/canonical) supplies immutable catalog maps.
22
+
23
+ ## Import forms
24
+
25
+ The root entry point exports the plugin, hook, and error declarations directly,
26
+ and the four stateful modules as namespaces:
27
+
28
+ ```ts
29
+ import { Config, Kernel, Plugins, Resolve } from "@smthrs/plugin"
30
+ import type { FlowsHooks, FlowsPlugin, PluginInput } from "@smthrs/plugin"
31
+ import { engineHooks, make, PluginError } from "@smthrs/plugin"
32
+ ```
33
+
34
+ Each module is also importable from its own subpath, which is the form the
35
+ [API reference](./api.md) uses:
36
+
37
+ ```ts
38
+ import * as Config from "@smthrs/plugin/Config"
39
+ import * as Kernel from "@smthrs/plugin/Kernel"
40
+ import * as Plugins from "@smthrs/plugin/Plugins"
41
+ import * as Resolve from "@smthrs/plugin/Resolve"
42
+ ```
43
+
44
+ The full list of public subpaths is `@smthrs/plugin/Config`,
45
+ `@smthrs/plugin/Hooks`, `@smthrs/plugin/Kernel`, `@smthrs/plugin/Plugin`,
46
+ `@smthrs/plugin/PluginError`, `@smthrs/plugin/Plugins`, and
47
+ `@smthrs/plugin/Resolve`. Two forms are blocked in the export map:
48
+ `@smthrs/plugin/internal/*` and `@smthrs/plugin/*/index`.
49
+ `@smthrs/plugin/package.json` is exported.
50
+
51
+ ## The augmentation specifier
52
+
53
+ A host that declares its own hooks augments the interface in the root module,
54
+ never in a subpath:
55
+
56
+ ```ts
57
+ declare module "@smthrs/plugin" {
58
+ interface FlowsHooks {
59
+ // your hooks
60
+ }
61
+ }
62
+ ```
63
+
64
+ `FlowsHooks` is declared in the entry point precisely so this specifier works:
65
+ TypeScript can augment an interface only in the module that declares it. See
66
+ [Declare hooks for your host](./guides/host-your-own-hooks.md).
67
+
68
+ ## Next step
69
+
70
+ Build a host with one hook of its own and run it in the
71
+ [Quickstart](./quickstart.md).
@@ -0,0 +1,151 @@
1
+ ---
2
+ title: "Quickstart"
3
+ description: "Build a host with a hook of its own: declare the catalog, write two plugins, run the kernel, and dispatch the hook to a typed result."
4
+ sidebar:
5
+ order: 2
6
+ ---
7
+
8
+ This quickstart builds a small host end to end. The host offers plugins one
9
+ extension point of its own, a `tools` waterfall, alongside the kernel's config
10
+ lifecycle. By the end you will have run the kernel, read a frozen configuration,
11
+ and dispatched a hook whose result is typed by the declaration you wrote.
12
+
13
+ The example uses two files: `host.ts` declares the hook catalog, and `main.ts`
14
+ defines the plugins and runs the host. There are no services to provide.
15
+
16
+ ## Prerequisites
17
+
18
+ - Node.js 26.4.0 or later.
19
+ - A TypeScript project with the package installed. Not on npm yet; see [Installation](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/flow/docs/installation.md#use-the-libraries).
20
+
21
+ ## Declare the host's hook catalog
22
+
23
+ A host declares its hooks twice: once in the type system, so plugin authors get
24
+ completion and excess-key errors, and once as a runtime catalog, so the kernel
25
+ can refuse a hook name nobody dispatches.
26
+
27
+ Create `host.ts`:
28
+
29
+ ```ts
30
+ import { engineHooks, type WaterfallHook } from "@smthrs/plugin"
31
+ import type * as Effect from "effect/Effect"
32
+
33
+ declare module "@smthrs/plugin" {
34
+ interface FlowsHooks {
35
+ /** Contributes or rewrites the tool names the host offers. */
36
+ readonly tools: WaterfallHook<
37
+ (tools: ReadonlyArray<string>) => Effect.Effect<ReadonlyArray<string> | void>
38
+ >
39
+ }
40
+ }
41
+
42
+ /** The runtime catalog: the kernel's two config hooks plus this host's one. */
43
+ export const hooks = Object.freeze(
44
+ {
45
+ ...engineHooks,
46
+ tools: "waterfall"
47
+ } as const
48
+ )
49
+ ```
50
+
51
+ The two declarations must agree. The interface says what compiles; the runtime
52
+ catalog says what dispatches. `WaterfallHook` in the interface and
53
+ `"waterfall"` in the catalog are the same claim, written for the two audiences.
54
+
55
+ ## Write two plugins
56
+
57
+ Add all remaining TypeScript snippets to `main.ts` in order, starting with the
58
+ complete import block and the first plugin.
59
+
60
+ A plugin is a record. This one contributes a configuration namespace and two
61
+ tools.
62
+
63
+ Create `main.ts` beside `host.ts`:
64
+
65
+ ```ts
66
+ import { type FlowsHooks, Kernel, make } from "@smthrs/plugin"
67
+ import * as Effect from "effect/Effect"
68
+ import { hooks } from "./host.ts"
69
+
70
+ const editor = make<FlowsHooks>({
71
+ name: "flows-plugin-editor",
72
+ hooks: {
73
+ config: () => Effect.succeed({ editor: { readOnly: false } }),
74
+ tools: (tools) => Effect.succeed([...tools, "read", "write"])
75
+ }
76
+ })
77
+ ```
78
+
79
+ `make` is an identity function that pins the literal to `FlowsPlugin<H>`. Pass
80
+ the hook interface explicitly, as above: it is what types the `tools` parameter
81
+ as `ReadonlyArray<string>` and what makes a misspelled hook key fail at the
82
+ definition site instead of at startup.
83
+
84
+ The second plugin runs first, because it is declared `enforce: "pre"`, and it
85
+ reads what the host resolved rather than adding to it:
86
+
87
+ ```ts
88
+ const audit = make<FlowsHooks>({
89
+ name: "flows-plugin-audit",
90
+ enforce: "pre",
91
+ hooks: {
92
+ configResolved: (config) => Effect.log(`resolved namespaces: ${Object.keys(config).join(", ")}`)
93
+ }
94
+ })
95
+ ```
96
+
97
+ ## Run the kernel
98
+
99
+ `Kernel.make` takes the plugin list, the pre-resolution configuration, and the
100
+ host's options. It resolves the list, runs the config waterfall, freezes the
101
+ result, notifies the observers, and merges the plugin layers:
102
+
103
+ ```ts
104
+ const kernel = await Effect.runPromise(
105
+ Kernel.make([editor, audit], { host: { name: "quickstart" } }, { target: "harness", hooks })
106
+ )
107
+
108
+ console.log(kernel.config)
109
+ // { host: { name: "quickstart" }, editor: { readOnly: false } }
110
+ ```
111
+
112
+ `target` names which kind of host you are, and it is what a plugin's
113
+ `apply: "engine" | "harness"` selector is compared against. It defaults to
114
+ `"engine"`, meaning the Smithers durable engine, so pass `"harness"` for any
115
+ other host, including this one.
116
+
117
+ ## Dispatch the hook
118
+
119
+ `kernel.plugins` is the dispatcher. A waterfall needs one more argument than the
120
+ other kinds, the merge function that folds each handler's answer into the value
121
+ the next handler sees:
122
+
123
+ ```ts
124
+ const tools = await Effect.runPromise(
125
+ kernel.plugins.waterfall("tools", [] as ReadonlyArray<string>, (_previous, next) => next)
126
+ )
127
+
128
+ console.log(tools) // ["read", "write"]
129
+ ```
130
+
131
+ The literal `"tools"` is checked against the hook interface: a name that is not
132
+ a waterfall in `FlowsHooks` does not compile, and `tools` is typed as the
133
+ handler's argument type, not as `unknown`.
134
+
135
+ ## What you just built
136
+
137
+ - A hook catalog only your host dispatches. Another host augmenting the same
138
+ interface still dispatches only the catalog it supplied.
139
+ - A plugin list resolved once into a frozen catalog. Mutating `editor` after
140
+ this point changes nothing about dispatch.
141
+ - A configuration every plugin agreed on, deeply copied and frozen before the
142
+ first plugin saw it.
143
+
144
+ ## Next steps
145
+
146
+ - [Hook kinds](./concepts/hook-kinds.md) for what `sequential`, `parallel`,
147
+ `first`, and `waterfall` each promise.
148
+ - [Control the order handlers run in](./guides/order-handlers.md) for `enforce`
149
+ and the per-hook `order`.
150
+ - [Contribute services from a plugin](./guides/contribute-services.md) to add a
151
+ `layer` to the plugin above.
@@ -0,0 +1,179 @@
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. In both cases no handler ran.
49
+
50
+ **What to change.** Make the catalog agree with the hook interface: spread
51
+ `engineHooks` for the shared hooks and write each of your own hooks with the
52
+ kind its type declares. `HookCatalog<H>` refuses a wrong kind for a declared
53
+ name at compile time.
54
+
55
+ ## invalid_plugin
56
+
57
+ **What happened.** A plugin, preset, option, or hook entry has a shape the
58
+ kernel will not accept. Common causes, each reported at its own path:
59
+
60
+ - A hook entry that is `undefined` or `null`. Declarations must be callable, so
61
+ a conditionally built hook is refused rather than skipped.
62
+ - An unknown property on a plugin record or on the options object.
63
+ - A name or version that is empty, whitespace only, longer than 256 UTF-16 code
64
+ units, or not well-formed text.
65
+ - `enforce` or `order` that is not `"pre"` or `"post"`.
66
+ - `apply` that is not `"engine"`, `"harness"`, or a function.
67
+ - `layer` that is not an Effect `Layer`.
68
+ - A preset array that is sparse, cyclic, repeated, or carries extra properties.
69
+ - An accessor, a symbol key, or a non-enumerable property anywhere in the
70
+ record.
71
+
72
+ **What to change.** Read `path`, fix that value. To omit an optional hook, leave
73
+ the key out of the `hooks` record instead of setting it to `undefined`.
74
+
75
+ ## apply_failed
76
+
77
+ **What happened.** An `apply` predicate threw. The raw failure is not retained,
78
+ because a predicate runs before the kernel has anywhere safe to put an arbitrary
79
+ value.
80
+
81
+ **What to change.** Make the predicate total. It receives the pre-resolution
82
+ configuration and must return a boolean without reading anything it cannot
83
+ guarantee is there. A predicate that returns a non-boolean fails with
84
+ `invalid_plugin` instead.
85
+
86
+ ## config_invalid
87
+
88
+ **What happened.** A configuration value is not bounded strict JSON, or it uses
89
+ a reserved root key. This covers the pre-resolution config, any patch a `config`
90
+ handler returned, and the merged result.
91
+
92
+ **What to change.** Read `path`. The frequent causes:
93
+
94
+ - `$.engine`, `$.retry`, `$.store`, or `$.plugins`: rename the namespace. The
95
+ kernel does not apply those policies and refuses to look like it does.
96
+ - An `undefined` member: omit the key, or write `null`. `{ endpoint: undefined }`
97
+ fails at `$.endpoint` rather than resolving to `{}`.
98
+ - A `Date`, `Map`, class instance, or non-finite number: encode it as JSON, or
99
+ keep it in a service instead of in configuration.
100
+ - `__proto__`, `constructor`, or `prototype` as a key at any depth.
101
+ - A tree over 1 MiB, 64 deep, 4,096 members, or 8,192 values. See
102
+ [Configuration](./concepts/configuration.md) for the whole list.
103
+
104
+ When a `config` handler returned the offending patch, the error also carries
105
+ that plugin and hook.
106
+
107
+ ## cache_environment_invalid
108
+
109
+ **What happened.** You declared `cacheEnvironment` and the kernel could not
110
+ build a complete identity from it. Either a selected plugin has no `version`, or
111
+ the environment does not match the `Action.CacheEnvironment` schema.
112
+
113
+ **What to change.** Add a `version` to every selected plugin, or drop
114
+ `cacheEnvironment` and let sealed keys stay run-local. Both are correct; a bare
115
+ plugin name folded into a cross-run cache key is not. See
116
+ [Declare a cache identity](./guides/cache-identity.md).
117
+
118
+ ## invalid_hook_result
119
+
120
+ **What happened.** A `first` hook handler returned something other than an
121
+ `Option`. The error names the plugin and the hook.
122
+
123
+ **What to change.** Return `Option.some(value)` to answer and `Option.none()` to
124
+ pass. A `first` hook stops at the first `some`, so returning a bare value would
125
+ make "no opinion" indistinguishable from "the answer is undefined".
126
+
127
+ ## resource_limit
128
+
129
+ **What happened.** The plugin input, the handler count, or a concurrency option
130
+ exceeded a published bound: 256 plugins, 1,024 handlers, 4,096 input nodes, 64
131
+ nested preset arrays, or a `parallelConcurrency` outside 1 through 256.
132
+
133
+ **What to change.** Split the composition, or raise nothing: these bounds are
134
+ fixed. A list this large is usually a preset included twice or an array building
135
+ itself in a loop. The [limits table](./concepts/resolution.md) has each bound
136
+ and the export that names it.
137
+
138
+ ## hook_failed
139
+
140
+ **What happened.** A handler failed, defected, threw synchronously, or returned
141
+ something that is not an Effect. The error names the plugin and the hook, and
142
+ the original failure is on `cause`.
143
+
144
+ **What to change.** Fix the handler. Where it surfaces depends on the kind: a
145
+ `sequential`, `first`, or `waterfall` hook fails the caller and stops dispatch
146
+ at that handler, while a `parallel` hook returns the error and keeps going. See
147
+ [Hook kinds](./concepts/hook-kinds.md).
148
+
149
+ ## layer_failed
150
+
151
+ **What happened.** A plugin's layer failed while it was being built. The error
152
+ names the plugin, with the layer's own failure on `cause`.
153
+
154
+ **What to change.** Fix the layer's acquisition, or make the plugin conditional
155
+ so it is not selected in compositions where its dependencies are absent. The
156
+ failure arrives when something provides `kernel.layer`, not when `Kernel.make`
157
+ returns.
158
+
159
+ ## Failures that are silent by design
160
+
161
+ **A `configResolved` handler failed and nothing happened.** Parallel observers
162
+ are lossy on purpose: their failures are collected, not raised. Read
163
+ `kernel.observerErrors` and log them. A composition that ignores that array
164
+ cannot tell a working observer from a broken one.
165
+
166
+ **A plugin was dropped and no error was reported.** Exclusion is not a failure.
167
+ `apply: "harness"` under the default `target: "engine"` drops the plugin
168
+ silently, which is exactly what a shared preset needs. Check
169
+ `kernel.plugins.resolved.plugins` for what was actually selected, and pass
170
+ `{ target: "harness" }` if your host is a harness.
171
+
172
+ **A hook never runs even though it resolved.** The kernel dispatches only the
173
+ config lifecycle. Every other hook runs when the host calls the dispatcher for
174
+ it, so a hook in the catalog that no host code dispatches is a handler nobody
175
+ calls.
176
+
177
+ **Mutating a plugin or a config object after startup changed nothing.**
178
+ Resolution copies everything it accepts. Dispatch reads the kernel's own frozen
179
+ records, never yours. Change the input and resolve again.