@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
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Inert JSON admission for plugin configuration and cache identity.
3
+ *
4
+ * Reflection is descriptor-only: accessors never execute, hostile proxies are
5
+ * refused, and accepted values are detached before they are frozen.
6
+ *
7
+ * @private
8
+ * @since 1.0.0-rc.0
9
+ */
10
+ import * as BoundedJson from "@smthrs/canonical/BoundedJson";
11
+ /**
12
+ * Strict JSON value accepted by the boundary.
13
+ *
14
+ * @private
15
+ * @since 1.0.0-rc.0
16
+ */
17
+ export type Json = BoundedJson.Json;
18
+ /**
19
+ * Resource bounds for one admitted tree.
20
+ *
21
+ * @private
22
+ * @since 1.0.0-rc.0
23
+ */
24
+ export type Limits = BoundedJson.StrictLimits;
25
+ /**
26
+ * Successful admission.
27
+ *
28
+ * @private
29
+ * @since 1.0.0-rc.0
30
+ */
31
+ export interface AdmissionSuccess {
32
+ readonly ok: true;
33
+ readonly value: Json;
34
+ }
35
+ /**
36
+ * Refusal with a stable value path.
37
+ *
38
+ * @private
39
+ * @since 1.0.0-rc.0
40
+ */
41
+ export interface AdmissionFailure {
42
+ readonly ok: false;
43
+ readonly path: string;
44
+ readonly complaint: string;
45
+ }
46
+ /**
47
+ * Result of inert JSON admission.
48
+ *
49
+ * @private
50
+ * @since 1.0.0-rc.0
51
+ */
52
+ export type Admission = AdmissionSuccess | AdmissionFailure;
53
+ /**
54
+ * Default configuration bounds.
55
+ *
56
+ * @private
57
+ * @since 1.0.0-rc.0
58
+ */
59
+ export declare const defaultLimits: Limits;
60
+ /**
61
+ * Tests whether a string contains paired UTF-16 surrogate units.
62
+ *
63
+ * @private
64
+ * @since 1.0.0-rc.0
65
+ */
66
+ export declare const isWellFormedText: (value: string) => boolean;
67
+ /**
68
+ * Copies one strict JSON tree under explicit resource bounds.
69
+ *
70
+ * @private
71
+ * @since 1.0.0-rc.0
72
+ */
73
+ export declare const admit: (input: unknown, limits?: Limits) => Admission;
74
+ /**
75
+ * Admits only a JSON record at the root.
76
+ *
77
+ * @private
78
+ * @since 1.0.0-rc.0
79
+ */
80
+ export declare const record: (input: unknown, limits?: Limits) => AdmissionSuccess | AdmissionFailure;
81
+ /**
82
+ * Merges two independently admitted records, retaining unchanged subtrees.
83
+ * Both operands must come from successful admission, and the patch must be a
84
+ * fresh copy: their trees must be disjoint. Each output branch then comes from
85
+ * at most one location in either tree, preserving the no-repeated-reference
86
+ * invariant without walking retained subtrees. Limits apply to the result.
87
+ *
88
+ * @private
89
+ * @since 1.0.0-rc.0
90
+ */
91
+ export declare const mergeRecords: (base: {
92
+ readonly [key: string]: Json;
93
+ }, patch: {
94
+ readonly [key: string]: Json;
95
+ }, limits?: Limits) => Admission;
96
+ //# sourceMappingURL=Boundary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Boundary.d.ts","sourceRoot":"","sources":["../../../src/internal/Boundary.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,WAAW,MAAM,+BAA+B,CAAA;AAE5D;;;;;GAKG;AACH,MAAM,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,CAAA;AAEnC;;;;;GAKG;AACH,MAAM,MAAM,MAAM,GAAG,WAAW,CAAC,YAAY,CAAA;AAE7C;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAA;IACjB,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAA;CACrB;AAED;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAA;IAClB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,gBAAgB,GAAG,gBAAgB,CAAA;AAE3D;;;;;GAKG;AACH,eAAO,MAAM,aAAa,EAAE,MAO1B,CAAA;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAAI,OAAO,MAAM,KAAG,OAA+B,CAAA;AA4EhF;;;;;GAKG;AACH,eAAO,MAAM,KAAK,GAAI,OAAO,OAAO,EAAE,SAAQ,MAAsB,KAAG,SAatE,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,MAAM,GACjB,OAAO,OAAO,EACd,SAAQ,MAAsB,KAC7B,gBAAgB,GAAG,gBAKrB,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,YAAY,GACvB,MAAM;IAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EACtC,OAAO;IAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EACvC,SAAQ,MAAsB,KAC7B,SA4CF,CAAA"}
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Inert JSON admission for plugin configuration and cache identity.
3
+ *
4
+ * Reflection is descriptor-only: accessors never execute, hostile proxies are
5
+ * refused, and accepted values are detached before they are frozen.
6
+ *
7
+ * @private
8
+ * @since 1.0.0-rc.0
9
+ */
10
+ import * as BoundedJson from "@smthrs/canonical/BoundedJson";
11
+ /**
12
+ * Default configuration bounds.
13
+ *
14
+ * @private
15
+ * @since 1.0.0-rc.0
16
+ */
17
+ export const defaultLimits = Object.freeze({
18
+ maxBytes: 1024 * 1024,
19
+ maxDepth: 64,
20
+ maxMembers: 4_096,
21
+ maxNodes: 8_192,
22
+ maxStringBytes: 64 * 1024,
23
+ maxKeyBytes: 1_024
24
+ });
25
+ /**
26
+ * Tests whether a string contains paired UTF-16 surrogate units.
27
+ *
28
+ * @private
29
+ * @since 1.0.0-rc.0
30
+ */
31
+ export const isWellFormedText = (value) => value.isWellFormed();
32
+ const childPath = (path, key) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? `${path}.${key}` : `${path}[${JSON.stringify(key)}]`;
33
+ const encodedStringBytes = (value) => BoundedJson.encodedStringBytes(value);
34
+ // Paths contain only well-formed text and JSON-escaped keys. Count their JSON
35
+ // bytes without encoding the whole path, reserving quotes and a truncation marker.
36
+ const boundedPath = (path) => {
37
+ let bytes = 2 + 3;
38
+ let prefix = "";
39
+ for (const character of path) {
40
+ const point = character.codePointAt(0);
41
+ bytes += character === "\"" || character === "\\"
42
+ ? 2
43
+ : point <= 0x7f
44
+ ? 1
45
+ : point <= 0x7ff
46
+ ? 2
47
+ : point <= 0xffff
48
+ ? 3
49
+ : 4;
50
+ if (bytes > 192)
51
+ return `${prefix}...`;
52
+ prefix += character;
53
+ }
54
+ return path;
55
+ };
56
+ // Only detached containers constructed here enter this map. Mutable caller
57
+ // objects and Object.freeze alone never establish admission.
58
+ const snapshots = new WeakMap();
59
+ const scalarTotals = (bytes) => ({ bytes, members: 0, nodes: 1, depth: 0 });
60
+ const total = (entries) => {
61
+ const totals = { bytes: 2 + Math.max(0, entries.size - 1), members: entries.size, nodes: 1, depth: 1 };
62
+ for (const entry of entries.values()) {
63
+ totals.bytes += entry.keyBytes + entry.totals.bytes;
64
+ totals.members += entry.totals.members;
65
+ totals.nodes += entry.totals.nodes;
66
+ totals.depth = Math.max(totals.depth, 1 + entry.totals.depth);
67
+ }
68
+ return totals;
69
+ };
70
+ const remember = (value) => {
71
+ const entries = new Map();
72
+ for (const [key, child] of Object.entries(value)) {
73
+ entries.set(key, {
74
+ keyBytes: Array.isArray(value) ? 0 : encodedStringBytes(key) + 1,
75
+ totals: typeof child === "object" && child !== null ?
76
+ snapshots.get(child).totals
77
+ : scalarTotals(typeof child === "string" ? encodedStringBytes(child) : String(child).length)
78
+ });
79
+ }
80
+ snapshots.set(value, { totals: total(entries), entries });
81
+ };
82
+ /**
83
+ * Copies one strict JSON tree under explicit resource bounds.
84
+ *
85
+ * @private
86
+ * @since 1.0.0-rc.0
87
+ */
88
+ export const admit = (input, limits = defaultLimits) => {
89
+ const result = BoundedJson.admitStrict(input, limits, {
90
+ ordinaryRecords: true,
91
+ boundedText: true,
92
+ onContainer: remember
93
+ });
94
+ return result.ok ? result : {
95
+ ...result,
96
+ path: boundedPath(result.path),
97
+ complaint: result.complaint === "must be dense and have no extra properties"
98
+ ? "must be a dense array with no extra properties" :
99
+ result.complaint
100
+ };
101
+ };
102
+ /**
103
+ * Admits only a JSON record at the root.
104
+ *
105
+ * @private
106
+ * @since 1.0.0-rc.0
107
+ */
108
+ export const record = (input, limits = defaultLimits) => {
109
+ const admitted = admit(input, limits);
110
+ return admitted.ok && (admitted.value === null || Array.isArray(admitted.value) || typeof admitted.value !== "object")
111
+ ? { ok: false, path: "$", complaint: "must be a JSON record" }
112
+ : admitted;
113
+ };
114
+ /**
115
+ * Merges two independently admitted records, retaining unchanged subtrees.
116
+ * Both operands must come from successful admission, and the patch must be a
117
+ * fresh copy: their trees must be disjoint. Each output branch then comes from
118
+ * at most one location in either tree, preserving the no-repeated-reference
119
+ * invariant without walking retained subtrees. Limits apply to the result.
120
+ *
121
+ * @private
122
+ * @since 1.0.0-rc.0
123
+ */
124
+ export const mergeRecords = (base, patch, limits = defaultLimits) => {
125
+ const merge = (left, right, path) => {
126
+ const entries = new Map(snapshots.get(left).entries);
127
+ const result = { ...left };
128
+ for (const [key, entry] of snapshots.get(right).entries) {
129
+ const previous = left[key];
130
+ const next = right[key];
131
+ if (typeof previous === "object" && previous !== null && !Array.isArray(previous) &&
132
+ typeof next === "object" && next !== null && !Array.isArray(next)) {
133
+ const merged = merge(previous, next, childPath(path, key));
134
+ if (!merged.ok)
135
+ return merged;
136
+ result[key] = merged.value;
137
+ entries.set(key, { keyBytes: entry.keyBytes, totals: snapshots.get(merged.value).totals });
138
+ }
139
+ else {
140
+ result[key] = next;
141
+ entries.set(key, entry);
142
+ }
143
+ }
144
+ const totals = total(entries);
145
+ const complaint = totals.members > limits.maxMembers ?
146
+ `exceeds the ${limits.maxMembers}-member limit`
147
+ : totals.nodes > limits.maxNodes ?
148
+ `exceeds the ${limits.maxNodes}-node limit`
149
+ : totals.bytes > limits.maxBytes ?
150
+ `exceeds the ${limits.maxBytes}-byte limit`
151
+ : totals.depth > limits.maxDepth ?
152
+ `exceeds the depth limit of ${limits.maxDepth}`
153
+ : undefined;
154
+ if (complaint !== undefined)
155
+ return { ok: false, path: boundedPath(path), complaint };
156
+ Object.freeze(result);
157
+ snapshots.set(result, { totals, entries });
158
+ return { ok: true, value: result };
159
+ };
160
+ return merge(base, patch, "$");
161
+ };
162
+ //# sourceMappingURL=Boundary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Boundary.js","sourceRoot":"","sources":["../../../src/internal/Boundary.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Inert JSON admission for plugin configuration and cache identity.\n *\n * Reflection is descriptor-only: accessors never execute, hostile proxies are\n * refused, and accepted values are detached before they are frozen.\n *\n * @private\n * @since 1.0.0-rc.0\n */\n\nimport * as BoundedJson from \"@smthrs/canonical/BoundedJson\"\n\n/**\n * Strict JSON value accepted by the boundary.\n *\n * @private\n * @since 1.0.0-rc.0\n */\nexport type Json = BoundedJson.Json\n\n/**\n * Resource bounds for one admitted tree.\n *\n * @private\n * @since 1.0.0-rc.0\n */\nexport type Limits = BoundedJson.StrictLimits\n\n/**\n * Successful admission.\n *\n * @private\n * @since 1.0.0-rc.0\n */\nexport interface AdmissionSuccess {\n readonly ok: true\n readonly value: Json\n}\n\n/**\n * Refusal with a stable value path.\n *\n * @private\n * @since 1.0.0-rc.0\n */\nexport interface AdmissionFailure {\n readonly ok: false\n readonly path: string\n readonly complaint: string\n}\n\n/**\n * Result of inert JSON admission.\n *\n * @private\n * @since 1.0.0-rc.0\n */\nexport type Admission = AdmissionSuccess | AdmissionFailure\n\n/**\n * Default configuration bounds.\n *\n * @private\n * @since 1.0.0-rc.0\n */\nexport const defaultLimits: Limits = Object.freeze({\n maxBytes: 1024 * 1024,\n maxDepth: 64,\n maxMembers: 4_096,\n maxNodes: 8_192,\n maxStringBytes: 64 * 1024,\n maxKeyBytes: 1_024\n})\n\n/**\n * Tests whether a string contains paired UTF-16 surrogate units.\n *\n * @private\n * @since 1.0.0-rc.0\n */\nexport const isWellFormedText = (value: string): boolean => value.isWellFormed()\n\nconst childPath = (path: string, key: string): string =>\n /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? `${path}.${key}` : `${path}[${JSON.stringify(key)}]`\n\nconst encodedStringBytes = (value: string): number => BoundedJson.encodedStringBytes(value)!\n\n// Paths contain only well-formed text and JSON-escaped keys. Count their JSON\n// bytes without encoding the whole path, reserving quotes and a truncation marker.\nconst boundedPath = (path: string): string => {\n let bytes = 2 + 3\n let prefix = \"\"\n for (const character of path) {\n const point = character.codePointAt(0)!\n bytes += character === \"\\\"\" || character === \"\\\\\"\n ? 2\n : point <= 0x7f\n ? 1\n : point <= 0x7ff\n ? 2\n : point <= 0xffff\n ? 3\n : 4\n if (bytes > 192) return `${prefix}...`\n prefix += character\n }\n return path\n}\n\ninterface Totals {\n bytes: number\n members: number\n nodes: number\n depth: number\n}\n\ninterface Entry {\n readonly keyBytes: number\n readonly totals: Totals\n}\n\ninterface Snapshot {\n readonly totals: Totals\n readonly entries: Map<string, Entry>\n}\n\n// Only detached containers constructed here enter this map. Mutable caller\n// objects and Object.freeze alone never establish admission.\nconst snapshots = new WeakMap<object, Snapshot>()\n\nconst scalarTotals = (bytes: number): Totals => ({ bytes, members: 0, nodes: 1, depth: 0 })\n\nconst total = (entries: ReadonlyMap<string, Entry>): Totals => {\n const totals: Totals = { bytes: 2 + Math.max(0, entries.size - 1), members: entries.size, nodes: 1, depth: 1 }\n for (const entry of entries.values()) {\n totals.bytes += entry.keyBytes + entry.totals.bytes\n totals.members += entry.totals.members\n totals.nodes += entry.totals.nodes\n totals.depth = Math.max(totals.depth, 1 + entry.totals.depth)\n }\n return totals\n}\n\nconst remember = (value: ReadonlyArray<Json> | { readonly [key: string]: Json }): void => {\n const entries = new Map<string, Entry>()\n for (const [key, child] of Object.entries(value)) {\n entries.set(key, {\n keyBytes: Array.isArray(value) ? 0 : encodedStringBytes(key) + 1,\n totals: typeof child === \"object\" && child !== null ?\n snapshots.get(child)!.totals\n : scalarTotals(typeof child === \"string\" ? encodedStringBytes(child) : String(child).length)\n })\n }\n snapshots.set(value, { totals: total(entries), entries })\n}\n\n/**\n * Copies one strict JSON tree under explicit resource bounds.\n *\n * @private\n * @since 1.0.0-rc.0\n */\nexport const admit = (input: unknown, limits: Limits = defaultLimits): Admission => {\n const result = BoundedJson.admitStrict(input, limits, {\n ordinaryRecords: true,\n boundedText: true,\n onContainer: remember\n })\n return result.ok ? result : {\n ...result,\n path: boundedPath(result.path),\n complaint: result.complaint === \"must be dense and have no extra properties\"\n ? \"must be a dense array with no extra properties\" :\n result.complaint\n }\n}\n\n/**\n * Admits only a JSON record at the root.\n *\n * @private\n * @since 1.0.0-rc.0\n */\nexport const record = (\n input: unknown,\n limits: Limits = defaultLimits\n): AdmissionSuccess | AdmissionFailure => {\n const admitted = admit(input, limits)\n return admitted.ok && (admitted.value === null || Array.isArray(admitted.value) || typeof admitted.value !== \"object\")\n ? { ok: false, path: \"$\", complaint: \"must be a JSON record\" }\n : admitted\n}\n\n/**\n * Merges two independently admitted records, retaining unchanged subtrees.\n * Both operands must come from successful admission, and the patch must be a\n * fresh copy: their trees must be disjoint. Each output branch then comes from\n * at most one location in either tree, preserving the no-repeated-reference\n * invariant without walking retained subtrees. Limits apply to the result.\n *\n * @private\n * @since 1.0.0-rc.0\n */\nexport const mergeRecords = (\n base: { readonly [key: string]: Json },\n patch: { readonly [key: string]: Json },\n limits: Limits = defaultLimits\n): Admission => {\n const merge = (\n left: { readonly [key: string]: Json },\n right: { readonly [key: string]: Json },\n path: string\n ): Admission => {\n const entries = new Map(snapshots.get(left)!.entries)\n const result = { ...left }\n for (const [key, entry] of snapshots.get(right)!.entries) {\n const previous = left[key]\n const next = right[key]!\n if (\n typeof previous === \"object\" && previous !== null && !Array.isArray(previous) &&\n typeof next === \"object\" && next !== null && !Array.isArray(next)\n ) {\n const merged = merge(\n previous as { readonly [key: string]: Json },\n next as { readonly [key: string]: Json },\n childPath(path, key)\n )\n if (!merged.ok) return merged\n result[key] = merged.value\n entries.set(key, { keyBytes: entry.keyBytes, totals: snapshots.get(merged.value as object)!.totals })\n } else {\n result[key] = next\n entries.set(key, entry)\n }\n }\n const totals = total(entries)\n const complaint = totals.members > limits.maxMembers ?\n `exceeds the ${limits.maxMembers}-member limit`\n : totals.nodes > limits.maxNodes ?\n `exceeds the ${limits.maxNodes}-node limit`\n : totals.bytes > limits.maxBytes ?\n `exceeds the ${limits.maxBytes}-byte limit`\n : totals.depth > limits.maxDepth ?\n `exceeds the depth limit of ${limits.maxDepth}`\n : undefined\n if (complaint !== undefined) return { ok: false, path: boundedPath(path), complaint }\n Object.freeze(result)\n snapshots.set(result, { totals, entries })\n return { ok: true, value: result }\n }\n return merge(base, patch, \"$\")\n}\n"]}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Effect layer composition for a resolved plugin list.
3
+ *
4
+ * @private
5
+ * @since 1.0.0-rc.0
6
+ */
7
+ import { Action } from "@smthrs/flow";
8
+ import * as Layer from "effect/Layer";
9
+ import type { FlowsPlugin } from "../Plugin.ts";
10
+ import { PluginError } from "../PluginError.ts";
11
+ /**
12
+ * Merges plugin layers left-to-right, wrapping each build failure as
13
+ * `layer_failed`, then seals the cache environment: it is supplied beneath
14
+ * the plugin layers and overrides anything they output, and an absent
15
+ * declaration stays absent.
16
+ *
17
+ * @private
18
+ * @since 1.0.0-rc.0
19
+ */
20
+ export declare const mergePluginLayers: <H>(plugins: ReadonlyArray<FlowsPlugin<H>>, cacheEnvironment: Action.CacheEnvironment | undefined) => Layer.Layer<any, PluginError, any>;
21
+ //# sourceMappingURL=mergePluginLayers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mergePluginLayers.d.ts","sourceRoot":"","sources":["../../../src/internal/mergePluginLayers.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAErC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAE/C;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,GAAI,CAAC,EACjC,SAAS,aAAa,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,EACtC,kBAAkB,MAAM,CAAC,gBAAgB,GAAG,SAAS,KACpD,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,CA+BnC,CAAA"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Effect layer composition for a resolved plugin list.
3
+ *
4
+ * @private
5
+ * @since 1.0.0-rc.0
6
+ */
7
+ import { Action } from "@smthrs/flow";
8
+ import * as Effect from "effect/Effect";
9
+ import * as Layer from "effect/Layer";
10
+ import { PluginError } from "../PluginError.js";
11
+ /**
12
+ * Merges plugin layers left-to-right, wrapping each build failure as
13
+ * `layer_failed`, then seals the cache environment: it is supplied beneath
14
+ * the plugin layers and overrides anything they output, and an absent
15
+ * declaration stays absent.
16
+ *
17
+ * @private
18
+ * @since 1.0.0-rc.0
19
+ */
20
+ export const mergePluginLayers = (plugins, cacheEnvironment) => {
21
+ const layers = plugins.flatMap((plugin) => plugin.layer
22
+ ? [
23
+ Layer.catchCause(plugin.layer, (cause) => Layer.effectDiscard(Effect.fail(new PluginError({
24
+ code: "layer_failed",
25
+ message: `plugin "${plugin.name}" failed to build its layer`,
26
+ plugin: plugin.name,
27
+ cause
28
+ }))))
29
+ ]
30
+ : []);
31
+ const merged = (layers.length === 0
32
+ ? Layer.empty
33
+ : layers.reduce((accumulated, next) => Layer.provideMerge(next, accumulated)));
34
+ // The sealed environment is provided beneath the plugin layers so they can
35
+ // read it, then merged last so it overrides any plugin output. An absent
36
+ // declaration is sealed as `undefined`, so no plugin layer can declare or
37
+ // replace the composition's cache identity.
38
+ const environment = Layer.succeed(Action.CurrentCacheEnvironment)(cacheEnvironment);
39
+ return Layer.merge(Layer.provideMerge(merged, environment), environment);
40
+ };
41
+ //# sourceMappingURL=mergePluginLayers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mergePluginLayers.js","sourceRoot":"","sources":["../../../src/internal/mergePluginLayers.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AACrC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAErC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAE/C;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAC/B,OAAsC,EACtC,gBAAqD,EACjB,EAAE;IACtC,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CACxC,MAAM,CAAC,KAAK;QACV,CAAC,CAAC;YACA,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CACvC,KAAK,CAAC,aAAa,CACjB,MAAM,CAAC,IAAI,CACT,IAAI,WAAW,CAAC;gBACd,IAAI,EAAE,cAAc;gBACpB,OAAO,EAAE,WAAW,MAAM,CAAC,IAAI,6BAA6B;gBAC5D,MAAM,EAAE,MAAM,CAAC,IAAI;gBACnB,KAAK;aACN,CAAC,CACH,CACF,CAAuC;SAC3C;QACD,CAAC,CAAC,EAAE,CACP,CAAA;IACD,MAAM,MAAM,GAAG,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC;QACjC,CAAC,CAAC,KAAK,CAAC,KAAK;QACb,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC,CAAuC,CAAA;IACtH,2EAA2E;IAC3E,yEAAyE;IACzE,0EAA0E;IAC1E,4CAA4C;IAC5C,MAAM,WAAW,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,uBAAuB,CAAC,CAAC,gBAAgB,CAIjF,CAAA;IACD,OAAO,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,YAAY,CAAC,MAAM,EAAE,WAAW,CAAC,EAAE,WAAW,CAAC,CAAA;AAC1E,CAAC,CAAA","sourcesContent":["/**\n * Effect layer composition for a resolved plugin list.\n *\n * @private\n * @since 1.0.0-rc.0\n */\n\nimport { Action } from \"@smthrs/flow\"\nimport * as Effect from \"effect/Effect\"\nimport * as Layer from \"effect/Layer\"\nimport type { FlowsPlugin } from \"../Plugin.ts\"\nimport { PluginError } from \"../PluginError.ts\"\n\n/**\n * Merges plugin layers left-to-right, wrapping each build failure as\n * `layer_failed`, then seals the cache environment: it is supplied beneath\n * the plugin layers and overrides anything they output, and an absent\n * declaration stays absent.\n *\n * @private\n * @since 1.0.0-rc.0\n */\nexport const mergePluginLayers = <H>(\n plugins: ReadonlyArray<FlowsPlugin<H>>,\n cacheEnvironment: Action.CacheEnvironment | undefined\n): Layer.Layer<any, PluginError, any> => {\n const layers = plugins.flatMap((plugin) =>\n plugin.layer\n ? [\n Layer.catchCause(plugin.layer, (cause) =>\n Layer.effectDiscard(\n Effect.fail(\n new PluginError({\n code: \"layer_failed\",\n message: `plugin \"${plugin.name}\" failed to build its layer`,\n plugin: plugin.name,\n cause\n })\n )\n )) as Layer.Layer<any, PluginError, any>\n ]\n : []\n )\n const merged = (layers.length === 0\n ? Layer.empty\n : layers.reduce((accumulated, next) => Layer.provideMerge(next, accumulated))) as Layer.Layer<any, PluginError, any>\n // The sealed environment is provided beneath the plugin layers so they can\n // read it, then merged last so it overrides any plugin output. An absent\n // declaration is sealed as `undefined`, so no plugin layer can declare or\n // replace the composition's cache identity.\n const environment = Layer.succeed(Action.CurrentCacheEnvironment)(cacheEnvironment) as unknown as Layer.Layer<\n any,\n PluginError,\n any\n >\n return Layer.merge(Layer.provideMerge(merged, environment), environment)\n}\n"]}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Cache identity admission for a resolved plugin list.
3
+ *
4
+ * @private
5
+ * @since 1.0.0-rc.0
6
+ */
7
+ import { Action } from "@smthrs/flow";
8
+ import * as Effect from "effect/Effect";
9
+ import type { FlowsPlugin } from "../Plugin.ts";
10
+ import { PluginError } from "../PluginError.ts";
11
+ /**
12
+ * Admits a caller cache environment and prepends one escaped `name@version`
13
+ * identity per plugin, in resolved order. An absent input stays absent.
14
+ *
15
+ * @private
16
+ * @since 1.0.0-rc.0
17
+ */
18
+ export declare const snapshotCacheEnvironment: <H>(input: unknown, plugins: ReadonlyArray<FlowsPlugin<H>>) => Effect.Effect<Action.CacheEnvironment | undefined, PluginError>;
19
+ //# sourceMappingURL=snapshotCacheEnvironment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshotCacheEnvironment.d.ts","sourceRoot":"","sources":["../../../src/internal/snapshotCacheEnvironment.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AACrC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAO/C;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,GAAI,CAAC,EACxC,OAAO,OAAO,EACd,SAAS,aAAa,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,KACrC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,gBAAgB,GAAG,SAAS,EAAE,WAAW,CAgDhE,CAAA"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Cache identity admission for a resolved plugin list.
3
+ *
4
+ * @private
5
+ * @since 1.0.0-rc.0
6
+ */
7
+ import { Action } from "@smthrs/flow";
8
+ import * as Effect from "effect/Effect";
9
+ import * as Schema from "effect/Schema";
10
+ import { PluginError } from "../PluginError.js";
11
+ import * as Boundary from "./Boundary.js";
12
+ const escapePluginIdentityPart = (value) =>
13
+ // Percent must be escaped first so literal escape-looking text remains distinct from the `@` escape added next.
14
+ value.replaceAll("%", "%25").replaceAll("@", "%40");
15
+ /**
16
+ * Admits a caller cache environment and prepends one escaped `name@version`
17
+ * identity per plugin, in resolved order. An absent input stays absent.
18
+ *
19
+ * @private
20
+ * @since 1.0.0-rc.0
21
+ */
22
+ export const snapshotCacheEnvironment = (input, plugins) => {
23
+ if (input === undefined)
24
+ return Effect.succeed(undefined);
25
+ const pluginIdentities = [];
26
+ for (const plugin of plugins) {
27
+ if (plugin.version === undefined) {
28
+ return Effect.fail(new PluginError({
29
+ code: "cache_environment_invalid",
30
+ message: `cache environment requires a version for plugin "${plugin.name}"`,
31
+ path: "$.version",
32
+ plugin: plugin.name
33
+ }));
34
+ }
35
+ pluginIdentities.push(`${escapePluginIdentityPart(plugin.name)}@${escapePluginIdentityPart(plugin.version)}`);
36
+ }
37
+ const admitted = Boundary.record(input);
38
+ if (!admitted.ok) {
39
+ return Effect.fail(new PluginError({
40
+ code: "cache_environment_invalid",
41
+ message: `cache environment ${admitted.complaint}`,
42
+ path: `$options.cacheEnvironment${admitted.path.slice(1)}`
43
+ }));
44
+ }
45
+ return Schema.decodeUnknownEffect(Action.CacheEnvironment)(admitted.value).pipe(Effect.mapError(() => new PluginError({
46
+ code: "cache_environment_invalid",
47
+ message: "cache environment does not match the complete cache identity schema",
48
+ path: "$options.cacheEnvironment"
49
+ })), Effect.map((environment) => {
50
+ const capabilities = {};
51
+ for (const name of Object.keys(environment.capabilities)) {
52
+ Object.defineProperty(capabilities, name, {
53
+ value: Object.freeze([...environment.capabilities[name]]),
54
+ enumerable: true
55
+ });
56
+ }
57
+ return Object.freeze({
58
+ layers: Object.freeze([...pluginIdentities, ...environment.layers]),
59
+ capabilities: Object.freeze(capabilities)
60
+ });
61
+ }));
62
+ };
63
+ //# sourceMappingURL=snapshotCacheEnvironment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshotCacheEnvironment.js","sourceRoot":"","sources":["../../../src/internal/snapshotCacheEnvironment.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Cache identity admission for a resolved plugin list.\n *\n * @private\n * @since 1.0.0-rc.0\n */\n\nimport { Action } from \"@smthrs/flow\"\nimport * as Effect from \"effect/Effect\"\nimport * as Schema from \"effect/Schema\"\nimport type { FlowsPlugin } from \"../Plugin.ts\"\nimport { PluginError } from \"../PluginError.ts\"\nimport * as Boundary from \"./Boundary.ts\"\n\nconst escapePluginIdentityPart = (value: string): string =>\n // Percent must be escaped first so literal escape-looking text remains distinct from the `@` escape added next.\n value.replaceAll(\"%\", \"%25\").replaceAll(\"@\", \"%40\")\n\n/**\n * Admits a caller cache environment and prepends one escaped `name@version`\n * identity per plugin, in resolved order. An absent input stays absent.\n *\n * @private\n * @since 1.0.0-rc.0\n */\nexport const snapshotCacheEnvironment = <H>(\n input: unknown,\n plugins: ReadonlyArray<FlowsPlugin<H>>\n): Effect.Effect<Action.CacheEnvironment | undefined, PluginError> => {\n if (input === undefined) return Effect.succeed(undefined)\n const pluginIdentities: Array<string> = []\n for (const plugin of plugins) {\n if (plugin.version === undefined) {\n return Effect.fail(\n new PluginError({\n code: \"cache_environment_invalid\",\n message: `cache environment requires a version for plugin \"${plugin.name}\"`,\n path: \"$.version\",\n plugin: plugin.name\n })\n )\n }\n pluginIdentities.push(`${escapePluginIdentityPart(plugin.name)}@${escapePluginIdentityPart(plugin.version)}`)\n }\n const admitted = Boundary.record(input)\n if (!admitted.ok) {\n return Effect.fail(\n new PluginError({\n code: \"cache_environment_invalid\",\n message: `cache environment ${admitted.complaint}`,\n path: `$options.cacheEnvironment${admitted.path.slice(1)}`\n })\n )\n }\n return Schema.decodeUnknownEffect(Action.CacheEnvironment)(admitted.value).pipe(\n Effect.mapError(() =>\n new PluginError({\n code: \"cache_environment_invalid\",\n message: \"cache environment does not match the complete cache identity schema\",\n path: \"$options.cacheEnvironment\"\n })\n ),\n Effect.map((environment) => {\n const capabilities: Record<string, ReadonlyArray<string>> = {}\n for (const name of Object.keys(environment.capabilities)) {\n Object.defineProperty(capabilities, name, {\n value: Object.freeze([...environment.capabilities[name]!]),\n enumerable: true\n })\n }\n return Object.freeze({\n layers: Object.freeze([...pluginIdentities, ...environment.layers]),\n capabilities: Object.freeze(capabilities)\n })\n })\n )\n}\n"]}
package/docs/README.md ADDED
@@ -0,0 +1,131 @@
1
+ ---
2
+ title: "@smthrs/plugin"
3
+ description: "A typed, bounded plugin kernel for Effect programs: Vite-style hooks and ordering, a config waterfall, and a resolution boundary that copies every value a caller hands it."
4
+ ---
5
+
6
+ `@smthrs/plugin` lets a TypeScript program accept plugins written by other
7
+ people. You name the extension points your program offers, a plugin author
8
+ hands you a plain record of handlers, and the kernel turns that list into an
9
+ ordered, immutable catalog you dispatch against.
10
+
11
+ It is Vite's plugin model, typed end to end and bounded at every edge: a plugin
12
+ is a plain record, a hook is an ordinary [Effect](https://effect.website),
13
+ ordering follows Vite's rules exactly, and no value a caller passes in survives
14
+ as a live reference into dispatch.
15
+
16
+ ## What it solves
17
+
18
+ An extensible program has to answer five questions before its first plugin
19
+ runs: which plugins are selected, what order their handlers run in, what a
20
+ handler may return, what a failing handler does to the caller, and whether a
21
+ plugin can change any of that after startup. This package answers all five, and
22
+ refuses the input that would leave one of them undecided:
23
+
24
+ - **Selection** is the `apply` field: a host target, or a predicate over the
25
+ configuration.
26
+ - **Order** is Vite's rule kept verbatim. `enforce` sorts the plugin list once,
27
+ and a per-hook `order` re-partitions it for a single hook.
28
+ - **Result and failure** come from the hook's kind. `sequential`, `parallel`,
29
+ `first`, and `waterfall` each promise something different, and the kind is
30
+ checked in the type system and again against the host's runtime catalog.
31
+ - **Change after startup** is not possible. Resolution copies every plugin
32
+ record, hook object, and configuration value it accepts, so dispatch never
33
+ reads memory a caller still holds.
34
+
35
+ Reach for it when you are the host: you have a composition, you want other
36
+ people's code inside it at named points, and you would rather refuse a
37
+ malformed plugin at startup with an error code and a value path than debug it
38
+ in production.
39
+
40
+ ## The hook catalog is yours
41
+
42
+ The kernel owns one catalog of its own, the two-step config lifecycle (`config`
43
+ and `configResolved`). Every other hook belongs to a host. You declare your
44
+ hooks with TypeScript module augmentation, hand the kernel the matching runtime
45
+ catalog, and dispatch that catalog and nothing else. The `FlowsHooks` interface
46
+ is open for augmentation and closed for dispatch, so another host augmenting
47
+ the same interface still dispatches only the hooks it supplied.
48
+
49
+ A plugin author writes a record with a `name` and some `hooks`. There is no base
50
+ class, no registration call, and no inheritance.
51
+
52
+ ## Install
53
+
54
+ Not on npm yet; see [Installation](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/flow/docs/installation.md#use-the-libraries).
55
+
56
+ Node.js 26.4.0 or later is required. For the runtime requirements, the import
57
+ forms, and the subpaths the export map blocks, see
58
+ [Installation](./installation.md).
59
+
60
+ ## The shortest real example
61
+
62
+ A plugin contributes a configuration namespace, and a second hook reads the
63
+ resolved result back:
64
+
65
+ ```ts
66
+ import { type FlowsPlugin, Kernel } from "@smthrs/plugin"
67
+ import { Effect } from "effect"
68
+
69
+ const feature: FlowsPlugin = {
70
+ name: "flows-plugin-feature",
71
+ hooks: {
72
+ config: () => Effect.succeed({ feature: { enabled: true } }),
73
+ configResolved: (config) => Effect.log(`feature enabled: ${config["feature"] !== undefined}`)
74
+ }
75
+ }
76
+
77
+ const kernel = await Effect.runPromise(Kernel.make([feature]))
78
+ ```
79
+
80
+ `Kernel.make` gives you four things:
81
+
82
+ - `kernel.config` is the frozen, deeply copied configuration every plugin
83
+ agreed on.
84
+ - `kernel.plugins` dispatches hooks.
85
+ - `kernel.layer` is every plugin's Effect services merged in resolved order.
86
+ - `kernel.observerErrors` holds the failures that `configResolved` observers
87
+ reported without failing startup.
88
+
89
+ ## Where this sits in Smithers
90
+
91
+ This package is the mechanism, not a host: on its own it dispatches only the
92
+ config lifecycle. The shipped host is [`@smthrs/agent`](/api/agent), the
93
+ Smithers agent loop, which augments `FlowsHooks` with three waterfall hooks of
94
+ its own (`cellRegistry`, `cellFlows`, and `cellModelRequest`), passes the
95
+ matching runtime catalog with `target: "harness"`, and dispatches them around
96
+ each frame of an agent cell. If you want to extend the Smithers agent rather
97
+ than build a host of your own, read that package first: its hooks are the ones
98
+ your plugin declares, and this package explains what those hooks promise.
99
+
100
+ Both sit under the `smthrs` command line, [`@smthrs/cli`](/api/cli), which runs
101
+ and inspects the durable flows a Smithers installation is made of. Start there
102
+ for the whole picture.
103
+
104
+ The kernel depends on [`@smthrs/flow`](/api/flow) for the
105
+ `Action.CacheEnvironment` schema used to
106
+ [declare a cache identity](./guides/cache-identity.md), and
107
+ [`@smthrs/canonical`](/api/canonical) for immutable catalog maps.
108
+
109
+ ## What this package does not do
110
+
111
+ The kernel is not an engine lifecycle registry. Retry, storage, concurrency,
112
+ cache, wait, journal, and ownership policy stay on the Effect service or
113
+ constructor option that applies them. Configuration refuses the root keys
114
+ `engine`, `retry`, `store`, and `plugins` so that a configuration file cannot
115
+ imply otherwise.
116
+
117
+ ## Where to go next
118
+
119
+ - [Installation](./installation.md) for the runtime requirements, the import
120
+ forms, and the subpaths that are not public.
121
+ - [Quickstart](./quickstart.md) to build a host with its own hook, run it, and
122
+ read a typed result.
123
+ - [Hook kinds](./concepts/hook-kinds.md) for what each of the four dispatch
124
+ shapes promises.
125
+ - [Resolution](./concepts/resolution.md) for how a plugin list becomes an
126
+ ordered, immutable catalog.
127
+ - [Configuration](./concepts/configuration.md) for the config waterfall and the
128
+ JSON boundary it enforces.
129
+ - [API reference](./api.md) for every export, with signatures and limits.
130
+ - [Troubleshooting](./troubleshooting.md) for each `PluginError` code, what
131
+ causes it, and what to change.