mycelia-kernel-plugin 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +248 -0
  3. package/bin/cli.js +433 -0
  4. package/package.json +63 -0
  5. package/src/builder/context-resolver.js +62 -0
  6. package/src/builder/dependency-graph-cache.js +105 -0
  7. package/src/builder/dependency-graph.js +141 -0
  8. package/src/builder/facet-validator.js +43 -0
  9. package/src/builder/hook-processor.js +271 -0
  10. package/src/builder/index.js +13 -0
  11. package/src/builder/subsystem-builder.js +104 -0
  12. package/src/builder/utils.js +165 -0
  13. package/src/contract/contracts/hierarchy.contract.js +60 -0
  14. package/src/contract/contracts/index.js +17 -0
  15. package/src/contract/contracts/listeners.contract.js +66 -0
  16. package/src/contract/contracts/processor.contract.js +47 -0
  17. package/src/contract/contracts/queue.contract.js +58 -0
  18. package/src/contract/contracts/router.contract.js +53 -0
  19. package/src/contract/contracts/scheduler.contract.js +65 -0
  20. package/src/contract/contracts/server.contract.js +88 -0
  21. package/src/contract/contracts/speak.contract.js +50 -0
  22. package/src/contract/contracts/storage.contract.js +107 -0
  23. package/src/contract/contracts/websocket.contract.js +90 -0
  24. package/src/contract/facet-contract-registry.js +155 -0
  25. package/src/contract/facet-contract.js +136 -0
  26. package/src/contract/index.js +63 -0
  27. package/src/core/create-hook.js +63 -0
  28. package/src/core/facet.js +189 -0
  29. package/src/core/index.js +3 -0
  30. package/src/hooks/listeners/handler-group-manager.js +88 -0
  31. package/src/hooks/listeners/listener-manager-policies.js +229 -0
  32. package/src/hooks/listeners/listener-manager.js +668 -0
  33. package/src/hooks/listeners/listener-registry.js +176 -0
  34. package/src/hooks/listeners/listener-statistics.js +106 -0
  35. package/src/hooks/listeners/pattern-matcher.js +283 -0
  36. package/src/hooks/listeners/use-listeners.js +164 -0
  37. package/src/hooks/queue/bounded-queue.js +341 -0
  38. package/src/hooks/queue/circular-buffer.js +231 -0
  39. package/src/hooks/queue/subsystem-queue-manager.js +198 -0
  40. package/src/hooks/queue/use-queue.js +96 -0
  41. package/src/hooks/speak/use-speak.js +79 -0
  42. package/src/index.js +49 -0
  43. package/src/manager/facet-manager-transaction.js +45 -0
  44. package/src/manager/facet-manager.js +570 -0
  45. package/src/manager/index.js +3 -0
  46. package/src/system/base-subsystem.js +416 -0
  47. package/src/system/base-subsystem.utils.js +106 -0
  48. package/src/system/index.js +4 -0
  49. package/src/system/standalone-plugin-system.js +70 -0
  50. package/src/utils/debug-flag.js +34 -0
  51. package/src/utils/find-facet.js +30 -0
  52. package/src/utils/logger.js +84 -0
  53. package/src/utils/semver.js +221 -0
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "mycelia-kernel-plugin",
3
+ "version": "1.0.0",
4
+ "description": "A sophisticated, dependency-aware plugin system with transaction safety and lifecycle management",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "author": {
8
+ "name": "lesfleursdelanuitdev",
9
+ "url": "https://github.com/lesfleursdelanuitdev"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/lesfleursdelanuitdev/mycelia-kernel-plugin-system.git"
14
+ },
15
+ "homepage": "https://github.com/lesfleursdelanuitdev/mycelia-kernel-plugin-system#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/lesfleursdelanuitdev/mycelia-kernel-plugin-system/issues"
18
+ },
19
+ "keywords": [
20
+ "plugin-system",
21
+ "hooks",
22
+ "facets",
23
+ "dependency-injection",
24
+ "lifecycle",
25
+ "composable",
26
+ "transaction-safe",
27
+ "dependency-resolution"
28
+ ],
29
+ "main": "./src/index.js",
30
+ "exports": {
31
+ ".": "./src/index.js",
32
+ "./core": "./src/core/index.js",
33
+ "./manager": "./src/manager/index.js",
34
+ "./builder": "./src/builder/index.js",
35
+ "./system": "./src/system/index.js",
36
+ "./contract": "./src/contract/index.js",
37
+ "./contract/contracts": "./src/contract/contracts/index.js"
38
+ },
39
+ "files": [
40
+ "src/",
41
+ "bin/",
42
+ "README.md",
43
+ "LICENSE"
44
+ ],
45
+ "bin": {
46
+ "mycelia-kernel-plugin": "./bin/cli.js"
47
+ },
48
+ "scripts": {
49
+ "test": "vitest run",
50
+ "test:watch": "vitest watch",
51
+ "test:coverage": "vitest run --coverage"
52
+ },
53
+ "devDependencies": {
54
+ "@eslint/js": "^9.36.0",
55
+ "eslint": "^9.36.0",
56
+ "globals": "^16.4.0",
57
+ "vitest": "^2.1.5"
58
+ },
59
+ "engines": {
60
+ "node": ">=18.0.0"
61
+ }
62
+ }
63
+
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Context Resolver Utilities
3
+ *
4
+ * Handles context resolution and deep merging for subsystem building.
5
+ */
6
+
7
+ /**
8
+ * Deep merge helper for objects (only merges plain objects, not arrays or other types)
9
+ * @param {object} target - Target object
10
+ * @param {object} source - Source object
11
+ * @returns {object} Merged object
12
+ */
13
+ export function deepMerge(target, source) {
14
+ const result = { ...target };
15
+ for (const key in source) {
16
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
17
+ const sourceValue = source[key];
18
+ const targetValue = result[key];
19
+
20
+ // Deep merge if both are plain objects (not arrays, null, or other types)
21
+ if (
22
+ sourceValue &&
23
+ typeof sourceValue === 'object' &&
24
+ !Array.isArray(sourceValue) &&
25
+ targetValue &&
26
+ typeof targetValue === 'object' &&
27
+ !Array.isArray(targetValue)
28
+ ) {
29
+ result[key] = deepMerge(targetValue, sourceValue);
30
+ } else {
31
+ // Override with source value
32
+ result[key] = sourceValue;
33
+ }
34
+ }
35
+ }
36
+ return result;
37
+ }
38
+
39
+ /**
40
+ * Pure ctx resolver (verification stays side-effect free).
41
+ *
42
+ * @param {BaseSubsystem} subsystem - Subsystem instance
43
+ * @param {Object} ctx - Additional context to merge
44
+ * @returns {Object} Resolved context object
45
+ */
46
+ export function resolveCtx(subsystem, ctx) {
47
+ const base = (subsystem.ctx && typeof subsystem.ctx === 'object') ? subsystem.ctx : {};
48
+ const extra = (ctx && typeof ctx === 'object') ? ctx : {};
49
+
50
+ // Deep merge ctx.config specifically, shallow merge everything else
51
+ const merged = { ...base, ...extra };
52
+
53
+ // If both have config objects, deep merge them
54
+ if (base.config && typeof base.config === 'object' &&
55
+ extra.config && typeof extra.config === 'object' &&
56
+ !Array.isArray(base.config) && !Array.isArray(extra.config)) {
57
+ merged.config = deepMerge(base.config, extra.config);
58
+ }
59
+
60
+ return merged;
61
+ }
62
+
@@ -0,0 +1,105 @@
1
+ /**
2
+ * DependencyGraphCache Class
3
+ *
4
+ * Bounded LRU (Least Recently Used) cache for dependency graph topological sort results.
5
+ * Uses Map's insertion order to track access order.
6
+ *
7
+ * On get(): delete and re-insert to move to end (most recent)
8
+ * On set(): if at capacity, delete first entry (least recent)
9
+ *
10
+ * @example
11
+ * const cache = new DependencyGraphCache(100);
12
+ * cache.set('hierarchy,listeners,processor', { valid: true, orderedKinds: [...] });
13
+ * const result = cache.get('hierarchy,listeners,processor');
14
+ */
15
+ export class DependencyGraphCache {
16
+ /**
17
+ * Create a new DependencyGraphCache
18
+ *
19
+ * @param {number} [capacity=100] - Maximum number of cached entries
20
+ */
21
+ constructor(capacity = 100) {
22
+ if (typeof capacity !== 'number' || capacity < 1) {
23
+ throw new Error('DependencyGraphCache: capacity must be a positive number');
24
+ }
25
+
26
+ this.capacity = capacity;
27
+ this.cache = new Map(); // key (sorted kinds string) → { valid: boolean, orderedKinds?: string[], error?: string }
28
+ }
29
+
30
+ /**
31
+ * Get cached result for a key (updates access order)
32
+ *
33
+ * @param {string} key - Cache key (sorted facet kinds string)
34
+ * @returns {{ valid: boolean, orderedKinds?: string[], error?: string }|null} Cached result or null
35
+ */
36
+ get(key) {
37
+ if (!this.cache.has(key)) {
38
+ return null;
39
+ }
40
+
41
+ // Move to end (most recently used) by deleting and re-inserting
42
+ const value = this.cache.get(key);
43
+ this.cache.delete(key);
44
+ this.cache.set(key, value);
45
+
46
+ return value;
47
+ }
48
+
49
+ /**
50
+ * Set cached result for a key
51
+ *
52
+ * @param {string} key - Cache key (sorted facet kinds string)
53
+ * @param {boolean} valid - Whether the result is valid
54
+ * @param {string[]} [orderedKinds] - Topologically sorted kinds (if valid)
55
+ * @param {string} [error] - Error message (if invalid)
56
+ */
57
+ set(key, valid, orderedKinds = null, error = null) {
58
+ // If at capacity, remove least recently used (first entry)
59
+ if (this.cache.size >= this.capacity && !this.cache.has(key)) {
60
+ const firstKey = this.cache.keys().next().value;
61
+ this.cache.delete(firstKey);
62
+ }
63
+
64
+ const value = {
65
+ valid,
66
+ ...(valid && orderedKinds ? { orderedKinds } : {}),
67
+ ...(!valid && error !== null ? { error } : {})
68
+ };
69
+
70
+ // Remove existing entry if present, then add to end (most recently used)
71
+ if (this.cache.has(key)) {
72
+ this.cache.delete(key);
73
+ }
74
+ this.cache.set(key, value);
75
+ }
76
+
77
+ /**
78
+ * Clear all cached entries
79
+ */
80
+ clear() {
81
+ this.cache.clear();
82
+ }
83
+
84
+ /**
85
+ * Get current cache size
86
+ *
87
+ * @returns {number} Number of cached entries
88
+ */
89
+ size() {
90
+ return this.cache.size;
91
+ }
92
+
93
+ /**
94
+ * Get cache statistics
95
+ * @returns {{capacity:number,size:number,keys:string[]}}
96
+ */
97
+ getStats() {
98
+ return {
99
+ capacity: this.capacity,
100
+ size: this.cache.size,
101
+ keys: Array.from(this.cache.keys())
102
+ };
103
+ }
104
+ }
105
+
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Dependency Graph Utilities
3
+ *
4
+ * Handles building dependency graphs and topological sorting for facet ordering.
5
+ */
6
+
7
+ /**
8
+ * Create cache key from sorted facet kinds
9
+ *
10
+ * @param {string[]} kinds - Array of facet kinds
11
+ * @returns {string} Cache key (sorted, comma-separated)
12
+ */
13
+ export function createCacheKey(kinds) {
14
+ return [...kinds].sort().join(',');
15
+ }
16
+
17
+ /**
18
+ * Build a dependency graph from hook metadata and facet dependencies.
19
+ *
20
+ * @param {Object} hooksByKind - Object mapping hook kinds to hook metadata
21
+ * @param {Object} facetsByKind - Object mapping facet kinds to Facet instances
22
+ * @param {BaseSubsystem} subsystem - Subsystem instance (optional, for future use)
23
+ * @returns {Object} Dependency graph with { graph, indeg, kinds }
24
+ */
25
+ export function buildDepGraph(hooksByKind, facetsByKind, subsystem = null) {
26
+ const kinds = Object.keys(facetsByKind);
27
+ const graph = new Map(); // dep -> Set(of dependents)
28
+ const indeg = new Map(); // kind -> indegree
29
+
30
+ for (const k of kinds) {
31
+ graph.set(k, new Set());
32
+ indeg.set(k, 0);
33
+ }
34
+
35
+ // First, add dependencies from hook metadata (hook.required)
36
+ // hooksByKind now contains arrays of hooks per kind
37
+ for (const [kind, hookList] of Object.entries(hooksByKind)) {
38
+ if (!Array.isArray(hookList)) continue;
39
+
40
+ // Use the last hook's metadata for dependency graph (most recent/enhanced version)
41
+ // The dependency graph is for facets, not hooks, so we use the final facet's dependencies
42
+ const lastHook = hookList[hookList.length - 1];
43
+ const hookDeps = (lastHook?.required && Array.isArray(lastHook.required)) ? lastHook.required : [];
44
+ const hookOverwrite = lastHook?.overwrite === true;
45
+
46
+ for (const dep of hookDeps) {
47
+ // Skip self-dependency for overwrite hooks (they need the original facet to exist,
48
+ // but they're replacing it, so no cycle in facet dependency graph)
49
+ if (dep === kind && hookOverwrite) {
50
+ // Still validate that the facet exists (the overwrite hook needs it)
51
+ if (!facetsByKind[dep]) {
52
+ const src = lastHook?.source || kind;
53
+ throw new Error(`Hook '${kind}' (from ${src}) requires missing facet '${dep}'.`);
54
+ }
55
+ // But don't add the dependency edge (no cycle in facet graph)
56
+ continue;
57
+ }
58
+ if (!facetsByKind[dep]) {
59
+ const src = lastHook?.source || kind;
60
+ throw new Error(`Hook '${kind}' (from ${src}) requires missing facet '${dep}'.`);
61
+ }
62
+ if (!graph.get(dep).has(kind)) {
63
+ graph.get(dep).add(kind);
64
+ indeg.set(kind, (indeg.get(kind) || 0) + 1);
65
+ }
66
+ }
67
+ }
68
+
69
+ // Then, add dependencies from facet metadata (facet.getDependencies())
70
+ for (const [kind, facet] of Object.entries(facetsByKind)) {
71
+ const facetDeps = (typeof facet.getDependencies === 'function' && facet.getDependencies()) || [];
72
+ for (const dep of facetDeps) {
73
+ if (!facetsByKind[dep]) {
74
+ const src = facet.getSource?.() || kind;
75
+ throw new Error(`Facet '${kind}' (from ${src}) depends on missing '${dep}'.`);
76
+ }
77
+ if (!graph.get(dep).has(kind)) {
78
+ graph.get(dep).add(kind);
79
+ indeg.set(kind, (indeg.get(kind) || 0) + 1);
80
+ }
81
+ }
82
+ }
83
+
84
+ return { graph, indeg, kinds };
85
+ }
86
+
87
+ /**
88
+ * Kahn topological sort with diagnostics and caching.
89
+ *
90
+ * @param {Object} graphData - Dependency graph data { graph, indeg, kinds }
91
+ * @param {DependencyGraphCache} graphCache - Optional cache for storing results
92
+ * @param {string} cacheKey - Optional cache key
93
+ * @returns {string[]} Ordered array of facet kinds
94
+ * @throws {Error} If a dependency cycle is detected
95
+ */
96
+ export function topoSort({ graph, indeg, kinds }, graphCache = null, cacheKey = null) {
97
+ // Check cache if provided
98
+ if (graphCache && cacheKey) {
99
+ const cached = graphCache.get(cacheKey);
100
+ if (cached) {
101
+ if (cached.valid) {
102
+ return cached.orderedKinds;
103
+ } else {
104
+ throw new Error(cached.error || 'Cached dependency graph error');
105
+ }
106
+ }
107
+ }
108
+
109
+ const q = [];
110
+ for (const k of kinds) if ((indeg.get(k) || 0) === 0) q.push(k);
111
+
112
+ const ordered = [];
113
+ while (q.length) {
114
+ const n = q.shift();
115
+ ordered.push(n);
116
+ for (const m of graph.get(n)) {
117
+ indeg.set(m, indeg.get(m) - 1);
118
+ if (indeg.get(m) === 0) q.push(m);
119
+ }
120
+ }
121
+
122
+ if (ordered.length !== kinds.length) {
123
+ const stuck = kinds.filter(k => (indeg.get(k) || 0) > 0);
124
+ const error = `Facet dependency cycle detected among: ${stuck.join(', ')}`;
125
+
126
+ // Cache invalid result
127
+ if (graphCache && cacheKey) {
128
+ graphCache.set(cacheKey, false, null, error);
129
+ }
130
+
131
+ throw new Error(error);
132
+ }
133
+
134
+ // Cache valid result
135
+ if (graphCache && cacheKey) {
136
+ graphCache.set(cacheKey, true, ordered, null);
137
+ }
138
+
139
+ return ordered;
140
+ }
141
+
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Facet Validator Utilities
3
+ *
4
+ * Handles validation of facets against their contracts.
5
+ */
6
+
7
+ /**
8
+ * Validate facets against their contracts
9
+ *
10
+ * Iterates through all collected facets and enforces their contracts if they have one.
11
+ * Throws immediately if any contract validation fails.
12
+ *
13
+ * @param {Object} facetsByKind - Object mapping facet kinds to Facet instances
14
+ * @param {Object} resolvedCtx - Resolved context object
15
+ * @param {BaseSubsystem} subsystem - Subsystem instance
16
+ * @param {FacetContractRegistry} contractRegistry - Contract registry to use for validation
17
+ * @throws {Error} If a facet has a contract that doesn't exist in the registry, or if contract enforcement fails
18
+ */
19
+ export function validateFacets(facetsByKind, resolvedCtx, subsystem, contractRegistry) {
20
+ for (const [kind, facet] of Object.entries(facetsByKind)) {
21
+ const contractName = facet.getContract?.();
22
+
23
+ // Skip if facet has no contract
24
+ if (!contractName || typeof contractName !== 'string' || !contractName.trim()) {
25
+ continue;
26
+ }
27
+
28
+ // Check if contract exists in registry
29
+ if (!contractRegistry.has(contractName)) {
30
+ const facetSource = facet.getSource?.() || '<unknown>';
31
+ throw new Error(`Facet '${kind}' (from ${facetSource}) has contract '${contractName}' which is not registered in the contract registry.`);
32
+ }
33
+
34
+ // Enforce the contract (will throw if validation fails)
35
+ try {
36
+ contractRegistry.enforce(contractName, resolvedCtx, subsystem.api, subsystem, facet);
37
+ } catch (error) {
38
+ const facetSource = facet.getSource?.() || '<unknown>';
39
+ throw new Error(`Facet '${kind}' (from ${facetSource}) failed contract validation for '${contractName}': ${error.message}`);
40
+ }
41
+ }
42
+ }
43
+
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Hook Processor Utilities
3
+ *
4
+ * Handles hook ordering, execution, and facet creation during subsystem verification.
5
+ */
6
+
7
+ import { Facet } from '../core/facet.js';
8
+
9
+ /**
10
+ * Order hooks based on their dependencies using topological sort.
11
+ *
12
+ * Handles multiple hooks per kind by assigning unique IDs and creating
13
+ * dependencies between overwrite hooks and their predecessors.
14
+ *
15
+ * @param {Array} hooks - Array of hook functions
16
+ * @returns {Array} Ordered array of hooks
17
+ */
18
+ export function orderHooksByDependencies(hooks) {
19
+ // Extract hook metadata (now returns arrays per kind)
20
+ const hooksByKind = extractHookMetadata(hooks);
21
+
22
+ // Flatten to get all hooks with unique IDs
23
+ const hookArray = [];
24
+ const hookIdMap = new Map(); // hookId -> { hook, kind, required, overwrite, index }
25
+ const kindToHookIds = new Map(); // kind -> [hookId1, hookId2, ...]
26
+
27
+ // Assign unique IDs to each hook
28
+ for (const [kind, hookList] of Object.entries(hooksByKind)) {
29
+ const hookIds = [];
30
+ for (const hookMeta of hookList) {
31
+ const hookId = `${kind}:${hookMeta.index}`; // e.g., "router:0", "router:1"
32
+ hookArray.push({
33
+ hookId,
34
+ hook: hookMeta.hook,
35
+ kind,
36
+ required: hookMeta.required,
37
+ overwrite: hookMeta.overwrite,
38
+ index: hookMeta.index
39
+ });
40
+ hookIdMap.set(hookId, hookArray[hookArray.length - 1]);
41
+ hookIds.push(hookId);
42
+ }
43
+ kindToHookIds.set(kind, hookIds);
44
+ }
45
+
46
+ // Build dependency graph using hook IDs
47
+ const hookDepGraph = new Map(); // hookId -> Set(dependent hookIds)
48
+ const hookIndeg = new Map(); // hookId -> indegree
49
+
50
+ // Initialize graph
51
+ for (const { hookId } of hookArray) {
52
+ hookDepGraph.set(hookId, new Set());
53
+ hookIndeg.set(hookId, 0);
54
+ }
55
+
56
+ // Add edges: if hook A requires kind X, find the hook(s) that create X
57
+ for (const { hookId, kind, required, overwrite, index } of hookArray) {
58
+ for (const depKind of required) {
59
+ const depHookIds = kindToHookIds.get(depKind) || [];
60
+
61
+ if (depHookIds.length === 0) {
62
+ // Dependency not in our hook list (might be external or not yet created)
63
+ continue;
64
+ }
65
+
66
+ // If this hook requires its own kind (overwrite scenario)
67
+ if (depKind === kind && overwrite) {
68
+ // Find the previous hook of this kind (the one it's overwriting)
69
+ if (index > 0) {
70
+ const previousHookId = `${kind}:${index - 1}`;
71
+ if (hookIdMap.has(previousHookId)) {
72
+ // Overwrite hook depends on the previous hook of same kind
73
+ hookDepGraph.get(previousHookId).add(hookId);
74
+ hookIndeg.set(hookId, (hookIndeg.get(hookId) || 0) + 1);
75
+ }
76
+ }
77
+ continue; // Skip adding other dependencies for self-kind
78
+ }
79
+
80
+ // For other dependencies, depend on the LAST hook that creates that kind
81
+ // (the most recent/enhanced version)
82
+ if (depHookIds.length > 0) {
83
+ const lastDepHookId = depHookIds[depHookIds.length - 1];
84
+ hookDepGraph.get(lastDepHookId).add(hookId);
85
+ hookIndeg.set(hookId, (hookIndeg.get(hookId) || 0) + 1);
86
+ }
87
+ }
88
+ }
89
+
90
+ // Topological sort on hook IDs
91
+ const orderedHookIds = [];
92
+ const queue = [];
93
+ for (const { hookId } of hookArray) {
94
+ if (hookIndeg.get(hookId) === 0) {
95
+ queue.push(hookId);
96
+ }
97
+ }
98
+
99
+ while (queue.length > 0) {
100
+ const hookId = queue.shift();
101
+ orderedHookIds.push(hookId);
102
+
103
+ for (const dependent of hookDepGraph.get(hookId)) {
104
+ const newIndeg = hookIndeg.get(dependent) - 1;
105
+ hookIndeg.set(dependent, newIndeg);
106
+ if (newIndeg === 0) {
107
+ queue.push(dependent);
108
+ }
109
+ }
110
+ }
111
+
112
+ // Build ordered hooks array
113
+ const orderedHooks = [];
114
+ for (const hookId of orderedHookIds) {
115
+ orderedHooks.push(hookIdMap.get(hookId).hook);
116
+ }
117
+
118
+ // Add any hooks that weren't in the dependency graph (shouldn't happen, but safety)
119
+ for (const { hookId, hook } of hookArray) {
120
+ if (!orderedHookIds.includes(hookId)) {
121
+ orderedHooks.push(hook);
122
+ }
123
+ }
124
+
125
+ return orderedHooks;
126
+ }
127
+
128
+ /**
129
+ * Execute hooks and create facets.
130
+ *
131
+ * @param {Array} orderedHooks - Ordered array of hooks to execute
132
+ * @param {Object} resolvedCtx - Resolved context object
133
+ * @param {BaseSubsystem} subsystem - Subsystem instance
134
+ * @param {Object} hooksByKind - Object mapping hook kinds to hook metadata
135
+ * @returns {Object} Object with { facetsByKind, hooksByKind }
136
+ */
137
+ export function executeHooksAndCreateFacets(orderedHooks, resolvedCtx, subsystem, hooksByKind) {
138
+ const facetsByKind = Object.create(null);
139
+
140
+ // Execute hooks in dependency order and create facets
141
+ for (const hook of orderedHooks) {
142
+ if (typeof hook !== 'function') continue;
143
+
144
+ const hookKind = hook.kind;
145
+ const hookSource = hook.source || '<unknown>';
146
+
147
+ let facet;
148
+ try {
149
+ facet = hook(resolvedCtx, subsystem.api, subsystem);
150
+ } catch (error) {
151
+ throw new Error(`Hook '${hookKind}' (from ${hookSource}) failed during execution: ${error.message}`);
152
+ }
153
+
154
+ if (!facet) continue;
155
+
156
+ if (!(facet instanceof Facet)) {
157
+ throw new Error(`Hook '${hookKind}' (from ${hookSource}) did not return a Facet instance (got ${typeof facet}).`);
158
+ }
159
+
160
+ const facetKind = facet.getKind?.();
161
+ if (!facetKind || typeof facetKind !== 'string') {
162
+ const facetSrc = facet.getSource?.() || '<unknown>';
163
+ throw new Error(`Facet from hook '${hookKind}' (source: ${hookSource}) missing valid kind (facet source: ${facetSrc}).`);
164
+ }
165
+
166
+ // Validate hook.kind matches facet.kind
167
+ if (hookKind !== facetKind) {
168
+ throw new Error(`Hook '${hookKind}' (from ${hookSource}) returned facet with mismatched kind '${facetKind}'.`);
169
+ }
170
+
171
+ // Check overwrite permission (also check facet.shouldOverwrite for consistency)
172
+ if (facetsByKind[facetKind]) {
173
+ // Get overwrite permission from the current hook (not stored metadata)
174
+ const currentHookOverwrite = hook.overwrite === true;
175
+ const facetOverwrite = facet.shouldOverwrite?.() === true;
176
+ if (!currentHookOverwrite && !facetOverwrite) {
177
+ const prevSrc = facetsByKind[facetKind].getSource?.() || facetKind;
178
+ throw new Error(`Duplicate facet kind '${facetKind}' from [${prevSrc}] and [${hookSource}]. Neither hook nor facet allows overwrite.`);
179
+ }
180
+ // Allow overwrite - replace the existing facet
181
+ facetsByKind[facetKind] = facet;
182
+ } else {
183
+ facetsByKind[facetKind] = facet;
184
+ }
185
+
186
+ // Make facet available in api.__facets during verification so later hooks can access it via subsystem.find()
187
+ // This is a temporary registration - facets will be properly added/initialized/attached in buildSubsystem
188
+ if (subsystem.api && subsystem.api.__facets) {
189
+ // Use the Proxy setter to add the facet temporarily (without init/attach)
190
+ subsystem.api.__facets[facetKind] = facet;
191
+ }
192
+ }
193
+
194
+ return { facetsByKind };
195
+ }
196
+
197
+ /**
198
+ * Extract hook metadata from hooks array.
199
+ *
200
+ * Stores multiple hooks per kind in registration order (arrays).
201
+ * This allows overwrite hooks to depend on previous hooks of the same kind.
202
+ *
203
+ * @param {Array} hooks - Array of hook functions
204
+ * @returns {Object} Object mapping hook kinds to arrays of hook metadata
205
+ * Each entry: { hook, required, source, overwrite, version, index }
206
+ */
207
+ export function extractHookMetadata(hooks) {
208
+ const hooksByKind = Object.create(null);
209
+
210
+ for (const hook of hooks) {
211
+ if (typeof hook !== 'function') continue;
212
+
213
+ const hookKind = hook.kind;
214
+ const hookVersion = hook.version || '0.0.0';
215
+ const hookOverwrite = hook.overwrite === true;
216
+ const hookRequired = (hook.required && Array.isArray(hook.required)) ? hook.required : [];
217
+ const hookSource = hook.source || '<unknown>';
218
+
219
+ if (!hookKind || typeof hookKind !== 'string') {
220
+ throw new Error(`Hook missing valid kind property (source: ${hookSource}).`);
221
+ }
222
+
223
+ // Store array of hooks per kind, maintaining registration order
224
+ if (!hooksByKind[hookKind]) {
225
+ hooksByKind[hookKind] = [];
226
+ }
227
+
228
+ hooksByKind[hookKind].push({
229
+ hook, // Store the actual hook function
230
+ required: hookRequired,
231
+ source: hookSource,
232
+ overwrite: hookOverwrite,
233
+ version: hookVersion,
234
+ index: hooksByKind[hookKind].length // Track position in array
235
+ });
236
+ }
237
+
238
+ return hooksByKind;
239
+ }
240
+
241
+ /**
242
+ * Validate hook.required dependencies exist.
243
+ *
244
+ * @param {Object} hooksByKind - Object mapping hook kinds to arrays of hook metadata
245
+ * @param {Object} facetsByKind - Object mapping facet kinds to Facet instances
246
+ * @param {BaseSubsystem} subsystem - Subsystem instance (optional, for future use)
247
+ */
248
+ export function validateHookDependencies(hooksByKind, facetsByKind, subsystem) {
249
+ for (const [kind, hookList] of Object.entries(hooksByKind)) {
250
+ if (!Array.isArray(hookList)) continue;
251
+
252
+ for (const hookMeta of hookList) {
253
+ for (const dep of hookMeta.required) {
254
+ // For self-dependency (overwrite hooks requiring their own kind),
255
+ // check if there's a previous hook of that kind
256
+ if (dep === kind && hookMeta.overwrite) {
257
+ // Check if there's a previous hook (index > 0 means there's a predecessor)
258
+ if (hookMeta.index === 0) {
259
+ throw new Error(`Hook '${kind}' (from ${hookMeta.source}) requires '${dep}' but is the first hook of that kind. Overwrite hooks must come after the hook they overwrite.`);
260
+ }
261
+ // Previous hook exists, dependency is satisfied
262
+ continue;
263
+ }
264
+ if (!facetsByKind[dep]) {
265
+ throw new Error(`Hook '${kind}' (from ${hookMeta.source}) requires missing facet '${dep}'.`);
266
+ }
267
+ }
268
+ }
269
+ }
270
+ }
271
+
@@ -0,0 +1,13 @@
1
+ export { SubsystemBuilder } from './subsystem-builder.js';
2
+ export { DependencyGraphCache } from './dependency-graph-cache.js';
3
+ export { buildDepGraph, topoSort, createCacheKey } from './dependency-graph.js';
4
+ export { validateFacets } from './facet-validator.js';
5
+ export { resolveCtx, deepMerge } from './context-resolver.js';
6
+ export {
7
+ extractHookMetadata,
8
+ orderHooksByDependencies,
9
+ executeHooksAndCreateFacets,
10
+ validateHookDependencies
11
+ } from './hook-processor.js';
12
+ export { verifySubsystemBuild, buildSubsystem, deepMerge as deepMergeUtils } from './utils.js';
13
+