@smthrs/plugin 0.0.0-stage → 1.0.0-rc.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.
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
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.3",
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.3",
149
+ "@smthrs/flow": "1.0.0-rc.3"
150
+ },
151
+ "devDependencies": {
152
+ "@effect/language-service": "0.87.2",
153
+ "@effect/vitest": "4.0.0-rc.115",
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.0-rc.115",
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.0-rc.115"
172
+ },
173
+ "scripts": {
174
+ "lint": "eslint src --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)
package/src/Hooks.ts ADDED
@@ -0,0 +1,212 @@
1
+ /**
2
+ * The typed hook surface: hook kinds, the hook entry shape, and the shared
3
+ * kernel's base hook catalog.
4
+ *
5
+ * The public package contract is documented at
6
+ * {@link https://smithers.sh/docs/reference/api/plugin}. The shared catalog is limited to
7
+ * `config` and `configResolved`;
8
+ * `@smthrs/agent` adds only `cellRegistry`, `cellFlows`, and
9
+ * `cellModelRequest`. Durable-core lifecycle policy is not a hook catalog.
10
+ *
11
+ * `FlowsHooks` is **open for augmentation, closed for dispatch**: a host
12
+ * declares its hooks with `declare module "@smthrs/plugin"`, supplies the
13
+ * matching runtime catalog, and dispatches only that bounded set.
14
+ *
15
+ * @since 1.0.0-rc.0
16
+ */
17
+
18
+ import type * as Effect from "effect/Effect"
19
+
20
+ /**
21
+ * Dispatch semantics of a hook, fixed by the core.
22
+ *
23
+ * @category models
24
+ * @since 1.0.0-rc.0
25
+ */
26
+ export type HookKind = "sequential" | "parallel" | "first" | "waterfall"
27
+
28
+ declare const HookTypeId: unique symbol
29
+
30
+ /**
31
+ * Phantom carrier that records a hook's kind and handler type in the type
32
+ * system without adding anything at runtime.
33
+ *
34
+ * @category models
35
+ * @since 1.0.0-rc.0
36
+ */
37
+ export interface HookMeta<K extends HookKind, F> {
38
+ readonly [HookTypeId]?: { readonly kind: K; readonly handler: F }
39
+ }
40
+
41
+ /**
42
+ * The object form of a hook entry: Vite's per-hook ordering object, verbatim.
43
+ *
44
+ * @category models
45
+ * @since 1.0.0-rc.0
46
+ */
47
+ export interface HookObject<F> {
48
+ readonly order?: "pre" | "post" | undefined
49
+ readonly handler: F
50
+ }
51
+
52
+ /**
53
+ * A hook entry is either the bare handler or `{ order?, handler }`.
54
+ *
55
+ * @category models
56
+ * @since 1.0.0-rc.0
57
+ */
58
+ export type HookEntry<K extends HookKind, F> = (F | HookObject<F>) & HookMeta<K, F>
59
+
60
+ /**
61
+ * Every handler runs, in resolved order, one at a time.
62
+ *
63
+ * @category models
64
+ * @since 1.0.0-rc.0
65
+ */
66
+ export type SequentialHook<F> = HookEntry<"sequential", F>
67
+
68
+ /**
69
+ * Every handler runs concurrently; results are ignored and failures are
70
+ * returned to the caller rather than failing it.
71
+ *
72
+ * @category models
73
+ * @since 1.0.0-rc.0
74
+ */
75
+ export type ParallelHook<F> = HookEntry<"parallel", F>
76
+
77
+ /**
78
+ * Handlers run in order until one returns `Option.some`.
79
+ *
80
+ * @category models
81
+ * @since 1.0.0-rc.0
82
+ */
83
+ export type FirstHook<F> = HookEntry<"first", F>
84
+
85
+ /**
86
+ * Each handler receives the previous handler's output.
87
+ *
88
+ * @category models
89
+ * @since 1.0.0-rc.0
90
+ */
91
+ export type WaterfallHook<F> = HookEntry<"waterfall", F>
92
+
93
+ /**
94
+ * Extracts the declared kind of a hook entry type.
95
+ *
96
+ * @category type-level
97
+ * @since 1.0.0-rc.0
98
+ */
99
+ export type KindOf<T> = T extends HookMeta<infer K, any> ? K : never
100
+
101
+ /**
102
+ * The runtime catalog a host supplies for a hook interface: each hook name
103
+ * mapped to its kind. A name declared in `H` must carry the kind its type
104
+ * declares. Names outside `H` are admitted with any kind, because the type
105
+ * system cannot enumerate an augmentable interface; resolution checks them
106
+ * at runtime and dispatch refuses a kind that disagrees with the catalog.
107
+ *
108
+ * @category type-level
109
+ * @since 1.0.0-rc.0
110
+ */
111
+ export type HookCatalog<H> =
112
+ & { readonly [K in keyof H & string]?: KindOf<H[K]> }
113
+ & { readonly [name: string]: HookKind | undefined }
114
+
115
+ /**
116
+ * Extracts the handler type of a hook entry type.
117
+ *
118
+ * @category type-level
119
+ * @since 1.0.0-rc.0
120
+ */
121
+ export type HandlerOf<T> = T extends HookMeta<any, infer F> ? F : never
122
+
123
+ /**
124
+ * Selects the hook names of a given kind from a hook interface.
125
+ *
126
+ * @category type-level
127
+ * @since 1.0.0-rc.0
128
+ */
129
+ export type KeysOfKind<H, K extends HookKind> =
130
+ & {
131
+ [P in keyof H]: KindOf<H[P]> extends K ? P : never
132
+ }[keyof H]
133
+ & string
134
+
135
+ /**
136
+ * Extracts the positional argument tuple accepted by a hook entry.
137
+ *
138
+ * @category type-level
139
+ * @since 1.0.0-rc.0
140
+ */
141
+ export type ArgsOf<T> = HandlerOf<T> extends (...args: infer A) => any ? A : never
142
+
143
+ /**
144
+ * Extracts the Effect returned by a hook entry.
145
+ *
146
+ * @category type-level
147
+ * @since 1.0.0-rc.0
148
+ */
149
+ export type ReturnOf<T> = HandlerOf<T> extends (...args: any) => infer R ? R : never
150
+
151
+ /**
152
+ * Extracts the success value returned by a hook entry's Effect.
153
+ *
154
+ * @category type-level
155
+ * @since 1.0.0-rc.0
156
+ */
157
+ export type SuccessOf<T> = ReturnOf<T> extends Effect.Effect<infer A, any, any> ? A : never
158
+
159
+ /**
160
+ * Extracts the context required by a hook entry's Effect.
161
+ *
162
+ * @category type-level
163
+ * @since 1.0.0-rc.0
164
+ */
165
+ export type ContextOf<T> = ReturnOf<T> extends Effect.Effect<any, any, infer R> ? R : never
166
+
167
+ /**
168
+ * Runtime catalog of the shared kernel's hook names and kinds.
169
+ *
170
+ * The type system cannot enumerate an augmentable interface, so this constant
171
+ * is what the `unknown_hook` guard checks against. A host passes its own
172
+ * superset to {@link resolve}. The cell harness adds only the hooks it
173
+ * dispatches; lifecycle and engine-policy hooks do not belong here.
174
+ *
175
+ * @category models
176
+ * @since 1.0.0-rc.0
177
+ */
178
+ const sharedHooks: { config: "waterfall"; configResolved: "parallel" } = Object.create(null)
179
+ Object.defineProperties(sharedHooks, {
180
+ config: { value: "waterfall", enumerable: true },
181
+ configResolved: { value: "parallel", enumerable: true }
182
+ })
183
+ /**
184
+ * Frozen runtime catalog for the shared configuration hooks.
185
+ *
186
+ * @category models
187
+ * @since 1.0.0-rc.0
188
+ */
189
+ export const engineHooks: Readonly<{
190
+ readonly config: "waterfall"
191
+ readonly configResolved: "parallel"
192
+ }> = Object.freeze(sharedHooks)
193
+
194
+ /**
195
+ * Normalizes a hook entry to its handler.
196
+ *
197
+ * @category utils
198
+ * @since 1.0.0-rc.0
199
+ */
200
+ export const handlerOf = (entry: unknown): (...args: Array<any>) => unknown =>
201
+ typeof entry === "function"
202
+ ? entry as (...args: Array<any>) => unknown
203
+ : (entry as HookObject<(...args: Array<any>) => unknown>).handler
204
+
205
+ /**
206
+ * Reads the per-hook `order` of a hook entry.
207
+ *
208
+ * @category utils
209
+ * @since 1.0.0-rc.0
210
+ */
211
+ export const orderOf = (entry: unknown): "pre" | "post" | undefined =>
212
+ typeof entry === "function" ? undefined : (entry as HookObject<unknown>).order