skynet-graph 1.2.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 (213) hide show
  1. package/CONTRIBUTING.md +22 -0
  2. package/LICENSE +661 -0
  3. package/README.md +245 -0
  4. package/bin/sg +20 -0
  5. package/concepts/README.md +23 -0
  6. package/concepts/_substrate/Claim/Confidence/Trusted.json +6 -0
  7. package/concepts/_substrate/Claim/Confidence.json +10 -0
  8. package/concepts/_substrate/Claim/Freshness/Stale.json +7 -0
  9. package/concepts/_substrate/Claim/Freshness.json +5 -0
  10. package/concepts/_substrate/Claim/Verification/Refuted.json +6 -0
  11. package/concepts/_substrate/Claim/Verification.json +6 -0
  12. package/concepts/_substrate/Claim.json +4 -0
  13. package/concepts/_substrate/Frontier/InBeam.json +6 -0
  14. package/concepts/_substrate/Frontier/Stuck.json +6 -0
  15. package/concepts/_substrate/Frontier.json +5 -0
  16. package/concepts/_substrate/Intake/ToTask.json +22 -0
  17. package/concepts/_substrate/Intake/Typed.json +10 -0
  18. package/concepts/_substrate/Intake.json +27 -0
  19. package/concepts/_substrate/Task/Answer.json +12 -0
  20. package/concepts/_substrate/Task/Complexity/Atomic.json +5 -0
  21. package/concepts/_substrate/Task/Complexity/Compound/Expansion.json +7 -0
  22. package/concepts/_substrate/Task/Complexity/Compound.json +6 -0
  23. package/concepts/_substrate/Task/Complexity.json +6 -0
  24. package/concepts/_substrate/Task/EvalComplexity.json +13 -0
  25. package/concepts/_substrate/Task/ReportUp.json +6 -0
  26. package/concepts/_substrate/Task/Rollup.json +10 -0
  27. package/concepts/_substrate/Task.json +5 -0
  28. package/concepts/clinical/Diagnosis/Medication.json +6 -0
  29. package/concepts/clinical/Diagnosis.json +15 -0
  30. package/concepts/clinical/Observation/LabValue/OutOfRange.json +6 -0
  31. package/concepts/clinical/Observation/LabValue.json +5 -0
  32. package/concepts/clinical/Observation.json +6 -0
  33. package/concepts/common/Document/pathBasket.json +6 -0
  34. package/concepts/common/Document.json +3 -0
  35. package/concepts/common/Edge/Distance.json +13 -0
  36. package/concepts/common/Edge/Stay/LongStay.json +8 -0
  37. package/concepts/common/Edge/Stay.json +8 -0
  38. package/concepts/common/Edge/Travel/LongTravel.json +16 -0
  39. package/concepts/common/Edge/Travel/ShortTravel.json +11 -0
  40. package/concepts/common/Edge/Travel/targetNode.json +7 -0
  41. package/concepts/common/Edge/Travel.json +7 -0
  42. package/concepts/common/Edge.json +3 -0
  43. package/concepts/common/Vertice.json +3 -0
  44. package/concepts/supply/Fulfillment.json +10 -0
  45. package/concepts/supply/Inventory/Reorder.json +7 -0
  46. package/concepts/supply/Inventory.json +5 -0
  47. package/concepts/supply/Procurement/SupplierConfirm.json +6 -0
  48. package/concepts/supply/Procurement.json +8 -0
  49. package/concepts/supply/Transport.json +5 -0
  50. package/doc/API.md +556 -0
  51. package/doc/CAPABILITIES.md +373 -0
  52. package/doc/MODELISATION.md +511 -0
  53. package/doc/architecture.md +267 -0
  54. package/doc/concept-as-graph.md +379 -0
  55. package/doc/concept-learning.md +168 -0
  56. package/doc/creative-loop-map.md +202 -0
  57. package/doc/original-2016-doc.md +148 -0
  58. package/doc/usage.md +501 -0
  59. package/lib/authoring/abstract.js +401 -0
  60. package/lib/authoring/abstraction.js +190 -0
  61. package/lib/authoring/adapt.js +393 -0
  62. package/lib/authoring/ancestry.js +216 -0
  63. package/lib/authoring/author.js +147 -0
  64. package/lib/authoring/bounded-merge.js +57 -0
  65. package/lib/authoring/canon.js +158 -0
  66. package/lib/authoring/clock.js +123 -0
  67. package/lib/authoring/combinator.js +92 -0
  68. package/lib/authoring/compete.js +76 -0
  69. package/lib/authoring/compose-hotspot.js +314 -0
  70. package/lib/authoring/compress.js +279 -0
  71. package/lib/authoring/concept-net.js +254 -0
  72. package/lib/authoring/concepts.js +66 -0
  73. package/lib/authoring/context-project.js +277 -0
  74. package/lib/authoring/contract.js +476 -0
  75. package/lib/authoring/corpus-pack.js +125 -0
  76. package/lib/authoring/cost-probe.js +86 -0
  77. package/lib/authoring/crystallize.js +555 -0
  78. package/lib/authoring/dag-decompose.js +256 -0
  79. package/lib/authoring/dataset-adapter.js +253 -0
  80. package/lib/authoring/debug-provider.js +92 -0
  81. package/lib/authoring/decompose.js +343 -0
  82. package/lib/authoring/emittability.js +236 -0
  83. package/lib/authoring/equilibrium.js +160 -0
  84. package/lib/authoring/extract.js +153 -0
  85. package/lib/authoring/forest.js +55 -0
  86. package/lib/authoring/forge-fallback.js +116 -0
  87. package/lib/authoring/givens.js +121 -0
  88. package/lib/authoring/glossary.js +177 -0
  89. package/lib/authoring/grammar-graph.js +191 -0
  90. package/lib/authoring/granularity.js +80 -0
  91. package/lib/authoring/graph-net.js +108 -0
  92. package/lib/authoring/ground.js +148 -0
  93. package/lib/authoring/higher-order.js +85 -0
  94. package/lib/authoring/hotspot.js +132 -0
  95. package/lib/authoring/hysteresis.js +85 -0
  96. package/lib/authoring/index.js +106 -0
  97. package/lib/authoring/lattice-morphism.js +61 -0
  98. package/lib/authoring/lattice-pack.js +148 -0
  99. package/lib/authoring/leaf-io.js +77 -0
  100. package/lib/authoring/library.js +156 -0
  101. package/lib/authoring/lifecycle.js +92 -0
  102. package/lib/authoring/loop.js +193 -0
  103. package/lib/authoring/master-loop.js +132 -0
  104. package/lib/authoring/mdl.js +209 -0
  105. package/lib/authoring/memo-stability.js +122 -0
  106. package/lib/authoring/method-explorer.js +138 -0
  107. package/lib/authoring/method-pack.js +181 -0
  108. package/lib/authoring/method.js +202 -0
  109. package/lib/authoring/mine.js +510 -0
  110. package/lib/authoring/mount.js +119 -0
  111. package/lib/authoring/negotiate.js +100 -0
  112. package/lib/authoring/parametric.js +139 -0
  113. package/lib/authoring/reaggregate.js +75 -0
  114. package/lib/authoring/rebalance.js +157 -0
  115. package/lib/authoring/recall.js +135 -0
  116. package/lib/authoring/registry.js +350 -0
  117. package/lib/authoring/relearn.js +147 -0
  118. package/lib/authoring/retention.js +89 -0
  119. package/lib/authoring/segment-proxy.js +114 -0
  120. package/lib/authoring/serve-leaf.js +64 -0
  121. package/lib/authoring/slot-aware-serve.js +77 -0
  122. package/lib/authoring/sound-invoke.js +59 -0
  123. package/lib/authoring/split-serve.js +74 -0
  124. package/lib/authoring/ste.js +239 -0
  125. package/lib/authoring/stock.js +101 -0
  126. package/lib/authoring/store.js +94 -0
  127. package/lib/authoring/supervise.js +138 -0
  128. package/lib/authoring/support.js +131 -0
  129. package/lib/authoring/task-mirror.js +51 -0
  130. package/lib/authoring/typed-loop.js +160 -0
  131. package/lib/authoring/validate.js +569 -0
  132. package/lib/authoring/widen.js +88 -0
  133. package/lib/combos/appliance.js +189 -0
  134. package/lib/combos/critique.js +377 -0
  135. package/lib/combos/defaults.js +86 -0
  136. package/lib/combos/durable-runner.js +115 -0
  137. package/lib/combos/forge.js +209 -0
  138. package/lib/combos/index.js +86 -0
  139. package/lib/combos/learning-library.js +253 -0
  140. package/lib/combos/mixture-serve.js +193 -0
  141. package/lib/combos/plan-loop.js +192 -0
  142. package/lib/combos/proxy-cache.js +272 -0
  143. package/lib/combos/self-mod.js +94 -0
  144. package/lib/durable/audit.js +102 -0
  145. package/lib/durable/checkpoint-store.js +550 -0
  146. package/lib/durable/fold.js +62 -0
  147. package/lib/durable/interpreter.js +242 -0
  148. package/lib/durable/xlate.js +185 -0
  149. package/lib/graph/Graph.js +2486 -0
  150. package/lib/graph/expr.js +307 -0
  151. package/lib/graph/index.js +26 -0
  152. package/lib/graph/log.js +134 -0
  153. package/lib/graph/objects/Concept.js +382 -0
  154. package/lib/graph/objects/Entity.js +503 -0
  155. package/lib/graph/objects/Node.js +57 -0
  156. package/lib/graph/objects/PathMap.js +557 -0
  157. package/lib/graph/objects/Segment.js +100 -0
  158. package/lib/graph/tasks/stabilize.js +59 -0
  159. package/lib/graph/tasks/taskflow.js +247 -0
  160. package/lib/index.js +122 -0
  161. package/lib/load.js +132 -0
  162. package/lib/providers/backends.js +117 -0
  163. package/lib/providers/borderline.js +129 -0
  164. package/lib/providers/cache.js +226 -0
  165. package/lib/providers/canonicalize.js +198 -0
  166. package/lib/providers/constat.js +107 -0
  167. package/lib/providers/geo.js +53 -0
  168. package/lib/providers/index.js +116 -0
  169. package/lib/providers/intake.js +263 -0
  170. package/lib/providers/llm-local.js +88 -0
  171. package/lib/providers/llm.js +236 -0
  172. package/lib/providers/local-host.js +197 -0
  173. package/lib/providers/merge-consistency.js +162 -0
  174. package/lib/providers/nogood.js +149 -0
  175. package/lib/providers/reason-loop.js +223 -0
  176. package/lib/providers/semiring.js +285 -0
  177. package/lib/providers/solver-fork.js +163 -0
  178. package/lib/providers/stats.js +228 -0
  179. package/lib/providers/verify.js +120 -0
  180. package/lib/runtime/index.js +135 -0
  181. package/lib/runtime/invoke-pool.js +92 -0
  182. package/lib/runtime/protocol.js +210 -0
  183. package/lib/runtime/transport-socket.js +77 -0
  184. package/lib/runtime/worker-entry.js +31 -0
  185. package/lib/sg/cli.js +601 -0
  186. package/lib/sg/log-sinks.js +214 -0
  187. package/lib/sg/mcp.js +395 -0
  188. package/lib/sg/proxy-run.js +66 -0
  189. package/lib/sg/serve-demo.js +185 -0
  190. package/lib/sg/serve.js +205 -0
  191. package/lib/sg/trace.js +118 -0
  192. package/lib/studio/protocol.js +45 -0
  193. package/lib/studio/public/app.js +310 -0
  194. package/lib/studio/public/components/ConceptEditor.js +43 -0
  195. package/lib/studio/public/components/ConceptTree.js +43 -0
  196. package/lib/studio/public/components/CorpusPanel.js +68 -0
  197. package/lib/studio/public/components/ForkTree.js +36 -0
  198. package/lib/studio/public/components/GrammarGraph.js +99 -0
  199. package/lib/studio/public/components/GraphCanvas.js +156 -0
  200. package/lib/studio/public/components/Inspector.js +48 -0
  201. package/lib/studio/public/components/LearningPanel.js +85 -0
  202. package/lib/studio/public/components/PromptConsole.js +46 -0
  203. package/lib/studio/public/components/ProviderTrace.js +39 -0
  204. package/lib/studio/public/components/SessionSplit.js +47 -0
  205. package/lib/studio/public/components/TilingOverlay.js +40 -0
  206. package/lib/studio/public/components/Timeline.js +57 -0
  207. package/lib/studio/public/index.html +26 -0
  208. package/lib/studio/public/styles.css +185 -0
  209. package/lib/studio/public/ws.js +58 -0
  210. package/lib/studio/server.js +130 -0
  211. package/lib/studio/session.js +397 -0
  212. package/lib/studio/studio.js +149 -0
  213. package/package.json +42 -0
@@ -0,0 +1,163 @@
1
+ /*
2
+ * Copyright 2026 Nathanael Braun
3
+ * @author : Nathanael BRAUN <pp9ping@gmail.com>
4
+ *
5
+ * This program is free software: you can redistribute it and/or modify
6
+ * it under the terms of the GNU Affero General Public License as published by
7
+ * the Free Software Foundation, either version 3 of the License, or
8
+ * (at your option) any later version.
9
+ *
10
+ * This program is distributed in the hope that it will be useful,
11
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ * GNU Affero General Public License for more details.
14
+ *
15
+ * You should have received a copy of the GNU Affero General Public License
16
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+ */
18
+ 'use strict';
19
+ /**
20
+ * C-regime solver-fork — search-where-D-propagates, with a snapped frontier (the
21
+ * Mixture-of-Reasoners "C regime"; doc/WIP/HANDOFF.md §7 Tier 1; SOTA subgraph-grammars
22
+ * brick ii; experiment E8). Zero core change.
23
+ *
24
+ * The deterministic D socle PROPAGATES to a fixpoint; it cannot SEARCH. A regime-C
25
+ * sub-graph delegates a constraint problem to a backtracking solver (Logic-LM style,
26
+ * Pan 2023) and EXPORTS ONLY the snapped model across the merge frontier — the colour/
27
+ * assignment enums + a `sat` bool — never the search internals (step counts, the search
28
+ * tree, continuous scores). So the parent stays deterministic and auditable, the barrier
29
+ * holds, and UNSAT is a discrete outcome the D grammar can gate on.
30
+ *
31
+ * const { createSolver, snappedFrontier, solverConceptTree } = require('./solver-fork');
32
+ * register(Graph, [ createSolver({ solve: myZ3Backend }) ]); // wires Solve::run
33
+ * const child = parent.fork(spec, { conceptMap: { common: solverConceptTree() } });
34
+ * await nextStable(child);
35
+ * parent.merge(child, 'prob', snappedFrontier({ targetId: 'prob' })); // only sat+model cross
36
+ *
37
+ * The solver backend is INJECTED (backend-agnostic, like createLLMProvider's `ask`): a
38
+ * `solve(spec) -> { sat, model, ...internals }` function. The packaged default is a
39
+ * dependency-free backtracking finite-domain CSP (`backtrackColoring`) so the regime is
40
+ * runnable out of the box; the architectural point — search, snapped frontier, auditable
41
+ * D parent — is independent of hand-rolled-vs-Z3.
42
+ */
43
+
44
+ // ---- reference C-regime backend: a dependency-free backtracking CSP (graph k-colouring) ----
45
+ // spec = { nodes:[...], edges:[[u,v],...], colors?:[...] } -> { sat, model, steps }.
46
+ // model = a node->colour assignment (the snapped enum the frontier carries); steps is a
47
+ // search-internal cost (stays in the fork — it must NOT cross the frontier).
48
+ function backtrackColoring( spec ) {
49
+ spec = spec || {};
50
+ var nodes = spec.nodes || [];
51
+ var edges = spec.edges || [];
52
+ var colors = spec.colors || ['R', 'G', 'B'];
53
+ var assign = {}, adj = {};
54
+ nodes.forEach(function ( n ) { adj[n] = []; });
55
+ edges.forEach(function ( e ) { adj[e[0]].push(e[1]); adj[e[1]].push(e[0]); });
56
+ var steps = 0;
57
+ function bt( i ) {
58
+ if ( i === nodes.length ) return true;
59
+ var n = nodes[i];
60
+ for ( var ci = 0; ci < colors.length; ci++ ) {
61
+ var c = colors[ci];
62
+ steps++;
63
+ if ( adj[n].every(function ( m ) { return assign[m] !== c; }) ) {
64
+ assign[n] = c;
65
+ if ( bt(i + 1) ) return true;
66
+ delete assign[n];
67
+ }
68
+ }
69
+ return false;
70
+ }
71
+ var sat = bt(0);
72
+ return { sat: sat, model: sat ? Object.assign({}, assign) : null, steps: steps };
73
+ }
74
+
75
+ /**
76
+ * Build the C-regime solver-fork provider fragment (host opt-in, like createVerifier).
77
+ * @param opts.solve injected backend `(spec) -> { sat, model, ...internals }`
78
+ * (default: backtrackColoring).
79
+ * @returns { Solve: { run } }
80
+ *
81
+ * Concept wiring (in the C-fork grammar — see solverConceptTree):
82
+ * { require:['toSolve'], provider:['Solve::run'], solve:{ specKey:'spec', as:'' } }
83
+ * `specKey` names the scope fact holding the problem spec (omit -> the whole scope fact
84
+ * bag is the spec). Emits the self-flag + <as>sat (bool) + <as>model (the snapped
85
+ * assignment, null when UNSAT) + any solver internals the backend returns (e.g. `steps`)
86
+ * — internals are kept on the CHILD and dropped by snappedFrontier at the merge.
87
+ */
88
+ function createSolver( opts ) {
89
+ opts = opts || {};
90
+ var solve = opts.solve || backtrackColoring;
91
+ return {
92
+ Solve: {
93
+ run: function ( graph, concept, scope, argz, cb ) {
94
+ var cfg = Object.assign({ as: '' }, concept._schema && concept._schema.solve, argz && argz[0]),
95
+ spec = cfg.specKey ? graph.getRef(cfg.specKey, scope) : scope._,
96
+ r = solve(spec) || {},
97
+ as = cfg.as || '',
98
+ facts = { $_id: '_parent' };
99
+ facts[concept._name] = true;
100
+ facts[as + 'sat'] = !!r.sat;
101
+ facts[as + 'model'] = r.sat ? (r.model != null ? r.model : null) : null;
102
+ // solver internals (steps, the search tree, …) are emitted on the child only;
103
+ // they document the search but must NOT cross the frontier (see snappedFrontier).
104
+ for ( var k in r ) if ( k !== 'sat' && k !== 'model' ) facts[as + k] = r[k];
105
+ cb(null, facts);
106
+ }
107
+ }
108
+ };
109
+ }
110
+
111
+ /**
112
+ * A merge-projection that crosses ONLY the snapped model across the frontier — the C-regime
113
+ * barrier: the colour/assignment enums + the `sat` bool cross; search internals (step counts,
114
+ * the search tree, continuous scores) stay in the fork. Pass the returned function as the
115
+ * 3rd arg to `parent.merge(child, targetId, project)`.
116
+ * @param opts.targetId the parent object id to write onto (required)
117
+ * @param opts.sourceId the child object holding the solved facts (default = targetId)
118
+ * @param opts.frontier keys allowed to cross (default ['sat','model'])
119
+ * @param opts.as the child's output prefix (default '')
120
+ */
121
+ function snappedFrontier( opts ) {
122
+ opts = opts || {};
123
+ var targetId = opts.targetId,
124
+ sourceId = opts.sourceId || targetId,
125
+ frontier = opts.frontier || ['sat', 'model'],
126
+ as = opts.as || '';
127
+ return function ( child ) {
128
+ var f = child.getEtty(sourceId)._,
129
+ tpl = { $$_id: targetId };
130
+ frontier.forEach(function ( k ) { tpl[k] = f[as + k]; }); // ONLY these keys cross
131
+ return tpl;
132
+ };
133
+ }
134
+
135
+ /**
136
+ * The C-fork grammar fragment: a `Solve` concept whose provider SEARCHES. Use it as the
137
+ * forked sub-graph's conceptMap so the fork runs the C regime.
138
+ * @param opts.require the trigger fact that marks an object as "to solve" (default 'toSolve')
139
+ * @param opts.specKey scope fact holding the spec (passed through to the provider)
140
+ * @param opts.as output-fact prefix (default '')
141
+ */
142
+ function solverConceptTree( opts ) {
143
+ opts = opts || {};
144
+ var solve = { as: opts.as || '' };
145
+ if ( opts.specKey ) solve.specKey = opts.specKey;
146
+ return {
147
+ childConcepts: {
148
+ Solve: {
149
+ _id: 'Solve', _name: 'Solve',
150
+ require: [opts.require || 'toSolve'],
151
+ provider: ['Solve::run'],
152
+ solve: solve
153
+ }
154
+ }
155
+ };
156
+ }
157
+
158
+ module.exports = {
159
+ backtrackColoring: backtrackColoring,
160
+ createSolver: createSolver,
161
+ snappedFrontier: snappedFrontier,
162
+ solverConceptTree: solverConceptTree
163
+ };
@@ -0,0 +1,228 @@
1
+ /*
2
+ * Copyright 2026 Nathanael Braun
3
+ * @author : Nathanael BRAUN <pp9ping@gmail.com>
4
+ *
5
+ * This program is free software: you can redistribute it and/or modify
6
+ * it under the terms of the GNU Affero General Public License as published by
7
+ * the Free Software Foundation, either version 3 of the License, or
8
+ * (at your option) any later version.
9
+ *
10
+ * This program is distributed in the hope that it will be useful,
11
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ * GNU Affero General Public License for more details.
14
+ *
15
+ * You should have received a copy of the GNU Affero General Public License
16
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+ */
18
+ 'use strict';
19
+ /**
20
+ * L1 graded-evidence statistics — hierarchical Beta-Binomial shrinkage along the IS-A
21
+ * backbone (the bounded probabilistic layer; doc/WIP/HANDOFF.md §7 Tier 2; experiment A).
22
+ * Zero core change, and ADDITIVE: the deterministic socle is untouched — this only writes
23
+ * SNAPPED reliability facts that a downstream `ensure` gates on (graded evidence as a
24
+ * discrete fact + a threshold, the L1 frame, never a raw float on a defeasant gate).
25
+ *
26
+ * The statistical point (A3): at small n a per-leaf success rate is high-variance; shrinking
27
+ * it toward the parent "grand mean" prior (θ̂ = (n·x̄ + κ·prior)/(n+κ)) cuts MSE ≈7× at n=2,
28
+ * with the advantage decaying as n grows. The barrier point (A2): the leaf consumes the prior
29
+ * SNAPPED to its own grain (the bucket midpoint), and the downstream gate keys on the snapped
30
+ * reliability RANK — so sub-bucket noise doesn't churn the memo (snapping costs only ~1–4% MSE).
31
+ *
32
+ * const { createStats, shrinkageConceptTree } = require('./stats');
33
+ * register(Graph, [ createStats() ]); // wires Stats::report / Stats::grandMean / Stats::shrink
34
+ * // pool the children's grand mean, then (a host wave later, once the prior exists)
35
+ * // shrink each leaf toward the SNAPPED prior and gate Trusted on the snapped rank.
36
+ *
37
+ * Hierarchical ordering: the grand-mean prior must exist before a leaf can shrink toward it.
38
+ * That is the "offline between revisions" pattern — pool first (its own settle), then add the
39
+ * shrink wave — not a live two-level reactive pool (which would need cross-object aggregation,
40
+ * roadmap #8). The pool reduces a race-free {__push} obs array gated on its cardinality.
41
+ */
42
+
43
+ // ---- reliability bands: snap a probability to a discrete rank + the band MIDPOINT (the
44
+ // snapped prior the synthesis mandates). 4 bands, edges 0.5/0.75/0.9. Override via opts.bands. ----
45
+ var RELIABILITY_BANDS = [
46
+ { max: 0.5, label: 'low', rank: 0, mid: 0.25 },
47
+ { max: 0.75, label: 'med', rank: 1, mid: 0.625 },
48
+ { max: 0.9, label: 'high', rank: 2, mid: 0.825 },
49
+ { label: 'certain', rank: 3, mid: 0.95 }
50
+ ];
51
+
52
+ function bandOf( p, bands ) {
53
+ bands = bands || RELIABILITY_BANDS;
54
+ for ( var i = 0; i < bands.length; i++ )
55
+ if ( bands[i].max == null || p < bands[i].max ) return { rank: bands[i].rank, label: bands[i].label, mid: bands[i].mid };
56
+ var last = bands[bands.length - 1];
57
+ return { rank: last.rank, label: last.label, mid: last.mid };
58
+ }
59
+
60
+ /**
61
+ * Beta-Binomial shrinkage of a leaf rate toward a prior.
62
+ * θ̂ = (n·x̄ + κ·prior) / (n + κ)
63
+ * @param o.succ leaf successes
64
+ * @param o.tot leaf trials (n)
65
+ * @param o.prior the (snapped) parent prior in [0,1]
66
+ * @param o.kappa shrinkage strength (pseudo-count); larger -> more pooling
67
+ * @returns { theta, raw, n } raw = the un-shrunk x̄ (0.5 when n=0)
68
+ */
69
+ function shrink( o ) {
70
+ o = o || {};
71
+ var n = Number(o.tot) || 0,
72
+ succ = Number(o.succ) || 0,
73
+ prior = o.prior == null ? 0.5 : Number(o.prior),
74
+ kappa = o.kappa == null ? 8 : Number(o.kappa),
75
+ raw = n ? succ / n : 0.5,
76
+ theta = (n * raw + kappa * prior) / (n + kappa);
77
+ return { theta: theta, raw: raw, n: n };
78
+ }
79
+
80
+ /**
81
+ * Empirical-Bayes shrinkage strength κ from a set of per-leaf raw rates at trial size n:
82
+ * κ ≈ n · (within-leaf sampling variance) / (between-leaf signal variance). Large when the
83
+ * leaves barely differ beyond sampling noise (pool hard); small when they genuinely spread.
84
+ * @param rawRates array of per-leaf x̄ in [0,1]
85
+ * @param n trials per leaf
86
+ */
87
+ function empiricalBayesKappa( rawRates, n ) {
88
+ var M = (rawRates || []).length;
89
+ if ( !M || !n ) return 0;
90
+ var gm = rawRates.reduce(function ( a, b ) { return a + b; }, 0) / M,
91
+ varRaw = rawRates.reduce(function ( a, b ) { return a + (b - gm) * (b - gm); }, 0) / M,
92
+ sampVar = gm * (1 - gm) / Math.max(1, n),
93
+ betweenVar = Math.max(1e-4, varRaw - sampVar);
94
+ return sampVar / betweenVar * n;
95
+ }
96
+
97
+ /**
98
+ * Build the L1 statistics provider fragment (host opt-in, like createVerifier).
99
+ * @param opts.bands default reliability bands (per-concept override via the concept's `stats.bands`)
100
+ * @param opts.kappa default shrinkage strength (default 8)
101
+ * @returns { Stats: { report, grandMean, shrink } }
102
+ *
103
+ * Concept wiring:
104
+ * Stats::report { require:['Cat'], provider:['Stats::report'], stats:{ poolId:'pool', succKey:'succ', totKey:'tot' } }
105
+ * Stats::grandMean { require:['PoolRoot'], ensure:['$obs.length==$expected'], provider:['Stats::grandMean'] }
106
+ * Stats::shrink { require:['Cat','Report'], provider:['Stats::shrink'], stats:{ poolId:'pool', priorKey:'pHat0Bucket', kappa:8 } }
107
+ * shrink emits <as>thetaHat / <as>rawHat / <as>relRank / <as>relBucket / <as>priorMid (gate on relRank).
108
+ */
109
+ function createStats( opts ) {
110
+ opts = opts || {};
111
+ var defaultBands = opts.bands,
112
+ defaultKappa = opts.kappa == null ? 8 : opts.kappa;
113
+
114
+ return {
115
+ Stats: {
116
+ // each leaf appends its (succ,tot) into the pool's obs array (race-free {__push}).
117
+ report: function ( graph, concept, scope, argz, cb ) {
118
+ var cfg = Object.assign({ poolId: 'pool', succKey: 'succ', totKey: 'tot' }, concept._schema && concept._schema.stats, argz && argz[0]),
119
+ push = { __push: { succ: scope._[cfg.succKey], tot: scope._[cfg.totKey] } },
120
+ poolWrite = { $$_id: cfg.poolId },
121
+ self = { $_id: '_parent' };
122
+ self[concept._name] = true;
123
+ poolWrite[cfg.obsKey || 'obs'] = push;
124
+ cb(null, [self, poolWrite]);
125
+ },
126
+
127
+ // reduce the pooled obs -> the grand mean, SNAPPED to its band (the prior the leaves consume).
128
+ grandMean: function ( graph, concept, scope, argz, cb ) {
129
+ var cfg = Object.assign({ as: '' }, concept._schema && concept._schema.stats, argz && argz[0]),
130
+ obs = scope._[cfg.obsKey || 'obs'] || [],
131
+ s = 0, t = 0;
132
+ for ( var i = 0; i < obs.length; i++ ) { s += obs[i].succ; t += obs[i].tot; }
133
+ var pHat0 = t ? s / t : 0.5,
134
+ b = bandOf(pHat0, cfg.bands || defaultBands),
135
+ as = cfg.as || '',
136
+ facts = { $_id: '_parent' };
137
+ facts[concept._name] = true;
138
+ facts[as + 'pHat0'] = pHat0;
139
+ facts[as + 'pHat0Rank'] = b.rank;
140
+ facts[as + 'pHat0Bucket'] = b.label;
141
+ facts[as + 'pHat0Mid'] = b.mid; // the snapped prior midpoint
142
+ facts[as + 'pooledSucc'] = s;
143
+ facts[as + 'pooledTot'] = t;
144
+ cb(null, facts);
145
+ },
146
+
147
+ // shrink the leaf toward the parent's SNAPPED prior; snap θ̂ to a reliability rank.
148
+ shrink: function ( graph, concept, scope, argz, cb ) {
149
+ var cfg = Object.assign({ poolId: 'pool', priorKey: 'pHat0Bucket', succKey: 'succ', totKey: 'tot', as: '' },
150
+ concept._schema && concept._schema.stats, argz && argz[0]),
151
+ bands = cfg.bands || defaultBands,
152
+ pool = graph.getEtty(cfg.poolId)._,
153
+ priorMid, r;
154
+ // consume the SNAPPED prior (the band midpoint) — barrier-clean, no continuous leak.
155
+ // Prefer an explicit midpoint fact (grandMean writes pHat0Mid); else resolve it from
156
+ // the pool's snapped band label.
157
+ if ( cfg.priorMidKey ) {
158
+ priorMid = pool[cfg.priorMidKey];
159
+ } else {
160
+ var label = pool[cfg.priorKey],
161
+ band = (bands || RELIABILITY_BANDS).filter(function ( x ) { return x.label === label; })[0];
162
+ priorMid = band ? band.mid : 0.5;
163
+ }
164
+ r = shrink({ succ: scope._[cfg.succKey], tot: scope._[cfg.totKey], prior: priorMid, kappa: cfg.kappa == null ? defaultKappa : cfg.kappa });
165
+ var b = bandOf(r.theta, bands),
166
+ as = cfg.as || '',
167
+ facts = { $_id: '_parent' };
168
+ facts[concept._name] = true;
169
+ facts[as + 'thetaHat'] = r.theta;
170
+ facts[as + 'rawHat'] = r.raw;
171
+ facts[as + 'relRank'] = b.rank;
172
+ facts[as + 'relBucket'] = b.label;
173
+ facts[as + 'priorMid'] = priorMid;
174
+ cb(null, facts);
175
+ }
176
+ }
177
+ };
178
+ }
179
+
180
+ /**
181
+ * A ready-made hierarchical-shrinkage fragment. Returns the two waves separately because the
182
+ * prior must exist before the leaves shrink (the offline-between-revisions ordering):
183
+ * .pool — the PoolRoot/Pool (grandMean) + Cat/Report concepts (wave 1, boot it);
184
+ * .shrink — the Reliability (shrink) + Trusted gate concept (wave 2, addConcept once pooled).
185
+ * @param opts.kappa shrinkage strength
186
+ * @param opts.trustedRank the Trusted gate threshold on the snapped rank (default 2 = high)
187
+ * @param opts.poolId the pool object id (default 'pool')
188
+ */
189
+ function shrinkageConceptTree( opts ) {
190
+ opts = opts || {};
191
+ var poolId = opts.poolId || 'pool',
192
+ trustedRank = opts.trustedRank == null ? 2 : opts.trustedRank,
193
+ stats = { poolId: poolId };
194
+ if ( opts.kappa != null ) stats.kappa = opts.kappa;
195
+ return {
196
+ pool: {
197
+ childConcepts: {
198
+ PoolRoot: {
199
+ _id: 'PoolRoot', _name: 'PoolRoot', require: ['PoolRoot'],
200
+ childConcepts: {
201
+ Pool: { _id: 'Pool', _name: 'Pool', require: ['PoolRoot'], ensure: ['$obs.length==$expected'], provider: ['Stats::grandMean'] }
202
+ }
203
+ },
204
+ Cat: {
205
+ _id: 'Cat', _name: 'Cat', require: ['Cat'],
206
+ childConcepts: {
207
+ Report: { _id: 'Report', _name: 'Report', require: ['Cat'], provider: ['Stats::report'], stats: { poolId: poolId } }
208
+ }
209
+ }
210
+ }
211
+ },
212
+ shrink: {
213
+ _id: 'Reliability', _name: 'Reliability', require: ['Cat', 'Report'], provider: ['Stats::shrink'], stats: stats,
214
+ childConcepts: {
215
+ Trusted: { _id: 'Trusted', _name: 'Trusted', require: ['Reliability'], ensure: ['$relRank>=' + trustedRank] }
216
+ }
217
+ }
218
+ };
219
+ }
220
+
221
+ module.exports = {
222
+ RELIABILITY_BANDS: RELIABILITY_BANDS,
223
+ bandOf: bandOf,
224
+ shrink: shrink,
225
+ empiricalBayesKappa: empiricalBayesKappa,
226
+ createStats: createStats,
227
+ shrinkageConceptTree: shrinkageConceptTree
228
+ };
@@ -0,0 +1,120 @@
1
+ /*
2
+ * Copyright 2026 Nathanael Braun
3
+ * @author : Nathanael BRAUN <pp9ping@gmail.com>
4
+ *
5
+ * This program is free software: you can redistribute it and/or modify
6
+ * it under the terms of the GNU Affero General Public License as published by
7
+ * the Free Software Foundation, either version 3 of the License, or
8
+ * (at your option) any later version.
9
+ *
10
+ * This program is distributed in the hope that it will be useful,
11
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ * GNU Affero General Public License for more details.
14
+ *
15
+ * You should have received a copy of the GNU Affero General Public License
16
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+ */
18
+ 'use strict';
19
+ /**
20
+ * Verification providers (roadmap #3) — the structural answer to K3 (coherence ≠ truth).
21
+ * The engine maintains COHERENCE, never TRUTH; verification makes unreliability *visible
22
+ * and non-propagating* by emitting discrete VERDICT facts that downstream concepts gate
23
+ * on via `ensure` (a refuted fact auto-retracts its dependents — refutation IS defeasance,
24
+ * no new engine path). Verdicts are discrete facts (the #1 typed-fact spine), never prose,
25
+ * and NEVER overwrite the checked fact (the graph is additive; experts don't fight).
26
+ *
27
+ * Three reliable patterns (all verified against the engine):
28
+ * 1. DETERMINISTIC verifier = a concept whose `ensure` IS the invariant (full `expr.js`
29
+ * grammar — uncapped). Its self-flag is the verdict; a target change re-tests it and
30
+ * auto-retracts. Consumers either nest under it (structural cascade) or `ensure` a
31
+ * verdict fact it writes. Prefer this — deterministic checkers >> LLM-refuters.
32
+ * 2. INDEPENDENT verdict provider (`Verify::check`) — for a check that must run as an
33
+ * effect (an external lookup, or an LLM-refuter): reads the target, runs a checker,
34
+ * writes a DISTINCT verdict fact + `VerifiedAgainst` provenance. Downstream gates via
35
+ * `ensure:["$xVerified == true"]`.
36
+ * 3. k-of-n VOTING (`Vote::tally`) — self-consistency: n strategies `{__push}` a vote
37
+ * into a grow-only array; a `Vote` concept gated `ensure:["$votes.length == $n"]`
38
+ * (the proven completion-gate) emits majority `consensus` + `confidence = agree/n`.
39
+ * Downstream gates `ensure:["$confidence >= threshold"]`.
40
+ *
41
+ * Independence discipline: a refuter must not be the same call that produced the fact
42
+ * (don't ask the hallucination to grade itself) — a deterministic checker, a different
43
+ * provider, or an adversarially-framed LLM. k-of-n over a biased model votes confidently
44
+ * wrong; treat `confidence` as a heuristic, never a proof.
45
+ */
46
+
47
+ // ---- deterministic checker library: (value, params) -> { pass, reason } ----
48
+ // Pure, side-effect-free, total (never throws). Add domain checks here as needed.
49
+ var checks = {
50
+ range : function ( v, p ) { var n = Number(v); return { pass: isFinite(n) && n >= p.min && n <= p.max, reason: 'range[' + p.min + ',' + p.max + ']' }; },
51
+ oneOf : function ( v, p ) { return { pass: (p.values || []).indexOf(v) !== -1, reason: 'oneOf' }; },
52
+ equals : function ( v, p ) { return { pass: v === p.to, reason: 'equals' }; },
53
+ approx : function ( v, p ) { var n = Number(v); return { pass: isFinite(n) && Math.abs(n - Number(p.to)) <= (p.tol || 0), reason: 'approx±' + (p.tol || 0) }; },
54
+ nonEmpty: function ( v ) { return { pass: v != null && (typeof v === 'string' || Array.isArray(v) ? v.length > 0 : true), reason: 'nonEmpty' }; }
55
+ };
56
+
57
+ // ---- majority vote over an array of discrete votes ----
58
+ function majority( votes ) {
59
+ var counts = {}, best = null, bestN = 0;
60
+ for ( var i = 0; i < votes.length; i++ ) {
61
+ var k = JSON.stringify(votes[i]);
62
+ counts[k] = (counts[k] || 0) + 1;
63
+ if ( counts[k] > bestN ) { bestN = counts[k]; best = votes[i]; }
64
+ }
65
+ return { value: best, agree: bestN, total: votes.length, confidence: votes.length ? bestN / votes.length : 0 };
66
+ }
67
+
68
+ /**
69
+ * Build the verification provider-map fragment.
70
+ * @param opts.checks extra deterministic checkers merged over the defaults.
71
+ * @returns { Verify: { check }, Vote: { tally } }
72
+ */
73
+ function createVerifier( opts ) {
74
+ opts = opts || {};
75
+ var lib = Object.assign({}, checks, opts.checks);
76
+
77
+ return {
78
+ // Verify::check — independent verdict provider. Concept wiring:
79
+ // { provider:['Verify::check'],
80
+ // verify: { target:'$x', check:'range', params:{min:0,max:100}, as:'x' } }
81
+ // Emits <as>Verdict / <as>Verified / <as>Reason / <as>VerifiedAgainst — never $x itself.
82
+ Verify: {
83
+ check: function ( graph, concept, scope, argz, cb ) {
84
+ var cfg = Object.assign({}, concept._schema && concept._schema.verify, argz && argz[0]),
85
+ prefix = cfg.as || concept._name,
86
+ checker = lib[cfg.check],
87
+ value = graph.getRef(cfg.target, scope),
88
+ res = checker ? checker(value, cfg.params || {}) : { pass: false, reason: 'unknown-check:' + cfg.check },
89
+ facts = { $_id: '_parent' };
90
+ facts[concept._name] = true;
91
+ facts[prefix + 'Verdict'] = res.pass ? 'pass' : 'fail';
92
+ facts[prefix + 'Verified'] = res.pass;
93
+ facts[prefix + 'Reason'] = res.reason;
94
+ facts[prefix + 'VerifiedAgainst'] = cfg.check;
95
+ cb(null, facts);
96
+ }
97
+ },
98
+ // Vote::tally — k-of-n consensus. Concept wiring:
99
+ // { require:['votes'], ensure:['$votes.length == $expected'], provider:['Vote::tally'],
100
+ // vote: { votesKey:'votes', as:'' } }
101
+ // Emits <as>consensus / <as>confidence / <as>agree / <as>total (flat by default).
102
+ Vote: {
103
+ tally: function ( graph, concept, scope, argz, cb ) {
104
+ var cfg = Object.assign({}, concept._schema && concept._schema.vote, argz && argz[0]),
105
+ prefix = cfg.as || '',
106
+ votes = graph.getRef(cfg.votesKey || 'votes', scope) || [],
107
+ m = majority(votes),
108
+ facts = { $_id: '_parent' };
109
+ facts[concept._name] = true;
110
+ facts[prefix + 'consensus'] = m.value;
111
+ facts[prefix + 'confidence'] = m.confidence;
112
+ facts[prefix + 'agree'] = m.agree;
113
+ facts[prefix + 'total'] = m.total;
114
+ cb(null, facts);
115
+ }
116
+ }
117
+ };
118
+ }
119
+
120
+ module.exports = { createVerifier: createVerifier, checks: checks, majority: majority };
@@ -0,0 +1,135 @@
1
+ /*
2
+ * Copyright 2026 Nathanael Braun
3
+ * @author : Nathanael BRAUN <pp9ping@gmail.com>
4
+ *
5
+ * This program is free software: you can redistribute it and/or modify
6
+ * it under the terms of the GNU Affero General Public License as published by
7
+ * the Free Software Foundation, either version 3 of the License, or
8
+ * (at your option) any later version.
9
+ *
10
+ * This program is distributed in the hope that it will be useful,
11
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ * GNU Affero General Public License for more details.
14
+ *
15
+ * You should have received a copy of the GNU Affero General Public License
16
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+ */
18
+ 'use strict';
19
+ /**
20
+ * Parent-side sub-graph runtime — dispatch graph parts to a warm worker for horizontal scale. The worker
21
+ * rehydrates from a conceptMap (JSON) + provider dir path + a seed snapshot (JSON); the one effect that can't be
22
+ * shipped — a parent-bound model `ask` — is PROXIED back over the channel and the parent's real `ask` answers it.
23
+ *
24
+ * const { createGraphWorker } = require('skynet-graph/lib/runtime');
25
+ * const w = createGraphWorker({ conceptMap, geo: true }); // default: worker_threads (same machine)
26
+ * const snapshot = await w.dispatch(seed); // -> graph.serialize()
27
+ * w.terminate();
28
+ *
29
+ * TRANSPORT (P5). The {init, dispatch, ask, result, log} protocol is transport-agnostic (see protocol.js). Two
30
+ * transports slot in behind the SAME channel seam:
31
+ * • worker_threads (default) — same-machine parallelism, spawns worker-entry.js;
32
+ * • socket (opts.address / transport) — a CROSS-INSTANCE runtime: connect to a `serveGraphWorker` on another
33
+ * process or machine. `createGraphWorker({ address:{path|port}, … })`.
34
+ * A host stands up the remote side with `serveGraphWorker({ path } | { port })`.
35
+ *
36
+ * @param {object} [opts.conceptMap] concept map (JSON) — or
37
+ * @param {string} [opts.concepts] a concept-set dir the worker can read
38
+ * @param {string} [opts.providers] provider dir/file the worker loads locally
39
+ * @param {boolean} [opts.geo] register the packaged Geo provider
40
+ * @param {boolean} [opts.llm] register the packaged LLM provider, wired to `opts.ask`
41
+ * @param {function} [opts.ask] parent model backend — answers proxied calls (prompt,opts)=>Promise
42
+ * @param {object} [opts.conf] data-only Graph cfg for dispatched graphs
43
+ * @param {object} [opts.logger] parent logger — worker log records re-emit into it (tagged {worker:true})
44
+ * @param {string} [opts.logLevel] threshold for the worker's forwarding logger (default 'verbose')
45
+ * @param {object} [opts.address] socket transport: { path } (unix) | { port[, host] } (TCP). Omit = threads.
46
+ * @param {string} [opts.transport] 'socket' | 'thread' (default). 'socket' requires opts.address.
47
+ * @returns {{ready, dispatch, terminate, worker?}}
48
+ */
49
+ var path = require('path');
50
+ var protocol = require('./protocol.js');
51
+
52
+ function threadChannel() {
53
+ var Worker = require('worker_threads').Worker;
54
+ var worker = new Worker(path.join(__dirname, 'worker-entry.js'));
55
+ return {
56
+ worker: worker,
57
+ send: function ( m ) { worker.postMessage(m); },
58
+ onMessage: function ( fn ) { worker.on('message', fn); },
59
+ onError: function ( fn ) { worker.on('error', fn); },
60
+ close: function () { return worker.terminate(); }
61
+ };
62
+ }
63
+
64
+ function createGraphWorker( opts ) {
65
+ opts = opts || {};
66
+
67
+ // ── socket transport: connect to a remote serveGraphWorker; the API awaits the connection lazily ──────────
68
+ if ( opts.transport === 'socket' || opts.address ) {
69
+ if ( !opts.address ) throw new Error("createGraphWorker transport 'socket' needs opts.address ({ path } | { port })");
70
+ var connect = require('./transport-socket.js').connect;
71
+ var connected = connect(opts.address).then(function ( channel ) { return protocol.attachClient(channel, opts); });
72
+ return {
73
+ ready: function () { return connected.then(function ( a ) { return a.ready(); }); },
74
+ dispatch: function ( seed, dopts ) { return connected.then(function ( a ) { return a.dispatch(seed, dopts); }); },
75
+ invoke: function ( iopts ) { return connected.then(function ( a ) { return a.invoke(iopts); }); },
76
+ terminate: function () { return connected.then(function ( a ) { return a.terminate(); }); }
77
+ };
78
+ }
79
+
80
+ // ── default: worker_threads (same-machine) ────────────────────────────────────────────────────────────────
81
+ var channel = threadChannel();
82
+ var api = protocol.attachClient(channel, opts);
83
+ api.worker = channel.worker; // back-compat: expose the raw Worker
84
+ return api;
85
+ }
86
+
87
+ /**
88
+ * One-shot convenience: spawn/connect a worker, dispatch a single seed, return the stabilized snapshot, tear down.
89
+ * @param {object} opts createGraphWorker opts + { seed, settleTimeout }
90
+ * @returns {Promise<object>} snapshot (graph.serialize())
91
+ */
92
+ function spawnGraph( opts ) {
93
+ opts = opts || {};
94
+ var w = createGraphWorker(opts);
95
+ return w.dispatch(opts.seed, opts).then(
96
+ function ( snap ) { return Promise.resolve(w.terminate()).then(function () { return snap; }); },
97
+ function ( err ) { return Promise.resolve(w.terminate()).then(function () { throw err; }); }
98
+ );
99
+ }
100
+
101
+ /**
102
+ * One-shot BOUNDED invoke (P1): spawn/connect a worker, invoke a single seed, return { summary, writeFootprint } —
103
+ * ONLY the declared frontier alphabet (Σ_sep) crosses back, not the whole snapshot — tear down.
104
+ * @param {object} opts createGraphWorker opts + { seed, boundedFrom, boundedKeys, settleTimeout }
105
+ * @returns {Promise<{summary:object, writeFootprint:string[]}>}
106
+ */
107
+ function invokeGraph( opts ) {
108
+ opts = opts || {};
109
+ var w = createGraphWorker(opts);
110
+ return w.invoke(opts).then(
111
+ function ( res ) { return Promise.resolve(w.terminate()).then(function () { return res; }); },
112
+ function ( err ) { return Promise.resolve(w.terminate()).then(function () { throw err; }); }
113
+ );
114
+ }
115
+
116
+ /**
117
+ * serveGraphWorker(opts) → Promise<{ server, address, close }>. Stand up a socket runtime that accepts sub-graph
118
+ * dispatches from remote `createGraphWorker({ address, … })` clients. opts.path (unix) or opts.port[/host] (TCP),
119
+ * or opts.address directly. `close()` stops accepting and resolves when the server is down.
120
+ */
121
+ function serveGraphWorker( opts ) {
122
+ opts = opts || {};
123
+ var serve = require('./transport-socket.js').serve;
124
+ var listen = opts.address || (opts.path != null ? { path: opts.path } : { port: opts.port, host: opts.host });
125
+ return serve(listen).then(function ( server ) {
126
+ return {
127
+ server: server,
128
+ address: server.address(),
129
+ close: function () { return new Promise(function ( res ) { server.close(function () { res(); }); }); }
130
+ };
131
+ });
132
+ }
133
+
134
+ module.exports = { createGraphWorker: createGraphWorker, spawnGraph: spawnGraph, invokeGraph: invokeGraph, serveGraphWorker: serveGraphWorker };
135
+ module.exports.createInvokePool = require('./invoke-pool.js').createInvokePool; // P3: warm-worker pool keyed by contract (N cases → 1 instance)