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.
- package/LICENSE +22 -0
- package/README.md +248 -0
- package/bin/cli.js +433 -0
- package/package.json +63 -0
- package/src/builder/context-resolver.js +62 -0
- package/src/builder/dependency-graph-cache.js +105 -0
- package/src/builder/dependency-graph.js +141 -0
- package/src/builder/facet-validator.js +43 -0
- package/src/builder/hook-processor.js +271 -0
- package/src/builder/index.js +13 -0
- package/src/builder/subsystem-builder.js +104 -0
- package/src/builder/utils.js +165 -0
- package/src/contract/contracts/hierarchy.contract.js +60 -0
- package/src/contract/contracts/index.js +17 -0
- package/src/contract/contracts/listeners.contract.js +66 -0
- package/src/contract/contracts/processor.contract.js +47 -0
- package/src/contract/contracts/queue.contract.js +58 -0
- package/src/contract/contracts/router.contract.js +53 -0
- package/src/contract/contracts/scheduler.contract.js +65 -0
- package/src/contract/contracts/server.contract.js +88 -0
- package/src/contract/contracts/speak.contract.js +50 -0
- package/src/contract/contracts/storage.contract.js +107 -0
- package/src/contract/contracts/websocket.contract.js +90 -0
- package/src/contract/facet-contract-registry.js +155 -0
- package/src/contract/facet-contract.js +136 -0
- package/src/contract/index.js +63 -0
- package/src/core/create-hook.js +63 -0
- package/src/core/facet.js +189 -0
- package/src/core/index.js +3 -0
- package/src/hooks/listeners/handler-group-manager.js +88 -0
- package/src/hooks/listeners/listener-manager-policies.js +229 -0
- package/src/hooks/listeners/listener-manager.js +668 -0
- package/src/hooks/listeners/listener-registry.js +176 -0
- package/src/hooks/listeners/listener-statistics.js +106 -0
- package/src/hooks/listeners/pattern-matcher.js +283 -0
- package/src/hooks/listeners/use-listeners.js +164 -0
- package/src/hooks/queue/bounded-queue.js +341 -0
- package/src/hooks/queue/circular-buffer.js +231 -0
- package/src/hooks/queue/subsystem-queue-manager.js +198 -0
- package/src/hooks/queue/use-queue.js +96 -0
- package/src/hooks/speak/use-speak.js +79 -0
- package/src/index.js +49 -0
- package/src/manager/facet-manager-transaction.js +45 -0
- package/src/manager/facet-manager.js +570 -0
- package/src/manager/index.js +3 -0
- package/src/system/base-subsystem.js +416 -0
- package/src/system/base-subsystem.utils.js +106 -0
- package/src/system/index.js +4 -0
- package/src/system/standalone-plugin-system.js +70 -0
- package/src/utils/debug-flag.js +34 -0
- package/src/utils/find-facet.js +30 -0
- package/src/utils/logger.js +84 -0
- 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
|
+
|